From 67a63d9ef51ccd21dc56c3354aa800121577469c Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Wed, 31 Jul 2024 20:10:51 -0400 Subject: [PATCH 001/185] chore: Update README.md to fix typo (#4374) --- docs/SUMMARY.md | 1 + docs/getting-started/architecture-and-components/README.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 1173a693ef..92fa3c692b 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -19,6 +19,7 @@ * [\[Alpha\] Saved dataset](getting-started/concepts/dataset.md) * [Architecture](getting-started/architecture-and-components/README.md) * [Overview](getting-started/architecture-and-components/overview.md) + * [Language](getting-started/architecture-and-components/language.md) * [Registry](getting-started/architecture-and-components/registry.md) * [Offline store](getting-started/architecture-and-components/offline-store.md) * [Online store](getting-started/architecture-and-components/online-store.md) diff --git a/docs/getting-started/architecture-and-components/README.md b/docs/getting-started/architecture-and-components/README.md index 3a2ebcf6ed..c364744bc6 100644 --- a/docs/getting-started/architecture-and-components/README.md +++ b/docs/getting-started/architecture-and-components/README.md @@ -1,7 +1,7 @@ # Architecture {% content-ref url="language.md" %} -[langauge.md](language.md) +[language.md](language.md) {% endcontent-ref %} {% content-ref url="overview.md" %} From 9a33fce695c54226b3afd7b998e284f358bab141 Mon Sep 17 00:00:00 2001 From: Tornike Gurgenidze Date: Tue, 6 Aug 2024 18:13:01 +0400 Subject: [PATCH 002/185] fix: Update java testcontainers to use Compose V2 (#4381) * fix: revert java testcontainers to use docker-based compose Signed-off-by: tokoko * fix: update testcontainers-java to Compose v2 Signed-off-by: tokoko --------- Signed-off-by: tokoko Co-authored-by: tokoko --- .devcontainer/Dockerfile | 13 ++++++ .devcontainer/devcontainer.json | 42 ++++++++++++++----- java/serving/pom.xml | 10 ++++- .../feast/serving/it/ServingEnvironment.java | 6 +-- 4 files changed, 55 insertions(+), 16 deletions(-) create mode 100644 .devcontainer/Dockerfile diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000000..84c3537fa2 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,13 @@ +FROM mcr.microsoft.com/vscode/devcontainers/python:3.9-buster + +USER vscode +RUN curl -LsSf https://astral.sh/uv/install.sh | sh +RUN curl -fsSL https://pixi.sh/install.sh | bash +ENV PATH=$PATH:/home/vscode/.cargo/bin +ENV PYTHON=3.9 +RUN uv venv ~/.local +ENV VIRTUAL_ENV=~/.local +ENV PATH=$VIRTUAL_ENV/bin:$PATH +USER root + + \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index e82fd04db4..04fcbb00aa 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,13 +1,33 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-dockerfile { - "name": "feast-devcontainer", - "image": "mcr.microsoft.com/devcontainers/base:ubuntu", - "features": { - "ghcr.io/devcontainers/features/docker-in-docker:2": { - "version": "latest" - }, - "ghcr.io/devcontainers/features/python:1": { - "version": "3.9" - } - }, - "postCreateCommand": "pip install -e '.[dev]' && make compile-protos-python" + "name": "feast-devcontainer", + "build": { + // Sets the run context to one level up instead of the .devcontainer folder. + "context": "..", + // Update the 'dockerFile' property if you aren't using the standard 'Dockerfile' filename. + "dockerfile": "Dockerfile" + }, + + // Features to add to the dev container. More info: https://containers.dev/features. + "features": { + "ghcr.io/devcontainers/features/docker-in-docker:2": { + "version": "latest" + }, + "ghcr.io/devcontainers-contrib/features/maven-sdkman:2": { + "jdkVersion": "11.0.24-amzn" + } + }, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Uncomment the next line to run commands after the container is created. + "postCreateCommand": "make install-python-ci-dependencies-uv-venv" + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" } diff --git a/java/serving/pom.xml b/java/serving/pom.xml index 93e4f81efe..cc3be9d166 100644 --- a/java/serving/pom.xml +++ b/java/serving/pom.xml @@ -348,15 +348,21 @@ org.testcontainers testcontainers - 1.16.2 + 1.20.1 test org.testcontainers junit-jupiter - 1.16.2 + 1.20.1 test + + org.junit.jupiter + junit-jupiter-api + 5.10.3 + test + org.awaitility awaitility diff --git a/java/serving/src/test/java/feast/serving/it/ServingEnvironment.java b/java/serving/src/test/java/feast/serving/it/ServingEnvironment.java index 356524399a..c1f0c448a7 100644 --- a/java/serving/src/test/java/feast/serving/it/ServingEnvironment.java +++ b/java/serving/src/test/java/feast/serving/it/ServingEnvironment.java @@ -39,13 +39,13 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; -import org.testcontainers.containers.DockerComposeContainer; +import org.testcontainers.containers.ComposeContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.junit.jupiter.Testcontainers; @Testcontainers abstract class ServingEnvironment { - static DockerComposeContainer environment; + static ComposeContainer environment; static int serverPort = getFreePort(); ServingServiceGrpc.ServingServiceBlockingStub servingStub; Injector injector; @@ -57,7 +57,7 @@ abstract class ServingEnvironment { @BeforeAll static void globalSetup() { environment = - new DockerComposeContainer( + new ComposeContainer( new File("src/test/resources/docker-compose/docker-compose-redis-it.yml")) .withExposedService("redis", 6379) .withExposedService( From 8f264b6807a07874dc01207c655baeef7dfaa7b2 Mon Sep 17 00:00:00 2001 From: "bdodla@expedia.com" <13788369+EXPEbdodla@users.noreply.github.com> Date: Tue, 6 Aug 2024 07:30:22 -0700 Subject: [PATCH 003/185] fix: Null value compatibility for unit timestamp list value type (#4378) Signed-off-by: Bhargav Dodla Co-authored-by: Bhargav Dodla --- sdk/python/feast/type_map.py | 17 +++++++++++------ sdk/python/tests/unit/test_type_map.py | 7 +++++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/sdk/python/feast/type_map.py b/sdk/python/feast/type_map.py index 4e9b54c631..e47e4fcbe4 100644 --- a/sdk/python/feast/type_map.py +++ b/sdk/python/feast/type_map.py @@ -396,13 +396,18 @@ def _python_value_to_proto_value( raise _type_err(item, valid_types[0]) if feast_value_type == ValueType.UNIX_TIMESTAMP_LIST: - int_timestamps_lists = ( - _python_datetime_to_int_timestamp(value) for value in values - ) return [ - # ProtoValue does actually accept `np.int_` but the typing complains. - ProtoValue(unix_timestamp_list_val=Int64List(val=ts)) # type: ignore - for ts in int_timestamps_lists + ( + # ProtoValue does actually accept `np.int_` but the typing complains. + ProtoValue( + unix_timestamp_list_val=Int64List( + val=_python_datetime_to_int_timestamp(value) # type: ignore + ) + ) + if value is not None + else ProtoValue() + ) + for value in values ] if feast_value_type == ValueType.BOOL_LIST: # ProtoValue does not support conversion of np.bool_ so we need to convert it to support np.bool_. diff --git a/sdk/python/tests/unit/test_type_map.py b/sdk/python/tests/unit/test_type_map.py index 39e3e7dafa..be8a25c163 100644 --- a/sdk/python/tests/unit/test_type_map.py +++ b/sdk/python/tests/unit/test_type_map.py @@ -60,7 +60,14 @@ def test_python_values_to_proto_values_bool(values): (np.array([b'["a","b","c"]']), ValueType.STRING_LIST, ["a", "b", "c"]), (np.array([b"[true,false]"]), ValueType.BOOL_LIST, [True, False]), (np.array([b"[1,0]"]), ValueType.BOOL_LIST, [True, False]), + (np.array([None]), ValueType.INT32_LIST, None), + (np.array([None]), ValueType.INT64_LIST, None), + (np.array([None]), ValueType.FLOAT_LIST, None), + (np.array([None]), ValueType.DOUBLE_LIST, None), + (np.array([None]), ValueType.BOOL_LIST, None), + (np.array([None]), ValueType.BYTES_LIST, None), (np.array([None]), ValueType.STRING_LIST, None), + (np.array([None]), ValueType.UNIX_TIMESTAMP_LIST, None), ([b"[1,2,3]"], ValueType.INT64_LIST, [1, 2, 3]), ([b"[1,2,3]"], ValueType.INT32_LIST, [1, 2, 3]), ([b"[1.5,2.5,3.5]"], ValueType.FLOAT_LIST, [1.5, 2.5, 3.5]), From 8eceff26ba00fd446d27ad5ce2ee9d039c57fd9a Mon Sep 17 00:00:00 2001 From: Tommy Hughes IV Date: Tue, 6 Aug 2024 09:40:36 -0500 Subject: [PATCH 004/185] fix: Add feast-operator files to semantic-release script (#4382) Signed-off-by: Tommy Hughes --- .releaserc.js | 1 + infra/feast-operator/Makefile | 2 +- infra/feast-operator/config/manager/kustomization.yaml | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.releaserc.js b/.releaserc.js index 114d65d1a2..c4ad52c9b2 100644 --- a/.releaserc.js +++ b/.releaserc.js @@ -66,6 +66,7 @@ module.exports = { "CHANGELOG.md", "java/pom.xml", "infra/charts/**/*.*", + "infra/feast-operator/**/*.*", "ui/package.json", "sdk/python/feast/ui/package.json", "sdk/python/feast/ui/yarn.lock" diff --git a/infra/feast-operator/Makefile b/infra/feast-operator/Makefile index f446437751..f52911f432 100644 --- a/infra/feast-operator/Makefile +++ b/infra/feast-operator/Makefile @@ -3,7 +3,7 @@ # To re-generate a bundle for another specific version without changing the standard setup, you can: # - use the VERSION as arg of the bundle target (e.g make bundle VERSION=0.0.2) # - use environment variables to overwrite this value (e.g export VERSION=0.0.2) -VERSION ?= 0.39.0 +VERSION ?= 0.40.0 # CHANNELS define the bundle channels used in the bundle. # Add a new line here if you would like to change its default config. (E.g CHANNELS = "candidate,fast,stable") diff --git a/infra/feast-operator/config/manager/kustomization.yaml b/infra/feast-operator/config/manager/kustomization.yaml index 1c111ac7ad..aba3224be6 100644 --- a/infra/feast-operator/config/manager/kustomization.yaml +++ b/infra/feast-operator/config/manager/kustomization.yaml @@ -5,4 +5,4 @@ kind: Kustomization images: - name: controller newName: feastdev/feast-operator - newTag: 0.39.0 + newTag: 0.40.0 From a571e08b97a95f8543d7dea27902c135ab3a4378 Mon Sep 17 00:00:00 2001 From: TS <67011812+tsisodia10@users.noreply.github.com> Date: Tue, 6 Aug 2024 10:46:40 -0400 Subject: [PATCH 005/185] feat: Instrument Feast using Prometheus and OpenTelemetry (#4366) feat: instrument feature store This commit adds opentelemetry to monitor Feast Signed-off-by: Twinkll Sisodia --- README.md | 3 +- infra/charts/feast-feature-server/README.md | 4 + .../feast-feature-server/opentelemetry.md | 108 +++++++++++++ .../samples/instrumentation.yaml | 19 +++ .../samples/otel-collector.yaml | 53 +++++++ .../feast-feature-server/samples/otel-sm.yaml | 16 ++ .../samples/prometheus.yaml | 15 ++ .../feast-feature-server/samples/rbac.yaml | 68 ++++++++ .../samples/service-monitor.yaml | 16 ++ .../feast-feature-server/samples/workflow.png | Bin 0 -> 183119 bytes .../templates/deployment.yaml | 13 +- .../templates/service.yaml | 6 + infra/charts/feast-feature-server/values.yaml | 6 + sdk/python/feast/cli.py | 9 ++ sdk/python/feast/feature_server.py | 38 +++++ sdk/python/feast/feature_store.py | 2 + .../feature_servers/multicloud/Dockerfile | 2 +- .../feature_servers/multicloud/Dockerfile.dev | 2 +- .../requirements/py3.10-ci-requirements.txt | 148 ++++++++++++++++-- .../requirements/py3.10-requirements.txt | 50 +++++- .../requirements/py3.11-ci-requirements.txt | 148 ++++++++++++++++-- .../requirements/py3.11-requirements.txt | 50 +++++- .../requirements/py3.9-ci-requirements.txt | 148 ++++++++++++++++-- .../requirements/py3.9-requirements.txt | 56 +++++-- .../online_store/test_remote_online_store.py | 17 +- setup.py | 6 + 26 files changed, 928 insertions(+), 75 deletions(-) create mode 100644 infra/charts/feast-feature-server/opentelemetry.md create mode 100644 infra/charts/feast-feature-server/samples/instrumentation.yaml create mode 100644 infra/charts/feast-feature-server/samples/otel-collector.yaml create mode 100644 infra/charts/feast-feature-server/samples/otel-sm.yaml create mode 100644 infra/charts/feast-feature-server/samples/prometheus.yaml create mode 100644 infra/charts/feast-feature-server/samples/rbac.yaml create mode 100644 infra/charts/feast-feature-server/samples/service-monitor.yaml create mode 100644 infra/charts/feast-feature-server/samples/workflow.png diff --git a/README.md b/README.md index 13c5db443c..ede28c4c95 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,6 @@ [![GitHub Release](https://img.shields.io/github/v/release/feast-dev/feast.svg?style=flat&sort=semver&color=blue)](https://github.com/feast-dev/feast/releases) ## Join us on Slack! - 👋👋👋 [Come say hi on Slack!](https://join.slack.com/t/feastopensource/signup) ## Overview @@ -231,4 +230,4 @@ Thanks goes to these incredible people: - + \ No newline at end of file diff --git a/infra/charts/feast-feature-server/README.md b/infra/charts/feast-feature-server/README.md index 63ff7cf61b..bff7820d1f 100644 --- a/infra/charts/feast-feature-server/README.md +++ b/infra/charts/feast-feature-server/README.md @@ -44,8 +44,12 @@ See [here](https://github.com/feast-dev/feast/tree/master/examples/python-helm-d | imagePullSecrets | list | `[]` | | | livenessProbe.initialDelaySeconds | int | `30` | | | livenessProbe.periodSeconds | int | `30` | | +| metrics.enabled | bool | `false` | | +| metrics.otelCollector.endpoint | string | `""` | | +| metrics.otelCollector.port | int | `4317` | | | nameOverride | string | `""` | | | nodeSelector | object | `{}` | | +| otel_service.name | string | `"otelcol"` | | | podAnnotations | object | `{}` | | | podSecurityContext | object | `{}` | | | readinessProbe.initialDelaySeconds | int | `20` | | diff --git a/infra/charts/feast-feature-server/opentelemetry.md b/infra/charts/feast-feature-server/opentelemetry.md new file mode 100644 index 0000000000..fc0930821e --- /dev/null +++ b/infra/charts/feast-feature-server/opentelemetry.md @@ -0,0 +1,108 @@ +## Adding Monitoring +To add monitoring to the Feast Feature Server, follow these steps: + +### Workflow + +Feast instrumentation Using OpenTelemetry and Prometheus - +![Workflow](samples/workflow.png) + +### Deploy Prometheus Operator +Follow the Prometheus Operator documentation to install the operator - +https://github.com/prometheus-operator/prometheus-operator/blob/main/Documentation/user-guides/getting-started.md + +### Deploy OpenTelemetry Operator +Before installing OTEL Operator, install `cert-manager` and validate the `pods` should spin up -- +``` +kubectl apply -f https://github.com/open-telemetry/opentelemetry-operator/releases/latest/download/opentelemetry-operator.yaml +``` + +Follow the documentation for further installation steps - +https://github.com/open-telemetry/opentelemetry-operator + +### Configure OpenTelemetry Collector +Add the OpenTelemetry Collector configuration under the metrics section in your values.yaml file. + +Example values.yaml: + +``` +metrics: + enabled: true + otelCollector: + endpoint: "otel-collector.default.svc.cluster.local:4317" #sample + headers: + api-key: "your-api-key" +``` + +### Add instrumentation annotation and environment variables in the deployment.yaml + +``` +template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + instrumentation.opentelemetry.io/inject-python: "true" +``` + +``` +- name: OTEL_EXPORTER_OTLP_ENDPOINT + value: http://{{ .Values.service.name }}-collector.{{ .Release.namespace }}.svc.cluster.local:{{ .Values.metrics.endpoint.port}} +- name: OTEL_EXPORTER_OTLP_INSECURE + value: "true" +``` + +### Add checks +Add metric checks to all manifests and deployment file - + +``` +{{ if .Values.metrics.enabled }} +apiVersion: opentelemetry.io/v1alpha1 +kind: Instrumentation +metadata: + name: feast-instrumentation +spec: + exporter: + endpoint: http://{{ .Values.service.name }}-collector.{{ .Release.Namespace }}.svc.cluster.local:4318 # This is the default port for the OpenTelemetry Collector + env: + propagators: + - tracecontext + - baggage + python: + env: + - name: OTEL_METRICS_EXPORTER + value: console,otlp_proto_http + - name: OTEL_LOGS_EXPORTER + value: otlp_proto_http + - name: OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED + value: "true" +{{end}} +``` + +### Add manifests to the chart +Add Instrumentation, OpenTelemetryCollector, ServiceMonitors, Prometheus Instance and RBAC rules as provided in the [samples/](https://github.com/feast-dev/feast/tree/91540703c483f1cd03b534a1a45bc4ccdcf79f81/infra/charts/feast-feature-server/samples) directory. + +For latest updates please refer the official repository - https://github.com/open-telemetry/opentelemetry-operator + +### Deploy Feast +Deploy Feast and set `metrics` value to `true`. + +Example - +``` +helm install feast-release infra/charts/feast-feature-server --set metric=true --set feature_store_yaml_base64="" +``` + +## See logs +Once the opentelemetry is deployed, you can search the logs to see the required metrics - + +``` +oc logs otelcol-collector-0 | grep "Name: feast_feature_server_memory_usage\|Value: 0.*" +oc logs otelcol-collector-0 | grep "Name: feast_feature_server_cpu_usage\|Value: 0.*" +``` +``` + -> Name: feast_feature_server_memory_usage +Value: 0.579426 +``` +``` +-> Name: feast_feature_server_cpu_usage +Value: 0.000000 +``` diff --git a/infra/charts/feast-feature-server/samples/instrumentation.yaml b/infra/charts/feast-feature-server/samples/instrumentation.yaml new file mode 100644 index 0000000000..8ade7ee1bd --- /dev/null +++ b/infra/charts/feast-feature-server/samples/instrumentation.yaml @@ -0,0 +1,19 @@ +apiVersion: opentelemetry.io/v1alpha1 +kind: Instrumentation +metadata: + name: feast-instrumentation +spec: + exporter: + endpoint: # eg: http://{{ .Values.service.name }}-collector.{{ .Release.Namespace }}.svc.cluster.local:4318 + env: + propagators: + - tracecontext + - baggage + python: + env: + - name: OTEL_METRICS_EXPORTER + value: console,otlp_proto_http + - name: OTEL_LOGS_EXPORTER + value: otlp_proto_http + - name: OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED + value: "true" \ No newline at end of file diff --git a/infra/charts/feast-feature-server/samples/otel-collector.yaml b/infra/charts/feast-feature-server/samples/otel-collector.yaml new file mode 100644 index 0000000000..d35a957e9b --- /dev/null +++ b/infra/charts/feast-feature-server/samples/otel-collector.yaml @@ -0,0 +1,53 @@ +# API reference https://github.com/open-telemetry/opentelemetry-operator/blob/main/docs/api.md +# Refs for v1beta1 config: https://github.com/open-telemetry/opentelemetry-operator/issues/3011#issuecomment-2154118998 +apiVersion: opentelemetry.io/v1beta1 +kind: OpenTelemetryCollector +metadata: + name: otelcol +spec: + mode: statefulset + image: otel/opentelemetry-collector-contrib:0.102.1 + targetAllocator: + enabled: true + serviceAccount: opentelemetry-targetallocator-sa + prometheusCR: + enabled: true + podMonitorSelector: {} + serviceMonitorSelector: {} + ## If uncommented, only service monitors with this label will get picked up + # app: feast + config: + receivers: + otlp: + protocols: + grpc: {} + http: {} + prometheus: + config: + scrape_configs: + - job_name: 'otelcol-collector' + scrape_interval: 10s + static_configs: + - targets: [ '0.0.0.0:8888' ] + + processors: + batch: {} + + exporters: + logging: + verbosity: detailed + + service: + pipelines: + traces: + receivers: [otlp] + processors: [batch] + exporters: [logging] + metrics: + receivers: [otlp, prometheus] + processors: [] + exporters: [logging] + logs: + receivers: [otlp] + processors: [batch] + exporters: [logging] \ No newline at end of file diff --git a/infra/charts/feast-feature-server/samples/otel-sm.yaml b/infra/charts/feast-feature-server/samples/otel-sm.yaml new file mode 100644 index 0000000000..88cb6a6b41 --- /dev/null +++ b/infra/charts/feast-feature-server/samples/otel-sm.yaml @@ -0,0 +1,16 @@ +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + labels: + app: feast + name: otel-sm-1 +spec: + endpoints: + - port: metrics + namespaceSelector: + matchNames: + - # helm value - {{ .Release.Namespace }} + selector: + matchLabels: + app.kubernetes.io/component: opentelemetry-collector + app.kubernetes.io/managed-by: opentelemetry-operator \ No newline at end of file diff --git a/infra/charts/feast-feature-server/samples/prometheus.yaml b/infra/charts/feast-feature-server/samples/prometheus.yaml new file mode 100644 index 0000000000..d960fadd1e --- /dev/null +++ b/infra/charts/feast-feature-server/samples/prometheus.yaml @@ -0,0 +1,15 @@ +kind: Prometheus +metadata: + name: prometheus +spec: + evaluationInterval: 30s + podMonitorSelector: + matchLabels: + app: feast + portName: web + replicas: 1 + scrapeInterval: 30s + serviceAccountName: prometheus-k8s + serviceMonitorSelector: + matchLabels: + app: feast \ No newline at end of file diff --git a/infra/charts/feast-feature-server/samples/rbac.yaml b/infra/charts/feast-feature-server/samples/rbac.yaml new file mode 100644 index 0000000000..195777d5fa --- /dev/null +++ b/infra/charts/feast-feature-server/samples/rbac.yaml @@ -0,0 +1,68 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: opentelemetry-targetallocator-sa +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: opentelemetry-targetallocator-role-1 + annotations: + meta.helm.sh/release-name: "feast-release" + meta.helm.sh/release-namespace: "feast-val" + labels: + app.kubernetes.io/managed-by: "Helm" +rules: +- apiGroups: + - monitoring.coreos.com + resources: + - servicemonitors + - podmonitors + verbs: + - '*' +- apiGroups: [""] + resources: + - namespaces + verbs: ["get", "list", "watch"] +- apiGroups: [""] + resources: + - nodes + - nodes/metrics + - services + - endpoints + - pods + verbs: ["get", "list", "watch"] +- apiGroups: [""] + resources: + - configmaps + verbs: ["get"] +- apiGroups: + - discovery.k8s.io + resources: + - endpointslices + verbs: ["get", "list", "watch"] +- apiGroups: + - networking.k8s.io + resources: + - ingresses + verbs: ["get", "list", "watch"] +- nonResourceURLs: ["/metrics"] + verbs: ["get"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: opentelemetry-targetallocator-rb-1 + annotations: + meta.helm.sh/release-name: "feast-release" + meta.helm.sh/release-namespace: "feast-val" + labels: + app.kubernetes.io/managed-by: "Helm" +subjects: + - kind: ServiceAccount + name: opentelemetry-targetallocator-sa + namespace: # helm value - {{ .Release.Namespace }} +roleRef: + kind: ClusterRole + name: opentelemetry-targetallocator-role-1 + apiGroup: rbac.authorization.k8s.io \ No newline at end of file diff --git a/infra/charts/feast-feature-server/samples/service-monitor.yaml b/infra/charts/feast-feature-server/samples/service-monitor.yaml new file mode 100644 index 0000000000..b120bde6d5 --- /dev/null +++ b/infra/charts/feast-feature-server/samples/service-monitor.yaml @@ -0,0 +1,16 @@ +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + labels: + app: feast + name: otel-sm +spec: + endpoints: + - port: metrics + namespaceSelector: + matchNames: + - # helm value - {{ .Release.Namespace }} + selector: + matchLabels: + app.kubernetes.io/component: opentelemetry-collector + app.kubernetes.io/managed-by: opentelemetry-operator \ No newline at end of file diff --git a/infra/charts/feast-feature-server/samples/workflow.png b/infra/charts/feast-feature-server/samples/workflow.png new file mode 100644 index 0000000000000000000000000000000000000000..18bc6e959e3ae02a38b49a8a2d77a6e56844477c GIT binary patch literal 183119 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 z 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

ThLp4*Iwn3qwPmR@(iIPksc{&_9K5{r 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 z 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 literal 224458 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

LCw)jZn5lU*q{ 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�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 From e90ac88054e9ff39f7f3f84955d87b66e5fa407e Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Wed, 7 Aug 2024 23:52:03 -0400 Subject: [PATCH 009/185] docs: Adding some content about the push vs pull model (#4385) * docs: Adding some content about the push vs pull model Signed-off-by: Francisco Javier Arceo * checking in progress...all of this was mostly autogenerated Signed-off-by: Francisco Javier Arceo * updated Signed-off-by: Francisco Javier Arceo * Update push-vs-pull-model.md * Update push-vs-pull-model.md * Update push-vs-pull-model.md --------- Signed-off-by: Francisco Javier Arceo --- docs/README.md | 12 ++++++++++- docs/SUMMARY.md | 1 + .../architecture-and-components/README.md | 4 ++++ .../push-vs-pull-model.md | 20 +++++++++++++++++++ 4 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 docs/getting-started/architecture-and-components/push-vs-pull-model.md diff --git a/docs/README.md b/docs/README.md index eea372ded0..d391069429 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,7 +6,7 @@ Feast (**Fea**ture **St**ore) is a customizable operational data system that re- Feast allows ML platform teams to: -* **Make features consistently available for training and serving** by managing an _offline store_ (to process historical data for scale-out batch scoring or model training), a low-latency _online store_ (to power real-time prediction)_,_ and a battle-tested _feature server_ (to serve pre-computed features online). +* **Make features consistently available for training and low-latency serving** by managing an _offline store_ (to process historical data for scale-out batch scoring or model training), a low-latency _online store_ (to power real-time prediction)_,_ and a battle-tested _feature server_ (to serve pre-computed features online). * **Avoid data leakage** by generating point-in-time correct feature sets so data scientists can focus on feature engineering rather than debugging error-prone dataset joining logic. This ensure that future feature values do not leak to models during training. * **Decouple ML from data infrastructure** by providing a single data access layer that abstracts feature storage from feature retrieval, ensuring models remain portable as you move from training models to serving models, from batch models to realtime models, and from one data infra system to another. @@ -16,6 +16,16 @@ Feast allows ML platform teams to: ![](assets/feast_marchitecture.png) +{% hint style="info" %} +**Note:** Feast uses a push model for online serving. This means that the feature store pushes feature values to the +online store, which reduces the latency of feature retrieval. This is more efficient than a pull model, where the model +serving system must make a request to the feature store to retrieve feature values. See +[this document](getting-started/architecture-and-components/push-vs-pull-model.md) for a more detailed discussion. +{% endhint %} + +{% hint style="info" %} +{% endhint %} + ## Who is Feast for? Feast helps ML platform teams with DevOps experience productionize real-time models. Feast can also help these teams build towards a feature platform that improves collaboration between engineers and data scientists. diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 92fa3c692b..87c3626254 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -20,6 +20,7 @@ * [Architecture](getting-started/architecture-and-components/README.md) * [Overview](getting-started/architecture-and-components/overview.md) * [Language](getting-started/architecture-and-components/language.md) + * [Push vs Pull Model](getting-started/architecture-and-components/push-vs-pull-model.md) * [Registry](getting-started/architecture-and-components/registry.md) * [Offline store](getting-started/architecture-and-components/offline-store.md) * [Online store](getting-started/architecture-and-components/online-store.md) diff --git a/docs/getting-started/architecture-and-components/README.md b/docs/getting-started/architecture-and-components/README.md index c364744bc6..050a430c97 100644 --- a/docs/getting-started/architecture-and-components/README.md +++ b/docs/getting-started/architecture-and-components/README.md @@ -8,6 +8,10 @@ [overview.md](overview.md) {% endcontent-ref %} +{% content-ref url="push-vs-pull-model.md" %} +[push-vs-pull-model.md](push-vs-pull-model.md) +{% endcontent-ref %} + {% content-ref url="registry.md" %} [registry.md](registry.md) {% endcontent-ref %} diff --git a/docs/getting-started/architecture-and-components/push-vs-pull-model.md b/docs/getting-started/architecture-and-components/push-vs-pull-model.md new file mode 100644 index 0000000000..a1f404221b --- /dev/null +++ b/docs/getting-started/architecture-and-components/push-vs-pull-model.md @@ -0,0 +1,20 @@ +# Push vs Pull Model + +Feast uses a [Push Model](https://en.wikipedia.org/wiki/Push_technology), i.e., +Data Producers push data to the feature store and Feast stores the feature values +in the online store, to serve features in real-time. + +In a [Pull Model](https://en.wikipedia.org/wiki/Pull_technology), Feast would +pull data from the data producers at request time and store the feature values in +the online store before serving them (storing them would actually be unneccessary). +This approach would incur additional network latency as Feast would need to orchestrate +a request to each data producer, which would mean the latency would be at least as long as +your slowest call. So, in order to serve features as fast as possible, we push data to +Feast and store the feature values in the online store. + +The trade-off with the Push Model is that strong consistency is not gauranteed out +of the box. Instead, stong consistency has to be explicitly designed for in orchestrating +the updates to Feast and the client usage. + +The significant advantage with this approach is that Feast is read-optimized for low-latency +feature retrieval. \ No newline at end of file From 3a32e8ae28110db0934fc26ec6992eb606fed012 Mon Sep 17 00:00:00 2001 From: "bdodla@expedia.com" <13788369+EXPEbdodla@users.noreply.github.com> Date: Thu, 8 Aug 2024 01:03:02 -0700 Subject: [PATCH 010/185] fix: Using get_type_hints instead of inspect signature for udf return annotation (#4391) fix: Using get_type_hints instead of inspect for udf return type Signed-off-by: Bhargav Dodla Co-authored-by: Bhargav Dodla --- sdk/python/feast/on_demand_feature_view.py | 4 +- .../infra/scaffolding/test_repo_operations.py | 56 ++++++++++++++++++- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/sdk/python/feast/on_demand_feature_view.py b/sdk/python/feast/on_demand_feature_view.py index 586f5d1bac..aeb1cc207a 100644 --- a/sdk/python/feast/on_demand_feature_view.py +++ b/sdk/python/feast/on_demand_feature_view.py @@ -3,7 +3,7 @@ import inspect import warnings from types import FunctionType -from typing import Any, Optional, Union +from typing import Any, Optional, Union, get_type_hints import dill import pandas as pd @@ -631,7 +631,7 @@ def mainify(obj) -> None: obj.__module__ = "__main__" def decorator(user_function): - return_annotation = inspect.signature(user_function).return_annotation + return_annotation = get_type_hints(user_function).get("return", inspect._empty) udf_string = dill.source.getsource(user_function) mainify(user_function) if mode == "pandas": diff --git a/sdk/python/tests/unit/infra/scaffolding/test_repo_operations.py b/sdk/python/tests/unit/infra/scaffolding/test_repo_operations.py index aa4ff1c40f..2d4972080a 100644 --- a/sdk/python/tests/unit/infra/scaffolding/test_repo_operations.py +++ b/sdk/python/tests/unit/infra/scaffolding/test_repo_operations.py @@ -1,3 +1,5 @@ +import os +import tempfile from contextlib import contextmanager from pathlib import Path from tempfile import TemporaryDirectory @@ -6,7 +8,13 @@ import assertpy -from feast.repo_operations import get_ignore_files, get_repo_files, read_feastignore +from feast.repo_operations import ( + get_ignore_files, + get_repo_files, + parse_repo, + read_feastignore, +) +from tests.utils.cli_repo_creator import CliRunner @contextmanager @@ -140,3 +148,49 @@ def test_feastignore_with_stars2(): (repo_root / "foo1/c.py").resolve(), ] ) + + +def test_parse_repo(): + "Test to ensure that the repo is parsed correctly" + runner = CliRunner() + with tempfile.TemporaryDirectory(dir=os.getcwd()) as temp_dir: + # Make sure the path is absolute by resolving any symlinks + temp_path = Path(temp_dir).resolve() + result = runner.run(["init", "my_project"], cwd=temp_path) + repo_path = Path(temp_path / "my_project" / "feature_repo") + assert result.returncode == 0 + + repo_contents = parse_repo(repo_path) + + assert len(repo_contents.data_sources) == 3 + assert len(repo_contents.feature_views) == 2 + assert len(repo_contents.on_demand_feature_views) == 2 + assert len(repo_contents.stream_feature_views) == 0 + assert len(repo_contents.entities) == 2 + assert len(repo_contents.feature_services) == 3 + + +def test_parse_repo_with_future_annotations(): + "Test to ensure that the repo is parsed correctly when using future annotations" + runner = CliRunner() + with tempfile.TemporaryDirectory(dir=os.getcwd()) as temp_dir: + # Make sure the path is absolute by resolving any symlinks + temp_path = Path(temp_dir).resolve() + result = runner.run(["init", "my_project"], cwd=temp_path) + repo_path = Path(temp_path / "my_project" / "feature_repo") + assert result.returncode == 0 + + with open(repo_path / "example_repo.py", "r") as f: + existing_content = f.read() + + with open(repo_path / "example_repo.py", "w") as f: + f.write("from __future__ import annotations" + "\n" + existing_content) + + repo_contents = parse_repo(repo_path) + + assert len(repo_contents.data_sources) == 3 + assert len(repo_contents.feature_views) == 2 + assert len(repo_contents.on_demand_feature_views) == 2 + assert len(repo_contents.stream_feature_views) == 0 + assert len(repo_contents.entities) == 2 + assert len(repo_contents.feature_services) == 3 From 419ca5e9523ff38f27141b79ae12ebb0646c6617 Mon Sep 17 00:00:00 2001 From: Job Almekinders <55230856+job-almekinders@users.noreply.github.com> Date: Thu, 8 Aug 2024 16:51:49 +0200 Subject: [PATCH 011/185] fix: Escape special characters in the Postgres password (#4394) * Apply fix Signed-off-by: Job Almekinders * Add special characters to postgres online store test Signed-off-by: Job Almekinders * Fix linting error Signed-off-by: Job Almekinders --------- Signed-off-by: Job Almekinders --- .../infra/utils/postgres/connection_utils.py | 16 +++++++++------- .../universal/online_store/postgres.py | 8 ++++---- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/sdk/python/feast/infra/utils/postgres/connection_utils.py b/sdk/python/feast/infra/utils/postgres/connection_utils.py index 7b37ea981f..3749fc2fc1 100644 --- a/sdk/python/feast/infra/utils/postgres/connection_utils.py +++ b/sdk/python/feast/infra/utils/postgres/connection_utils.py @@ -5,6 +5,7 @@ import psycopg import pyarrow as pa from psycopg import AsyncConnection, Connection +from psycopg.conninfo import make_conninfo from psycopg_pool import AsyncConnectionPool, ConnectionPool from feast.infra.utils.postgres.postgres_config import PostgreSQLConfig @@ -55,13 +56,14 @@ async def _get_connection_pool_async(config: PostgreSQLConfig) -> AsyncConnectio def _get_conninfo(config: PostgreSQLConfig) -> str: """Get the `conninfo` argument required for connection objects.""" - return ( - f"postgresql://{config.user}" - f":{config.password}" - f"@{config.host}" - f":{int(config.port)}" - f"/{config.database}" - ) + psycopg_config = { + "user": config.user, + "password": config.password, + "host": config.host, + "port": int(config.port), + "dbname": config.database, + } + return make_conninfo(conninfo="", **psycopg_config) def _get_conn_kwargs(config: PostgreSQLConfig) -> Dict[str, Any]: diff --git a/sdk/python/tests/integration/feature_repos/universal/online_store/postgres.py b/sdk/python/tests/integration/feature_repos/universal/online_store/postgres.py index e409862641..622ee99e14 100644 --- a/sdk/python/tests/integration/feature_repos/universal/online_store/postgres.py +++ b/sdk/python/tests/integration/feature_repos/universal/online_store/postgres.py @@ -16,7 +16,7 @@ def __init__(self, project_name: str, **kwargs): self.container = PostgresContainer( "postgres:16", username="root", - password="test", + password="test!@#$%", dbname="test", ).with_exposed_ports(5432) @@ -26,7 +26,7 @@ def create_online_store(self) -> Dict[str, str]: "host": "localhost", "type": "postgres", "user": "root", - "password": "test", + "password": "test!@#$%", "database": "test", "port": self.container.get_exposed_port(5432), } @@ -42,7 +42,7 @@ def __init__(self, project_name: str, **kwargs): self.container = ( DockerContainer("pgvector/pgvector:pg16") .with_env("POSTGRES_USER", "root") - .with_env("POSTGRES_PASSWORD", "test") + .with_env("POSTGRES_PASSWORD", "test!@#$%") .with_env("POSTGRES_DB", "test") .with_exposed_ports(5432) .with_volume_mapping( @@ -65,7 +65,7 @@ def create_online_store(self) -> Dict[str, Any]: "host": "localhost", "type": "postgres", "user": "root", - "password": "test", + "password": "test!@#$%", "database": "test", "pgvector_enabled": True, "vector_len": 2, From 5215a2139a9d824dc2d8f45181bd177a1e8e9561 Mon Sep 17 00:00:00 2001 From: Artur Kolakowski <36094018+arturkolakowski@users.noreply.github.com> Date: Tue, 13 Aug 2024 05:41:46 -0400 Subject: [PATCH 012/185] feat: Allow feast snowflake to read in byte string for private-key authentication (#4384) * allow feast snowflake to read in byte string for private-key authentication Signed-off-by: Artur * Update type hint for to use Union instead of | syntax Signed-off-by: Artur * Update type hint for private_key to use Union instead of | syntax Signed-off-by: Artur * Update type hint in parse_private_key_path Signed-off-by: Artur * added private_key_content in Snowflake configs to support key-pair auth by reading in byte string Signed-off-by: Artur * fix incompatible linting types Signed-off-by: Artur * remove unused Union import Signed-off-by: Artur * fix formating Signed-off-by: Artur --------- Signed-off-by: Artur Co-authored-by: Artur --- .../infra/materialization/snowflake_engine.py | 3 +++ .../feast/infra/offline_stores/snowflake.py | 3 +++ .../feast/infra/online_stores/snowflake.py | 3 +++ sdk/python/feast/infra/registry/snowflake.py | 3 +++ .../infra/utils/snowflake/snowflake_utils.py | 26 +++++++++++++++---- 5 files changed, 33 insertions(+), 5 deletions(-) diff --git a/sdk/python/feast/infra/materialization/snowflake_engine.py b/sdk/python/feast/infra/materialization/snowflake_engine.py index f77239398e..5d0f08c2f5 100644 --- a/sdk/python/feast/infra/materialization/snowflake_engine.py +++ b/sdk/python/feast/infra/materialization/snowflake_engine.py @@ -70,6 +70,9 @@ class SnowflakeMaterializationEngineConfig(FeastConfigBaseModel): private_key: Optional[str] = None """ Snowflake private key file path""" + private_key_content: Optional[bytes] = None + """ Snowflake private key stored as bytes""" + private_key_passphrase: Optional[str] = None """ Snowflake private key file passphrase""" diff --git a/sdk/python/feast/infra/offline_stores/snowflake.py b/sdk/python/feast/infra/offline_stores/snowflake.py index 96552ff87e..ada6c99c98 100644 --- a/sdk/python/feast/infra/offline_stores/snowflake.py +++ b/sdk/python/feast/infra/offline_stores/snowflake.py @@ -107,6 +107,9 @@ class SnowflakeOfflineStoreConfig(FeastConfigBaseModel): private_key: Optional[str] = None """ Snowflake private key file path""" + private_key_content: Optional[bytes] = None + """ Snowflake private key stored as bytes""" + private_key_passphrase: Optional[str] = None """ Snowflake private key file passphrase""" diff --git a/sdk/python/feast/infra/online_stores/snowflake.py b/sdk/python/feast/infra/online_stores/snowflake.py index fef804a377..6f39bdd0f6 100644 --- a/sdk/python/feast/infra/online_stores/snowflake.py +++ b/sdk/python/feast/infra/online_stores/snowflake.py @@ -53,6 +53,9 @@ class SnowflakeOnlineStoreConfig(FeastConfigBaseModel): private_key: Optional[str] = None """ Snowflake private key file path""" + private_key_content: Optional[bytes] = None + """ Snowflake private key stored as bytes""" + private_key_passphrase: Optional[str] = None """ Snowflake private key file passphrase""" diff --git a/sdk/python/feast/infra/registry/snowflake.py b/sdk/python/feast/infra/registry/snowflake.py index f2bc09e7e4..ac4f52dc06 100644 --- a/sdk/python/feast/infra/registry/snowflake.py +++ b/sdk/python/feast/infra/registry/snowflake.py @@ -96,6 +96,9 @@ class SnowflakeRegistryConfig(RegistryConfig): private_key: Optional[str] = None """ Snowflake private key file path""" + private_key_content: Optional[bytes] = None + """ Snowflake private key stored as bytes""" + private_key_passphrase: Optional[str] = None """ Snowflake private key file passphrase""" diff --git a/sdk/python/feast/infra/utils/snowflake/snowflake_utils.py b/sdk/python/feast/infra/utils/snowflake/snowflake_utils.py index dd965c4bed..b9035b40db 100644 --- a/sdk/python/feast/infra/utils/snowflake/snowflake_utils.py +++ b/sdk/python/feast/infra/utils/snowflake/snowflake_utils.py @@ -84,9 +84,11 @@ def __enter__(self): # https://docs.snowflake.com/en/user-guide/python-connector-example.html#using-key-pair-authentication-key-pair-rotation # https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication - if "private_key" in kwargs: + if "private_key" in kwargs or "private_key_content" in kwargs: kwargs["private_key"] = parse_private_key_path( - kwargs["private_key"], kwargs["private_key_passphrase"] + kwargs.get("private_key_passphrase"), + kwargs.get("private_key"), + kwargs.get("private_key_content"), ) try: @@ -510,13 +512,27 @@ def chunk_helper(lst: pd.DataFrame, n: int) -> Iterator[Tuple[int, pd.DataFrame] yield int(i / n), lst[i : i + n] -def parse_private_key_path(key_path: str, private_key_passphrase: str) -> bytes: - with open(key_path, "rb") as key: +def parse_private_key_path( + private_key_passphrase: str, + key_path: Optional[str] = None, + private_key_content: Optional[bytes] = None, +) -> bytes: + """Returns snowflake pkb by parsing and reading either from key path or private_key_content as byte string.""" + if private_key_content: p_key = serialization.load_pem_private_key( - key.read(), + private_key_content, password=private_key_passphrase.encode(), backend=default_backend(), ) + elif key_path: + with open(key_path, "rb") as key: + p_key = serialization.load_pem_private_key( + key.read(), + password=private_key_passphrase.encode(), + backend=default_backend(), + ) + else: + raise ValueError("Please provide key_path or private_key_content.") pkb = p_key.private_bytes( encoding=serialization.Encoding.DER, From a8a98c7401ac06212783868adc5e702d6667bc82 Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Wed, 14 Aug 2024 10:02:00 -0400 Subject: [PATCH 013/185] Update pull_request_template.md --- .github/pull_request_template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index b7d630e8bc..7849c24976 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -5,7 +5,7 @@ 3. If your change introduces any API changes, make sure to update the integration tests here: https://github.com/feast-dev/feast/tree/master/sdk/python/tests 4. Make sure documentation is updated for your PR! 5. Make sure your commits are signed: https://github.com/feast-dev/feast/blob/master/CONTRIBUTING.md#signing-off-commits -6. Make sure your PR title follows conventional commits (e.g. fix: [description] vs feat: [description]) +6. Make sure your PR title follows conventional commits (e.g. fix: [Description of ...], feat: [Description of ...], chore: [Description of ...], refactor: [Description of ...]) --> From 409e6f62943ee69a13087618b4592e35c799e34d Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Wed, 14 Aug 2024 14:17:31 -0400 Subject: [PATCH 014/185] chore: Update Feast documentation to add information about write patterns and feature transformations (#4400) * merging changes Signed-off-by: Francisco Javier Arceo * merging Signed-off-by: Francisco Javier Arceo * updated Signed-off-by: Francisco Javier Arceo * Update write-patterns.md Signed-off-by: Francisco Javier Arceo * Update write-patterns.md Signed-off-by: Francisco Javier Arceo * Update write-patterns.md Signed-off-by: Francisco Javier Arceo * Update write-patterns.md Adding some clarity. Signed-off-by: Francisco Javier Arceo * Update write-patterns.md Signed-off-by: Francisco Javier Arceo * chore: Update feature-transformetion.md (#4405) Signed-off-by: Francisco Javier Arceo * updated readme Signed-off-by: Francisco Javier Arceo * updated summary and readme Signed-off-by: Francisco Javier Arceo * updated docs Signed-off-by: Francisco Javier Arceo * Updated readme Signed-off-by: Francisco Javier Arceo * updated more Signed-off-by: Francisco Javier Arceo * updated transformation Signed-off-by: Francisco Javier Arceo * updated urls Signed-off-by: Francisco Javier Arceo * refactoring and renaming architecture-and-components to components and architecture Signed-off-by: Francisco Javier Arceo * updated urls Signed-off-by: Francisco Javier Arceo * moving files from architecture-and-components to components/ Signed-off-by: Francisco Javier Arceo * had a typo in components Signed-off-by: Francisco Javier Arceo * Cleaned everything up Signed-off-by: Francisco Javier Arceo --------- Signed-off-by: Francisco Javier Arceo --- docs/README.md | 32 ++++----- docs/SUMMARY.md | 22 +++--- docs/getting-started/architecture/README.md | 21 ++++++ .../architecture/feature-transformation.md | 20 ++++++ .../language.md | 0 docs/getting-started/architecture/overview.md | 18 +++++ .../push-vs-pull-model.md | 16 +++-- .../architecture/write-patterns.md | 67 +++++++++++++++++++ .../README.md | 14 +--- .../batch-materialization-engine.md | 0 .../offline-store.md | 0 .../online-store.md | 0 .../overview.md | 8 --- .../provider.md | 0 .../registry.md | 0 .../stream-processor.md | 0 docs/getting-started/concepts/dataset.md | 2 +- docs/getting-started/faq.md | 2 +- docs/getting-started/quickstart.md | 2 +- docs/how-to-guides/scaling-feast.md | 2 +- .../reference/batch-materialization/README.md | 2 +- docs/reference/codebase-structure.md | 2 +- .../feature-servers/python-feature-server.md | 2 +- docs/reference/offline-stores/README.md | 2 +- docs/reference/online-stores/README.md | 2 +- docs/reference/providers/README.md | 2 +- sdk/python/feast/templates/gcp/README.md | 2 +- sdk/python/feast/templates/local/README.md | 2 +- 28 files changed, 181 insertions(+), 61 deletions(-) create mode 100644 docs/getting-started/architecture/README.md create mode 100644 docs/getting-started/architecture/feature-transformation.md rename docs/getting-started/{architecture-and-components => architecture}/language.md (100%) create mode 100644 docs/getting-started/architecture/overview.md rename docs/getting-started/{architecture-and-components => architecture}/push-vs-pull-model.md (66%) create mode 100644 docs/getting-started/architecture/write-patterns.md rename docs/getting-started/{architecture-and-components => components}/README.md (63%) rename docs/getting-started/{architecture-and-components => components}/batch-materialization-engine.md (100%) rename docs/getting-started/{architecture-and-components => components}/offline-store.md (100%) rename docs/getting-started/{architecture-and-components => components}/online-store.md (100%) rename docs/getting-started/{architecture-and-components => components}/overview.md (85%) rename docs/getting-started/{architecture-and-components => components}/provider.md (100%) rename docs/getting-started/{architecture-and-components => components}/registry.md (100%) rename docs/getting-started/{architecture-and-components => components}/stream-processor.md (100%) diff --git a/docs/README.md b/docs/README.md index d391069429..6652eaddc8 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,7 +2,16 @@ ## What is Feast? -Feast (**Fea**ture **St**ore) is a customizable operational data system that re-uses existing infrastructure to manage and serve machine learning features to realtime models. +Feast (**Fea**ture **St**ore) is an [open-source](https://github.com/feast-dev/feast) feature store that helps teams +operate production ML systems at scale by allowing them to define, manage, validate, and serve features for production +AI/ML. + +Feast's feature store is composed of two foundational components: (1) an [offline store](getting-started/components/offline-store.md) +for historical feature extraction used in model training and an (2) [online store](getting-started/components/online-store.md) +for serving features at low-latency in production systems and applications. + +Feast is a configurable operational data system that re-uses existing infrastructure to manage and serve machine learning +features to realtime models. For more details please review our [architecture](getting-started/architecture/overview.md). Feast allows ML platform teams to: @@ -20,38 +29,31 @@ Feast allows ML platform teams to: **Note:** Feast uses a push model for online serving. This means that the feature store pushes feature values to the online store, which reduces the latency of feature retrieval. This is more efficient than a pull model, where the model serving system must make a request to the feature store to retrieve feature values. See -[this document](getting-started/architecture-and-components/push-vs-pull-model.md) for a more detailed discussion. -{% endhint %} - -{% hint style="info" %} +[this document](getting-started/architecture/push-vs-pull-model.md) for a more detailed discussion. {% endhint %} ## Who is Feast for? -Feast helps ML platform teams with DevOps experience productionize real-time models. Feast can also help these teams build towards a feature platform that improves collaboration between engineers and data scientists. +Feast helps ML platform/MLOps teams with DevOps experience productionize real-time models. Feast also helps these teams +build a feature platform that improves collaboration between data engineers, software engineers, machine learning +engineers, and data scientists. Feast is likely **not** the right tool if you - * are in an organization that’s just getting started with ML and is not yet sure what the business impact of ML is -* rely primarily on unstructured data -* need very low latency feature retrieval (e.g. p99 feature retrieval << 10ms) -* have a small team to support a large number of use cases ## What Feast is not? ### Feast is not -* **an** [**ETL**](https://en.wikipedia.org/wiki/Extract,\_transform,\_load) / [**ELT**](https://en.wikipedia.org/wiki/Extract,\_load,\_transform) **system:** Feast is not (and does not plan to become) a general purpose data transformation or pipelining system. Users often leverage tools like [dbt](https://www.getdbt.com) to manage upstream data transformations. +* **an** [**ETL**](https://en.wikipedia.org/wiki/Extract,\_transform,\_load) / [**ELT**](https://en.wikipedia.org/wiki/Extract,\_load,\_transform) **system.** Feast is not a general purpose data pipelining system. Users often leverage tools like [dbt](https://www.getdbt.com) to manage upstream data transformations. Feast does support some [transformations](getting-started/architecture/feature-transformetion.md). * **a data orchestration tool:** Feast does not manage or orchestrate complex workflow DAGs. It relies on upstream data pipelines to produce feature values and integrations with tools like [Airflow](https://airflow.apache.org) to make features consistently available. * **a data warehouse:** Feast is not a replacement for your data warehouse or the source of truth for all transformed data in your organization. Rather, Feast is a light-weight downstream layer that can serve data from an existing data warehouse (or other data sources) to models in production. * **a database:** Feast is not a database, but helps manage data stored in other systems (e.g. BigQuery, Snowflake, DynamoDB, Redis) to make features consistently available at training / serving time ### Feast does not _fully_ solve - * **reproducible model training / model backtesting / experiment management**: Feast captures feature and model metadata, but does not version-control datasets / labels or manage train / test splits. Other tools like [DVC](https://dvc.org/), [MLflow](https://www.mlflow.org/), and [Kubeflow](https://www.kubeflow.org/) are better suited for this. -* **batch + streaming feature engineering**: Feast primarily processes already transformed feature values but is investing in supporting batch and streaming transformations. +* **batch feature engineering**: Feast supports on demand and streaming transformations. Feast is also investing in supporting batch transformations. * **native streaming feature integration:** Feast enables users to push streaming features, but does not pull from streaming sources or manage streaming pipelines. -* **feature sharing**: Feast has experimental functionality to enable discovery and cataloguing of feature metadata with a [Feast web UI (alpha)](https://docs.feast.dev/reference/alpha-web-ui). Feast also has community contributed plugins with [DataHub](https://datahubproject.io/docs/generated/ingestion/sources/feast/) and [Amundsen](https://github.com/amundsen-io/amundsen/blob/4a9d60176767c4d68d1cad5b093320ea22e26a49/databuilder/databuilder/extractor/feast\_extractor.py). * **lineage:** Feast helps tie feature values to model versions, but is not a complete solution for capturing end-to-end lineage from raw data sources to model versions. Feast also has community contributed plugins with [DataHub](https://datahubproject.io/docs/generated/ingestion/sources/feast/) and [Amundsen](https://github.com/amundsen-io/amundsen/blob/4a9d60176767c4d68d1cad5b093320ea22e26a49/databuilder/databuilder/extractor/feast\_extractor.py). * **data quality / drift detection**: Feast has experimental integrations with [Great Expectations](https://greatexpectations.io/), but is not purpose built to solve data drift / data quality issues. This requires more sophisticated monitoring across data pipelines, served feature values, labels, and model versions. @@ -74,7 +76,7 @@ Explore the following resources to get started with Feast: * [Quickstart](getting-started/quickstart.md) is the fastest way to get started with Feast * [Concepts](getting-started/concepts/) describes all important Feast API concepts -* [Architecture](getting-started/architecture-and-components/) describes Feast's overall architecture. +* [Architecture](getting-started/architecture/) describes Feast's overall architecture. * [Tutorials](tutorials/tutorials-overview/) shows full examples of using Feast in machine learning applications. * [Running Feast with Snowflake/GCP/AWS](how-to-guides/feast-snowflake-gcp-aws/) provides a more in-depth guide to using Feast. * [Reference](reference/feast-cli-commands.md) contains detailed API and design documents. diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 87c3626254..a6a40fc91d 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -17,15 +17,19 @@ * [Point-in-time joins](getting-started/concepts/point-in-time-joins.md) * [Registry](getting-started/concepts/registry.md) * [\[Alpha\] Saved dataset](getting-started/concepts/dataset.md) -* [Architecture](getting-started/architecture-and-components/README.md) - * [Overview](getting-started/architecture-and-components/overview.md) - * [Language](getting-started/architecture-and-components/language.md) - * [Push vs Pull Model](getting-started/architecture-and-components/push-vs-pull-model.md) - * [Registry](getting-started/architecture-and-components/registry.md) - * [Offline store](getting-started/architecture-and-components/offline-store.md) - * [Online store](getting-started/architecture-and-components/online-store.md) - * [Batch Materialization Engine](getting-started/architecture-and-components/batch-materialization-engine.md) - * [Provider](getting-started/architecture-and-components/provider.md) +* [Architecture](getting-started/architecture/README.md) + * [Overview](getting-started/architecture/overview.md) + * [Language](getting-started/architecture/language.md) + * [Push vs Pull Model](getting-started/architecture/push-vs-pull-model.md) + * [Write Patterns](getting-started/architecture/write-patterns.md) + * [Feature Transformation](getting-started/architecture/feature-transformation.md) +* [Components](getting-started/components/README.md) + * [Overview](getting-started/components/overview.md) + * [Registry](getting-started/components/registry.md) + * [Offline store](getting-started/components/offline-store.md) + * [Online store](getting-started/components/online-store.md) + * [Batch Materialization Engine](getting-started/components/batch-materialization-engine.md) + * [Provider](getting-started/components/provider.md) * [Third party integrations](getting-started/third-party-integrations.md) * [FAQ](getting-started/faq.md) diff --git a/docs/getting-started/architecture/README.md b/docs/getting-started/architecture/README.md new file mode 100644 index 0000000000..a45f4ed6ec --- /dev/null +++ b/docs/getting-started/architecture/README.md @@ -0,0 +1,21 @@ +# Architecture + +{% content-ref url="overview.md" %} +[overview.md](overview.md) +{% endcontent-ref %} + +{% content-ref url="language.md" %} +[language.md](language.md) +{% endcontent-ref %} + +{% content-ref url="push-vs-pull-model.md" %} +[push-vs-pull-model.md](push-vs-pull-model.md) +{% endcontent-ref %} + +{% content-ref url="write-patterns.md" %} +[write-patterns.md](write-patterns.md) +{% endcontent-ref %} + +{% content-ref url="feature-transformation-model.md" %} +[feature-transformation.md](feature-transformation.md) +{% endcontent-ref %} diff --git a/docs/getting-started/architecture/feature-transformation.md b/docs/getting-started/architecture/feature-transformation.md new file mode 100644 index 0000000000..457e71d85e --- /dev/null +++ b/docs/getting-started/architecture/feature-transformation.md @@ -0,0 +1,20 @@ +# Feature Transformation + +A *feature transformation* is a function that takes some set of input data and +returns some set of output data. Feature transformations can happen on either raw data or derived data. + +Feature transformations can be executed by three types of "transformation engines": + +1. The Feast Feature Server +2. An Offline Store (e.g., Snowflake, BigQuery, DuckDB, Spark, etc.) +3. A Stream processor (e.g., Flink or Spark Streaming) + +The three transformation engines are coupled with the [communication pattern used for writes](write-patterns.md). + +Importantly, this implies that different feature transformation code may be +used under different transformation engines, so understanding the tradeoffs of +when to use which transformation engine/communication pattern is extremely critical to +the success of your implementation. + +In general, we recommend transformation engines and network calls to be chosen by aligning it with what is most +appropriate for the data producer, feature/model usage, and overall product. \ No newline at end of file diff --git a/docs/getting-started/architecture-and-components/language.md b/docs/getting-started/architecture/language.md similarity index 100% rename from docs/getting-started/architecture-and-components/language.md rename to docs/getting-started/architecture/language.md diff --git a/docs/getting-started/architecture/overview.md b/docs/getting-started/architecture/overview.md new file mode 100644 index 0000000000..7d1180bfd1 --- /dev/null +++ b/docs/getting-started/architecture/overview.md @@ -0,0 +1,18 @@ +# Overview + +![Feast Architecture Diagram](<../../assets/feast_marchitecture.png>) + +Feast's architecture is designed to be flexible and scalable. It is composed of several components that work together to provide a feature store that can be used to serve features for training and inference. + +* Feast uses a [Push Model](push-vs-pull-model.md) to ingest data from different sources and store feature values in the +online store. +This allows Feast to serve features in real-time with low latency. + +* Feast supports On Demand and Streaming Transformations for [feature computation](feature-transformation.md) and + will support Batch transformations in the future. For Streaming and Batch, Feast requires a separate Feature Transformation + Engine (in the batch case, this is typically your Offline Store). We are exploring adding a default streaming engine to Feast. + +* Domain expertise is recommended when integrating a data source with Feast understand the [tradeoffs from different + write patterns](write-patterns.md) to your application + +* We recommend [using Python](language.md) for your Feature Store microservice. As mentioned in the document, precomputing features is the recommended optimal path to ensure low latency performance. Reducing feature serving to a lightweight database lookup is the ideal pattern, which means the marginal overhead of Python should be tolerable. Because of this we believe the pros of Python outweigh the costs, as reimplementing feature logic is undesirable. Java and Go Clients are also available for online feature retrieval. diff --git a/docs/getting-started/architecture-and-components/push-vs-pull-model.md b/docs/getting-started/architecture/push-vs-pull-model.md similarity index 66% rename from docs/getting-started/architecture-and-components/push-vs-pull-model.md rename to docs/getting-started/architecture/push-vs-pull-model.md index a1f404221b..b205e97fc5 100644 --- a/docs/getting-started/architecture-and-components/push-vs-pull-model.md +++ b/docs/getting-started/architecture/push-vs-pull-model.md @@ -6,15 +6,23 @@ in the online store, to serve features in real-time. In a [Pull Model](https://en.wikipedia.org/wiki/Pull_technology), Feast would pull data from the data producers at request time and store the feature values in -the online store before serving them (storing them would actually be unneccessary). +the online store before serving them (storing them would actually be unnecessary). This approach would incur additional network latency as Feast would need to orchestrate a request to each data producer, which would mean the latency would be at least as long as your slowest call. So, in order to serve features as fast as possible, we push data to Feast and store the feature values in the online store. -The trade-off with the Push Model is that strong consistency is not gauranteed out -of the box. Instead, stong consistency has to be explicitly designed for in orchestrating +The trade-off with the Push Model is that strong consistency is not guaranteed out +of the box. Instead, strong consistency has to be explicitly designed for in orchestrating the updates to Feast and the client usage. The significant advantage with this approach is that Feast is read-optimized for low-latency -feature retrieval. \ No newline at end of file +feature retrieval. + +# How to Push + +Implicit in the Push model are decisions about _how_ and _when_ to push feature values to the online store. + +From a developer's perspective, there are three ways to push feature values to the online store with different tradeoffs. + +They are discussed further in the [Write Patterns](getting-started/architecture/write-patterns.md) section. diff --git a/docs/getting-started/architecture/write-patterns.md b/docs/getting-started/architecture/write-patterns.md new file mode 100644 index 0000000000..4674b5504d --- /dev/null +++ b/docs/getting-started/architecture/write-patterns.md @@ -0,0 +1,67 @@ +# Writing Data to Feast + +Feast uses a [Push Model](getting-started/architecture/push-vs-pull-model.md) to push features to the online store. + +This has two important consequences: (1) communication patterns between the Data Producer (i.e., the client) and Feast (i.e,. the server) and (2) feature computation and +_feature value_ write patterns to Feast's online store. + +Data Producers (i.e., services that generate data) send data to Feast so that Feast can write feature values to the online store. That data can +be either raw data where Feast computes and stores the feature values or precomputed feature values. + +## Communication Patterns + +There are two ways a client (or Data Producer) can *_send_* data to the online store: + +1. Synchronously + - Using a synchronous API call for a small number of entities or a single entity (e.g., using the [`push` or `write_to_online_store` methods](../../reference/data-sources/push.md#pushing-data)) or the Feature Server's [`push` endpoint](../../reference/feature-servers/python-feature-server.md#pushing-features-to-the-online-and-offline-stores)) +2. Asynchronously + - Using an asynchronous API call for a small number of entities or a single entity (e.g., using the [`push` or `write_to_online_store` methods](../../reference/data-sources/push.md#pushing-data)) or the Feature Server's [`push` endpoint](../../reference/feature-servers/python-feature-server.md#pushing-features-to-the-online-and-offline-stores)) + - Using a "batch job" for a large number of entities (e.g., using a [batch materialization engine](../components/batch-materialization-engine.md)) + +Note, in some contexts, developers may "batch" a group of entities together and write them to the online store in a +single API call. This is a common pattern when writing data to the online store to reduce write loads but we would +not qualify this as a batch job. + +## Feature Value Write Patterns + +Writing feature values to the online store (i.e., the server) can be done in two ways: Precomputing the transformations client-side or Computing the transformations On Demand server-side. + +### Combining Approaches + +In some scenarios, a combination of Precomputed and On Demand transformations may be optimal. + +When selecting feature value write patterns, one must consider the specific requirements of your application, the acceptable correctness of the data, the latency tolerance, and the computational resources available. Making deliberate choices can help the performance and reliability of your service. + +There are two ways the client can write *feature values* to the online store: + +1. Precomputing transformations +2. Computing transformations On Demand +3. Hybrid (Precomputed + On Demand) + +### 1. Precomputing Transformations +Precomputed transformations can happen outside of Feast (e.g., via some batch job or streaming application) or inside of the Feast feature server when writing to the online store via the `push` or `write-to-online-store` api. + +### 2. Computing Transformations On Demand +On Demand transformations can only happen inside of Feast at either (1) the time of the client's request or (2) when the data producer writes to the online store. + +### 3. Hybrid (Precomputed + On Demand) +The hybrid approach allows for precomputed transformations to happen inside or outside of Feast and have the On Demand transformations happen at client request time. This is particularly convenient for "Time Since Last" types of features (e.g., time since purchase). + +## Tradeoffs + +When deciding between synchronous and asynchronous data writes, several tradeoffs should be considered: + +- **Data Consistency**: Asynchronous writes allow Data Producers to send data without waiting for the write operation to complete, which can lead to situations where the data in the online store is stale. This might be acceptable in scenarios where absolute freshness is not critical. However, for critical operations, such as calculating loan amounts in financial applications, stale data can lead to incorrect decisions, making synchronous writes essential. +- **Correctness**: The risk of data being out-of-date must be weighed against the operational requirements. For instance, in a lending application, having up-to-date feature data can be crucial for correctness (depending upon the features and raw data), thus favoring synchronous writes. In less sensitive contexts, the eventual consistency offered by asynchronous writes might be sufficient. +- **Service Coupling**: Synchronous writes result in tighter coupling between services. If a write operation fails, it can cause the dependent service operation to fail as well, which might be a significant drawback in systems requiring high reliability and independence between services. +- **Application Latency**: Asynchronous writes typically reduce the perceived latency from the client's perspective because the client does not wait for the write operation to complete. This can enhance the user experience and efficiency in environments where operations are not critically dependent on immediate data freshness. + +The table below can help guide the most appropriate data write and feature computation strategies based on specific application needs and data sensitivity. + +| Data Write Type | Feature Computation | Scenario | Recommended Approach | +|----------|-----------------|---------------------|----------------------| +| Asynchronous | On Demand | Data-intensive applications tolerant to staleness | Opt for asynchronous writes with on-demand computation to balance load and manage resource usage efficiently. | +| Asynchronous | Precomputed | High volume, non-critical data processing | Use asynchronous batch jobs with precomputed transformations for efficiency and scalability. | +| Synchronous | On Demand | High-stakes decision making | Use synchronous writes with on-demand feature computation to ensure data freshness and correctness. | +| Synchronous | Precomputed | User-facing applications requiring quick feedback | Use synchronous writes with precomputed features to reduce latency and improve user experience. | +| Synchronous | Hybrid (Precomputed + On Demand) | High-stakes decision making that want to optimize for latency under constraints| Use synchronous writes with precomputed features where possible and a select set of on demand computations to reduce latency and improve user experience. | diff --git a/docs/getting-started/architecture-and-components/README.md b/docs/getting-started/components/README.md similarity index 63% rename from docs/getting-started/architecture-and-components/README.md rename to docs/getting-started/components/README.md index 050a430c97..d468714bd4 100644 --- a/docs/getting-started/architecture-and-components/README.md +++ b/docs/getting-started/components/README.md @@ -1,16 +1,4 @@ -# Architecture - -{% content-ref url="language.md" %} -[language.md](language.md) -{% endcontent-ref %} - -{% content-ref url="overview.md" %} -[overview.md](overview.md) -{% endcontent-ref %} - -{% content-ref url="push-vs-pull-model.md" %} -[push-vs-pull-model.md](push-vs-pull-model.md) -{% endcontent-ref %} +# Components {% content-ref url="registry.md" %} [registry.md](registry.md) diff --git a/docs/getting-started/architecture-and-components/batch-materialization-engine.md b/docs/getting-started/components/batch-materialization-engine.md similarity index 100% rename from docs/getting-started/architecture-and-components/batch-materialization-engine.md rename to docs/getting-started/components/batch-materialization-engine.md diff --git a/docs/getting-started/architecture-and-components/offline-store.md b/docs/getting-started/components/offline-store.md similarity index 100% rename from docs/getting-started/architecture-and-components/offline-store.md rename to docs/getting-started/components/offline-store.md diff --git a/docs/getting-started/architecture-and-components/online-store.md b/docs/getting-started/components/online-store.md similarity index 100% rename from docs/getting-started/architecture-and-components/online-store.md rename to docs/getting-started/components/online-store.md diff --git a/docs/getting-started/architecture-and-components/overview.md b/docs/getting-started/components/overview.md similarity index 85% rename from docs/getting-started/architecture-and-components/overview.md rename to docs/getting-started/components/overview.md index f4d543cd5a..393f436e5b 100644 --- a/docs/getting-started/architecture-and-components/overview.md +++ b/docs/getting-started/components/overview.md @@ -28,11 +28,3 @@ A complete Feast deployment contains the following components: * **Batch Materialization Engine:** The [Batch Materialization Engine](batch-materialization-engine.md) component launches a process which loads data into the online store from the offline store. By default, Feast uses a local in-process engine implementation to materialize data. However, additional infrastructure can be used for a more scalable materialization process. * **Online Store:** The online store is a database that stores only the latest feature values for each entity. The online store is either populated through materialization jobs or through [stream ingestion](../../reference/data-sources/push.md). * **Offline Store:** The offline store persists batch data that has been ingested into Feast. This data is used for producing training datasets. For feature retrieval and materialization, Feast does not manage the offline store directly, but runs queries against it. However, offline stores can be configured to support writes if Feast configures logging functionality of served features. - -{% hint style="info" %} -Java and Go Clients are also available for online feature retrieval. - -In general, we recommend [using Python](language.md) for your Feature Store microservice. - -As mentioned in the document, precomputing features is the recommended optimal path to ensure low latency performance. Reducing feature serving to a lightweight database lookup is the ideal pattern, which means the marginal overhead of Python should be tolerable. Because of this we believe the pros of Python outweigh the costs, as reimplementing feature logic is undesirable. -{% endhint %} diff --git a/docs/getting-started/architecture-and-components/provider.md b/docs/getting-started/components/provider.md similarity index 100% rename from docs/getting-started/architecture-and-components/provider.md rename to docs/getting-started/components/provider.md diff --git a/docs/getting-started/architecture-and-components/registry.md b/docs/getting-started/components/registry.md similarity index 100% rename from docs/getting-started/architecture-and-components/registry.md rename to docs/getting-started/components/registry.md diff --git a/docs/getting-started/architecture-and-components/stream-processor.md b/docs/getting-started/components/stream-processor.md similarity index 100% rename from docs/getting-started/architecture-and-components/stream-processor.md rename to docs/getting-started/components/stream-processor.md diff --git a/docs/getting-started/concepts/dataset.md b/docs/getting-started/concepts/dataset.md index d55adb4703..829ad4284e 100644 --- a/docs/getting-started/concepts/dataset.md +++ b/docs/getting-started/concepts/dataset.md @@ -2,7 +2,7 @@ Feast datasets allow for conveniently saving dataframes that include both features and entities to be subsequently used for data analysis and model training. [Data Quality Monitoring](https://docs.google.com/document/d/110F72d4NTv80p35wDSONxhhPBqWRwbZXG4f9mNEMd98) was the primary motivation for creating dataset concept. -Dataset's metadata is stored in the Feast registry and raw data (features, entities, additional input keys and timestamp) is stored in the [offline store](../architecture-and-components/offline-store.md). +Dataset's metadata is stored in the Feast registry and raw data (features, entities, additional input keys and timestamp) is stored in the [offline store](../components/offline-store.md). Dataset can be created from: diff --git a/docs/getting-started/faq.md b/docs/getting-started/faq.md index d603e12ab6..6567ae181d 100644 --- a/docs/getting-started/faq.md +++ b/docs/getting-started/faq.md @@ -29,7 +29,7 @@ Feature views once they are used by a feature service are intended to be immutab ### What is the difference between data sources and the offline store? -The data source itself defines the underlying data warehouse table in which the features are stored. The offline store interface defines the APIs required to make an arbitrary compute layer work for Feast (e.g. pulling features given a set of feature views from their sources, exporting the data set results to different formats). Please see [data sources](concepts/data-ingestion.md) and [offline store](architecture-and-components/offline-store.md) for more details. +The data source itself defines the underlying data warehouse table in which the features are stored. The offline store interface defines the APIs required to make an arbitrary compute layer work for Feast (e.g. pulling features given a set of feature views from their sources, exporting the data set results to different formats). Please see [data sources](concepts/data-ingestion.md) and [offline store](components/offline-store.md) for more details. ### Is it possible to have offline and online stores from different providers? diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md index 01c039e9c5..ffc01c9d6e 100644 --- a/docs/getting-started/quickstart.md +++ b/docs/getting-started/quickstart.md @@ -623,6 +623,6 @@ show up in the upcoming concepts + architecture + tutorial pages as well. ## Next steps * Read the [Concepts](concepts/) page to understand the Feast data model. -* Read the [Architecture](architecture-and-components/) page. +* Read the [Architecture](architecture/) page. * Check out our [Tutorials](../tutorials/tutorials-overview/) section for more examples on how to use Feast. * Follow our [Running Feast with Snowflake/GCP/AWS](../how-to-guides/feast-snowflake-gcp-aws/) guide for a more in-depth tutorial on using Feast. diff --git a/docs/how-to-guides/scaling-feast.md b/docs/how-to-guides/scaling-feast.md index ce63f027c9..7e4f27b1dd 100644 --- a/docs/how-to-guides/scaling-feast.md +++ b/docs/how-to-guides/scaling-feast.md @@ -20,7 +20,7 @@ The recommended solution in this case is to use the [SQL based registry](../tuto The default Feast materialization process is an in-memory process, which pulls data from the offline store before writing it to the online store. However, this process does not scale for large data sets, since it's executed on a single-process. -Feast supports pluggable [Materialization Engines](../getting-started/architecture-and-components/batch-materialization-engine.md), that allow the materialization process to be scaled up. +Feast supports pluggable [Materialization Engines](../getting-started/components/batch-materialization-engine.md), that allow the materialization process to be scaled up. Aside from the local process, Feast supports a [Lambda-based materialization engine](https://rtd.feast.dev/en/master/#alpha-lambda-based-engine), and a [Bytewax-based materialization engine](https://rtd.feast.dev/en/master/#bytewax-engine). Users may also be able to build an engine to scale up materialization using existing infrastructure in their organizations. \ No newline at end of file diff --git a/docs/reference/batch-materialization/README.md b/docs/reference/batch-materialization/README.md index 8511fd81d0..a05d6d75e5 100644 --- a/docs/reference/batch-materialization/README.md +++ b/docs/reference/batch-materialization/README.md @@ -1,6 +1,6 @@ # Batch materialization -Please see [Batch Materialization Engine](../../getting-started/architecture-and-components/batch-materialization-engine.md) for an explanation of batch materialization engines. +Please see [Batch Materialization Engine](../../getting-started/components/batch-materialization-engine.md) for an explanation of batch materialization engines. {% page-ref page="snowflake.md" %} diff --git a/docs/reference/codebase-structure.md b/docs/reference/codebase-structure.md index 8eb5572679..7077e48fef 100644 --- a/docs/reference/codebase-structure.md +++ b/docs/reference/codebase-structure.md @@ -34,7 +34,7 @@ There are also several important submodules: * `ui/` contains the embedded Web UI, to be launched on the `feast ui` command. Of these submodules, `infra/` is the most important. -It contains the interfaces for the [provider](getting-started/architecture-and-components/provider.md), [offline store](getting-started/architecture-and-components/offline-store.md), [online store](getting-started/architecture-and-components/online-store.md), [batch materialization engine](getting-started/architecture-and-components/batch-materialization-engine.md), and [registry](getting-started/architecture-and-components/registry.md), as well as all of their individual implementations. +It contains the interfaces for the [provider](getting-started/components/provider.md), [offline store](getting-started/components/offline-store.md), [online store](getting-started/components/online-store.md), [batch materialization engine](getting-started/components/batch-materialization-engine.md), and [registry](getting-started/components/registry.md), as well as all of their individual implementations. ``` $ tree --dirsfirst -L 1 infra diff --git a/docs/reference/feature-servers/python-feature-server.md b/docs/reference/feature-servers/python-feature-server.md index 0d8a0aef75..33dfe77ae1 100644 --- a/docs/reference/feature-servers/python-feature-server.md +++ b/docs/reference/feature-servers/python-feature-server.md @@ -153,7 +153,7 @@ curl -X POST \ ### Pushing features to the online and offline stores -The Python feature server also exposes an endpoint for [push sources](../../data-sources/push.md). This endpoint allows you to push data to the online and/or offline store. +The Python feature server also exposes an endpoint for [push sources](../data-sources/push.md). This endpoint allows you to push data to the online and/or offline store. The request definition for `PushMode` is a string parameter `to` where the options are: \[`"online"`, `"offline"`, `"online_and_offline"`]. diff --git a/docs/reference/offline-stores/README.md b/docs/reference/offline-stores/README.md index 33eca6d426..87c92bfcf8 100644 --- a/docs/reference/offline-stores/README.md +++ b/docs/reference/offline-stores/README.md @@ -1,6 +1,6 @@ # Offline stores -Please see [Offline Store](../../getting-started/architecture-and-components/offline-store.md) for a conceptual explanation of offline stores. +Please see [Offline Store](../../getting-started/components/offline-store.md) for a conceptual explanation of offline stores. {% content-ref url="overview.md" %} [overview.md](overview.md) diff --git a/docs/reference/online-stores/README.md b/docs/reference/online-stores/README.md index 0acf6701f9..bf5419b249 100644 --- a/docs/reference/online-stores/README.md +++ b/docs/reference/online-stores/README.md @@ -1,6 +1,6 @@ # Online stores -Please see [Online Store](../../getting-started/architecture-and-components/online-store.md) for an explanation of online stores. +Please see [Online Store](../../getting-started/components/online-store.md) for an explanation of online stores. {% content-ref url="overview.md" %} [overview.md](overview.md) diff --git a/docs/reference/providers/README.md b/docs/reference/providers/README.md index 20686a1e14..925ae8ebc1 100644 --- a/docs/reference/providers/README.md +++ b/docs/reference/providers/README.md @@ -1,6 +1,6 @@ # Providers -Please see [Provider](../../getting-started/architecture-and-components/provider.md) for an explanation of providers. +Please see [Provider](../../getting-started/components/provider.md) for an explanation of providers. {% page-ref page="local.md" %} diff --git a/sdk/python/feast/templates/gcp/README.md b/sdk/python/feast/templates/gcp/README.md index 7929dc2bdf..bc9e51769c 100644 --- a/sdk/python/feast/templates/gcp/README.md +++ b/sdk/python/feast/templates/gcp/README.md @@ -11,7 +11,7 @@ You can run the overall workflow with `python test_workflow.py`. ## To move from this into a more production ready workflow: 1. `feature_store.yaml` points to a local file as a registry. You'll want to setup a remote file (e.g. in S3/GCS) or a SQL registry. See [registry docs](https://docs.feast.dev/getting-started/concepts/registry) for more details. -2. This example uses an already setup BigQuery Feast data warehouse as the [offline store](https://docs.feast.dev/getting-started/architecture-and-components/offline-store) +2. This example uses an already setup BigQuery Feast data warehouse as the [offline store](https://docs.feast.dev/getting-started/components/offline-store) to generate training data. You'll need to connect your own BigQuery instance to make this work. 3. Setup CI/CD + dev vs staging vs prod environments to automatically update the registry as you change Feast feature definitions. See [docs](https://docs.feast.dev/how-to-guides/running-feast-in-production#1.-automatically-deploying-changes-to-your-feature-definitions). 4. (optional) Regularly scheduled materialization to power low latency feature retrieval (e.g. via Airflow). See [Batch data ingestion](https://docs.feast.dev/getting-started/concepts/data-ingestion#batch-data-ingestion) diff --git a/sdk/python/feast/templates/local/README.md b/sdk/python/feast/templates/local/README.md index daf3a686fb..1e617cc442 100644 --- a/sdk/python/feast/templates/local/README.md +++ b/sdk/python/feast/templates/local/README.md @@ -18,7 +18,7 @@ You can run the overall workflow with `python test_workflow.py`. - You can see your options if you run `feast init --help`. 2. `feature_store.yaml` points to a local file as a registry. You'll want to setup a remote file (e.g. in S3/GCS) or a SQL registry. See [registry docs](https://docs.feast.dev/getting-started/concepts/registry) for more details. -3. This example uses a file [offline store](https://docs.feast.dev/getting-started/architecture-and-components/offline-store) +3. This example uses a file [offline store](https://docs.feast.dev/getting-started/components/offline-store) to generate training data. It does not scale. We recommend instead using a data warehouse such as BigQuery, Snowflake, Redshift. There is experimental support for Spark as well. 4. Setup CI/CD + dev vs staging vs prod environments to automatically update the registry as you change Feast feature definitions. See [docs](https://docs.feast.dev/how-to-guides/running-feast-in-production#1.-automatically-deploying-changes-to-your-feature-definitions). From c42d9fd6da85f098914d9113536bd826f7e17501 Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Wed, 14 Aug 2024 16:56:40 -0400 Subject: [PATCH 015/185] chore: Update arch/README.md (#4411) Update README.md --- docs/getting-started/architecture/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started/architecture/README.md b/docs/getting-started/architecture/README.md index a45f4ed6ec..35990373d7 100644 --- a/docs/getting-started/architecture/README.md +++ b/docs/getting-started/architecture/README.md @@ -16,6 +16,6 @@ [write-patterns.md](write-patterns.md) {% endcontent-ref %} -{% content-ref url="feature-transformation-model.md" %} +{% content-ref url="feature-transformation.md" %} [feature-transformation.md](feature-transformation.md) {% endcontent-ref %} From 8851722000549a7639ecff88de9b29f4f00c3b48 Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Thu, 15 Aug 2024 12:28:08 -0400 Subject: [PATCH 016/185] chore: Update language.md (#4412) Update language.md --- docs/getting-started/architecture/language.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/getting-started/architecture/language.md b/docs/getting-started/architecture/language.md index 916dff28d7..cff0fc467b 100644 --- a/docs/getting-started/architecture/language.md +++ b/docs/getting-started/architecture/language.md @@ -1,10 +1,10 @@ # Python: The Language of Production Machine Learning -Use Python to serve your features online. +Use Python to serve your features. ## Why should you use Python to Serve features for Machine Learning? -Python has emerged as the primary language for machine learning, and this extends to feature serving and there are five main reasons Feast recommends using a microservice in Feast. +Python has emerged as the primary language for machine learning, and this extends to feature serving and there are five main reasons Feast recommends using a microservice written in Python. ## 1. Python is the language of Machine Learning From 721ec74f17ee95e375054f21135e54e0687104a7 Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Fri, 16 Aug 2024 03:00:03 -0400 Subject: [PATCH 017/185] feat: Create ADOPTERS.md (#4410) * Create ADOPTERS.md * Update ADOPTERS.md --- community/ADOPTERS.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 community/ADOPTERS.md diff --git a/community/ADOPTERS.md b/community/ADOPTERS.md new file mode 100644 index 0000000000..a16fbef379 --- /dev/null +++ b/community/ADOPTERS.md @@ -0,0 +1,15 @@ +# Adopters of Feast + +Below are the adopters of Feast. If you are using Feast please add +yourself into the following list by a pull request. Please keep the list in +alphabetical order. + +| Organization | Contact | GitHub Username | +| ------------ | ------- | ------- | +| Affirm | Francisco Javier Arceo | franciscojavierarceo | +| Bank of Georgia | Tornike Gurgenidze | tokoko | +| Get Ground | Zhiling Chen | zhilingc | +| Gojek | Pradithya Aria Pura | pradithya | +| Twitter | David Liu | mavysavydav| +| Shopify | Matt Delacour | MattDelac | +| Snowflake | Miles Adkins | sfc-gh-madkins | From 0baeeb5ec524c1e6209edab9605ca8a098a2ec88 Mon Sep 17 00:00:00 2001 From: Dan Baron <84331438+danbaron63@users.noreply.github.com> Date: Fri, 16 Aug 2024 08:01:29 +0100 Subject: [PATCH 018/185] fix: Using repo_config parameter in teardown to allow for feature-store-yaml overrides (#4413) * fix: using repo_config parameter in teardown to allow for feature-store-yaml overrides Signed-off-by: Dan Baron * fix: fixing linting and formatting issues in tests Signed-off-by: Dan Baron * fix: removing unnecessary Path object construction Signed-off-by: Dan Baron --------- Signed-off-by: Dan Baron --- sdk/python/feast/repo_operations.py | 2 +- sdk/python/feast/transformation/pandas_transformation.py | 9 ++++----- .../feature_repos/universal/data_sources/file.py | 7 ++++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/sdk/python/feast/repo_operations.py b/sdk/python/feast/repo_operations.py index a3100ca9d7..0a89ab72ca 100644 --- a/sdk/python/feast/repo_operations.py +++ b/sdk/python/feast/repo_operations.py @@ -359,7 +359,7 @@ def apply_total(repo_config: RepoConfig, repo_path: Path, skip_source_validation def teardown(repo_config: RepoConfig, repo_path: Optional[str]): # Cannot pass in both repo_path and repo_config to FeatureStore. - feature_store = FeatureStore(repo_path=repo_path, config=None) + feature_store = FeatureStore(repo_path=repo_path, config=repo_config) feature_store.teardown() diff --git a/sdk/python/feast/transformation/pandas_transformation.py b/sdk/python/feast/transformation/pandas_transformation.py index 41e437fb6b..ac31a4fa20 100644 --- a/sdk/python/feast/transformation/pandas_transformation.py +++ b/sdk/python/feast/transformation/pandas_transformation.py @@ -1,5 +1,4 @@ -from types import FunctionType -from typing import Any +from typing import Any, Callable import dill import pandas as pd @@ -15,7 +14,7 @@ class PandasTransformation: - def __init__(self, udf: FunctionType, udf_string: str = ""): + def __init__(self, udf: Callable[[Any], Any], udf_string: str = ""): """ Creates an PandasTransformation object. @@ -30,11 +29,11 @@ def __init__(self, udf: FunctionType, udf_string: str = ""): def transform_arrow( self, pa_table: pyarrow.Table, features: list[Field] ) -> pyarrow.Table: - output_df_pandas = self.udf.__call__(pa_table.to_pandas()) + output_df_pandas = self.udf(pa_table.to_pandas()) return pyarrow.Table.from_pandas(output_df_pandas) def transform(self, input_df: pd.DataFrame) -> pd.DataFrame: - return self.udf.__call__(input_df) + return self.udf(input_df) def infer_features(self, random_input: dict[str, list[Any]]) -> list[Field]: df = pd.DataFrame.from_dict(random_input) diff --git a/sdk/python/tests/integration/feature_repos/universal/data_sources/file.py b/sdk/python/tests/integration/feature_repos/universal/data_sources/file.py index 5174e16046..e505986350 100644 --- a/sdk/python/tests/integration/feature_repos/universal/data_sources/file.py +++ b/sdk/python/tests/integration/feature_repos/universal/data_sources/file.py @@ -5,6 +5,7 @@ import tempfile import uuid from pathlib import Path +from subprocess import Popen from typing import Any, Dict, List, Optional import pandas as pd @@ -367,7 +368,7 @@ class RemoteOfflineStoreDataSourceCreator(FileDataSourceCreator): def __init__(self, project_name: str, *args, **kwargs): super().__init__(project_name) self.server_port: int = 0 - self.proc = None + self.proc: Optional[Popen[bytes]] = None def setup(self, registry: RegistryConfig): parent_offline_config = super().create_offline_store_config() @@ -382,13 +383,13 @@ def setup(self, registry: RegistryConfig): repo_path = Path(tempfile.mkdtemp()) with open(repo_path / "feature_store.yaml", "w") as outfile: yaml.dump(config.model_dump(by_alias=True), outfile) - repo_path = str(repo_path.resolve()) + repo_path = repo_path.resolve() self.server_port = free_port() host = "0.0.0.0" cmd = [ "feast", - "-c" + repo_path, + "-c" + str(repo_path), "serve_offline", "--host", host, From cebbe045597b85e1ae4394a8c14741e88347a6b8 Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Fri, 16 Aug 2024 03:30:15 -0400 Subject: [PATCH 019/185] feat: Updating docs to include model inference guidelines (#4416) Signed-off-by: Francisco Javier Arceo --- docs/SUMMARY.md | 1 + docs/getting-started/architecture/README.md | 4 + .../architecture/feature-transformation.md | 1 + .../architecture/model-inference.md | 88 +++++++++++++++++++ docs/getting-started/architecture/overview.md | 7 +- 5 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 docs/getting-started/architecture/model-inference.md diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index a6a40fc91d..0060ae729e 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -23,6 +23,7 @@ * [Push vs Pull Model](getting-started/architecture/push-vs-pull-model.md) * [Write Patterns](getting-started/architecture/write-patterns.md) * [Feature Transformation](getting-started/architecture/feature-transformation.md) + * [Feature Serving and Model Inference](getting-started/architecture/model-inference.md) * [Components](getting-started/components/README.md) * [Overview](getting-started/components/overview.md) * [Registry](getting-started/components/registry.md) diff --git a/docs/getting-started/architecture/README.md b/docs/getting-started/architecture/README.md index 35990373d7..f824164339 100644 --- a/docs/getting-started/architecture/README.md +++ b/docs/getting-started/architecture/README.md @@ -19,3 +19,7 @@ {% content-ref url="feature-transformation.md" %} [feature-transformation.md](feature-transformation.md) {% endcontent-ref %} + +{% content-ref url="model-inference.md" %} +[model-inference.md](model-inference.md) +{% endcontent-ref %} diff --git a/docs/getting-started/architecture/feature-transformation.md b/docs/getting-started/architecture/feature-transformation.md index 457e71d85e..1a15d4c3a5 100644 --- a/docs/getting-started/architecture/feature-transformation.md +++ b/docs/getting-started/architecture/feature-transformation.md @@ -3,6 +3,7 @@ A *feature transformation* is a function that takes some set of input data and returns some set of output data. Feature transformations can happen on either raw data or derived data. +## Feature Transformation Engines Feature transformations can be executed by three types of "transformation engines": 1. The Feast Feature Server diff --git a/docs/getting-started/architecture/model-inference.md b/docs/getting-started/architecture/model-inference.md new file mode 100644 index 0000000000..4fe2859c55 --- /dev/null +++ b/docs/getting-started/architecture/model-inference.md @@ -0,0 +1,88 @@ +# Feature Serving and Model Inference + +Production machine learning systems can choose from four approaches to serving machine learning predictions (the output +of model inference): +1. Online model inference with online features +2. Precomputed (batch) model predictions without online features +3. Online model inference with online features and cached predictions +4. Online model inference without features + +*Note: online features can be sourced from batch, streaming, or request data sources.* + +These three approaches have different tradeoffs but, in general, have significant implementation differences. + +## 1. Online Model Inference with Online Features +Online model inference with online features is a powerful approach to serving data-driven machine learning applications. +This requires a feature store to serve online features and a model server to serve model predictions (e.g., KServe). +This approach is particularly useful for applications where request-time data is required to run inference. +```python +features = store.get_online_features( + feature_refs=[ + "user_data:click_through_rate", + "user_data:number_of_clicks", + "user_data:average_page_duration", + ], + entity_rows=[{"user_id": 1}], +) +model_predictions = model_server.predict(features) +``` + +## 2. Precomputed (Batch) Model Predictions without Online Features +Typically, Machine Learning teams find serving precomputed model predictions to be the most straightforward to implement. +This approach simply treats the model predictions as a feature and serves them from the feature store using the standard +Feast sdk. +```python +model_predictions = store.get_online_features( + feature_refs=[ + "user_data:model_predictions", + ], + entity_rows=[{"user_id": 1}], +) +``` +Notice that the model server is not involved in this approach. Instead, the model predictions are precomputed and +materialized to the online store. + +While this approach can lead to quick impact for different business use cases, it suffers from stale data as well +as only serving users/entities that were available at the time of the batch computation. In some cases, this tradeoff +may be tolerable. + +## 3. Online Model Inference with Online Features and Cached Predictions +This approach is the most sophisticated where inference is optimized for low-latency by caching predictions and running +model inference when data producers write features to the online store. This approach is particularly useful for +applications where features are coming from multiple data sources, the model is computationally expensive to run, or +latency is a significant constraint. + +```python +# Client Reads +features = store.get_online_features( + feature_refs=[ + "user_data:click_through_rate", + "user_data:number_of_clicks", + "user_data:average_page_duration", + "user_data:model_predictions", + ], + entity_rows=[{"user_id": 1}], +) +if features.to_dict().get('user_data:model_predictions') is None: + model_predictions = model_server.predict(features) + store.write_to_online_store(feature_view_name="user_data", df=pd.DataFrame(model_predictions)) +``` +Note that in this case a seperate call to `write_to_online_store` is required when the underlying data changes and +predictions change along with it. + +```python +# Client Writes from the Data Producer +user_data = request.POST.get('user_data') +model_predictions = model_server.predict(user_data) # assume this includes `user_data` in the Data Frame +store.write_to_online_store(feature_view_name="user_data", df=pd.DataFrame(model_predictions)) +``` +While this requires additional writes for every data producer, this approach will result in the lowest latency for +model inference. + +## 4. Online Model Inference without Features +This approach does not require Feast. The model server can directly serve predictions without any features. This +approach is common in Large Language Models (LLMs) and other models that do not require features to make predictions. + +Note that generative models using Retrieval Augmented Generation (RAG) do require features where the +[document embeddings](../../reference/alpha-vector-database.md) are treated as features, which Feast supports +(this would fall under "Online Model Inference with Online Features"). \ No newline at end of file diff --git a/docs/getting-started/architecture/overview.md b/docs/getting-started/architecture/overview.md index 7d1180bfd1..44fa5ac260 100644 --- a/docs/getting-started/architecture/overview.md +++ b/docs/getting-started/architecture/overview.md @@ -8,9 +8,10 @@ Feast's architecture is designed to be flexible and scalable. It is composed of online store. This allows Feast to serve features in real-time with low latency. -* Feast supports On Demand and Streaming Transformations for [feature computation](feature-transformation.md) and - will support Batch transformations in the future. For Streaming and Batch, Feast requires a separate Feature Transformation - Engine (in the batch case, this is typically your Offline Store). We are exploring adding a default streaming engine to Feast. +* Feast supports [feature transformation](feature-transformation.md) for On Demand and Streaming data sources and + will support Batch transformations in the future. For Streaming and Batch data sources, Feast requires a separate +[Feature Transformation Engine](feature-transformation.md#feature-transformation-engines) (in the batch case, this is +typically your Offline Store). We are exploring adding a default streaming engine to Feast. * Domain expertise is recommended when integrating a data source with Feast understand the [tradeoffs from different write patterns](write-patterns.md) to your application From 23c6c862e1da4e9523530eb48c7ce79319dc442d Mon Sep 17 00:00:00 2001 From: Shuchu Han Date: Fri, 16 Aug 2024 03:31:24 -0400 Subject: [PATCH 020/185] fix: Retire pytz library (#4406) * fix: Remove pytz. Signed-off-by: Shuchu Han * fix: Keep the pytz.UTC part in dask.py Signed-off-by: Shuchu Han --------- Signed-off-by: Shuchu Han --- sdk/python/feast/driver_test_data.py | 13 +++++++++---- sdk/python/feast/embedded_go/type_map.py | 4 ++-- sdk/python/feast/feature_logging.py | 4 ++-- .../feast/infra/materialization/snowflake_engine.py | 8 +++++--- .../contrib/athena_offline_store/athena.py | 9 ++++----- .../contrib/postgres_offline_store/postgres.py | 7 +++---- .../contrib/spark_offline_store/spark.py | 12 ++++-------- .../trino_offline_store/connectors/upload.py | 5 ++--- sdk/python/feast/infra/offline_stores/dask.py | 12 ++++++++---- sdk/python/feast/infra/offline_stores/ibis.py | 11 +++++------ sdk/python/feast/infra/offline_stores/redshift.py | 11 +++++------ sdk/python/feast/infra/offline_stores/snowflake.py | 11 +++++------ .../infra/online_stores/contrib/elasticsearch.py | 13 +++---------- .../hazelcast_online_store.py | 5 ++--- .../online_stores/contrib/ikv_online_store/ikv.py | 5 ++--- .../contrib/mysql_online_store/mysql.py | 13 +++---------- .../feast/infra/online_stores/contrib/postgres.py | 11 +++-------- .../contrib/singlestore_online_store/singlestore.py | 5 ++--- sdk/python/feast/infra/online_stores/redis.py | 5 ++--- sdk/python/feast/registry_server.py | 8 ++++---- .../templates/aws/feature_repo/test_workflow.py | 9 +++++---- .../feast/templates/snowflake/test_workflow.py | 9 +++++---- sdk/python/feast/utils.py | 5 ++--- sdk/python/tests/data/data_creator.py | 12 ++++++------ .../integration/materialization/test_snowflake.py | 9 ++++----- .../registration/test_universal_registry.py | 11 +++++------ sdk/python/tests/utils/e2e_test_validation.py | 8 ++++---- sdk/python/tests/utils/feature_records.py | 3 +-- sdk/python/tests/utils/test_log_creator.py | 4 ++-- 29 files changed, 109 insertions(+), 133 deletions(-) diff --git a/sdk/python/feast/driver_test_data.py b/sdk/python/feast/driver_test_data.py index defeb404a3..23f1f12477 100644 --- a/sdk/python/feast/driver_test_data.py +++ b/sdk/python/feast/driver_test_data.py @@ -1,10 +1,11 @@ # This module generates dummy data to be used for tests and examples. import itertools +from datetime import timedelta, timezone from enum import Enum import numpy as np import pandas as pd -from pytz import FixedOffset, timezone, utc +from zoneinfo import ZoneInfo from feast.infra.offline_stores.offline_utils import ( DEFAULT_ENTITY_DF_EVENT_TIMESTAMP_COL, @@ -22,11 +23,15 @@ def _convert_event_timestamp(event_timestamp: pd.Timestamp, t: EventTimestampTyp if t == EventTimestampType.TZ_NAIVE: return event_timestamp elif t == EventTimestampType.TZ_AWARE_UTC: - return event_timestamp.replace(tzinfo=utc) + return event_timestamp.replace(tzinfo=timezone.utc) elif t == EventTimestampType.TZ_AWARE_FIXED_OFFSET: - return event_timestamp.replace(tzinfo=utc).astimezone(FixedOffset(60)) + return event_timestamp.replace(tzinfo=timezone.utc).astimezone( + tz=timezone(timedelta(minutes=60)) + ) elif t == EventTimestampType.TZ_AWARE_US_PACIFIC: - return event_timestamp.replace(tzinfo=utc).astimezone(timezone("US/Pacific")) + return event_timestamp.replace(tzinfo=timezone.utc).astimezone( + tz=ZoneInfo("US/Pacific") + ) def create_orders_df( diff --git a/sdk/python/feast/embedded_go/type_map.py b/sdk/python/feast/embedded_go/type_map.py index e70dc3be86..8f467c57ca 100644 --- a/sdk/python/feast/embedded_go/type_map.py +++ b/sdk/python/feast/embedded_go/type_map.py @@ -1,12 +1,12 @@ +from datetime import timezone from typing import List import pyarrow as pa -import pytz from feast.protos.feast.types import Value_pb2 from feast.types import Array, PrimitiveFeastType -PA_TIMESTAMP_TYPE = pa.timestamp("s", tz=pytz.UTC) +PA_TIMESTAMP_TYPE = pa.timestamp("s", tz=timezone.utc) ARROW_TYPE_TO_PROTO_FIELD = { pa.int32(): "int32_val", diff --git a/sdk/python/feast/feature_logging.py b/sdk/python/feast/feature_logging.py index 2843f87121..9bd5d8a91c 100644 --- a/sdk/python/feast/feature_logging.py +++ b/sdk/python/feast/feature_logging.py @@ -1,8 +1,8 @@ import abc +from datetime import timezone from typing import TYPE_CHECKING, Dict, Optional, Type, cast import pyarrow as pa -from pytz import UTC from feast.data_source import DataSource from feast.embedded_go.type_map import FEAST_TYPE_TO_ARROW_TYPE, PA_TIMESTAMP_TYPE @@ -97,7 +97,7 @@ def get_schema(self, registry: "BaseRegistry") -> pa.Schema: ) # system columns - fields[LOG_TIMESTAMP_FIELD] = pa.timestamp("us", tz=UTC) + fields[LOG_TIMESTAMP_FIELD] = pa.timestamp("us", tz=timezone.utc) fields[LOG_DATE_FIELD] = pa.date32() fields[REQUEST_ID_FIELD] = pa.string() diff --git a/sdk/python/feast/infra/materialization/snowflake_engine.py b/sdk/python/feast/infra/materialization/snowflake_engine.py index 5d0f08c2f5..9f9f41c83d 100644 --- a/sdk/python/feast/infra/materialization/snowflake_engine.py +++ b/sdk/python/feast/infra/materialization/snowflake_engine.py @@ -1,14 +1,13 @@ import os import shutil from dataclasses import dataclass -from datetime import datetime +from datetime import datetime, timezone from typing import Callable, List, Literal, Optional, Sequence, Union import click import pandas as pd from colorama import Fore, Style from pydantic import ConfigDict, Field, StrictStr -from pytz import utc from tqdm import tqdm import feast @@ -276,7 +275,10 @@ def _materialize_one( execute_snowflake_statement(conn, query).fetchall()[0][0] / 1_000_000_000 ) - if last_commit_change_time < start_date.astimezone(tz=utc).timestamp(): + if ( + last_commit_change_time + < start_date.astimezone(tz=timezone.utc).timestamp() + ): return SnowflakeMaterializationJob( job_id=job_id, status=MaterializationJobStatus.SUCCEEDED ) diff --git a/sdk/python/feast/infra/offline_stores/contrib/athena_offline_store/athena.py b/sdk/python/feast/infra/offline_stores/contrib/athena_offline_store/athena.py index ce731f0198..ea0d6386cb 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/athena_offline_store/athena.py +++ b/sdk/python/feast/infra/offline_stores/contrib/athena_offline_store/athena.py @@ -1,6 +1,6 @@ import contextlib import uuid -from datetime import datetime +from datetime import datetime, timezone from pathlib import Path from typing import ( Callable, @@ -19,7 +19,6 @@ import pyarrow import pyarrow as pa from pydantic import StrictStr -from pytz import utc from feast import OnDemandFeatureView from feast.data_source import DataSource @@ -100,8 +99,8 @@ def pull_latest_from_table_or_query( athena_client = aws_utils.get_athena_data_client(config.offline_store.region) s3_resource = aws_utils.get_s3_resource(config.offline_store.region) - start_date = start_date.astimezone(tz=utc) - end_date = end_date.astimezone(tz=utc) + start_date = start_date.astimezone(tz=timezone.utc) + end_date = end_date.astimezone(tz=timezone.utc) query = f""" SELECT @@ -151,7 +150,7 @@ def pull_all_from_table_or_query( query = f""" SELECT {field_string} FROM {from_expression} - WHERE {timestamp_field} BETWEEN TIMESTAMP '{start_date.astimezone(tz=utc).strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]}' AND TIMESTAMP '{end_date.astimezone(tz=utc).strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]}' + WHERE {timestamp_field} BETWEEN TIMESTAMP '{start_date.astimezone(tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]}' AND TIMESTAMP '{end_date.astimezone(tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]}' {"AND "+date_partition_column+" >= '"+start_date.strftime('%Y-%m-%d')+"' AND "+date_partition_column+" <= '"+end_date.strftime('%Y-%m-%d')+"' " if date_partition_column != "" and date_partition_column is not None else ''} """ diff --git a/sdk/python/feast/infra/offline_stores/contrib/postgres_offline_store/postgres.py b/sdk/python/feast/infra/offline_stores/contrib/postgres_offline_store/postgres.py index c4740a960e..5239cfb474 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/postgres_offline_store/postgres.py +++ b/sdk/python/feast/infra/offline_stores/contrib/postgres_offline_store/postgres.py @@ -1,6 +1,6 @@ import contextlib from dataclasses import asdict -from datetime import datetime +from datetime import datetime, timezone from typing import ( Any, Callable, @@ -20,7 +20,6 @@ import pyarrow as pa from jinja2 import BaseLoader, Environment from psycopg import sql -from pytz import utc from feast.data_source import DataSource from feast.errors import InvalidEntityType, ZeroColumnQueryResult, ZeroRowsQueryResult @@ -214,8 +213,8 @@ def pull_all_from_table_or_query( join_key_columns + feature_name_columns + [timestamp_field] ) - start_date = start_date.astimezone(tz=utc) - end_date = end_date.astimezone(tz=utc) + start_date = start_date.astimezone(tz=timezone.utc) + end_date = end_date.astimezone(tz=timezone.utc) query = f""" SELECT {field_string} diff --git a/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/spark.py b/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/spark.py index 2d5a00c296..2896d565d3 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/spark.py +++ b/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/spark.py @@ -2,7 +2,7 @@ import tempfile import uuid import warnings -from datetime import datetime +from datetime import datetime, timezone from typing import Any, Callable, Dict, List, Optional, Tuple, Union import numpy as np @@ -14,7 +14,6 @@ from pydantic import StrictStr from pyspark import SparkConf from pyspark.sql import SparkSession -from pytz import utc from feast import FeatureView, OnDemandFeatureView from feast.data_source import DataSource @@ -284,8 +283,8 @@ def pull_all_from_table_or_query( fields = ", ".join(join_key_columns + feature_name_columns + [timestamp_field]) from_expression = data_source.get_table_query_string() - start_date = start_date.astimezone(tz=utc) - end_date = end_date.astimezone(tz=utc) + start_date = start_date.astimezone(tz=timezone.utc) + end_date = end_date.astimezone(tz=timezone.utc) query = f""" SELECT {fields} @@ -520,13 +519,10 @@ def _upload_entity_df( entity_df[event_timestamp_col], utc=True ) spark_session.createDataFrame(entity_df).createOrReplaceTempView(table_name) - return elif isinstance(entity_df, str): spark_session.sql(entity_df).createOrReplaceTempView(table_name) - return elif isinstance(entity_df, pyspark.sql.DataFrame): entity_df.createOrReplaceTempView(table_name) - return else: raise InvalidEntityType(type(entity_df)) @@ -534,7 +530,7 @@ def _upload_entity_df( def _format_datetime(t: datetime) -> str: # Since Hive does not support timezone, need to transform to utc. if t.tzinfo: - t = t.astimezone(tz=utc) + t = t.astimezone(tz=timezone.utc) dt = t.strftime("%Y-%m-%d %H:%M:%S.%f") return dt diff --git a/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/connectors/upload.py b/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/connectors/upload.py index 9e2ea3708d..1b55199193 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/connectors/upload.py +++ b/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/connectors/upload.py @@ -18,13 +18,12 @@ ``` """ -from datetime import datetime +from datetime import datetime, timezone from typing import Any, Dict, Iterator, Optional, Set import numpy as np import pandas as pd import pyarrow -from pytz import utc from feast.infra.offline_stores.contrib.trino_offline_store.trino_queries import Trino from feast.infra.offline_stores.contrib.trino_offline_store.trino_type_map import ( @@ -141,7 +140,7 @@ def _format_value(row: pd.Series, schema: Dict[str, Any]) -> str: def format_datetime(t: datetime) -> str: if t.tzinfo: - t = t.astimezone(tz=utc) + t = t.astimezone(tz=timezone.utc) return t.strftime("%Y-%m-%d %H:%M:%S.%f") diff --git a/sdk/python/feast/infra/offline_stores/dask.py b/sdk/python/feast/infra/offline_stores/dask.py index 4a63baf646..52ad88d299 100644 --- a/sdk/python/feast/infra/offline_stores/dask.py +++ b/sdk/python/feast/infra/offline_stores/dask.py @@ -1,6 +1,6 @@ import os import uuid -from datetime import datetime +from datetime import datetime, timezone from pathlib import Path from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, Union @@ -178,6 +178,8 @@ def evaluate_historical_retrieval(): entity_df_event_timestamp_col_type = entity_df_with_features.dtypes[ entity_df_event_timestamp_col ] + + # TODO: need to figure out why the value of entity_df_event_timestamp_col_type.tz is pytz.UTC if ( not hasattr(entity_df_event_timestamp_col_type, "tz") or entity_df_event_timestamp_col_type.tz != pytz.UTC @@ -189,7 +191,7 @@ def evaluate_historical_retrieval(): ].apply( lambda x: x if x.tzinfo is not None - else x.replace(tzinfo=pytz.utc) + else x.replace(tzinfo=timezone.utc) ) ) @@ -616,6 +618,7 @@ def _normalize_timestamp( if created_timestamp_column: created_timestamp_column_type = df_to_join_types[created_timestamp_column] + # TODO: need to figure out why the value of timestamp_field_type.tz is pytz.UTC if not hasattr(timestamp_field_type, "tz") or timestamp_field_type.tz != pytz.UTC: # if you are querying for the event timestamp field, we have to deduplicate if len(df_to_join[timestamp_field].shape) > 1: @@ -624,10 +627,11 @@ def _normalize_timestamp( # Make sure all timestamp fields are tz-aware. We default tz-naive fields to UTC df_to_join[timestamp_field] = df_to_join[timestamp_field].apply( - lambda x: x if x.tzinfo is not None else x.replace(tzinfo=pytz.utc), + lambda x: x if x.tzinfo else x.replace(tzinfo=timezone.utc), meta=(timestamp_field, "datetime64[ns, UTC]"), ) + # TODO: need to figure out why the value of created_timestamp_column_type.tz is pytz.UTC if created_timestamp_column and ( not hasattr(created_timestamp_column_type, "tz") or created_timestamp_column_type.tz != pytz.UTC @@ -640,7 +644,7 @@ def _normalize_timestamp( df_to_join[created_timestamp_column] = df_to_join[ created_timestamp_column ].apply( - lambda x: x if x.tzinfo is not None else x.replace(tzinfo=pytz.utc), + lambda x: x if x.tzinfo else x.replace(tzinfo=timezone.utc), meta=(timestamp_field, "datetime64[ns, UTC]"), ) diff --git a/sdk/python/feast/infra/offline_stores/ibis.py b/sdk/python/feast/infra/offline_stores/ibis.py index 4de16cbda3..61c477baec 100644 --- a/sdk/python/feast/infra/offline_stores/ibis.py +++ b/sdk/python/feast/infra/offline_stores/ibis.py @@ -1,7 +1,7 @@ import random import string import uuid -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Any, Callable, Dict, List, Optional, Tuple, Union @@ -12,7 +12,6 @@ import pyarrow from ibis.expr import datatypes as dt from ibis.expr.types import Table -from pytz import utc from feast.data_source import DataSource from feast.feature_logging import LoggingConfig, LoggingSource @@ -55,8 +54,8 @@ def pull_latest_from_table_or_query_ibis( fields = join_key_columns + feature_name_columns + [timestamp_field] if created_timestamp_column: fields.append(created_timestamp_column) - start_date = start_date.astimezone(tz=utc) - end_date = end_date.astimezone(tz=utc) + start_date = start_date.astimezone(tz=timezone.utc) + end_date = end_date.astimezone(tz=timezone.utc) table = data_source_reader(data_source) @@ -265,8 +264,8 @@ def pull_all_from_table_or_query_ibis( staging_location_endpoint_override: Optional[str] = None, ) -> RetrievalJob: fields = join_key_columns + feature_name_columns + [timestamp_field] - start_date = start_date.astimezone(tz=utc) - end_date = end_date.astimezone(tz=utc) + start_date = start_date.astimezone(tz=timezone.utc) + end_date = end_date.astimezone(tz=timezone.utc) table = data_source_reader(data_source) diff --git a/sdk/python/feast/infra/offline_stores/redshift.py b/sdk/python/feast/infra/offline_stores/redshift.py index cec21c35c1..ed76f830f3 100644 --- a/sdk/python/feast/infra/offline_stores/redshift.py +++ b/sdk/python/feast/infra/offline_stores/redshift.py @@ -1,6 +1,6 @@ import contextlib import uuid -from datetime import datetime +from datetime import datetime, timezone from pathlib import Path from typing import ( Any, @@ -21,7 +21,6 @@ import pyarrow as pa from dateutil import parser from pydantic import StrictStr, model_validator -from pytz import utc from feast import OnDemandFeatureView, RedshiftSource from feast.data_source import DataSource @@ -127,8 +126,8 @@ def pull_latest_from_table_or_query( ) s3_resource = aws_utils.get_s3_resource(config.offline_store.region) - start_date = start_date.astimezone(tz=utc) - end_date = end_date.astimezone(tz=utc) + start_date = start_date.astimezone(tz=timezone.utc) + end_date = end_date.astimezone(tz=timezone.utc) query = f""" SELECT @@ -174,8 +173,8 @@ def pull_all_from_table_or_query( ) s3_resource = aws_utils.get_s3_resource(config.offline_store.region) - start_date = start_date.astimezone(tz=utc) - end_date = end_date.astimezone(tz=utc) + start_date = start_date.astimezone(tz=timezone.utc) + end_date = end_date.astimezone(tz=timezone.utc) query = f""" SELECT {field_string} diff --git a/sdk/python/feast/infra/offline_stores/snowflake.py b/sdk/python/feast/infra/offline_stores/snowflake.py index ada6c99c98..9418171a96 100644 --- a/sdk/python/feast/infra/offline_stores/snowflake.py +++ b/sdk/python/feast/infra/offline_stores/snowflake.py @@ -3,7 +3,7 @@ import os import uuid import warnings -from datetime import datetime +from datetime import datetime, timezone from functools import reduce from pathlib import Path from typing import ( @@ -25,7 +25,6 @@ import pandas as pd import pyarrow from pydantic import ConfigDict, Field, StrictStr -from pytz import utc from feast import OnDemandFeatureView from feast.data_source import DataSource @@ -196,8 +195,8 @@ def pull_latest_from_table_or_query( with GetSnowflakeConnection(config.offline_store) as conn: snowflake_conn = conn - start_date = start_date.astimezone(tz=utc) - end_date = end_date.astimezone(tz=utc) + start_date = start_date.astimezone(tz=timezone.utc) + end_date = end_date.astimezone(tz=timezone.utc) query = f""" SELECT @@ -248,8 +247,8 @@ def pull_all_from_table_or_query( with GetSnowflakeConnection(config.offline_store) as conn: snowflake_conn = conn - start_date = start_date.astimezone(tz=utc) - end_date = end_date.astimezone(tz=utc) + start_date = start_date.astimezone(tz=timezone.utc) + end_date = end_date.astimezone(tz=timezone.utc) query = f""" SELECT {field_string} diff --git a/sdk/python/feast/infra/online_stores/contrib/elasticsearch.py b/sdk/python/feast/infra/online_stores/contrib/elasticsearch.py index 429327e651..c26b4199ae 100644 --- a/sdk/python/feast/infra/online_stores/contrib/elasticsearch.py +++ b/sdk/python/feast/infra/online_stores/contrib/elasticsearch.py @@ -6,7 +6,6 @@ from datetime import datetime from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple -import pytz from elasticsearch import Elasticsearch, helpers from feast import Entity, FeatureView, RepoConfig @@ -15,6 +14,7 @@ from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto from feast.protos.feast.types.Value_pb2 import Value as ValueProto from feast.repo_config import FeastConfigBaseModel +from feast.utils import to_naive_utc class ElasticSearchOnlineStoreConfig(FeastConfigBaseModel): @@ -96,9 +96,9 @@ def online_write_batch( entity_key_serialization_version=config.entity_key_serialization_version, ) encoded_entity_key = base64.b64encode(entity_key_bin).decode("utf-8") - timestamp = _to_naive_utc(timestamp) + timestamp = to_naive_utc(timestamp) if created_ts is not None: - created_ts = _to_naive_utc(created_ts) + created_ts = to_naive_utc(created_ts) for feature_name, value in values.items(): encoded_value = base64.b64encode(value.SerializeToString()).decode( "utf-8" @@ -267,10 +267,3 @@ def retrieve_online_documents( ) ) return result - - -def _to_naive_utc(ts: datetime): - if ts.tzinfo is None: - return ts - else: - return ts.astimezone(pytz.utc).replace(tzinfo=None) diff --git a/sdk/python/feast/infra/online_stores/contrib/hazelcast_online_store/hazelcast_online_store.py b/sdk/python/feast/infra/online_stores/contrib/hazelcast_online_store/hazelcast_online_store.py index 497d8909af..c56d394c21 100644 --- a/sdk/python/feast/infra/online_stores/contrib/hazelcast_online_store/hazelcast_online_store.py +++ b/sdk/python/feast/infra/online_stores/contrib/hazelcast_online_store/hazelcast_online_store.py @@ -23,7 +23,6 @@ from datetime import datetime, timezone from typing import Any, Callable, Dict, List, Literal, Optional, Sequence, Tuple -import pytz from hazelcast.client import HazelcastClient from hazelcast.core import HazelcastJsonValue from hazelcast.discovery import HazelcastCloudDiscovery @@ -167,10 +166,10 @@ def online_write_batch( entity_key_serialization_version=2, ) ).decode("utf-8") - event_ts_utc = pytz.utc.localize(event_ts, is_dst=None).timestamp() + event_ts_utc = event_ts.astimezone(tz=timezone.utc).timestamp() created_ts_utc = 0.0 if created_ts is not None: - created_ts_utc = pytz.utc.localize(created_ts, is_dst=None).timestamp() + created_ts_utc = created_ts.astimezone(tz=timezone.utc).timestamp() for feature_name, value in values.items(): feature_value = base64.b64encode(value.SerializeToString()).decode( "utf-8" diff --git a/sdk/python/feast/infra/online_stores/contrib/ikv_online_store/ikv.py b/sdk/python/feast/infra/online_stores/contrib/ikv_online_store/ikv.py index 6b721bddf8..c8f0ad65c9 100644 --- a/sdk/python/feast/infra/online_stores/contrib/ikv_online_store/ikv.py +++ b/sdk/python/feast/infra/online_stores/contrib/ikv_online_store/ikv.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, timezone from typing import ( Any, Callable, @@ -11,7 +11,6 @@ Tuple, ) -import pytz from google.protobuf.timestamp_pb2 import Timestamp from ikvpy.client import IKVReader, IKVWriter from ikvpy.clientoptions import ClientOptions, ClientOptionsBuilder @@ -163,7 +162,7 @@ def _decode_fields_for_primary_key( if dt_bytes: proto_timestamp = Timestamp() proto_timestamp.ParseFromString(dt_bytes) - dt = datetime.fromtimestamp(proto_timestamp.seconds, tz=pytz.utc) + dt = datetime.fromtimestamp(proto_timestamp.seconds, tz=timezone.utc) # decode other features features = {} diff --git a/sdk/python/feast/infra/online_stores/contrib/mysql_online_store/mysql.py b/sdk/python/feast/infra/online_stores/contrib/mysql_online_store/mysql.py index 26916a9fcb..64111ca42c 100644 --- a/sdk/python/feast/infra/online_stores/contrib/mysql_online_store/mysql.py +++ b/sdk/python/feast/infra/online_stores/contrib/mysql_online_store/mysql.py @@ -4,7 +4,6 @@ from typing import Any, Callable, Dict, List, Literal, Optional, Sequence, Tuple import pymysql -import pytz from pydantic import StrictStr from pymysql.connections import Connection from pymysql.cursors import Cursor @@ -15,6 +14,7 @@ from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto from feast.protos.feast.types.Value_pb2 import Value as ValueProto from feast.repo_config import FeastConfigBaseModel +from feast.utils import to_naive_utc class MySQLOnlineStoreConfig(FeastConfigBaseModel): @@ -74,9 +74,9 @@ def online_write_batch( entity_key, entity_key_serialization_version=2, ).hex() - timestamp = _to_naive_utc(timestamp) + timestamp = to_naive_utc(timestamp) if created_ts is not None: - created_ts = _to_naive_utc(created_ts) + created_ts = to_naive_utc(created_ts) for feature_name, val in values.items(): self.write_to_table( @@ -223,10 +223,3 @@ def _drop_table_and_index(cur: Cursor, project: str, table: FeatureView) -> None def _table_id(project: str, table: FeatureView) -> str: return f"{project}_{table.name}" - - -def _to_naive_utc(ts: datetime) -> datetime: - if ts.tzinfo is None: - return ts - else: - return ts.astimezone(pytz.utc).replace(tzinfo=None) diff --git a/sdk/python/feast/infra/online_stores/contrib/postgres.py b/sdk/python/feast/infra/online_stores/contrib/postgres.py index ff73a4a347..8c6d3e0b99 100644 --- a/sdk/python/feast/infra/online_stores/contrib/postgres.py +++ b/sdk/python/feast/infra/online_stores/contrib/postgres.py @@ -16,7 +16,6 @@ Union, ) -import pytz from psycopg import AsyncConnection, sql from psycopg.connection import Connection from psycopg_pool import AsyncConnectionPool, ConnectionPool @@ -24,6 +23,9 @@ from feast import Entity from feast.feature_view import FeatureView from feast.infra.key_encoding_utils import get_list_val_str, serialize_entity_key +from feast.infra.online_stores.contrib.singlestore_online_store.singlestore import ( + _to_naive_utc, +) from feast.infra.online_stores.online_store import OnlineStore from feast.infra.utils.postgres.connection_utils import ( _get_conn, @@ -472,10 +474,3 @@ def _drop_table_and_index(table_name): sql.Identifier(table_name), sql.Identifier(f"{table_name}_ek"), ) - - -def _to_naive_utc(ts: datetime): - if ts.tzinfo is None: - return ts - else: - return ts.astimezone(pytz.utc).replace(tzinfo=None) diff --git a/sdk/python/feast/infra/online_stores/contrib/singlestore_online_store/singlestore.py b/sdk/python/feast/infra/online_stores/contrib/singlestore_online_store/singlestore.py index e17a059c1a..3e921afcea 100644 --- a/sdk/python/feast/infra/online_stores/contrib/singlestore_online_store/singlestore.py +++ b/sdk/python/feast/infra/online_stores/contrib/singlestore_online_store/singlestore.py @@ -1,10 +1,9 @@ from __future__ import absolute_import from collections import defaultdict -from datetime import datetime +from datetime import datetime, timezone from typing import Any, Callable, Dict, List, Literal, Optional, Sequence, Tuple -import pytz import singlestoredb from pydantic import StrictStr from singlestoredb.connection import Connection, Cursor @@ -232,4 +231,4 @@ def _to_naive_utc(ts: datetime) -> datetime: if ts.tzinfo is None: return ts else: - return ts.astimezone(pytz.utc).replace(tzinfo=None) + return ts.astimezone(tz=timezone.utc).replace(tzinfo=None) diff --git a/sdk/python/feast/infra/online_stores/redis.py b/sdk/python/feast/infra/online_stores/redis.py index 5f0156f620..59892fcbe0 100644 --- a/sdk/python/feast/infra/online_stores/redis.py +++ b/sdk/python/feast/infra/online_stores/redis.py @@ -13,7 +13,7 @@ # limitations under the License. import json import logging -from datetime import datetime +from datetime import datetime, timezone from enum import Enum from typing import ( Any, @@ -28,7 +28,6 @@ Union, ) -import pytz from google.protobuf.timestamp_pb2 import Timestamp from pydantic import StrictStr @@ -457,5 +456,5 @@ def _get_features_for_entity( if not res: return None, None else: - timestamp = datetime.fromtimestamp(res_ts.seconds, tz=pytz.utc) + timestamp = datetime.fromtimestamp(res_ts.seconds, tz=timezone.utc) return timestamp, res diff --git a/sdk/python/feast/registry_server.py b/sdk/python/feast/registry_server.py index 4a96ba76a8..53acb9f625 100644 --- a/sdk/python/feast/registry_server.py +++ b/sdk/python/feast/registry_server.py @@ -1,9 +1,8 @@ from concurrent import futures -from datetime import datetime +from datetime import datetime, timezone import grpc from google.protobuf.empty_pb2 import Empty -from pytz import utc from feast import FeatureStore from feast.data_source import DataSource @@ -314,10 +313,11 @@ def ApplyMaterialization( feature_view=FeatureView.from_proto(request.feature_view), project=request.project, start_date=datetime.fromtimestamp( - request.start_date.seconds + request.start_date.nanos / 1e9, tz=utc + request.start_date.seconds + request.start_date.nanos / 1e9, + tz=timezone.utc, ), end_date=datetime.fromtimestamp( - request.end_date.seconds + request.end_date.nanos / 1e9, tz=utc + request.end_date.seconds + request.end_date.nanos / 1e9, tz=timezone.utc ), commit=request.commit, ) diff --git a/sdk/python/feast/templates/aws/feature_repo/test_workflow.py b/sdk/python/feast/templates/aws/feature_repo/test_workflow.py index 59ac1f0ee7..092399e03c 100644 --- a/sdk/python/feast/templates/aws/feature_repo/test_workflow.py +++ b/sdk/python/feast/templates/aws/feature_repo/test_workflow.py @@ -1,9 +1,8 @@ import random import subprocess -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone import pandas as pd -from pytz import utc from feast import FeatureStore from feast.data_source import PushMode @@ -71,9 +70,11 @@ def run_demo(): def fetch_historical_features_entity_sql(store: FeatureStore, for_batch_scoring): end_date = ( - datetime.now().replace(microsecond=0, second=0, minute=0).astimezone(tz=utc) + datetime.now() + .replace(microsecond=0, second=0, minute=0) + .astimezone(tz=timezone.utc) ) - start_date = (end_date - timedelta(days=60)).astimezone(tz=utc) + start_date = (end_date - timedelta(days=60)).astimezone(tz=timezone.utc) # For batch scoring, we want the latest timestamps if for_batch_scoring: print( diff --git a/sdk/python/feast/templates/snowflake/test_workflow.py b/sdk/python/feast/templates/snowflake/test_workflow.py index 3c44342881..f60b014874 100644 --- a/sdk/python/feast/templates/snowflake/test_workflow.py +++ b/sdk/python/feast/templates/snowflake/test_workflow.py @@ -1,10 +1,9 @@ import random import subprocess -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone import pandas as pd import yaml -from pytz import utc from feast import FeatureStore from feast.data_source import PushMode @@ -75,9 +74,11 @@ def run_demo(): def fetch_historical_features_entity_sql(store: FeatureStore, for_batch_scoring): end_date = ( - datetime.now().replace(microsecond=0, second=0, minute=0).astimezone(tz=utc) + datetime.now() + .replace(microsecond=0, second=0, minute=0) + .astimezone(tz=timezone.utc) ) - start_date = (end_date - timedelta(days=60)).astimezone(tz=utc) + start_date = (end_date - timedelta(days=60)).astimezone(tz=timezone.utc) project_name = yaml.safe_load(open("feature_repo/feature_store.yaml"))["project"] table_name = f"{project_name}_feast_driver_hourly_stats" diff --git a/sdk/python/feast/utils.py b/sdk/python/feast/utils.py index 0467393aa2..5862cd4630 100644 --- a/sdk/python/feast/utils.py +++ b/sdk/python/feast/utils.py @@ -25,7 +25,6 @@ import pyarrow from dateutil.tz import tzlocal from google.protobuf.timestamp_pb2 import Timestamp -from pytz import utc from feast.constants import FEAST_FS_YAML_FILE_PATH_ENV_NAME from feast.entity import Entity @@ -63,7 +62,7 @@ def get_user_agent(): def make_tzaware(t: datetime) -> datetime: """We assume tz-naive datetimes are UTC""" if t.tzinfo is None: - return t.replace(tzinfo=utc) + return t.replace(tzinfo=timezone.utc) else: return t @@ -81,7 +80,7 @@ def to_naive_utc(ts: datetime) -> datetime: if ts.tzinfo is None: return ts else: - return ts.astimezone(utc).replace(tzinfo=None) + return ts.astimezone(timezone.utc).replace(tzinfo=None) def maybe_local_tz(t: datetime) -> datetime: diff --git a/sdk/python/tests/data/data_creator.py b/sdk/python/tests/data/data_creator.py index 15d09c5a40..5d6cffeb9d 100644 --- a/sdk/python/tests/data/data_creator.py +++ b/sdk/python/tests/data/data_creator.py @@ -1,8 +1,8 @@ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import Dict, List, Optional import pandas as pd -from pytz import timezone, utc +from zoneinfo import ZoneInfo from feast.types import FeastType, Float32, Int32, Int64, String from feast.utils import _utc_now @@ -27,11 +27,11 @@ def create_basic_driver_dataset( ts - timedelta(hours=3), # Use different time zones to test tz-naive -> tz-aware conversion (ts - timedelta(hours=4)) - .replace(tzinfo=utc) - .astimezone(tz=timezone("Europe/Berlin")), + .replace(tzinfo=timezone.utc) + .astimezone(tz=ZoneInfo("Europe/Berlin")), (ts - timedelta(hours=1)) - .replace(tzinfo=utc) - .astimezone(tz=timezone("US/Pacific")), + .replace(tzinfo=timezone.utc) + .astimezone(tz=ZoneInfo("US/Pacific")), ], "created_ts": [ts, ts, ts, ts, ts], } diff --git a/sdk/python/tests/integration/materialization/test_snowflake.py b/sdk/python/tests/integration/materialization/test_snowflake.py index f12191363b..f53c3ca753 100644 --- a/sdk/python/tests/integration/materialization/test_snowflake.py +++ b/sdk/python/tests/integration/materialization/test_snowflake.py @@ -1,8 +1,7 @@ import os -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone import pytest -from pytz import utc from feast import Field from feast.entity import Entity @@ -150,7 +149,7 @@ def test_snowflake_materialization_consistency_internal_with_lists( now = _utc_now() full_feature_names = True - start_date = (now - timedelta(hours=5)).replace(tzinfo=utc) + start_date = (now - timedelta(hours=5)).replace(tzinfo=timezone.utc) end_date = split_dt fs.materialize( feature_views=[driver_stats_fv.name], @@ -165,7 +164,7 @@ def test_snowflake_materialization_consistency_internal_with_lists( "string": ["3"] * 2, "bytes": [b"3"] * 2, "bool": [False] * 2, - "datetime": [datetime(1981, 1, 1, tzinfo=utc)] * 2, + "datetime": [datetime(1981, 1, 1, tzinfo=timezone.utc)] * 2, } expected_value = [] if feature_is_empty_list else expected_values[feature_dtype] @@ -234,7 +233,7 @@ def test_snowflake_materialization_entityless_fv(): now = _utc_now() - start_date = (now - timedelta(hours=5)).replace(tzinfo=utc) + start_date = (now - timedelta(hours=5)).replace(tzinfo=timezone.utc) end_date = split_dt fs.materialize( feature_views=[overall_stats_fv.name], diff --git a/sdk/python/tests/integration/registration/test_universal_registry.py b/sdk/python/tests/integration/registration/test_universal_registry.py index b0738c8419..9dcd1b5b91 100644 --- a/sdk/python/tests/integration/registration/test_universal_registry.py +++ b/sdk/python/tests/integration/registration/test_universal_registry.py @@ -14,7 +14,7 @@ import logging import os import time -from datetime import timedelta +from datetime import timedelta, timezone from tempfile import mkstemp from unittest import mock @@ -22,7 +22,6 @@ import pandas as pd import pytest from pytest_lazyfixture import lazy_fixture -from pytz import utc from testcontainers.core.container import DockerContainer from testcontainers.core.waiting_utils import wait_for_logs from testcontainers.minio import MinioContainer @@ -802,8 +801,8 @@ def odfv1(feature_df: pd.DataFrame) -> pd.DataFrame: # Simulate materialization current_date = _utc_now() - end_date = current_date.replace(tzinfo=utc) - start_date = (current_date - timedelta(days=1)).replace(tzinfo=utc) + end_date = current_date.replace(tzinfo=timezone.utc) + start_date = (current_date - timedelta(days=1)).replace(tzinfo=timezone.utc) test_registry.apply_materialization(feature_view, project, start_date, end_date) materialized_feature_view = test_registry.get_feature_view( "my_feature_view_1", project @@ -871,8 +870,8 @@ def odfv1(feature_df: pd.DataFrame) -> pd.DataFrame: # Simulate materialization a second time current_date = _utc_now() - end_date_1 = current_date.replace(tzinfo=utc) - start_date_1 = (current_date - timedelta(days=1)).replace(tzinfo=utc) + end_date_1 = current_date.replace(tzinfo=timezone.utc) + start_date_1 = (current_date - timedelta(days=1)).replace(tzinfo=timezone.utc) test_registry.apply_materialization( updated_feature_view, project, start_date_1, end_date_1 ) diff --git a/sdk/python/tests/utils/e2e_test_validation.py b/sdk/python/tests/utils/e2e_test_validation.py index 1a8bedc796..a08e8fef42 100644 --- a/sdk/python/tests/utils/e2e_test_validation.py +++ b/sdk/python/tests/utils/e2e_test_validation.py @@ -1,13 +1,12 @@ import math import os import time -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Dict, List, Optional, Union import pandas as pd import yaml -from pytz import utc from feast import FeatureStore, FeatureView, RepoConfig from feast.utils import _utc_now @@ -39,7 +38,7 @@ def validate_offline_online_store_consistency( # Run materialize() # use both tz-naive & tz-aware timestamps to test that they're both correctly handled - start_date = (now - timedelta(hours=5)).replace(tzinfo=utc) + start_date = (now - timedelta(hours=5)).replace(tzinfo=timezone.utc) end_date = split_dt fs.materialize(feature_views=[fv.name], start_date=start_date, end_date=end_date) @@ -87,7 +86,8 @@ def validate_offline_online_store_consistency( and updated_fv.materialization_intervals[0][0] == start_date and updated_fv.materialization_intervals[0][1] == end_date and updated_fv.materialization_intervals[1][0] == end_date - and updated_fv.materialization_intervals[1][1] == now.replace(tzinfo=utc) + and updated_fv.materialization_intervals[1][1] + == now.replace(tzinfo=timezone.utc) ) # check result of materialize_incremental() diff --git a/sdk/python/tests/utils/feature_records.py b/sdk/python/tests/utils/feature_records.py index bd3567c9ee..e81666eaa5 100644 --- a/sdk/python/tests/utils/feature_records.py +++ b/sdk/python/tests/utils/feature_records.py @@ -5,7 +5,6 @@ import pandas as pd import pytest from pandas.testing import assert_frame_equal as pd_assert_frame_equal -from pytz import utc from feast import FeatureService, FeatureStore, utils from feast.errors import FeatureNameCollisionError @@ -16,7 +15,7 @@ def convert_timestamp_records_to_utc( records: List[Dict[str, Any]], column: str ) -> List[Dict[str, Any]]: for record in records: - record[column] = utils.make_tzaware(record[column]).astimezone(utc) + record[column] = utils.make_tzaware(record[column]).astimezone(timezone.utc) return records diff --git a/sdk/python/tests/utils/test_log_creator.py b/sdk/python/tests/utils/test_log_creator.py index 987c8d77ef..3e432e11bf 100644 --- a/sdk/python/tests/utils/test_log_creator.py +++ b/sdk/python/tests/utils/test_log_creator.py @@ -1,7 +1,7 @@ import contextlib -import datetime import tempfile import uuid +from datetime import timedelta from pathlib import Path from typing import Iterator, List, Union @@ -80,7 +80,7 @@ def prepare_logs( logs_df[REQUEST_ID_FIELD] = [str(uuid.uuid4()) for _ in range(num_rows)] logs_df[LOG_TIMESTAMP_FIELD] = pd.Series( np.random.randint(0, 7 * 24 * 3600, num_rows) - ).map(lambda secs: pd.Timestamp.utcnow() - datetime.timedelta(seconds=secs)) + ).map(lambda secs: pd.Timestamp.utcnow() - timedelta(seconds=secs)) logs_df[LOG_DATE_FIELD] = logs_df[LOG_TIMESTAMP_FIELD].dt.date for projection in feature_service.feature_view_projections: From 160cd36542e37202bf4d836ad4e6b1d0fefb6779 Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Fri, 16 Aug 2024 23:25:35 -0400 Subject: [PATCH 021/185] Update model-inference.md --- .../architecture/model-inference.md | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/docs/getting-started/architecture/model-inference.md b/docs/getting-started/architecture/model-inference.md index 4fe2859c55..3a061603c1 100644 --- a/docs/getting-started/architecture/model-inference.md +++ b/docs/getting-started/architecture/model-inference.md @@ -3,7 +3,7 @@ Production machine learning systems can choose from four approaches to serving machine learning predictions (the output of model inference): 1. Online model inference with online features -2. Precomputed (batch) model predictions without online features +2. Offline mode inference without online features 3. Online model inference with online features and cached predictions 4. Online model inference without features @@ -27,10 +27,13 @@ features = store.get_online_features( model_predictions = model_server.predict(features) ``` -## 2. Precomputed (Batch) Model Predictions without Online Features +## 2. Offline Model Inference without Online Features Typically, Machine Learning teams find serving precomputed model predictions to be the most straightforward to implement. This approach simply treats the model predictions as a feature and serves them from the feature store using the standard -Feast sdk. +Feast sdk. These model predictions are typically generated through some batch process where the model scores are precomputed. +As a concrete example, the batch process can be as simple as a script that runs model inference locally for a set of users that +can output a CSV. This output file could be used for materialization so that the model could be served online as shown in the +code below. ```python model_predictions = store.get_online_features( feature_refs=[ @@ -85,4 +88,10 @@ approach is common in Large Language Models (LLMs) and other models that do not Note that generative models using Retrieval Augmented Generation (RAG) do require features where the [document embeddings](../../reference/alpha-vector-database.md) are treated as features, which Feast supports -(this would fall under "Online Model Inference with Online Features"). \ No newline at end of file +(this would fall under "Online Model Inference with Online Features"). + +### Client Orchestration +Implicit in the code examples above is a design choice about how clients orchestrate calls to get features and run model inference. +The examples had a Feast-centric pattern because they are inputs to the model, so the sequencing is fairly obvious. +An alternative approach can be Inference-centric where a client would call an inference endpoint and the inference +service would be responsible for orchestration. From 09b026bf365c29246c0c4a1ed538c5ccfe21a9c8 Mon Sep 17 00:00:00 2001 From: Tornike Gurgenidze Date: Sat, 17 Aug 2024 23:25:48 +0400 Subject: [PATCH 022/185] chore: Auto-detect python version in Makefile (#4419) --- Makefile | 13 +++++++------ docs/project/development-guide.md | 20 ++++++++++++-------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/Makefile b/Makefile index 5e3bd0d913..7119bad856 100644 --- a/Makefile +++ b/Makefile @@ -21,6 +21,7 @@ ifeq ($(shell uname -s), Darwin) OS = osx endif TRINO_VERSION ?= 376 +PYTHON_VERSION = ${shell python --version | grep -Eo '[0-9]\.[0-9]+'} # General @@ -37,22 +38,22 @@ build: protos build-java build-docker # Python SDK install-python-ci-dependencies: - python -m piptools sync sdk/python/requirements/py$(PYTHON)-ci-requirements.txt + python -m piptools sync sdk/python/requirements/py$(PYTHON_VERSION)-ci-requirements.txt pip install --no-deps -e . python setup.py build_python_protos --inplace install-python-ci-dependencies-uv: - uv pip sync --system sdk/python/requirements/py$(PYTHON)-ci-requirements.txt + uv pip sync --system sdk/python/requirements/py$(PYTHON_VERSION)-ci-requirements.txt uv pip install --system --no-deps -e . python setup.py build_python_protos --inplace install-python-ci-dependencies-uv-venv: - uv pip sync sdk/python/requirements/py$(PYTHON)-ci-requirements.txt + uv pip sync sdk/python/requirements/py$(PYTHON_VERSION)-ci-requirements.txt uv pip install --no-deps -e . python setup.py build_python_protos --inplace lock-python-ci-dependencies: - uv pip compile --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py$(PYTHON)-ci-requirements.txt + uv pip compile --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py$(PYTHON_VERSION)-ci-requirements.txt package-protos: cp -r ${ROOT_DIR}/protos ${ROOT_DIR}/sdk/python/feast/protos @@ -61,11 +62,11 @@ compile-protos-python: python setup.py build_python_protos --inplace install-python: - python -m piptools sync sdk/python/requirements/py$(PYTHON)-requirements.txt + python -m piptools sync sdk/python/requirements/py$(PYTHON_VERSION)-requirements.txt python setup.py develop lock-python-dependencies: - uv pip compile --system --no-strip-extras setup.py --output-file sdk/python/requirements/py$(PYTHON)-requirements.txt + uv pip compile --system --no-strip-extras setup.py --output-file sdk/python/requirements/py$(PYTHON_VERSION)-requirements.txt lock-python-dependencies-all: pixi run --environment py39 --manifest-path infra/scripts/pixi/pixi.toml "uv pip compile --system --no-strip-extras setup.py --output-file sdk/python/requirements/py3.9-requirements.txt" diff --git a/docs/project/development-guide.md b/docs/project/development-guide.md index e3b09294bc..cec4f68daf 100644 --- a/docs/project/development-guide.md +++ b/docs/project/development-guide.md @@ -132,8 +132,7 @@ Setting up your development environment for Feast Python SDK / CLI: source venv/bin/activate ``` 4. (M1 Mac only): Follow the [dev guide](https://github.com/feast-dev/feast/issues/2105) -5. Install uv -It is recommended to use uv for managing python dependencies. +5. Install uv. It is recommended to use uv for managing python dependencies. ```sh curl -LsSf https://astral.sh/uv/install.sh | sh ``` @@ -145,21 +144,26 @@ pip install uv ``` make build-ui ``` -7. (Optional) install pixi -pixi is necessary to run step 8 for all python versions at once. +7. (Optional) install pixi. pixi is necessary to run step 8 for all python versions at once. ```sh curl -fsSL https://pixi.sh/install.sh | bash ``` -8. (Optional): Recompile python lock files -If you make changes to requirements or simply want to update python lock files to reflect latest versioons. +8. (Optional): Recompile python lock files. Only when you make changes to requirements or simply want to update python lock files to reflect latest versioons. ```sh make lock-python-dependencies-all ``` -9. Install development dependencies for Feast Python SDK / CLI -This will install package versions from the lock file, install editable version of feast and compile protobufs. +9. Install development dependencies for Feast Python SDK / CLI. This will install package versions from the lock file, install editable version of feast and compile protobufs. + +If running inside a virtual environment: +```sh +make install-python-ci-dependencies-uv-venv +``` + +Otherwise: ```sh make install-python-ci-dependencies-uv ``` + 10. Spin up Docker Image ```sh docker build -t docker-whale -f ./sdk/python/feast/infra/feature_servers/multicloud/Dockerfile . From d235832b78027b98df8e8a9e434a51a0c78b3092 Mon Sep 17 00:00:00 2001 From: "bdodla@expedia.com" <13788369+EXPEbdodla@users.noreply.github.com> Date: Sun, 18 Aug 2024 09:46:17 -0700 Subject: [PATCH 023/185] fix: Default to pandas mode if not specified in ODFV proto in database (#4420) --- sdk/python/feast/on_demand_feature_view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/python/feast/on_demand_feature_view.py b/sdk/python/feast/on_demand_feature_view.py index aeb1cc207a..47fcf29926 100644 --- a/sdk/python/feast/on_demand_feature_view.py +++ b/sdk/python/feast/on_demand_feature_view.py @@ -346,7 +346,7 @@ def from_proto( ], sources=sources, feature_transformation=transformation, - mode=on_demand_feature_view_proto.spec.mode, + mode=on_demand_feature_view_proto.spec.mode or "pandas", description=on_demand_feature_view_proto.spec.description, tags=dict(on_demand_feature_view_proto.spec.tags), owner=on_demand_feature_view_proto.spec.owner, From 8181007dfb7c672f9984815f0813b181076f6926 Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Mon, 19 Aug 2024 12:30:36 -0400 Subject: [PATCH 024/185] chore: Update SUMMARY.md (#4422) Update SUMMARY.md --- docs/SUMMARY.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 0060ae729e..3cc2511288 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -8,6 +8,13 @@ ## Getting started * [Quickstart](getting-started/quickstart.md) +* [Architecture](getting-started/architecture/README.md) + * [Overview](getting-started/architecture/overview.md) + * [Language](getting-started/architecture/language.md) + * [Push vs Pull Model](getting-started/architecture/push-vs-pull-model.md) + * [Write Patterns](getting-started/architecture/write-patterns.md) + * [Feature Transformation](getting-started/architecture/feature-transformation.md) + * [Feature Serving and Model Inference](getting-started/architecture/model-inference.md) * [Concepts](getting-started/concepts/README.md) * [Overview](getting-started/concepts/overview.md) * [Data ingestion](getting-started/concepts/data-ingestion.md) @@ -17,13 +24,6 @@ * [Point-in-time joins](getting-started/concepts/point-in-time-joins.md) * [Registry](getting-started/concepts/registry.md) * [\[Alpha\] Saved dataset](getting-started/concepts/dataset.md) -* [Architecture](getting-started/architecture/README.md) - * [Overview](getting-started/architecture/overview.md) - * [Language](getting-started/architecture/language.md) - * [Push vs Pull Model](getting-started/architecture/push-vs-pull-model.md) - * [Write Patterns](getting-started/architecture/write-patterns.md) - * [Feature Transformation](getting-started/architecture/feature-transformation.md) - * [Feature Serving and Model Inference](getting-started/architecture/model-inference.md) * [Components](getting-started/components/README.md) * [Overview](getting-started/components/overview.md) * [Registry](getting-started/components/registry.md) From 66a0a38e72b9ff67dde971e0e0b239e9cfa7f27b Mon Sep 17 00:00:00 2001 From: Daniel Dowler <12484302+dandawg@users.noreply.github.com> Date: Mon, 19 Aug 2024 14:44:03 -0600 Subject: [PATCH 025/185] docs: Updated README template and fixed links to be consistent for HTML (#4423) * chore: fixed README template to be consistent with current README Signed-off-by: dandawg <12484302+dandawg@users.noreply.github.com> * docs: markdown links consistency with html Signed-off-by: dandawg <12484302+dandawg@users.noreply.github.com> --------- Signed-off-by: dandawg <12484302+dandawg@users.noreply.github.com> --- .github/pull_request_template.md | 2 +- docs/project/development-guide.md | 20 ++++++++++---------- infra/templates/README.md.jinja2 | 3 +++ 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 7849c24976..40986a87db 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,6 +1,6 @@ %BQ`n{qZQmCu0MT8gYHDMy_GLh?6ED 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%0DJ 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
%NIwF-+sr1~`Bj(o3 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 literal 0 HcmV?d00001 diff --git a/docs/getting-started/architecture/rbac.md b/docs/getting-started/architecture/rbac.md new file mode 100644 index 0000000000..9a51fba6ac --- /dev/null +++ b/docs/getting-started/architecture/rbac.md @@ -0,0 +1,56 @@ +# Role-Based Access Control (RBAC) in Feast + +## Introduction + +Role-Based Access Control (RBAC) is a security mechanism that restricts access to resources based on the roles of individual users within an organization. In the context of the Feast, RBAC ensures that only authorized users or groups can access or modify specific resources, thereby maintaining data security and operational integrity. + +## Functional Requirements + +The RBAC implementation in Feast is designed to: + +- **Assign Permissions**: Allow administrators to assign permissions for various operations and resources to users or groups based on their roles. +- **Seamless Integration**: Integrate smoothly with existing business code without requiring significant modifications. +- **Backward Compatibility**: Maintain support for non-authorized models as the default to ensure backward compatibility. + +## Business Goals + +The primary business goals of implementing RBAC in the Feast are: + +1. **Feature Sharing**: Enable multiple teams to share the feature store while ensuring controlled access. This allows for collaborative work without compromising data security. +2. **Access Control Management**: Prevent unauthorized access to team-specific resources and spaces, governing the operations that each user or group can perform. + +## Reference Architecture + +Feast operates as a collection of connected services, each enforcing authorization permissions. The architecture is designed as a distributed microservices system with the following key components: + +- **Service Endpoints**: These enforce authorization permissions, ensuring that only authorized requests are processed. +- **Client Integration**: Clients authenticate with feature servers by attaching authorization token to each request. +- **Service-to-Service Communication**: This is always granted. + +![rbac.jpg](rbac.jpg) + +## Permission Model + +The RBAC system in Feast uses a permission model that defines the following concepts: + +- **Resource**: An object within Feast that needs to be secured against unauthorized access. +- **Action**: A logical operation performed on a resource, such as Create, Describe, Update, Delete, Read, or write operations. +- **Policy**: A set of rules that enforce authorization decisions on resources. The default implementation uses role-based policies. + + + +## Authorization Architecture + +The authorization architecture in Feast is built with the following components: + +- **Token Extractor**: Extracts the authorization token from the request header. +- **Token Parser**: Parses the token to retrieve user details. +- **Policy Enforcer**: Validates the secured endpoint against the retrieved user details. +- **Token Injector**: Adds the authorization token to each secured request header. + + + + + + + diff --git a/docs/getting-started/components/README.md b/docs/getting-started/components/README.md index d468714bd4..e1c000abce 100644 --- a/docs/getting-started/components/README.md +++ b/docs/getting-started/components/README.md @@ -19,3 +19,7 @@ {% content-ref url="provider.md" %} [provider.md](provider.md) {% endcontent-ref %} + +{% content-ref url="authz_manager.md" %} +[authz_manager.md](authz_manager.md) +{% endcontent-ref %} diff --git a/docs/getting-started/components/authz_manager.md b/docs/getting-started/components/authz_manager.md new file mode 100644 index 0000000000..09ca4d1366 --- /dev/null +++ b/docs/getting-started/components/authz_manager.md @@ -0,0 +1,102 @@ +# Authorization Manager +An Authorization Manager is an instance of the `AuthManager` class that is plugged into one of the Feast servers to extract user details from the current request and inject them into the [permissions](../../getting-started/concepts/permissions.md) framework. + +{% hint style="info" %} +**Note**: Feast does not provide authentication capabilities; it is the client's responsibility to manage the authentication token and pass it to +the Feast server, which then validates the token and extracts user details from the configured authentication server. +{% endhint %} + +Two authorization managers are supported out-of-the-box: +* One using a configurable OIDC server to extract the user details. +* One using the Kubernetes RBAC resources to extract the user details. + +These instances are created when the Feast servers are initialized, according to the authorization configuration defined in +their own `feature_store.yaml`. + +Feast servers and clients must have consistent authorization configuration, so that the client proxies can automatically inject +the authorization tokens that the server can properly identify and use to enforce permission validations. + + +## Design notes +The server-side implementation of the authorization functionality is defined [here](./../../../sdk/python/feast/permissions/server). +Few of the key models, classes to understand the authorization implementation on the client side can be found [here](./../../../sdk/python/feast/permissions/client). + +## Configuring Authorization +The authorization is configured using a dedicated `auth` section in the `feature_store.yaml` configuration. + +**Note**: As a consequence, when deploying the Feast servers with the Helm [charts](../../../infra/charts/feast-feature-server/README.md), +the `feature_store_yaml_base64` value must include the `auth` section to specify the authorization configuration. + +### No Authorization +This configuration applies the default `no_auth` authorization: +```yaml +project: my-project +auth: + type: no_auth +... +``` + +### OIDC Authorization +With OIDC authorization, the Feast client proxies retrieve the JWT token from an OIDC server (or [Identity Provider](https://openid.net/developers/how-connect-works/)) +and append it in every request to a Feast server, using an [Authorization Bearer Token](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#bearer). + +The server, in turn, uses the same OIDC server to validate the token and extract the user roles from the token itself. + +Some assumptions are made in the OIDC server configuration: +* The OIDC token refers to a client with roles matching the RBAC roles of the configured `Permission`s (*) +* The roles are exposed in the access token passed to the server + +(*) Please note that **the role match is case-sensitive**, e.g. the name of the role in the OIDC server and in the `Permission` configuration +must be exactly the same. + +For example, the access token for a client `app` of a user with `reader` role should have the following `resource_access` section: +```json +{ + "resource_access": { + "app": { + "roles": [ + "reader" + ] + }, +} +``` + +An example of OIDC authorization configuration is the following: +```yaml +project: my-project +auth: + type: oidc + client_id: _CLIENT_ID__ + client_secret: _CLIENT_SECRET__ + realm: _REALM__ + auth_server_url: _OIDC_SERVER_URL_ + auth_discovery_url: _OIDC_SERVER_URL_/realms/master/.well-known/openid-configuration +... +``` + +In case of client configuration, the following settings must be added to specify the current user: +```yaml +auth: + ... + username: _USERNAME_ + password: _PASSWORD_ +``` + +### Kubernetes RBAC Authorization +With Kubernetes RBAC Authorization, the client uses the service account token as the authorizarion bearer token, and the +server fetches the associated roles from the Kubernetes RBAC resources. + +An example of Kubernetes RBAC authorization configuration is the following: +{% hint style="info" %} +**NOTE**: This configuration will only work if you deploy feast on Openshift or a Kubernetes platform. +{% endhint %} +```yaml +project: my-project +auth: + type: kubernetes +... +``` + +In case the client cannot run on the same cluster as the servers, the client token can be injected using the `LOCAL_K8S_TOKEN` +environment variable on the client side. The value must refer to the token of a service account created on the servers cluster +and linked to the desired RBAC roles. \ No newline at end of file diff --git a/docs/getting-started/components/overview.md b/docs/getting-started/components/overview.md index 393f436e5b..0ee3835de6 100644 --- a/docs/getting-started/components/overview.md +++ b/docs/getting-started/components/overview.md @@ -28,3 +28,4 @@ A complete Feast deployment contains the following components: * **Batch Materialization Engine:** The [Batch Materialization Engine](batch-materialization-engine.md) component launches a process which loads data into the online store from the offline store. By default, Feast uses a local in-process engine implementation to materialize data. However, additional infrastructure can be used for a more scalable materialization process. * **Online Store:** The online store is a database that stores only the latest feature values for each entity. The online store is either populated through materialization jobs or through [stream ingestion](../../reference/data-sources/push.md). * **Offline Store:** The offline store persists batch data that has been ingested into Feast. This data is used for producing training datasets. For feature retrieval and materialization, Feast does not manage the offline store directly, but runs queries against it. However, offline stores can be configured to support writes if Feast configures logging functionality of served features. +* **Authorization manager**: The authorization manager detects authentication tokens from client requests to Feast servers and uses this information to enforce permission policies on the requested services. diff --git a/docs/getting-started/concepts/README.md b/docs/getting-started/concepts/README.md index e805e3b486..1769a2d741 100644 --- a/docs/getting-started/concepts/README.md +++ b/docs/getting-started/concepts/README.md @@ -31,3 +31,7 @@ {% content-ref url="dataset.md" %} [dataset.md](dataset.md) {% endcontent-ref %} + +{% content-ref url="permission.md" %} +[permission.md](permission.md) +{% endcontent-ref %} diff --git a/docs/getting-started/concepts/permission.md b/docs/getting-started/concepts/permission.md new file mode 100644 index 0000000000..5bca1bd568 --- /dev/null +++ b/docs/getting-started/concepts/permission.md @@ -0,0 +1,112 @@ +# Permission + +## Overview + +The Feast permissions model allows to configure granular permission policies to all the resources defined in a feature store. + +The configured permissions are stored in the Feast registry and accessible through the CLI and the registry APIs. + +The permission authorization enforcement is performed when requests are executed through one of the Feast (Python) servers +- The online feature server (REST) +- The offline feature server (Arrow Flight) +- The registry server (gRPC) + +Note that there is no permission enforcement when accessing the Feast API with a local provider. + +## Concepts + +The permission model is based on the following components: +- A `resource` is a Feast object that we want to secure against unauthorized access. + - We assume that the resource has a `name` attribute and optional dictionary of associated key-value `tags`. +- An `action` is a logical operation executed on the secured resource, like: + - `create`: Create an instance. + - `describe`: Access the instance state. + - `update`: Update the instance state. + - `delete`: Delete an instance. + - `read`: Read both online and offline stores. + - `read_online`: Read the online store. + - `read_offline`: Read the offline store. + - `write`: Write on any store. + - `write_online`: Write to the online store. + - `write_offline`: Write to the offline store. +- A `policy` identifies the rule for enforcing authorization decisions on secured resources, based on the current user. + - A default implementation is provided for role-based policies, using the user roles to grant or deny access to the requested actions + on the secured resources. + +The `Permission` class identifies a single permission configured on the feature store and is identified by these attributes: +- `name`: The permission name. +- `types`: The list of protected resource types. Defaults to all managed types, e.g. the `ALL_RESOURCE_TYPES` alias. All sub-classes are included in the resource match. +- `name_pattern`: A regex to match the resource name. Defaults to `None`, meaning that no name filtering is applied +- `required_tags`: Dictionary of key-value pairs that must match the resource tags. Defaults to `None`, meaning that no tags filtering is applied. +- `actions`: The actions authorized by this permission. Defaults to `ALL_VALUES`, an alias defined in the `action` module. +- `policy`: The policy to be applied to validate a client request. + +To simplify configuration, several constants are defined to streamline the permissions setup: +- In module `feast.feast_object`: + - `ALL_RESOURCE_TYPES` is the list of all the `FeastObject` types. + - `ALL_FEATURE_VIEW_TYPES` is the list of all the feature view types, including those not inheriting from `FeatureView` type like + `OnDemandFeatureView`. +- In module `feast.permissions.action`: + - `ALL_ACTIONS` is the list of all managed actions. + - `READ` includes all the read actions for online and offline store. + - `WRITE` includes all the write actions for online and offline store. + - `CRUD` includes all the state management actions to create, describe, update or delete a Feast resource. + +Given the above definitions, the feature store can be configured with granular control over each resource, enabling partitioned access by +teams to meet organizational requirements for service and data sharing, and protection of sensitive information. + +The `feast` CLI includes a new `permissions` command to list the registered permissions, with options to identify the matching resources for each configured permission and the existing resources that are not covered by any permission. + +{% hint style="info" %} +**Note**: Feast resources that do not match any of the configured permissions are not secured by any authorization policy, meaning any user can execute any action on such resources. +{% endhint %} + +## Definition examples +This permission definition grants access to the resource state and the ability to read all of the stores for any feature view or +feature service to all users with the role `super-reader`: +```py +Permission( + name="feature-reader", + types=[FeatureView, FeatureService], + policy=RoleBasedPolicy(roles=["super-reader"]), + actions=[AuthzedAction.DESCRIBE, READ], +) +``` + +This example grants permission to write on all the data sources with `risk_level` tag set to `high` only to users with role `admin` or `data_team`: +```py +Permission( + name="ds-writer", + types=[DataSource], + required_tags={"risk_level": "high"}, + policy=RoleBasedPolicy(roles=["admin", "data_team"]), + actions=[AuthzedAction.WRITE], +) +``` + +{% hint style="info" %} +**Note**: When using multiple roles in a role-based policy, the user must be granted at least one of the specified roles. +{% endhint %} + + +The following permission grants authorization to read the offline store of all the feature views including `risky` in the name, to users with role `trusted`: + +```py +Permission( + name="reader", + types=[FeatureView], + name_pattern=".*risky.*", + policy=RoleBasedPolicy(roles=["trusted"]), + actions=[AuthzedAction.READ_OFFLINE], +) +``` + +## Authorization configuration +In order to leverage the permission functionality, the `auth` section is needed in the `feature_store.yaml` configuration. +Currently, Feast supports OIDC and Kubernetes RBAC authorization protocols. + +The default configuration, if you don't specify the `auth` configuration section, is `no_auth`, indicating that no permission +enforcement is applied. + +The `auth` section includes a `type` field specifying the actual authorization protocol, and protocol-specific fields that +are specified in [Authorization Manager](../components/authz_manager.md). diff --git a/docs/reference/feast-cli-commands.md b/docs/reference/feast-cli-commands.md index afcfcfef64..be31720034 100644 --- a/docs/reference/feast-cli-commands.md +++ b/docs/reference/feast-cli-commands.md @@ -24,6 +24,7 @@ Commands: init Create a new Feast repository materialize Run a (non-incremental) materialization job to... materialize-incremental Run an incremental materialization job to ingest... + permissions Access permissions registry-dump Print contents of the metadata registry teardown Tear down deployed feature store infrastructure version Display Feast SDK version @@ -155,6 +156,143 @@ Load data from feature views into the online store, beginning from either the pr feast materialize-incremental 2022-01-01T00:00:00 ``` +## Permissions + +### List permissions +List all registered permission + +```text +feast permissions list + +Options: + --tags TEXT Filter by tags (e.g. --tags 'key:value' --tags 'key:value, + key:value, ...'). Items return when ALL tags match. + -v, --verbose Print the resources matching each configured permission +``` + +```text ++-----------------------+-------------+-----------------------+-----------+----------------+-------------------------+ +| NAME | TYPES | NAME_PATTERN | ACTIONS | ROLES | REQUIRED_TAGS | ++=======================+=============+=======================+===========+================+================+========+ +| reader_permission1234 | FeatureView | transformed_conv_rate | DESCRIBE | reader | - | ++-----------------------+-------------+-----------------------+-----------+----------------+-------------------------+ +| writer_permission1234 | FeatureView | transformed_conv_rate | CREATE | writer | - | ++-----------------------+-------------+-----------------------+-----------+----------------+-------------------------+ +| special | FeatureView | special.* | DESCRIBE | admin | test-key2 : test-value2 | +| | | | UPDATE | special-reader | test-key : test-value | ++-----------------------+-------------+-----------------------+-----------+----------------+-------------------------+ +``` + +`verbose` option describes the resources matching each configured permission: + +```text +feast permissions list -v +``` + +```text +Permissions: + +permissions +├── reader_permission1234 ['reader'] +│ └── FeatureView: none +└── writer_permission1234 ['writer'] + ├── FeatureView: none + │── OnDemandFeatureView: ['transformed_conv_rate_fresh', 'transformed_conv_rate'] + └── BatchFeatureView: ['driver_hourly_stats', 'driver_hourly_stats_fresh'] +``` + +### Describe a permission +Describes the provided permission + +```text +feast permissions describe permission-name +name: permission-name +types: +- FEATURE_VIEW +namePattern: transformed_conv_rate +requiredTags: + required1: required-value1 + required2: required-value2 +actions: +- DESCRIBE +policy: + roleBasedPolicy: + roles: + - reader +tags: + key1: value1 + key2: value2 + +``` + +### List of the configured roles +List all the configured roles + +```text +feast permissions list-roles + +Options: + --verbose Print the resources and actions permitted to each configured + role +``` + +```text +ROLE NAME +admin +reader +writer +``` + +`verbose` option describes the resources and actions permitted to each managed role: + +```text +feast permissions list-roles -v +``` + +```text +ROLE NAME RESOURCE NAME RESOURCE TYPE PERMITTED ACTIONS +admin driver_hourly_stats_source FileSource CREATE + DELETE + QUERY_OFFLINE + QUERY_ONLINE + DESCRIBE + UPDATE +admin vals_to_add RequestSource CREATE + DELETE + QUERY_OFFLINE + QUERY_ONLINE + DESCRIBE + UPDATE +admin driver_stats_push_source PushSource CREATE + DELETE + QUERY_OFFLINE + QUERY_ONLINE + DESCRIBE + UPDATE +admin driver_hourly_stats_source FileSource CREATE + DELETE + QUERY_OFFLINE + QUERY_ONLINE + DESCRIBE + UPDATE +admin vals_to_add RequestSource CREATE + DELETE + QUERY_OFFLINE + QUERY_ONLINE + DESCRIBE + UPDATE +admin driver_stats_push_source PushSource CREATE + DELETE + QUERY_OFFLINE + QUERY_ONLINE + DESCRIBE + UPDATE +reader driver_hourly_stats FeatureView DESCRIBE +reader driver_hourly_stats_fresh FeatureView DESCRIBE +... +``` + + ## Teardown Tear down deployed feature store infrastructure diff --git a/docs/reference/feature-servers/offline-feature-server.md b/docs/reference/feature-servers/offline-feature-server.md index 6c2fdf7a25..1db5adacd8 100644 --- a/docs/reference/feature-servers/offline-feature-server.md +++ b/docs/reference/feature-servers/offline-feature-server.md @@ -33,3 +33,20 @@ Please see the detail how to configure offline store client [remote-offline-stor The set of functionalities supported by remote offline stores is the same as those supported by offline stores with the SDK, which are described in detail [here](../offline-stores/overview.md#functionality). +# Offline Feature Server Permissions and Access Control + +## API Endpoints and Permissions + +| Endpoint | Resource Type | Permission | Description | +| ------------------------------------- |------------------|---------------|---------------------------------------------------| +| offline_write_batch | FeatureView | Write Offline | Write a batch of data to the offline store | +| write_logged_features | FeatureService | Write Offline | Write logged features to the offline store | +| persist | DataSource | Write Offline | Persist the result of a read in the offline store | +| get_historical_features | FeatureView | Read Offline | Retrieve historical features | +| pull_all_from_table_or_query | DataSource | Read Offline | Pull all data from a table or read it | +| pull_latest_from_table_or_query | DataSource | Read Offline | Pull the latest data from a table or read it | + + +## How to configure Authentication and Authorization ? + +Please refer the [page](./../../../docs/getting-started/concepts/permission.md) for more details on how to configure authentication and authorization. \ No newline at end of file diff --git a/docs/reference/feature-servers/python-feature-server.md b/docs/reference/feature-servers/python-feature-server.md index 33dfe77ae1..255b85e606 100644 --- a/docs/reference/feature-servers/python-feature-server.md +++ b/docs/reference/feature-servers/python-feature-server.md @@ -199,3 +199,19 @@ requests.post( "http://localhost:6566/push", data=json.dumps(push_data)) ``` + +# Online Feature Server Permissions and Access Control + +## API Endpoints and Permissions + +| Endpoint | Resource Type | Permission | Description | +| ---------------------------- |---------------------------------|-------------------------------------------------------| ------------------------------------------------------------------------ | +| /get-online-features | FeatureView,OnDemandFeatureView | Read Online | Get online features from the feature store | +| /push | FeatureView | Write Online, Write Offline, Write Online and Offline | Push features to the feature store (online, offline, or both) | +| /write-to-online-store | FeatureView | Write Online | Write features to the online store | +| /materialize | FeatureView | Write Online | Materialize features within a specified time range | +| /materialize-incremental | FeatureView | Write Online | Incrementally materialize features up to a specified timestamp | + +## How to configure Authentication and Authorization ? + +Please refer the [page](./../../../docs/getting-started/concepts/permission.md) for more details on how to configure authentication and authorization. \ No newline at end of file diff --git a/docs/reference/offline-stores/remote-offline-store.md b/docs/reference/offline-stores/remote-offline-store.md index 0179e0f06f..8057ae3284 100644 --- a/docs/reference/offline-stores/remote-offline-store.md +++ b/docs/reference/offline-stores/remote-offline-store.md @@ -25,4 +25,7 @@ The complete example can be find under [remote-offline-store-example](../../../e ## How to configure the server -Please see the detail how to configure offline feature server [offline-feature-server.md](../feature-servers/offline-feature-server.md) \ No newline at end of file +Please see the detail how to configure offline feature server [offline-feature-server.md](../feature-servers/offline-feature-server.md) + +## How to configure Authentication and Authorization +Please refer the [page](./../../../docs/getting-started/concepts/permission.md) for more details on how to configure authentication and authorization. diff --git a/docs/reference/online-stores/remote.md b/docs/reference/online-stores/remote.md index c560fa6f22..4dd4fb65b5 100644 --- a/docs/reference/online-stores/remote.md +++ b/docs/reference/online-stores/remote.md @@ -11,11 +11,17 @@ The registry is pointing to registry of remote feature store. If it is not acces {% code title="feature_store.yaml" %} ```yaml project: my-local-project - registry: /remote/data/registry.db - provider: local - online_store: - path: http://localhost:6566 - type: remote - entity_key_serialization_version: 2 +registry: /remote/data/registry.db +provider: local +online_store: + path: http://localhost:6566 + type: remote +entity_key_serialization_version: 2 +auth: + type: no_auth ``` -{% endcode %} \ No newline at end of file +{% endcode %} + +## How to configure Authentication and Authorization +Please refer the [page](./../../../docs/getting-started/concepts/permission.md) for more details on how to configure authentication and authorization. + diff --git a/docs/reference/registry/registry-permissions.md b/docs/reference/registry/registry-permissions.md new file mode 100644 index 0000000000..65508ef5b2 --- /dev/null +++ b/docs/reference/registry/registry-permissions.md @@ -0,0 +1,45 @@ +# Registry Permissions and Access Control + + +## API Endpoints and Permissions + +| Endpoint | Resource Type | Permission | Description | +| ------------------------ |---------------------|------------------------| -------------------------------------------------------------- | +| ApplyEntity | Entity | Create, Update, Delete | Apply an entity to the registry | +| GetEntity | Entity | Read | Get an entity from the registry | +| ListEntities | Entity | Read | List entities in the registry | +| DeleteEntity | Entity | Delete | Delete an entity from the registry | +| ApplyDataSource | DataSource | Create, Update, Delete | Apply a data source to the registry | +| GetDataSource | DataSource | Read | Get a data source from the registry | +| ListDataSources | DataSource | Read | List data sources in the registry | +| DeleteDataSource | DataSource | Delete | Delete a data source from the registry | +| ApplyFeatureView | FeatureView | Create, Update, Delete | Apply a feature view to the registry | +| GetFeatureView | FeatureView | Read | Get a feature view from the registry | +| ListFeatureViews | FeatureView | Read | List feature views in the registry | +| DeleteFeatureView | FeatureView | Delete | Delete a feature view from the registry | +| GetStreamFeatureView | StreamFeatureView | Read | Get a stream feature view from the registry | +| ListStreamFeatureViews | StreamFeatureView | Read | List stream feature views in the registry | +| GetOnDemandFeatureView | OnDemandFeatureView | Read | Get an on-demand feature view from the registry | +| ListOnDemandFeatureViews | OnDemandFeatureView | Read | List on-demand feature views in the registry | +| ApplyFeatureService | FeatureService | Create, Update, Delete | Apply a feature service to the registry | +| GetFeatureService | FeatureService | Read | Get a feature service from the registry | +| ListFeatureServices | FeatureService | Read | List feature services in the registry | +| DeleteFeatureService | FeatureService | Delete | Delete a feature service from the registry | +| ApplySavedDataset | SavedDataset | Create, Update, Delete | Apply a saved dataset to the registry | +| GetSavedDataset | SavedDataset | Read | Get a saved dataset from the registry | +| ListSavedDatasets | SavedDataset | Read | List saved datasets in the registry | +| DeleteSavedDataset | SavedDataset | Delete | Delete a saved dataset from the registry | +| ApplyValidationReference | ValidationReference | Create, Update, Delete | Apply a validation reference to the registry | +| GetValidationReference | ValidationReference | Read | Get a validation reference from the registry | +| ListValidationReferences | ValidationReference | Read | List validation references in the registry | +| DeleteValidationReference| ValidationReference | Delete | Delete a validation reference from the registry | +| ApplyPermission | Permission | Create, Update, Delete | Apply a permission to the registry | +| GetPermission | Permission | Read | Get a permission from the registry | +| ListPermissions | Permission | Read | List permissions in the registry | +| DeletePermission | Permission | Delete | Delete a permission from the registry | +| Commit | | None | Commit changes to the registry | +| Refresh | | None | Refresh the registry | +| Proto | | None | Get the proto representation of the registry | + +## How to configure Authentication and Authorization +Please refer the [page](./../../../docs/getting-started/concepts/permission.md) for more details on how to configure authentication and authorization. diff --git a/protos/feast/core/Permission.proto b/protos/feast/core/Permission.proto new file mode 100644 index 0000000000..57958d3d81 --- /dev/null +++ b/protos/feast/core/Permission.proto @@ -0,0 +1,69 @@ +syntax = "proto3"; +package feast.core; + +option go_package = "github.com/feast-dev/feast/go/protos/feast/core"; +option java_outer_classname = "PermissionProto"; +option java_package = "feast.proto.core"; + +import "feast/core/Policy.proto"; +import "google/protobuf/timestamp.proto"; + +message Permission { + // User-specified specifications of this permission. + PermissionSpec spec = 1; + + // System-populated metadata for this permission. + PermissionMeta meta = 2; +} + +message PermissionSpec { + enum AuthzedAction { + CREATE = 0; + DESCRIBE = 1; + UPDATE = 2; + DELETE = 3; + READ_ONLINE = 4; + READ_OFFLINE = 5; + WRITE_ONLINE = 6; + WRITE_OFFLINE = 7; + } + + // Name of the permission. Must be unique. Not updated. + string name = 1; + + // Name of Feast project. + string project = 2; + + enum Type { + FEATURE_VIEW = 0; + ON_DEMAND_FEATURE_VIEW = 1; + BATCH_FEATURE_VIEW = 2; + STREAM_FEATURE_VIEW= 3; + ENTITY = 4; + FEATURE_SERVICE = 5; + DATA_SOURCE = 6; + VALIDATION_REFERENCE = 7; + SAVED_DATASET = 8; + PERMISSION = 9; + } + + repeated Type types = 3; + + string name_pattern = 4; + + map required_tags = 5; + + // List of actions. + repeated AuthzedAction actions = 6; + + // the policy. + Policy policy = 7; + + // User defined metadata + map tags = 8; +} + +message PermissionMeta { + google.protobuf.Timestamp created_timestamp = 1; + google.protobuf.Timestamp last_updated_timestamp = 2; +} diff --git a/protos/feast/core/Policy.proto b/protos/feast/core/Policy.proto new file mode 100644 index 0000000000..7ad42b9797 --- /dev/null +++ b/protos/feast/core/Policy.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; +package feast.core; + +option go_package = "github.com/feast-dev/feast/go/protos/feast/core"; +option java_outer_classname = "PolicyProto"; +option java_package = "feast.proto.core"; + +message Policy { + // Name of the policy. + string name = 1; + + // Name of Feast project. + string project = 2; + + oneof policy_type { + RoleBasedPolicy role_based_policy = 3; + } +} + +message RoleBasedPolicy { + // List of roles in this policy. + repeated string roles = 1; +} diff --git a/protos/feast/core/Registry.proto b/protos/feast/core/Registry.proto index 0c3f8a53f9..b4f1ffb0a3 100644 --- a/protos/feast/core/Registry.proto +++ b/protos/feast/core/Registry.proto @@ -32,8 +32,9 @@ import "feast/core/DataSource.proto"; import "feast/core/SavedDataset.proto"; import "feast/core/ValidationProfile.proto"; import "google/protobuf/timestamp.proto"; +import "feast/core/Permission.proto"; -// Next id: 16 +// Next id: 17 message Registry { repeated Entity entities = 1; repeated FeatureTable feature_tables = 2; @@ -51,6 +52,7 @@ message Registry { string registry_schema_version = 3; // to support migrations; incremented when schema is changed string version_id = 4; // version id, random string generated on each update of the data; now used only for debugging purposes google.protobuf.Timestamp last_updated = 5; + repeated Permission permissions = 16; } message ProjectMetadata { diff --git a/protos/feast/registry/RegistryServer.proto b/protos/feast/registry/RegistryServer.proto index 44529f5409..928354077b 100644 --- a/protos/feast/registry/RegistryServer.proto +++ b/protos/feast/registry/RegistryServer.proto @@ -14,6 +14,7 @@ import "feast/core/FeatureService.proto"; import "feast/core/SavedDataset.proto"; import "feast/core/ValidationProfile.proto"; import "feast/core/InfraObject.proto"; +import "feast/core/Permission.proto"; service RegistryServer{ // Entity RPCs @@ -59,7 +60,13 @@ service RegistryServer{ rpc GetValidationReference (GetValidationReferenceRequest) returns (feast.core.ValidationReference) {} rpc ListValidationReferences (ListValidationReferencesRequest) returns (ListValidationReferencesResponse) {} rpc DeleteValidationReference (DeleteValidationReferenceRequest) returns (google.protobuf.Empty) {} - + + // Permission RPCs + rpc ApplyPermission (ApplyPermissionRequest) returns (google.protobuf.Empty) {} + rpc GetPermission (GetPermissionRequest) returns (feast.core.Permission) {} + rpc ListPermissions (ListPermissionsRequest) returns (ListPermissionsResponse) {} + rpc DeletePermission (DeletePermissionRequest) returns (google.protobuf.Empty) {} + rpc ApplyMaterialization (ApplyMaterializationRequest) returns (google.protobuf.Empty) {} rpc ListProjectMetadata (ListProjectMetadataRequest) returns (ListProjectMetadataResponse) {} rpc UpdateInfra (UpdateInfraRequest) returns (google.protobuf.Empty) {} @@ -277,6 +284,7 @@ message GetSavedDatasetRequest { message ListSavedDatasetsRequest { string project = 1; bool allow_cache = 2; + map tags = 3; } message ListSavedDatasetsResponse { @@ -306,6 +314,7 @@ message GetValidationReferenceRequest { message ListValidationReferencesRequest { string project = 1; bool allow_cache = 2; + map tags = 3; } message ListValidationReferencesResponse { @@ -316,4 +325,34 @@ message DeleteValidationReferenceRequest { string name = 1; string project = 2; bool commit = 3; -} \ No newline at end of file +} + +// Permissions + +message ApplyPermissionRequest { + feast.core.Permission permission = 1; + string project = 2; + bool commit = 3; +} + +message GetPermissionRequest { + string name = 1; + string project = 2; + bool allow_cache = 3; +} + +message ListPermissionsRequest { + string project = 1; + bool allow_cache = 2; + map tags = 3; +} + +message ListPermissionsResponse { + repeated feast.core.Permission permissions = 1; +} + +message DeletePermissionRequest { + string name = 1; + string project = 2; + bool commit = 3; +} diff --git a/sdk/python/docs/source/feast.rst b/sdk/python/docs/source/feast.rst index 95fbea8d7a..83137574dd 100644 --- a/sdk/python/docs/source/feast.rst +++ b/sdk/python/docs/source/feast.rst @@ -12,6 +12,7 @@ Subpackages feast.embedded_go feast.infra feast.loaders + feast.permissions feast.protos feast.transformation feast.ui @@ -251,6 +252,14 @@ feast.proto\_json module :undoc-members: :show-inheritance: +feast.prova module +------------------ + +.. automodule:: feast.prova + :members: + :undoc-members: + :show-inheritance: + feast.registry\_server module ----------------------------- diff --git a/sdk/python/feast/cli.py b/sdk/python/feast/cli.py index f4e3e97d27..737704dd36 100644 --- a/sdk/python/feast/cli.py +++ b/sdk/python/feast/cli.py @@ -16,23 +16,27 @@ from datetime import datetime from importlib.metadata import version as importlib_version from pathlib import Path -from typing import List, Optional +from typing import Any, List, Optional import click import yaml +from bigtree import Node from colorama import Fore, Style from dateutil import parser from pygments import formatters, highlight, lexers -from feast import utils +import feast.cli_utils as cli_utils +from feast import BatchFeatureView, Entity, FeatureService, StreamFeatureView, utils from feast.constants import ( DEFAULT_FEATURE_TRANSFORMATION_SERVER_PORT, DEFAULT_OFFLINE_SERVER_PORT, DEFAULT_REGISTRY_SERVER_PORT, ) +from feast.data_source import DataSource from feast.errors import FeastObjectNotFoundException, FeastProviderLoginError from feast.feature_view import FeatureView from feast.on_demand_feature_view import OnDemandFeatureView +from feast.permissions.policy import RoleBasedPolicy from feast.repo_config import load_repo_config from feast.repo_operations import ( apply_total, @@ -44,6 +48,7 @@ registry_dump, teardown, ) +from feast.saved_dataset import SavedDataset, ValidationReference from feast.utils import maybe_local_tz _logger = logging.getLogger(__name__) @@ -879,5 +884,253 @@ def validate( exit(1) +@cli.group(name="permissions") +def feast_permissions_cmd(): + """ + Access permissions + """ + pass + + +@feast_permissions_cmd.command(name="list") +@click.option( + "--verbose", + "-v", + is_flag=True, + help="Print the resources matching each configured permission", +) +@tagsOption +@click.pass_context +def feast_permissions_list_command(ctx: click.Context, verbose: bool, tags: list[str]): + from tabulate import tabulate + + table: list[Any] = [] + tags_filter = utils.tags_list_to_dict(tags) + + store = create_feature_store(ctx) + + permissions = store.list_permissions(tags=tags_filter) + + root_node = Node("permissions") + roles: set[str] = set() + + for p in permissions: + policy = p.policy + if not verbose: + cli_utils.handle_not_verbose_permissions_command(p, policy, table) + else: + if isinstance(policy, RoleBasedPolicy) and len(policy.get_roles()) > 0: + roles = set(policy.get_roles()) + permission_node = Node( + p.name + " " + str(list(roles)), parent=root_node + ) + else: + permission_node = Node(p.name, parent=root_node) + + for feast_type in p.types: + if feast_type in [ + FeatureView, + OnDemandFeatureView, + BatchFeatureView, + StreamFeatureView, + ]: + cli_utils.handle_fv_verbose_permissions_command( + feast_type, # type: ignore[arg-type] + p, + permission_node, + store, + tags_filter, + ) + elif feast_type == Entity: + cli_utils.handle_entity_verbose_permissions_command( + feast_type, # type: ignore[arg-type] + p, + permission_node, + store, + tags_filter, + ) + elif feast_type == FeatureService: + cli_utils.handle_fs_verbose_permissions_command( + feast_type, # type: ignore[arg-type] + p, + permission_node, + store, + tags_filter, + ) + elif feast_type == DataSource: + cli_utils.handle_ds_verbose_permissions_command( + feast_type, # type: ignore[arg-type] + p, + permission_node, + store, + tags_filter, + ) + elif feast_type == ValidationReference: + cli_utils.handle_vr_verbose_permissions_command( + feast_type, # type: ignore[arg-type] + p, + permission_node, + store, + tags_filter, + ) + elif feast_type == SavedDataset: + cli_utils.handle_sd_verbose_permissions_command( + feast_type, # type: ignore[arg-type] + p, + permission_node, + store, + tags_filter, + ) + + if not verbose: + print( + tabulate( + table, + headers=[ + "NAME", + "TYPES", + "NAME_PATTERN", + "ACTIONS", + "ROLES", + "REQUIRED_TAGS", + ], + tablefmt="plain", + ) + ) + else: + cli_utils.print_permission_verbose_example() + + print("Permissions:") + print("") + root_node.show() + + +@feast_permissions_cmd.command("describe") +@click.argument("name", type=click.STRING) +@click.pass_context +def permission_describe(ctx: click.Context, name: str): + """ + Describe a permission + """ + store = create_feature_store(ctx) + + try: + permission = store.get_permission(name) + except FeastObjectNotFoundException as e: + print(e) + exit(1) + + print( + yaml.dump( + yaml.safe_load(str(permission)), default_flow_style=False, sort_keys=False + ) + ) + + +@feast_permissions_cmd.command(name="check") +@click.pass_context +def feast_permissions_check_command(ctx: click.Context): + """ + Validate the permissions configuration + """ + from tabulate import tabulate + + all_unsecured_table: list[Any] = [] + store = create_feature_store(ctx) + permissions = store.list_permissions() + objects = cli_utils.fetch_all_feast_objects( + store=store, + ) + + print( + f"{Style.BRIGHT + Fore.RED}The following resources are not secured by any permission configuration:{Style.RESET_ALL}" + ) + for o in objects: + cli_utils.handle_permissions_check_command( + object=o, permissions=permissions, table=all_unsecured_table + ) + print( + tabulate( + all_unsecured_table, + headers=[ + "NAME", + "TYPE", + ], + tablefmt="plain", + ) + ) + + all_unsecured_actions_table: list[Any] = [] + print( + f"{Style.BRIGHT + Fore.RED}The following actions are not secured by any permission configuration (Note: this might not be a security concern, depending on the used APIs):{Style.RESET_ALL}" + ) + for o in objects: + cli_utils.handle_permissions_check_command_with_actions( + object=o, permissions=permissions, table=all_unsecured_actions_table + ) + print( + tabulate( + all_unsecured_actions_table, + headers=[ + "NAME", + "TYPE", + "UNSECURED ACTIONS", + ], + tablefmt="plain", + ) + ) + + +@feast_permissions_cmd.command(name="list-roles") +@click.option( + "--verbose", + "-v", + is_flag=True, + help="Print the resources and actions permitted to each configured role", +) +@click.pass_context +def feast_permissions_list_roles_command(ctx: click.Context, verbose: bool): + """ + List all the configured roles + """ + from tabulate import tabulate + + table: list[Any] = [] + store = create_feature_store(ctx) + permissions = store.list_permissions() + if not verbose: + cli_utils.handler_list_all_permissions_roles( + permissions=permissions, table=table + ) + print( + tabulate( + table, + headers=[ + "ROLE NAME", + ], + tablefmt="grid", + ) + ) + else: + objects = cli_utils.fetch_all_feast_objects( + store=store, + ) + cli_utils.handler_list_all_permissions_roles_verbose( + objects=objects, permissions=permissions, table=table + ) + print( + tabulate( + table, + headers=[ + "ROLE NAME", + "RESOURCE NAME", + "RESOURCE TYPE", + "PERMITTED ACTIONS", + ], + tablefmt="plain", + ) + ) + + if __name__ == "__main__": cli() diff --git a/sdk/python/feast/cli_utils.py b/sdk/python/feast/cli_utils.py new file mode 100644 index 0000000000..edfdab93e3 --- /dev/null +++ b/sdk/python/feast/cli_utils.py @@ -0,0 +1,329 @@ +from typing import Any, Optional + +from bigtree import Node +from colorama import Fore, Style + +from feast import ( + BatchFeatureView, + FeatureService, + FeatureStore, + FeatureView, + OnDemandFeatureView, + StreamFeatureView, +) +from feast.feast_object import FeastObject +from feast.permissions.action import ALL_ACTIONS +from feast.permissions.decision import DecisionEvaluator +from feast.permissions.permission import Permission +from feast.permissions.policy import Policy, RoleBasedPolicy +from feast.permissions.user import User + + +def print_permission_verbose_example(): + print("") + print( + f"{Style.BRIGHT + Fore.GREEN}The structure of the {Style.BRIGHT + Fore.WHITE}feast-permissions list --verbose {Style.BRIGHT + Fore.GREEN}command will be as in the following example:" + ) + print("") + print(f"{Style.DIM}For example: {Style.RESET_ALL}{Style.BRIGHT + Fore.GREEN}") + print("") + explanation_root_node = Node("permissions") + explanation_permission_node = Node( + "permission_1" + " " + str(["role names list"]), + parent=explanation_root_node, + ) + Node( + FeatureView.__name__ + ": " + str(["feature view names"]), + parent=explanation_permission_node, + ) + Node(FeatureService.__name__ + ": none", parent=explanation_permission_node) + Node("..", parent=explanation_permission_node) + Node( + "permission_2" + " " + str(["role names list"]), + parent=explanation_root_node, + ) + Node("..", parent=explanation_root_node) + explanation_root_node.show() + print( + f""" +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------{Style.RESET_ALL} + """ + ) + + +def handle_sd_verbose_permissions_command( + feast_type: list[FeastObject], + p: Permission, + policy_node: Node, + store: FeatureStore, + tags_filter: Optional[dict[str, str]], +): + saved_datasets = store.list_saved_datasets(tags=tags_filter) + saved_datasets_names = set() + for sd in saved_datasets: + if p.match_resource(sd): + saved_datasets_names.add(sd.name) + if len(saved_datasets_names) > 0: + Node( + feast_type.__name__ + ": " + str(list(saved_datasets_names)), # type: ignore[union-attr, attr-defined] + parent=policy_node, + ) + else: + Node(feast_type.__name__ + ": none", parent=policy_node) # type: ignore[union-attr, attr-defined] + + +def handle_vr_verbose_permissions_command( + feast_type: list[FeastObject], + p: Permission, + policy_node: Node, + store: FeatureStore, + tags_filter: Optional[dict[str, str]], +): + validation_references = store.list_validation_references(tags=tags_filter) + validation_references_names = set() + for vr in validation_references: + if p.match_resource(vr): + validation_references_names.add(vr.name) + if len(validation_references_names) > 0: + Node( + feast_type.__name__ + ": " + str(list(validation_references_names)), # type: ignore[union-attr, attr-defined] + parent=policy_node, + ) + else: + Node(feast_type.__name__ + ": none", parent=policy_node) # type: ignore[union-attr, attr-defined] + + +def handle_ds_verbose_permissions_command( + feast_type: list[FeastObject], + p: Permission, + policy_node: Node, + store: FeatureStore, + tags_filter: Optional[dict[str, str]], +): + data_sources = store.list_data_sources(tags=tags_filter) + data_sources_names = set() + for ds in data_sources: + if p.match_resource(ds): + data_sources_names.add(ds.name) + if len(data_sources_names) > 0: + Node( + feast_type.__name__ + ": " + str(list(data_sources_names)), # type: ignore[union-attr, attr-defined] + parent=policy_node, + ) + else: + Node(feast_type.__name__ + ": none", parent=policy_node) # type: ignore[union-attr, attr-defined] + + +def handle_fs_verbose_permissions_command( + feast_type: list[FeastObject], + p: Permission, + policy_node: Node, + store: FeatureStore, + tags_filter: Optional[dict[str, str]], +): + feature_services = store.list_feature_services(tags=tags_filter) + feature_services_names = set() + for fs in feature_services: + if p.match_resource(fs): + feature_services_names.add(fs.name) + if len(feature_services_names) > 0: + Node( + feast_type.__name__ + ": " + str(list(feature_services_names)), # type: ignore[union-attr, attr-defined] + parent=policy_node, + ) + else: + Node(feast_type.__name__ + ": none", parent=policy_node) # type: ignore[union-attr, attr-defined] + + +def handle_entity_verbose_permissions_command( + feast_type: list[FeastObject], + p: Permission, + policy_node: Node, + store: FeatureStore, + tags_filter: Optional[dict[str, str]], +): + entities = store.list_entities(tags=tags_filter) + entities_names = set() + for e in entities: + if p.match_resource(e): + entities_names.add(e.name) + if len(entities_names) > 0: + Node(feast_type.__name__ + ": " + str(list(entities_names)), parent=policy_node) # type: ignore[union-attr, attr-defined] + else: + Node(feast_type.__name__ + ": none", parent=policy_node) # type: ignore[union-attr, attr-defined] + + +def handle_fv_verbose_permissions_command( + feast_type: list[FeastObject], + p: Permission, + policy_node: Node, + store: FeatureStore, + tags_filter: Optional[dict[str, str]], +): + feature_views = [] + feature_views_names = set() + if feast_type == FeatureView: + feature_views = store.list_all_feature_views(tags=tags_filter) # type: ignore[assignment] + elif feast_type == OnDemandFeatureView: + feature_views = store.list_on_demand_feature_views( + tags=tags_filter # type: ignore[assignment] + ) + elif feast_type == BatchFeatureView: + feature_views = store.list_batch_feature_views(tags=tags_filter) # type: ignore[assignment] + elif feast_type == StreamFeatureView: + feature_views = store.list_stream_feature_views( + tags=tags_filter # type: ignore[assignment] + ) + for fv in feature_views: + if p.match_resource(fv): + feature_views_names.add(fv.name) + if len(feature_views_names) > 0: + Node( + feast_type.__name__ + " " + str(list(feature_views_names)), # type: ignore[union-attr, attr-defined] + parent=policy_node, + ) + else: + Node(feast_type.__name__ + ": none", parent=policy_node) # type: ignore[union-attr, attr-defined] + + +def handle_not_verbose_permissions_command( + p: Permission, policy: Policy, table: list[Any] +): + roles: set[str] = set() + if isinstance(policy, RoleBasedPolicy): + roles = set(policy.get_roles()) + table.append( + [ + p.name, + _to_multi_line([t.__name__ for t in p.types]), # type: ignore[union-attr, attr-defined] + p.name_pattern, + _to_multi_line([a.value.upper() for a in p.actions]), + _to_multi_line(sorted(roles)), + _dict_to_multi_line(p.required_tags), + ], + ) + + +def fetch_all_feast_objects(store: FeatureStore) -> list[FeastObject]: + objects: list[FeastObject] = [] + objects.extend(store.list_entities()) + objects.extend(store.list_all_feature_views()) + objects.extend(store.list_batch_feature_views()) + objects.extend(store.list_feature_services()) + objects.extend(store.list_data_sources()) + objects.extend(store.list_validation_references()) + objects.extend(store.list_saved_datasets()) + objects.extend(store.list_permissions()) + return objects + + +def handle_permissions_check_command( + object: FeastObject, permissions: list[Permission], table: list[Any] +): + for p in permissions: + if p.match_resource(object): + return + table.append( + [ + object.name, + type(object).__name__, + ] + ) + + +def handle_permissions_check_command_with_actions( + object: FeastObject, permissions: list[Permission], table: list[Any] +): + unmatched_actions = ALL_ACTIONS.copy() + for p in permissions: + if p.match_resource(object): + for action in ALL_ACTIONS: + if p.match_actions([action]) and action in unmatched_actions: + unmatched_actions.remove(action) + + if unmatched_actions: + table.append( + [ + object.name, + type(object).__name__, + _to_multi_line([a.value.upper() for a in unmatched_actions]), + ] + ) + + +def fetch_all_permission_roles(permissions: list[Permission]) -> list[str]: + all_roles = set() + for p in permissions: + if isinstance(p.policy, RoleBasedPolicy) and len(p.policy.get_roles()) > 0: + all_roles.update(p.policy.get_roles()) + + return sorted(all_roles) + + +def handler_list_all_permissions_roles(permissions: list[Permission], table: list[Any]): + all_roles = fetch_all_permission_roles(permissions) + for role in all_roles: + table.append( + [ + role, + ] + ) + + +def handler_list_all_permissions_roles_verbose( + objects: list[FeastObject], permissions: list[Permission], table: list[Any] +): + all_roles = fetch_all_permission_roles(permissions) + + for role in all_roles: + for o in objects: + permitted_actions = ALL_ACTIONS.copy() + for action in ALL_ACTIONS: + # Following code is derived from enforcer.enforce_policy but has a different return type and does not raise PermissionError + matching_permissions = [ + p + for p in permissions + if p.match_resource(o) and p.match_actions([action]) + ] + + if matching_permissions: + evaluator = DecisionEvaluator( + len(matching_permissions), + ) + for p in matching_permissions: + permission_grant, permission_explanation = ( + p.policy.validate_user(user=User(username="", roles=[role])) + ) + evaluator.add_grant( + permission_grant, + f"Permission {p.name} denied access: {permission_explanation}", + ) + + if evaluator.is_decided(): + grant, explanations = evaluator.grant() + if not grant: + permitted_actions.remove(action) + break + else: + permitted_actions.remove(action) + + table.append( + [ + role, + o.name, + type(o).__name__, + _to_multi_line([a.value.upper() for a in permitted_actions]), + ] + ) + + +def _to_multi_line(values: list[str]) -> str: + if not values: + return "-" + return "\n".join(values) + + +def _dict_to_multi_line(values: dict[str, str]) -> str: + if not values: + return "-" + return "\n".join([f"{key} : {value}" for key, value in values.items()]) diff --git a/sdk/python/feast/diff/registry_diff.py b/sdk/python/feast/diff/registry_diff.py index 9236b087d4..6235025adc 100644 --- a/sdk/python/feast/diff/registry_diff.py +++ b/sdk/python/feast/diff/registry_diff.py @@ -10,6 +10,7 @@ from feast.feature_view import DUMMY_ENTITY_NAME from feast.infra.registry.base_registry import BaseRegistry from feast.infra.registry.registry import FEAST_OBJECT_TYPES, FeastObjectType +from feast.permissions.permission import Permission from feast.protos.feast.core.DataSource_pb2 import DataSource as DataSourceProto from feast.protos.feast.core.Entity_pb2 import Entity as EntityProto from feast.protos.feast.core.FeatureService_pb2 import ( @@ -20,6 +21,7 @@ OnDemandFeatureView as OnDemandFeatureViewProto, ) from feast.protos.feast.core.OnDemandFeatureView_pb2 import OnDemandFeatureViewSpec +from feast.protos.feast.core.Permission_pb2 import Permission as PermissionProto from feast.protos.feast.core.SavedDataset_pb2 import SavedDataset as SavedDatasetProto from feast.protos.feast.core.StreamFeatureView_pb2 import ( StreamFeatureView as StreamFeatureViewProto, @@ -111,6 +113,7 @@ def tag_objects_for_keep_delete_update_add( StreamFeatureViewProto, ValidationReferenceProto, SavedDatasetProto, + PermissionProto, ) @@ -354,6 +357,15 @@ def apply_diff_to_registry( project, commit=False, ) + elif feast_object_diff.feast_object_type == FeastObjectType.PERMISSION: + permission_obj = cast( + Permission, feast_object_diff.current_feast_object + ) + registry.delete_permission( + permission_obj.name, + project, + commit=False, + ) if feast_object_diff.transition_type in [ TransitionType.CREATE, @@ -387,6 +399,12 @@ def apply_diff_to_registry( project, commit=False, ) + elif feast_object_diff.feast_object_type == FeastObjectType.PERMISSION: + registry.apply_permission( + cast(Permission, feast_object_diff.new_feast_object), + project, + commit=False, + ) if commit: registry.commit() diff --git a/sdk/python/feast/errors.py b/sdk/python/feast/errors.py index c4c1157626..ffafe31125 100644 --- a/sdk/python/feast/errors.py +++ b/sdk/python/feast/errors.py @@ -223,6 +223,13 @@ def __init__(self, online_store_class_name: str): ) +class FeastInvalidAuthConfigClass(Exception): + def __init__(self, auth_config_class_name: str): + super().__init__( + f"Auth Config Class '{auth_config_class_name}' should end with the string `AuthConfig`.'" + ) + + class FeastInvalidBaseClass(Exception): def __init__(self, class_name: str, class_type: str): super().__init__( @@ -391,6 +398,19 @@ def __init__(self, input_dict: dict): ) +class PermissionNotFoundException(Exception): + def __init__(self, name, project): + super().__init__(f"Permission {name} does not exist in project {project}") + + +class PermissionObjectNotFoundException(FeastObjectNotFoundException): + def __init__(self, name, project=None): + if project: + super().__init__(f"Permission {name} does not exist in project {project}") + else: + super().__init__(f"Permission {name} does not exist") + + class ZeroRowsQueryResult(Exception): def __init__(self, query: str): super().__init__(f"This query returned zero rows:\n{query}") diff --git a/sdk/python/feast/feast_object.py b/sdk/python/feast/feast_object.py index d9505dcb9f..dfe29b7128 100644 --- a/sdk/python/feast/feast_object.py +++ b/sdk/python/feast/feast_object.py @@ -1,4 +1,4 @@ -from typing import Union +from typing import Union, get_args from .batch_feature_view import BatchFeatureView from .data_source import DataSource @@ -6,11 +6,13 @@ from .feature_service import FeatureService from .feature_view import FeatureView from .on_demand_feature_view import OnDemandFeatureView +from .permissions.permission import Permission from .protos.feast.core.DataSource_pb2 import DataSource as DataSourceProto from .protos.feast.core.Entity_pb2 import EntitySpecV2 from .protos.feast.core.FeatureService_pb2 import FeatureServiceSpec from .protos.feast.core.FeatureView_pb2 import FeatureViewSpec from .protos.feast.core.OnDemandFeatureView_pb2 import OnDemandFeatureViewSpec +from .protos.feast.core.Permission_pb2 import PermissionSpec as PermissionSpec from .protos.feast.core.SavedDataset_pb2 import SavedDatasetSpec from .protos.feast.core.StreamFeatureView_pb2 import StreamFeatureViewSpec from .protos.feast.core.ValidationProfile_pb2 import ( @@ -30,6 +32,7 @@ DataSource, ValidationReference, SavedDataset, + Permission, ] FeastObjectSpecProto = Union[ @@ -41,4 +44,13 @@ DataSourceProto, ValidationReferenceProto, SavedDatasetSpec, + PermissionSpec, +] + +ALL_RESOURCE_TYPES = list(get_args(FeastObject)) +ALL_FEATURE_VIEW_TYPES = [ + FeatureView, + OnDemandFeatureView, + BatchFeatureView, + StreamFeatureView, ] diff --git a/sdk/python/feast/feature_server.py b/sdk/python/feast/feature_server.py index 908c9741c2..7f24580b7a 100644 --- a/sdk/python/feast/feature_server.py +++ b/sdk/python/feast/feature_server.py @@ -9,9 +9,8 @@ import pandas as pd import psutil from dateutil import parser -from fastapi import FastAPI, HTTPException, Request, Response, status +from fastapi import Depends, FastAPI, HTTPException, Request, Response, status from fastapi.logger import logger -from fastapi.params import Depends from google.protobuf.json_format import MessageToDict from prometheus_client import Gauge, start_http_server from pydantic import BaseModel @@ -20,7 +19,16 @@ from feast import proto_json, utils from feast.constants import DEFAULT_FEATURE_SERVER_REGISTRY_TTL from feast.data_source import PushMode -from feast.errors import PushSourceNotFoundException +from feast.errors import FeatureViewNotFoundException, PushSourceNotFoundException +from feast.permissions.action import WRITE, AuthzedAction +from feast.permissions.security_manager import assert_permissions +from feast.permissions.server.rest import inject_user_details +from feast.permissions.server.utils import ( + ServerType, + init_auth_manager, + init_security_manager, + str_to_auth_manager_type, +) # Define prometheus metrics cpu_usage_gauge = Gauge( @@ -93,23 +101,48 @@ async def lifespan(app: FastAPI): async def get_body(request: Request): return await request.body() - @app.post("/get-online-features") + # TODO RBAC: complete the dependencies for the other endpoints + @app.post( + "/get-online-features", + dependencies=[Depends(inject_user_details)], + ) def get_online_features(body=Depends(get_body)): try: body = json.loads(body) + full_feature_names = body.get("full_feature_names", False) + entity_rows = body["entities"] # Initialize parameters for FeatureStore.get_online_features(...) call if "feature_service" in body: - features = store.get_feature_service( + feature_service = store.get_feature_service( body["feature_service"], allow_cache=True ) + assert_permissions( + resource=feature_service, actions=[AuthzedAction.READ_ONLINE] + ) + features = feature_service else: features = body["features"] - - full_feature_names = body.get("full_feature_names", False) + all_feature_views, all_on_demand_feature_views = ( + utils._get_feature_views_to_use( + store.registry, + store.project, + features, + allow_cache=True, + hide_dummy_entity=False, + ) + ) + for feature_view in all_feature_views: + assert_permissions( + resource=feature_view, actions=[AuthzedAction.READ_ONLINE] + ) + for od_feature_view in all_on_demand_feature_views: + assert_permissions( + resource=od_feature_view, actions=[AuthzedAction.READ_ONLINE] + ) response_proto = store.get_online_features( features=features, - entity_rows=body["entities"], + entity_rows=entity_rows, full_feature_names=full_feature_names, ).proto @@ -123,21 +156,46 @@ def get_online_features(body=Depends(get_body)): # Raise HTTPException to return the error message to the client raise HTTPException(status_code=500, detail=str(e)) - @app.post("/push") + @app.post("/push", dependencies=[Depends(inject_user_details)]) def push(body=Depends(get_body)): try: request = PushFeaturesRequest(**json.loads(body)) df = pd.DataFrame(request.df) + actions = [] if request.to == "offline": to = PushMode.OFFLINE + actions = [AuthzedAction.WRITE_OFFLINE] elif request.to == "online": to = PushMode.ONLINE + actions = [AuthzedAction.WRITE_ONLINE] elif request.to == "online_and_offline": to = PushMode.ONLINE_AND_OFFLINE + actions = WRITE else: raise ValueError( f"{request.to} is not a supported push format. Please specify one of these ['online', 'offline', 'online_and_offline']." ) + + from feast.data_source import PushSource + + all_fvs = store.list_feature_views( + allow_cache=request.allow_registry_cache + ) + store.list_stream_feature_views( + allow_cache=request.allow_registry_cache + ) + fvs_with_push_sources = { + fv + for fv in all_fvs + if ( + fv.stream_source is not None + and isinstance(fv.stream_source, PushSource) + and fv.stream_source.name == request.push_source_name + ) + } + + for feature_view in fvs_with_push_sources: + assert_permissions(resource=feature_view, actions=actions) + store.push( push_source_name=request.push_source_name, df=df, @@ -155,15 +213,29 @@ def push(body=Depends(get_body)): # Raise HTTPException to return the error message to the client raise HTTPException(status_code=500, detail=str(e)) - @app.post("/write-to-online-store") + @app.post("/write-to-online-store", dependencies=[Depends(inject_user_details)]) def write_to_online_store(body=Depends(get_body)): try: request = WriteToFeatureStoreRequest(**json.loads(body)) df = pd.DataFrame(request.df) + feature_view_name = request.feature_view_name + allow_registry_cache = request.allow_registry_cache + try: + feature_view = store.get_stream_feature_view( + feature_view_name, allow_registry_cache=allow_registry_cache + ) + except FeatureViewNotFoundException: + feature_view = store.get_feature_view( + feature_view_name, allow_registry_cache=allow_registry_cache + ) + + assert_permissions( + resource=feature_view, actions=[AuthzedAction.WRITE_ONLINE] + ) store.write_to_online_store( - feature_view_name=request.feature_view_name, + feature_view_name=feature_view_name, df=df, - allow_registry_cache=request.allow_registry_cache, + allow_registry_cache=allow_registry_cache, ) except Exception as e: # Print the original exception on the server side @@ -175,10 +247,14 @@ def write_to_online_store(body=Depends(get_body)): def health(): return Response(status_code=status.HTTP_200_OK) - @app.post("/materialize") + @app.post("/materialize", dependencies=[Depends(inject_user_details)]) def materialize(body=Depends(get_body)): try: request = MaterializeRequest(**json.loads(body)) + for feature_view in request.feature_views: + assert_permissions( + resource=feature_view, actions=[AuthzedAction.WRITE_ONLINE] + ) store.materialize( utils.make_tzaware(parser.parse(request.start_ts)), utils.make_tzaware(parser.parse(request.end_ts)), @@ -190,10 +266,14 @@ def materialize(body=Depends(get_body)): # Raise HTTPException to return the error message to the client raise HTTPException(status_code=500, detail=str(e)) - @app.post("/materialize-incremental") + @app.post("/materialize-incremental", dependencies=[Depends(inject_user_details)]) def materialize_incremental(body=Depends(get_body)): try: request = MaterializeIncrementalRequest(**json.loads(body)) + for feature_view in request.feature_views: + assert_permissions( + resource=feature_view, actions=[AuthzedAction.WRITE_ONLINE] + ) store.materialize_incremental( utils.make_tzaware(parser.parse(request.end_ts)), request.feature_views ) @@ -231,15 +311,15 @@ def load(self): def monitor_resources(self, interval: int = 5): """Function to monitor and update CPU and memory usage metrics.""" - print(f"Start monitor_resources({interval})") + logger.debug(f"Starting resource monitoring with interval {interval} seconds") p = psutil.Process() - print(f"PID is {p.pid}") + logger.debug(f"PID is {p.pid}") while True: with p.oneshot(): cpu_usage = p.cpu_percent() memory_usage = p.memory_percent() - print(f"cpu_usage is {cpu_usage}") - print(f"memory_usage is {memory_usage}") + logger.debug(f"CPU usage: {cpu_usage}%, Memory usage: {memory_usage}%") + logger.debug(f"CPU usage: {cpu_usage}%, Memory usage: {memory_usage}%") cpu_usage_gauge.set(cpu_usage) memory_usage_gauge.set(memory_usage) time.sleep(interval) @@ -256,15 +336,27 @@ def start_server( metrics: bool, ): if metrics: - print("Start Prometheus Server") + logger.info("Starting Prometheus Server") start_http_server(8000) - print("Start a background thread to monitor CPU and memory usage") + logger.debug("Starting background thread to monitor CPU and memory usage") monitoring_thread = threading.Thread( target=monitor_resources, args=(5,), daemon=True ) monitoring_thread.start() + logger.debug("start_server called") + auth_type = str_to_auth_manager_type(store.config.auth_config.type) + logger.info(f"Auth type: {auth_type}") + init_security_manager(auth_type=auth_type, fs=store) + logger.debug("Security manager initialized successfully") + init_auth_manager( + auth_type=auth_type, + server_type=ServerType.REST, + auth_config=store.config.auth_config, + ) + logger.debug("Auth manager initialized successfully") + if sys.platform != "win32": FeastServeApplication( store=store, diff --git a/sdk/python/feast/feature_store.py b/sdk/python/feast/feature_store.py index 77638f5a62..a03706e56f 100644 --- a/sdk/python/feast/feature_store.py +++ b/sdk/python/feast/feature_store.py @@ -76,6 +76,7 @@ from feast.infra.registry.sql import SqlRegistry from feast.on_demand_feature_view import OnDemandFeatureView from feast.online_response import OnlineResponse +from feast.permissions.permission import Permission from feast.protos.feast.core.InfraObject_pb2 import Infra as InfraProto from feast.protos.feast.serving.ServingService_pb2 import ( FieldStatus, @@ -157,9 +158,16 @@ def __init__( elif registry_config and registry_config.registry_type == "remote": from feast.infra.registry.remote import RemoteRegistry - self._registry = RemoteRegistry(registry_config, self.config.project, None) + self._registry = RemoteRegistry( + registry_config, self.config.project, None, self.config.auth_config + ) else: - r = Registry(self.config.project, registry_config, repo_path=self.repo_path) + r = Registry( + self.config.project, + registry_config, + repo_path=self.repo_path, + auth_config=self.config.auth_config, + ) r._initialize_registry(self.config.project) self._registry = r @@ -199,7 +207,10 @@ def refresh_registry(self): """ registry_config = self.config.registry registry = Registry( - self.config.project, registry_config, repo_path=self.repo_path + self.config.project, + registry_config, + repo_path=self.repo_path, + auth_config=self.config.auth_config, ) registry.refresh(self.config.project) @@ -734,7 +745,8 @@ def plan( ... on_demand_feature_views=list(), ... stream_feature_views=list(), ... entities=[driver], - ... feature_services=list())) # register entity and feature view + ... feature_services=list(), + ... permissions=list())) # register entity and feature view """ # Validate and run inference on all the objects to be registered. self._validate_all_feature_views( @@ -798,6 +810,7 @@ def apply( StreamFeatureView, FeatureService, ValidationReference, + Permission, List[FeastObject], ], objects_to_delete: Optional[List[FeastObject]] = None, @@ -869,6 +882,7 @@ def apply( validation_references_to_update = [ ob for ob in objects if isinstance(ob, ValidationReference) ] + permissions_to_update = [ob for ob in objects if isinstance(ob, Permission)] batch_sources_to_add: List[DataSource] = [] for data_source in data_sources_set_to_update: @@ -924,10 +938,15 @@ def apply( self._registry.apply_validation_reference( validation_references, project=self.project, commit=False ) + for permission in permissions_to_update: + self._registry.apply_permission( + permission, project=self.project, commit=False + ) entities_to_delete = [] views_to_delete = [] sfvs_to_delete = [] + permissions_to_delete = [] if not partial: # Delete all registry objects that should not exist. entities_to_delete = [ @@ -956,6 +975,9 @@ def apply( validation_references_to_delete = [ ob for ob in objects_to_delete if isinstance(ob, ValidationReference) ] + permissions_to_delete = [ + ob for ob in objects_to_delete if isinstance(ob, Permission) + ] for data_source in data_sources_to_delete: self._registry.delete_data_source( @@ -985,6 +1007,10 @@ def apply( self._registry.delete_validation_reference( validation_references.name, project=self.project, commit=False ) + for permission in permissions_to_delete: + self._registry.delete_permission( + permission.name, project=self.project, commit=False + ) tables_to_delete: List[FeatureView] = ( views_to_delete + sfvs_to_delete if not partial else [] # type: ignore @@ -1915,6 +1941,72 @@ def get_validation_reference( ref._dataset = self.get_saved_dataset(ref.dataset_name) return ref + def list_validation_references( + self, allow_cache: bool = False, tags: Optional[dict[str, str]] = None + ) -> List[ValidationReference]: + """ + Retrieves the list of validation references from the registry. + + Args: + allow_cache: Whether to allow returning validation references from a cached registry. + tags: Filter by tags. + + Returns: + A list of validation references. + """ + return self._registry.list_validation_references( + self.project, allow_cache=allow_cache, tags=tags + ) + + def list_permissions( + self, allow_cache: bool = False, tags: Optional[dict[str, str]] = None + ) -> List[Permission]: + """ + Retrieves the list of permissions from the registry. + + Args: + allow_cache: Whether to allow returning permissions from a cached registry. + tags: Filter by tags. + + Returns: + A list of permissions. + """ + return self._registry.list_permissions( + self.project, allow_cache=allow_cache, tags=tags + ) + + def get_permission(self, name: str) -> Permission: + """ + Retrieves a permission from the registry. + + Args: + name: Name of the permission. + + Returns: + The specified permission. + + Raises: + PermissionObjectNotFoundException: The permission could not be found. + """ + return self._registry.get_permission(name, self.project) + + def list_saved_datasets( + self, allow_cache: bool = False, tags: Optional[dict[str, str]] = None + ) -> List[SavedDataset]: + """ + Retrieves the list of saved datasets from the registry. + + Args: + allow_cache: Whether to allow returning saved datasets from a cached registry. + tags: Filter by tags. + + Returns: + A list of saved datasets. + """ + return self._registry.list_saved_datasets( + self.project, allow_cache=allow_cache, tags=tags + ) + def _print_materialization_log( start_date, end_date, num_feature_views: int, online_store: str diff --git a/sdk/python/feast/infra/offline_stores/remote.py b/sdk/python/feast/infra/offline_stores/remote.py index dc657017d9..40239c8950 100644 --- a/sdk/python/feast/infra/offline_stores/remote.py +++ b/sdk/python/feast/infra/offline_stores/remote.py @@ -27,6 +27,9 @@ RetrievalMetadata, ) from feast.infra.registry.base_registry import BaseRegistry +from feast.permissions.client.arrow_flight_auth_interceptor import ( + build_arrow_flight_client, +) from feast.repo_config import FeastConfigBaseModel, RepoConfig from feast.saved_dataset import SavedDatasetStorage @@ -69,7 +72,11 @@ def _to_df_internal(self, timeout: Optional[int] = None) -> pd.DataFrame: # This is where do_get service is invoked def _to_arrow_internal(self, timeout: Optional[int] = None) -> pa.Table: return _send_retrieve_remote( - self.api, self.api_parameters, self.entity_df, self.table, self.client + self.api, + self.api_parameters, + self.entity_df, + self.table, + self.client, ) @property @@ -128,8 +135,9 @@ def get_historical_features( ) -> RemoteRetrievalJob: assert isinstance(config.offline_store, RemoteOfflineStoreConfig) - # Initialize the client connection - client = RemoteOfflineStore.init_client(config) + client = build_arrow_flight_client( + config.offline_store.host, config.offline_store.port, config.auth_config + ) feature_view_names = [fv.name for fv in feature_views] name_aliases = [fv.projection.name_alias for fv in feature_views] @@ -163,7 +171,9 @@ def pull_all_from_table_or_query( assert isinstance(config.offline_store, RemoteOfflineStoreConfig) # Initialize the client connection - client = RemoteOfflineStore.init_client(config) + client = build_arrow_flight_client( + config.offline_store.host, config.offline_store.port, config.auth_config + ) api_parameters = { "data_source_name": data_source.name, @@ -194,7 +204,9 @@ def pull_latest_from_table_or_query( assert isinstance(config.offline_store, RemoteOfflineStoreConfig) # Initialize the client connection - client = RemoteOfflineStore.init_client(config) + client = build_arrow_flight_client( + config.offline_store.host, config.offline_store.port, config.auth_config + ) api_parameters = { "data_source_name": data_source.name, @@ -227,7 +239,9 @@ def write_logged_features( data = pyarrow.parquet.read_table(data, use_threads=False, pre_buffer=False) # Initialize the client connection - client = RemoteOfflineStore.init_client(config) + client = build_arrow_flight_client( + config.offline_store.host, config.offline_store.port, config.auth_config + ) api_parameters = { "feature_service_name": source._feature_service.name, @@ -251,7 +265,9 @@ def offline_write_batch( assert isinstance(config.offline_store, RemoteOfflineStoreConfig) # Initialize the client connection - client = RemoteOfflineStore.init_client(config) + client = build_arrow_flight_client( + config.offline_store.host, config.offline_store.port, config.auth_config + ) feature_view_names = [feature_view.name] name_aliases = [feature_view.projection.name_alias] @@ -270,13 +286,6 @@ def offline_write_batch( entity_df=None, ) - @staticmethod - def init_client(config): - location = f"grpc://{config.offline_store.host}:{config.offline_store.port}" - client = fl.connect(location=location) - logger.info(f"Connecting FlightClient at {location}") - return client - def _create_retrieval_metadata(feature_refs: List[str], entity_df: pd.DataFrame): entity_schema = _get_entity_schema( @@ -331,11 +340,20 @@ def _send_retrieve_remote( table: pa.Table, client: fl.FlightClient, ): - command_descriptor = _call_put(api, api_parameters, client, entity_df, table) + command_descriptor = _call_put( + api, + api_parameters, + client, + entity_df, + table, + ) return _call_get(client, command_descriptor) -def _call_get(client: fl.FlightClient, command_descriptor: fl.FlightDescriptor): +def _call_get( + client: fl.FlightClient, + command_descriptor: fl.FlightDescriptor, +): flight = client.get_flight_info(command_descriptor) ticket = flight.endpoints[0].ticket reader = client.do_get(ticket) @@ -384,10 +402,7 @@ def _put_parameters( else: updatedTable = _create_empty_table() - writer, _ = client.do_put( - command_descriptor, - updatedTable.schema, - ) + writer, _ = client.do_put(command_descriptor, updatedTable.schema) writer.write_table(updatedTable) writer.close() diff --git a/sdk/python/feast/infra/online_stores/remote.py b/sdk/python/feast/infra/online_stores/remote.py index 19e1b7d515..93fbcaf771 100644 --- a/sdk/python/feast/infra/online_stores/remote.py +++ b/sdk/python/feast/infra/online_stores/remote.py @@ -16,11 +16,13 @@ from datetime import datetime from typing import Any, Callable, Dict, List, Literal, Optional, Sequence, Tuple -import requests from pydantic import StrictStr from feast import Entity, FeatureView, RepoConfig from feast.infra.online_stores.online_store import OnlineStore +from feast.permissions.client.http_auth_requests_wrapper import ( + get_http_auth_requests_session, +) from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto from feast.protos.feast.types.Value_pb2 import Value as ValueProto from feast.repo_config import FeastConfigBaseModel @@ -70,7 +72,7 @@ def online_read( req_body = self._construct_online_read_api_json_request( entity_keys, table, requested_features ) - response = requests.post( + response = get_http_auth_requests_session(config.auth_config).post( f"{config.online_store.path}/get-online-features", data=req_body ) if response.status_code == 200: diff --git a/sdk/python/feast/infra/registry/base_registry.py b/sdk/python/feast/infra/registry/base_registry.py index 03bec64830..33adb6b7c9 100644 --- a/sdk/python/feast/infra/registry/base_registry.py +++ b/sdk/python/feast/infra/registry/base_registry.py @@ -28,6 +28,7 @@ from feast.feature_view import FeatureView from feast.infra.infra_object import Infra from feast.on_demand_feature_view import OnDemandFeatureView +from feast.permissions.permission import Permission from feast.project_metadata import ProjectMetadata from feast.protos.feast.core.Entity_pb2 import Entity as EntityProto from feast.protos.feast.core.FeatureService_pb2 import ( @@ -37,6 +38,7 @@ from feast.protos.feast.core.OnDemandFeatureView_pb2 import ( OnDemandFeatureView as OnDemandFeatureViewProto, ) +from feast.protos.feast.core.Permission_pb2 import Permission as PermissionProto from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto from feast.protos.feast.core.SavedDataset_pb2 import SavedDataset as SavedDatasetProto from feast.protos.feast.core.StreamFeatureView_pb2 import ( @@ -457,7 +459,10 @@ def delete_saved_dataset(self, name: str, project: str, commit: bool = True): @abstractmethod def list_saved_datasets( - self, project: str, allow_cache: bool = False + self, + project: str, + allow_cache: bool = False, + tags: Optional[dict[str, str]] = None, ) -> List[SavedDataset]: """ Retrieves a list of all saved datasets in specified project @@ -465,6 +470,7 @@ def list_saved_datasets( Args: project: Feast project allow_cache: Whether to allow returning this dataset from a cached registry + tags: Filter by tags Returns: Returns the list of SavedDatasets @@ -521,17 +527,21 @@ def get_validation_reference( # TODO: Needs to be implemented. def list_validation_references( - self, project: str, allow_cache: bool = False + self, + project: str, + allow_cache: bool = False, + tags: Optional[dict[str, str]] = None, ) -> List[ValidationReference]: """ Retrieve a list of validation references from the registry Args: - allow_cache: Allow returning feature views from the cached registry - project: Filter feature views based on project name + project: Filter validation references based on project name + allow_cache: Allow returning validation references from the cached registry + tags: Filter by tags Returns: - List of request feature views + List of request validation references """ raise NotImplementedError @@ -590,6 +600,69 @@ def get_user_metadata( self, project: str, feature_view: BaseFeatureView ) -> Optional[bytes]: ... + # Permission operations + @abstractmethod + def apply_permission( + self, permission: Permission, project: str, commit: bool = True + ): + """ + Registers a single permission with Feast + + Args: + permission: A permission that will be registered + project: Feast project that this permission belongs to + commit: Whether to immediately commit to the registry + """ + raise NotImplementedError + + @abstractmethod + def delete_permission(self, name: str, project: str, commit: bool = True): + """ + Deletes a permission or raises an exception if not found. + + Args: + name: Name of permission + project: Feast project that this permission belongs to + commit: Whether the change should be persisted immediately + """ + raise NotImplementedError + + @abstractmethod + def get_permission( + self, name: str, project: str, allow_cache: bool = False + ) -> Permission: + """ + Retrieves a permission. + + Args: + name: Name of permission + project: Feast project that this permission belongs to + allow_cache: Whether to allow returning this permission from a cached registry + + Returns: + Returns either the specified permission, or raises an exception if none is found + """ + raise NotImplementedError + + @abstractmethod + def list_permissions( + self, + project: str, + allow_cache: bool = False, + tags: Optional[dict[str, str]] = None, + ) -> List[Permission]: + """ + Retrieve a list of permissions from the registry + + Args: + project: Filter permission based on project name + allow_cache: Whether to allow returning permissions from a cached registry + + Returns: + List of permissions + """ + raise NotImplementedError + @abstractmethod def proto(self) -> RegistryProto: """ @@ -716,6 +789,13 @@ def to_dict(self, project: str) -> Dict[str, List[Any]]: registry_dict["infra"].append( self._message_to_sorted_dict(infra_object.to_proto()) ) + for permission in sorted( + self.list_permissions(project=project), key=lambda ds: ds.name + ): + registry_dict["permissions"].append( + self._message_to_sorted_dict(permission.to_proto()) + ) + return registry_dict @staticmethod @@ -732,4 +812,6 @@ def deserialize_registry_values(serialized_proto, feast_obj_type) -> Any: return OnDemandFeatureViewProto.FromString(serialized_proto) if feast_obj_type == FeatureService: return FeatureServiceProto.FromString(serialized_proto) + if feast_obj_type == Permission: + return PermissionProto.FromString(serialized_proto) return None diff --git a/sdk/python/feast/infra/registry/caching_registry.py b/sdk/python/feast/infra/registry/caching_registry.py index 298639028d..611d67de96 100644 --- a/sdk/python/feast/infra/registry/caching_registry.py +++ b/sdk/python/feast/infra/registry/caching_registry.py @@ -14,6 +14,7 @@ from feast.infra.registry import proto_registry_utils from feast.infra.registry.base_registry import BaseRegistry from feast.on_demand_feature_view import OnDemandFeatureView +from feast.permissions.permission import Permission from feast.project_metadata import ProjectMetadata from feast.saved_dataset import SavedDataset, ValidationReference from feast.stream_feature_view import StreamFeatureView @@ -249,18 +250,23 @@ def get_saved_dataset( return self._get_saved_dataset(name, project) @abstractmethod - def _list_saved_datasets(self, project: str) -> List[SavedDataset]: + def _list_saved_datasets( + self, project: str, tags: Optional[dict[str, str]] = None + ) -> List[SavedDataset]: pass def list_saved_datasets( - self, project: str, allow_cache: bool = False + self, + project: str, + allow_cache: bool = False, + tags: Optional[dict[str, str]] = None, ) -> List[SavedDataset]: if allow_cache: self._refresh_cached_registry_if_necessary() return proto_registry_utils.list_saved_datasets( - self.cached_registry_proto, project + self.cached_registry_proto, project, tags ) - return self._list_saved_datasets(project) + return self._list_saved_datasets(project, tags) @abstractmethod def _get_validation_reference(self, name: str, project: str) -> ValidationReference: @@ -277,18 +283,23 @@ def get_validation_reference( return self._get_validation_reference(name, project) @abstractmethod - def _list_validation_references(self, project: str) -> List[ValidationReference]: + def _list_validation_references( + self, project: str, tags: Optional[dict[str, str]] = None + ) -> List[ValidationReference]: pass def list_validation_references( - self, project: str, allow_cache: bool = False + self, + project: str, + allow_cache: bool = False, + tags: Optional[dict[str, str]] = None, ) -> List[ValidationReference]: if allow_cache: self._refresh_cached_registry_if_necessary() return proto_registry_utils.list_validation_references( - self.cached_registry_proto, project + self.cached_registry_proto, project, tags ) - return self._list_validation_references(project) + return self._list_validation_references(project, tags) @abstractmethod def _list_project_metadata(self, project: str) -> List[ProjectMetadata]: @@ -311,6 +322,39 @@ def _get_infra(self, project: str) -> Infra: def get_infra(self, project: str, allow_cache: bool = False) -> Infra: return self._get_infra(project) + @abstractmethod + def _get_permission(self, name: str, project: str) -> Permission: + pass + + def get_permission( + self, name: str, project: str, allow_cache: bool = False + ) -> Permission: + if allow_cache: + self._refresh_cached_registry_if_necessary() + return proto_registry_utils.get_permission( + self.cached_registry_proto, name, project + ) + return self._get_permission(name, project) + + @abstractmethod + def _list_permissions( + self, project: str, tags: Optional[dict[str, str]] + ) -> List[Permission]: + pass + + def list_permissions( + self, + project: str, + allow_cache: bool = False, + tags: Optional[dict[str, str]] = None, + ) -> List[Permission]: + if allow_cache: + self._refresh_cached_registry_if_necessary() + return proto_registry_utils.list_permissions( + self.cached_registry_proto, project, tags + ) + return self._list_permissions(project, tags) + def refresh(self, project: Optional[str] = None): if project: project_metadata = proto_registry_utils.get_project_metadata( diff --git a/sdk/python/feast/infra/registry/proto_registry_utils.py b/sdk/python/feast/infra/registry/proto_registry_utils.py index 0e85f5b0a9..f67808aab5 100644 --- a/sdk/python/feast/infra/registry/proto_registry_utils.py +++ b/sdk/python/feast/infra/registry/proto_registry_utils.py @@ -10,12 +10,14 @@ EntityNotFoundException, FeatureServiceNotFoundException, FeatureViewNotFoundException, + PermissionObjectNotFoundException, SavedDatasetNotFound, ValidationReferenceNotFound, ) from feast.feature_service import FeatureService from feast.feature_view import FeatureView from feast.on_demand_feature_view import OnDemandFeatureView +from feast.permissions.permission import Permission from feast.project_metadata import ProjectMetadata from feast.protos.feast.core.Registry_pb2 import ProjectMetadata as ProjectMetadataProto from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto @@ -252,24 +254,28 @@ def list_data_sources( return data_sources -@registry_proto_cache +@registry_proto_cache_with_tags def list_saved_datasets( - registry_proto: RegistryProto, project: str + registry_proto: RegistryProto, project: str, tags: Optional[dict[str, str]] ) -> List[SavedDataset]: saved_datasets = [] for saved_dataset in registry_proto.saved_datasets: - if saved_dataset.spec.project == project: + if saved_dataset.spec.project == project and utils.has_all_tags( + saved_dataset.tags, tags + ): saved_datasets.append(SavedDataset.from_proto(saved_dataset)) return saved_datasets -@registry_proto_cache +@registry_proto_cache_with_tags def list_validation_references( - registry_proto: RegistryProto, project: str + registry_proto: RegistryProto, project: str, tags: Optional[dict[str, str]] ) -> List[ValidationReference]: validation_references = [] for validation_reference in registry_proto.validation_references: - if validation_reference.project == project: + if validation_reference.project == project and utils.has_all_tags( + validation_reference.tags, tags + ): validation_references.append( ValidationReference.from_proto(validation_reference) ) @@ -285,3 +291,28 @@ def list_project_metadata( for project_metadata in registry_proto.project_metadata if project_metadata.project == project ] + + +@registry_proto_cache_with_tags +def list_permissions( + registry_proto: RegistryProto, project: str, tags: Optional[dict[str, str]] +) -> List[Permission]: + permissions = [] + for permission_proto in registry_proto.permissions: + if permission_proto.spec.project == project and utils.has_all_tags( + permission_proto.spec.tags, tags + ): + permissions.append(Permission.from_proto(permission_proto)) + return permissions + + +def get_permission( + registry_proto: RegistryProto, name: str, project: str +) -> Permission: + for permission_proto in registry_proto.permissions: + if ( + permission_proto.spec.project == project + and permission_proto.spec.name == name + ): + return Permission.from_proto(permission_proto) + raise PermissionObjectNotFoundException(name=name, project=project) diff --git a/sdk/python/feast/infra/registry/registry.py b/sdk/python/feast/infra/registry/registry.py index fe44e6253a..366f3aacaa 100644 --- a/sdk/python/feast/infra/registry/registry.py +++ b/sdk/python/feast/infra/registry/registry.py @@ -31,6 +31,7 @@ EntityNotFoundException, FeatureServiceNotFoundException, FeatureViewNotFoundException, + PermissionNotFoundException, ValidationReferenceNotFound, ) from feast.feature_service import FeatureService @@ -41,6 +42,8 @@ from feast.infra.registry.base_registry import BaseRegistry from feast.infra.registry.registry_store import NoopRegistryStore from feast.on_demand_feature_view import OnDemandFeatureView +from feast.permissions.auth_model import AuthConfig, NoAuthConfig +from feast.permissions.permission import Permission from feast.project_metadata import ProjectMetadata from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto from feast.repo_config import RegistryConfig @@ -73,6 +76,7 @@ class FeastObjectType(Enum): ON_DEMAND_FEATURE_VIEW = "on demand feature view" STREAM_FEATURE_VIEW = "stream feature view" FEATURE_SERVICE = "feature service" + PERMISSION = "permission" @staticmethod def get_objects_from_registry( @@ -91,6 +95,7 @@ def get_objects_from_registry( FeastObjectType.FEATURE_SERVICE: registry.list_feature_services( project=project ), + FeastObjectType.PERMISSION: registry.list_permissions(project=project), } @staticmethod @@ -104,6 +109,7 @@ def get_objects_from_repo_contents( FeastObjectType.ON_DEMAND_FEATURE_VIEW: repo_contents.on_demand_feature_views, FeastObjectType.STREAM_FEATURE_VIEW: repo_contents.stream_feature_views, FeastObjectType.FEATURE_SERVICE: repo_contents.feature_services, + FeastObjectType.PERMISSION: repo_contents.permissions, } @@ -160,6 +166,7 @@ def __new__( project: str, registry_config: Optional[RegistryConfig], repo_path: Optional[Path], + auth_config: AuthConfig = NoAuthConfig(), ): # We override __new__ so that we can inspect registry_config and create a SqlRegistry without callers # needing to make any changes. @@ -174,7 +181,7 @@ def __new__( elif registry_config and registry_config.registry_type == "remote": from feast.infra.registry.remote import RemoteRegistry - return RemoteRegistry(registry_config, project, repo_path) + return RemoteRegistry(registry_config, project, repo_path, auth_config) else: return super(Registry, cls).__new__(cls) @@ -183,6 +190,7 @@ def __init__( project: str, registry_config: Optional[RegistryConfig], repo_path: Optional[Path], + auth_config: AuthConfig = NoAuthConfig(), ): """ Create the Registry object. @@ -194,6 +202,7 @@ def __init__( """ self._refresh_lock = Lock() + self._auth_config = auth_config if registry_config: registry_store_type = registry_config.registry_store_type @@ -211,7 +220,7 @@ def __init__( ) def clone(self) -> "Registry": - new_registry = Registry("project", None, None) + new_registry = Registry("project", None, None, self._auth_config) new_registry.cached_registry_proto_ttl = timedelta(seconds=0) new_registry.cached_registry_proto = ( self.cached_registry_proto.__deepcopy__() @@ -307,9 +316,6 @@ def apply_data_source( if existing_data_source_proto.name == data_source.name: del registry.data_sources[idx] data_source_proto = data_source.to_proto() - data_source_proto.data_source_class_type = ( - f"{data_source.__class__.__module__}.{data_source.__class__.__name__}" - ) data_source_proto.project = project data_source_proto.data_source_class_type = ( f"{data_source.__class__.__module__}.{data_source.__class__.__name__}" @@ -709,12 +715,15 @@ def get_saved_dataset( return proto_registry_utils.get_saved_dataset(registry_proto, name, project) def list_saved_datasets( - self, project: str, allow_cache: bool = False + self, + project: str, + allow_cache: bool = False, + tags: Optional[dict[str, str]] = None, ) -> List[SavedDataset]: registry_proto = self._get_registry_proto( project=project, allow_cache=allow_cache ) - return proto_registry_utils.list_saved_datasets(registry_proto, project) + return proto_registry_utils.list_saved_datasets(registry_proto, project, tags) def apply_validation_reference( self, @@ -751,12 +760,17 @@ def get_validation_reference( ) def list_validation_references( - self, project: str, allow_cache: bool = False + self, + project: str, + allow_cache: bool = False, + tags: Optional[dict[str, str]] = None, ) -> List[ValidationReference]: registry_proto = self._get_registry_proto( project=project, allow_cache=allow_cache ) - return proto_registry_utils.list_validation_references(registry_proto, project) + return proto_registry_utils.list_validation_references( + registry_proto, project, tags + ) def delete_validation_reference(self, name: str, project: str, commit: bool = True): registry_proto = self._prepare_registry_for_changes(project) @@ -905,3 +919,62 @@ def _existing_feature_view_names_to_fvs(self) -> Dict[str, Message]: fv.spec.name: fv for fv in self.cached_registry_proto.stream_feature_views } return {**odfvs, **fvs, **sfv} + + def get_permission( + self, name: str, project: str, allow_cache: bool = False + ) -> Permission: + registry_proto = self._get_registry_proto( + project=project, allow_cache=allow_cache + ) + return proto_registry_utils.get_permission(registry_proto, name, project) + + def list_permissions( + self, + project: str, + allow_cache: bool = False, + tags: Optional[dict[str, str]] = None, + ) -> List[Permission]: + registry_proto = self._get_registry_proto( + project=project, allow_cache=allow_cache + ) + return proto_registry_utils.list_permissions(registry_proto, project, tags) + + def apply_permission( + self, permission: Permission, project: str, commit: bool = True + ): + now = _utc_now() + if not permission.created_timestamp: + permission.created_timestamp = now + permission.last_updated_timestamp = now + + registry = self._prepare_registry_for_changes(project) + for idx, existing_permission_proto in enumerate(registry.permissions): + if ( + existing_permission_proto.spec.name == permission.name + and existing_permission_proto.spec.project == project + ): + permission.created_timestamp = ( + existing_permission_proto.meta.created_timestamp.ToDatetime() + ) + del registry.permissions[idx] + + permission_proto = permission.to_proto() + permission_proto.spec.project = project + registry.permissions.append(permission_proto) + if commit: + self.commit() + + def delete_permission(self, name: str, project: str, commit: bool = True): + self._prepare_registry_for_changes(project) + assert self.cached_registry_proto + + for idx, permission_proto in enumerate(self.cached_registry_proto.permissions): + if ( + permission_proto.spec.name == name + and permission_proto.spec.project == project + ): + del self.cached_registry_proto.permissions[idx] + if commit: + self.commit() + return + raise PermissionNotFoundException(name, project) diff --git a/sdk/python/feast/infra/registry/remote.py b/sdk/python/feast/infra/registry/remote.py index 9fa6d8ebee..618628bc07 100644 --- a/sdk/python/feast/infra/registry/remote.py +++ b/sdk/python/feast/infra/registry/remote.py @@ -15,6 +15,15 @@ from feast.infra.infra_object import Infra from feast.infra.registry.base_registry import BaseRegistry from feast.on_demand_feature_view import OnDemandFeatureView +from feast.permissions.auth.auth_type import AuthType +from feast.permissions.auth_model import ( + AuthConfig, + NoAuthConfig, +) +from feast.permissions.client.grpc_client_auth_interceptor import ( + GrpcClientAuthHeaderInterceptor, +) +from feast.permissions.permission import Permission from feast.project_metadata import ProjectMetadata from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto from feast.protos.feast.registry import RegistryServer_pb2, RegistryServer_pb2_grpc @@ -38,31 +47,32 @@ def __init__( registry_config: Union[RegistryConfig, RemoteRegistryConfig], project: str, repo_path: Optional[Path], + auth_config: AuthConfig = NoAuthConfig(), ): - self.channel = grpc.insecure_channel(registry_config.path) - self.stub = RegistryServer_pb2_grpc.RegistryServerStub(self.channel) + self.auth_config = auth_config + channel = grpc.insecure_channel(registry_config.path) + if self.auth_config.type != AuthType.NONE.value: + auth_header_interceptor = GrpcClientAuthHeaderInterceptor(auth_config) + channel = grpc.intercept_channel(channel, auth_header_interceptor) + self.stub = RegistryServer_pb2_grpc.RegistryServerStub(channel) def apply_entity(self, entity: Entity, project: str, commit: bool = True): request = RegistryServer_pb2.ApplyEntityRequest( entity=entity.to_proto(), project=project, commit=commit ) - self.stub.ApplyEntity(request) def delete_entity(self, name: str, project: str, commit: bool = True): request = RegistryServer_pb2.DeleteEntityRequest( name=name, project=project, commit=commit ) - self.stub.DeleteEntity(request) def get_entity(self, name: str, project: str, allow_cache: bool = False) -> Entity: request = RegistryServer_pb2.GetEntityRequest( name=name, project=project, allow_cache=allow_cache ) - response = self.stub.GetEntity(request) - return Entity.from_proto(response) def list_entities( @@ -74,9 +84,7 @@ def list_entities( request = RegistryServer_pb2.ListEntitiesRequest( project=project, allow_cache=allow_cache, tags=tags ) - response = self.stub.ListEntities(request) - return [Entity.from_proto(entity) for entity in response.entities] def apply_data_source( @@ -85,14 +93,12 @@ def apply_data_source( request = RegistryServer_pb2.ApplyDataSourceRequest( data_source=data_source.to_proto(), project=project, commit=commit ) - self.stub.ApplyDataSource(request) def delete_data_source(self, name: str, project: str, commit: bool = True): request = RegistryServer_pb2.DeleteDataSourceRequest( name=name, project=project, commit=commit ) - self.stub.DeleteDataSource(request) def get_data_source( @@ -101,9 +107,7 @@ def get_data_source( request = RegistryServer_pb2.GetDataSourceRequest( name=name, project=project, allow_cache=allow_cache ) - response = self.stub.GetDataSource(request) - return DataSource.from_proto(response) def list_data_sources( @@ -115,9 +119,7 @@ def list_data_sources( request = RegistryServer_pb2.ListDataSourcesRequest( project=project, allow_cache=allow_cache, tags=tags ) - response = self.stub.ListDataSources(request) - return [ DataSource.from_proto(data_source) for data_source in response.data_sources ] @@ -128,14 +130,12 @@ def apply_feature_service( request = RegistryServer_pb2.ApplyFeatureServiceRequest( feature_service=feature_service.to_proto(), project=project, commit=commit ) - self.stub.ApplyFeatureService(request) def delete_feature_service(self, name: str, project: str, commit: bool = True): request = RegistryServer_pb2.DeleteFeatureServiceRequest( name=name, project=project, commit=commit ) - self.stub.DeleteFeatureService(request) def get_feature_service( @@ -144,9 +144,7 @@ def get_feature_service( request = RegistryServer_pb2.GetFeatureServiceRequest( name=name, project=project, allow_cache=allow_cache ) - response = self.stub.GetFeatureService(request) - return FeatureService.from_proto(response) def list_feature_services( @@ -158,9 +156,7 @@ def list_feature_services( request = RegistryServer_pb2.ListFeatureServicesRequest( project=project, allow_cache=allow_cache, tags=tags ) - response = self.stub.ListFeatureServices(request) - return [ FeatureService.from_proto(feature_service) for feature_service in response.feature_services @@ -196,7 +192,6 @@ def delete_feature_view(self, name: str, project: str, commit: bool = True): request = RegistryServer_pb2.DeleteFeatureViewRequest( name=name, project=project, commit=commit ) - self.stub.DeleteFeatureView(request) def get_stream_feature_view( @@ -205,9 +200,7 @@ def get_stream_feature_view( request = RegistryServer_pb2.GetStreamFeatureViewRequest( name=name, project=project, allow_cache=allow_cache ) - response = self.stub.GetStreamFeatureView(request) - return StreamFeatureView.from_proto(response) def list_stream_feature_views( @@ -219,9 +212,7 @@ def list_stream_feature_views( request = RegistryServer_pb2.ListStreamFeatureViewsRequest( project=project, allow_cache=allow_cache, tags=tags ) - response = self.stub.ListStreamFeatureViews(request) - return [ StreamFeatureView.from_proto(stream_feature_view) for stream_feature_view in response.stream_feature_views @@ -233,9 +224,7 @@ def get_on_demand_feature_view( request = RegistryServer_pb2.GetOnDemandFeatureViewRequest( name=name, project=project, allow_cache=allow_cache ) - response = self.stub.GetOnDemandFeatureView(request) - return OnDemandFeatureView.from_proto(response) def list_on_demand_feature_views( @@ -247,9 +236,7 @@ def list_on_demand_feature_views( request = RegistryServer_pb2.ListOnDemandFeatureViewsRequest( project=project, allow_cache=allow_cache, tags=tags ) - response = self.stub.ListOnDemandFeatureViews(request) - return [ OnDemandFeatureView.from_proto(on_demand_feature_view) for on_demand_feature_view in response.on_demand_feature_views @@ -261,9 +248,7 @@ def get_feature_view( request = RegistryServer_pb2.GetFeatureViewRequest( name=name, project=project, allow_cache=allow_cache ) - response = self.stub.GetFeatureView(request) - return FeatureView.from_proto(response) def list_feature_views( @@ -275,7 +260,6 @@ def list_feature_views( request = RegistryServer_pb2.ListFeatureViewsRequest( project=project, allow_cache=allow_cache, tags=tags ) - response = self.stub.ListFeatureViews(request) return [ @@ -304,7 +288,6 @@ def apply_materialization( end_date=end_date_timestamp, commit=commit, ) - self.stub.ApplyMaterialization(request) def apply_saved_dataset( @@ -316,14 +299,12 @@ def apply_saved_dataset( request = RegistryServer_pb2.ApplySavedDatasetRequest( saved_dataset=saved_dataset.to_proto(), project=project, commit=commit ) - self.stub.ApplyFeatureService(request) def delete_saved_dataset(self, name: str, project: str, commit: bool = True): request = RegistryServer_pb2.DeleteSavedDatasetRequest( name=name, project=project, commit=commit ) - self.stub.DeleteSavedDataset(request) def get_saved_dataset( @@ -332,20 +313,19 @@ def get_saved_dataset( request = RegistryServer_pb2.GetSavedDatasetRequest( name=name, project=project, allow_cache=allow_cache ) - response = self.stub.GetSavedDataset(request) - return SavedDataset.from_proto(response) def list_saved_datasets( - self, project: str, allow_cache: bool = False + self, + project: str, + allow_cache: bool = False, + tags: Optional[dict[str, str]] = None, ) -> List[SavedDataset]: request = RegistryServer_pb2.ListSavedDatasetsRequest( - project=project, allow_cache=allow_cache + project=project, allow_cache=allow_cache, tags=tags ) - response = self.stub.ListSavedDatasets(request) - return [ SavedDataset.from_proto(saved_dataset) for saved_dataset in response.saved_datasets @@ -362,14 +342,12 @@ def apply_validation_reference( project=project, commit=commit, ) - self.stub.ApplyValidationReference(request) def delete_validation_reference(self, name: str, project: str, commit: bool = True): request = RegistryServer_pb2.DeleteValidationReferenceRequest( name=name, project=project, commit=commit ) - self.stub.DeleteValidationReference(request) def get_validation_reference( @@ -378,20 +356,19 @@ def get_validation_reference( request = RegistryServer_pb2.GetValidationReferenceRequest( name=name, project=project, allow_cache=allow_cache ) - response = self.stub.GetValidationReference(request) - return ValidationReference.from_proto(response) def list_validation_references( - self, project: str, allow_cache: bool = False + self, + project: str, + allow_cache: bool = False, + tags: Optional[dict[str, str]] = None, ) -> List[ValidationReference]: request = RegistryServer_pb2.ListValidationReferencesRequest( - project=project, allow_cache=allow_cache + project=project, allow_cache=allow_cache, tags=tags ) - response = self.stub.ListValidationReferences(request) - return [ ValidationReference.from_proto(validation_reference) for validation_reference in response.validation_references @@ -403,25 +380,20 @@ def list_project_metadata( request = RegistryServer_pb2.ListProjectMetadataRequest( project=project, allow_cache=allow_cache ) - response = self.stub.ListProjectMetadata(request) - return [ProjectMetadata.from_proto(pm) for pm in response.project_metadata] def update_infra(self, infra: Infra, project: str, commit: bool = True): request = RegistryServer_pb2.UpdateInfraRequest( infra=infra.to_proto(), project=project, commit=commit ) - self.stub.UpdateInfra(request) def get_infra(self, project: str, allow_cache: bool = False) -> Infra: request = RegistryServer_pb2.GetInfraRequest( project=project, allow_cache=allow_cache ) - response = self.stub.GetInfra(request) - return Infra.from_proto(response) def apply_user_metadata( @@ -437,6 +409,47 @@ def get_user_metadata( ) -> Optional[bytes]: pass + def apply_permission( + self, permission: Permission, project: str, commit: bool = True + ): + permission_proto = permission.to_proto() + permission_proto.spec.project = project + + request = RegistryServer_pb2.ApplyPermissionRequest( + permission=permission_proto, project=project, commit=commit + ) + self.stub.ApplyPermission(request) + + def delete_permission(self, name: str, project: str, commit: bool = True): + request = RegistryServer_pb2.DeletePermissionRequest( + name=name, project=project, commit=commit + ) + self.stub.DeletePermission(request) + + def get_permission( + self, name: str, project: str, allow_cache: bool = False + ) -> Permission: + request = RegistryServer_pb2.GetPermissionRequest( + name=name, project=project, allow_cache=allow_cache + ) + response = self.stub.GetPermission(request) + + return Permission.from_proto(response) + + def list_permissions( + self, + project: str, + allow_cache: bool = False, + tags: Optional[dict[str, str]] = None, + ) -> List[Permission]: + request = RegistryServer_pb2.ListPermissionsRequest( + project=project, allow_cache=allow_cache, tags=tags + ) + response = self.stub.ListPermissions(request) + return [ + Permission.from_proto(permission) for permission in response.permissions + ] + def proto(self) -> RegistryProto: return self.stub.Proto(Empty()) @@ -445,7 +458,6 @@ def commit(self): def refresh(self, project: Optional[str] = None): request = RegistryServer_pb2.RefreshRequest(project=str(project)) - self.stub.Refresh(request) def teardown(self): diff --git a/sdk/python/feast/infra/registry/snowflake.py b/sdk/python/feast/infra/registry/snowflake.py index ac4f52dc06..801b90afe3 100644 --- a/sdk/python/feast/infra/registry/snowflake.py +++ b/sdk/python/feast/infra/registry/snowflake.py @@ -18,6 +18,7 @@ EntityNotFoundException, FeatureServiceNotFoundException, FeatureViewNotFoundException, + PermissionNotFoundException, SavedDatasetNotFound, ValidationReferenceNotFound, ) @@ -31,6 +32,7 @@ execute_snowflake_statement, ) from feast.on_demand_feature_view import OnDemandFeatureView +from feast.permissions.permission import Permission from feast.project_metadata import ProjectMetadata from feast.protos.feast.core.DataSource_pb2 import DataSource as DataSourceProto from feast.protos.feast.core.Entity_pb2 import Entity as EntityProto @@ -42,6 +44,7 @@ from feast.protos.feast.core.OnDemandFeatureView_pb2 import ( OnDemandFeatureView as OnDemandFeatureViewProto, ) +from feast.protos.feast.core.Permission_pb2 import Permission as PermissionProto from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto from feast.protos.feast.core.SavedDataset_pb2 import SavedDataset as SavedDatasetProto from feast.protos.feast.core.StreamFeatureView_pb2 import ( @@ -342,6 +345,17 @@ def _apply_object( self._set_last_updated_metadata(update_datetime, project) + def apply_permission( + self, permission: Permission, project: str, commit: bool = True + ): + return self._apply_object( + "PERMISSIONS", + project, + "PERMISSION_NAME", + permission, + "PERMISSION_PROTO", + ) + # delete operations def delete_data_source(self, name: str, project: str, commit: bool = True): return self._delete_object( @@ -421,6 +435,15 @@ def _delete_object( return cursor.rowcount + def delete_permission(self, name: str, project: str, commit: bool = True): + return self._delete_object( + "PERMISSIONS", + name, + project, + "PERMISSION_NAME", + PermissionNotFoundException, + ) + # get operations def get_data_source( self, name: str, project: str, allow_cache: bool = False @@ -619,6 +642,25 @@ def _get_object( else: return None + def get_permission( + self, name: str, project: str, allow_cache: bool = False + ) -> Permission: + if allow_cache: + self._refresh_cached_registry_if_necessary() + return proto_registry_utils.get_permission( + self.cached_registry_proto, name, project + ) + return self._get_object( + "PERMISSIONS", + name, + project, + PermissionProto, + Permission, + "PERMISSION_NAME", + "PERMISSION_PROTO", + PermissionNotFoundException, + ) + # list operations def list_data_sources( self, @@ -716,12 +758,15 @@ def list_on_demand_feature_views( ) def list_saved_datasets( - self, project: str, allow_cache: bool = False + self, + project: str, + allow_cache: bool = False, + tags: Optional[dict[str, str]] = None, ) -> List[SavedDataset]: if allow_cache: self._refresh_cached_registry_if_necessary() return proto_registry_utils.list_saved_datasets( - self.cached_registry_proto, project + self.cached_registry_proto, project, tags ) return self._list_objects( "SAVED_DATASETS", @@ -729,6 +774,7 @@ def list_saved_datasets( SavedDatasetProto, SavedDataset, "SAVED_DATASET_PROTO", + tags=tags, ) def list_stream_feature_views( @@ -752,7 +798,10 @@ def list_stream_feature_views( ) def list_validation_references( - self, project: str, allow_cache: bool = False + self, + project: str, + allow_cache: bool = False, + tags: Optional[dict[str, str]] = None, ) -> List[ValidationReference]: return self._list_objects( "VALIDATION_REFERENCES", @@ -760,6 +809,7 @@ def list_validation_references( ValidationReferenceProto, ValidationReference, "VALIDATION_REFERENCE_PROTO", + tags=tags, ) def _list_objects( @@ -793,6 +843,26 @@ def _list_objects( return objects return [] + def list_permissions( + self, + project: str, + allow_cache: bool = False, + tags: Optional[dict[str, str]] = None, + ) -> List[Permission]: + if allow_cache: + self._refresh_cached_registry_if_necessary() + return proto_registry_utils.list_permissions( + self.cached_registry_proto, project + ) + return self._list_objects( + "PERMISSIONS", + project, + PermissionProto, + Permission, + "PERMISSION_PROTO", + tags, + ) + def apply_materialization( self, feature_view: FeatureView, @@ -934,6 +1004,7 @@ def proto(self) -> RegistryProto: (self.list_saved_datasets, r.saved_datasets), (self.list_validation_references, r.validation_references), (self.list_project_metadata, r.project_metadata), + (self.list_permissions, r.permissions), ]: objs: List[Any] = lister(project) # type: ignore if objs: @@ -964,6 +1035,7 @@ def _get_all_projects(self) -> Set[str]: "FEATURE_VIEWS", "ON_DEMAND_FEATURE_VIEWS", "STREAM_FEATURE_VIEWS", + "PERMISSIONS", ] with GetSnowflakeConnection(self.registry_config) as conn: diff --git a/sdk/python/feast/infra/registry/sql.py b/sdk/python/feast/infra/registry/sql.py index a2b16a3a09..90c6e82e7d 100644 --- a/sdk/python/feast/infra/registry/sql.py +++ b/sdk/python/feast/infra/registry/sql.py @@ -30,6 +30,7 @@ EntityNotFoundException, FeatureServiceNotFoundException, FeatureViewNotFoundException, + PermissionNotFoundException, SavedDatasetNotFound, ValidationReferenceNotFound, ) @@ -38,6 +39,7 @@ from feast.infra.infra_object import Infra from feast.infra.registry.caching_registry import CachingRegistry from feast.on_demand_feature_view import OnDemandFeatureView +from feast.permissions.permission import Permission from feast.project_metadata import ProjectMetadata from feast.protos.feast.core.DataSource_pb2 import DataSource as DataSourceProto from feast.protos.feast.core.Entity_pb2 import Entity as EntityProto @@ -49,6 +51,7 @@ from feast.protos.feast.core.OnDemandFeatureView_pb2 import ( OnDemandFeatureView as OnDemandFeatureViewProto, ) +from feast.protos.feast.core.Permission_pb2 import Permission as PermissionProto from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto from feast.protos.feast.core.SavedDataset_pb2 import SavedDataset as SavedDatasetProto from feast.protos.feast.core.StreamFeatureView_pb2 import ( @@ -149,6 +152,15 @@ Column("infra_proto", LargeBinary, nullable=False), ) +permissions = Table( + "permissions", + metadata, + Column("permission_name", String(255), primary_key=True), + Column("project_id", String(50), primary_key=True), + Column("last_updated_timestamp", BigInteger, nullable=False), + Column("permission_proto", LargeBinary, nullable=False), +) + class FeastMetadataKeys(Enum): LAST_UPDATED_TIMESTAMP = "last_updated_timestamp" @@ -207,6 +219,7 @@ def teardown(self): on_demand_feature_views, saved_datasets, validation_references, + permissions, }: with self.engine.begin() as conn: stmt = delete(t) @@ -319,13 +332,16 @@ def _get_validation_reference(self, name: str, project: str) -> ValidationRefere not_found_exception=ValidationReferenceNotFound, ) - def _list_validation_references(self, project: str) -> List[ValidationReference]: + def _list_validation_references( + self, project: str, tags: Optional[dict[str, str]] = None + ) -> List[ValidationReference]: return self._list_objects( table=validation_references, project=project, proto_class=ValidationReferenceProto, python_class=ValidationReference, proto_field_name="validation_reference_proto", + tags=tags, ) def _list_entities( @@ -447,13 +463,16 @@ def _list_feature_views( tags=tags, ) - def _list_saved_datasets(self, project: str) -> List[SavedDataset]: + def _list_saved_datasets( + self, project: str, tags: Optional[dict[str, str]] = None + ) -> List[SavedDataset]: return self._list_objects( saved_datasets, project, SavedDatasetProto, SavedDataset, "saved_dataset_proto", + tags=tags, ) def _list_on_demand_feature_views( @@ -666,6 +685,7 @@ def proto(self) -> RegistryProto: (self.list_saved_datasets, r.saved_datasets), (self.list_validation_references, r.validation_references), (self.list_project_metadata, r.project_metadata), + (self.list_permissions, r.permissions), ]: objs: List[Any] = lister(project) # type: ignore if objs: @@ -721,6 +741,7 @@ def _apply_object( "saved_dataset_proto", "feature_view_proto", "feature_service_proto", + "permission_proto", ]: deserialized_proto = self.deserialize_registry_values( row._mapping[proto_field_name], type(obj) @@ -917,6 +938,7 @@ def _get_all_projects(self) -> Set[str]: feature_views, on_demand_feature_views, stream_feature_views, + permissions, }: stmt = select(table) rows = conn.execute(stmt).all() @@ -924,3 +946,44 @@ def _get_all_projects(self) -> Set[str]: projects.add(row._mapping["project_id"]) return projects + + def _get_permission(self, name: str, project: str) -> Permission: + return self._get_object( + table=permissions, + name=name, + project=project, + proto_class=PermissionProto, + python_class=Permission, + id_field_name="permission_name", + proto_field_name="permission_proto", + not_found_exception=PermissionNotFoundException, + ) + + def _list_permissions( + self, project: str, tags: Optional[dict[str, str]] + ) -> List[Permission]: + return self._list_objects( + permissions, + project, + PermissionProto, + Permission, + "permission_proto", + tags=tags, + ) + + def apply_permission( + self, permission: Permission, project: str, commit: bool = True + ): + return self._apply_object( + permissions, project, "permission_name", permission, "permission_proto" + ) + + def delete_permission(self, name: str, project: str, commit: bool = True): + with self.engine.begin() as conn: + stmt = delete(permissions).where( + permissions.c.permission_name == name, + permissions.c.project_id == project, + ) + rows = conn.execute(stmt) + if rows.rowcount < 1: + raise PermissionNotFoundException(name, project) diff --git a/sdk/python/feast/infra/utils/snowflake/registry/snowflake_table_creation.sql b/sdk/python/feast/infra/utils/snowflake/registry/snowflake_table_creation.sql index aa35caeac4..021d175b4e 100644 --- a/sdk/python/feast/infra/utils/snowflake/registry/snowflake_table_creation.sql +++ b/sdk/python/feast/infra/utils/snowflake/registry/snowflake_table_creation.sql @@ -80,4 +80,12 @@ CREATE TABLE IF NOT EXISTS REGISTRY_PATH."VALIDATION_REFERENCES" ( last_updated_timestamp TIMESTAMP_LTZ NOT NULL, validation_reference_proto BINARY NOT NULL, PRIMARY KEY (validation_reference_name, project_id) -) +); + +CREATE TABLE IF NOT EXISTS REGISTRY_PATH."PERMISSIONS" ( + permission_name VARCHAR, + project_id VARCHAR, + last_updated_timestamp TIMESTAMP_LTZ NOT NULL, + permission_proto BINARY NOT NULL, + PRIMARY KEY (permission_name, project_id) +); diff --git a/sdk/python/feast/infra/utils/snowflake/registry/snowflake_table_deletion.sql b/sdk/python/feast/infra/utils/snowflake/registry/snowflake_table_deletion.sql index a355c72062..780424abd1 100644 --- a/sdk/python/feast/infra/utils/snowflake/registry/snowflake_table_deletion.sql +++ b/sdk/python/feast/infra/utils/snowflake/registry/snowflake_table_deletion.sql @@ -17,3 +17,5 @@ DROP TABLE IF EXISTS REGISTRY_PATH."SAVED_DATASETS"; DROP TABLE IF EXISTS REGISTRY_PATH."STREAM_FEATURE_VIEWS"; DROP TABLE IF EXISTS REGISTRY_PATH."VALIDATION_REFERENCES" + +DROP TABLE IF EXISTS REGISTRY_PATH."PERMISSIONS" diff --git a/sdk/python/feast/offline_server.py b/sdk/python/feast/offline_server.py index be92620d68..839acada93 100644 --- a/sdk/python/feast/offline_server.py +++ b/sdk/python/feast/offline_server.py @@ -3,7 +3,7 @@ import logging import traceback from datetime import datetime -from typing import Any, Dict, List +from typing import Any, Dict, List, cast import pyarrow as pa import pyarrow.flight as fl @@ -12,14 +12,33 @@ from feast.feature_logging import FeatureServiceLoggingSource from feast.feature_view import DUMMY_ENTITY_NAME from feast.infra.offline_stores.offline_utils import get_offline_store_from_config +from feast.permissions.action import AuthzedAction +from feast.permissions.security_manager import assert_permissions +from feast.permissions.server.arrow import ( + arrowflight_middleware, + inject_user_details_decorator, +) +from feast.permissions.server.utils import ( + ServerType, + init_auth_manager, + init_security_manager, + str_to_auth_manager_type, +) from feast.saved_dataset import SavedDatasetStorage logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) class OfflineServer(fl.FlightServerBase): def __init__(self, store: FeatureStore, location: str, **kwargs): - super(OfflineServer, self).__init__(location, **kwargs) + super(OfflineServer, self).__init__( + location, + middleware=arrowflight_middleware( + str_to_auth_manager_type(store.config.auth_config.type) + ), + **kwargs, + ) self._location = location # A dictionary of configured flights, e.g. API calls received and not yet served self.flights: Dict[str, Any] = {} @@ -41,6 +60,7 @@ def _make_flight_info(self, key: Any, descriptor: fl.FlightDescriptor): return fl.FlightInfo(schema, descriptor, endpoints, -1, -1) + @inject_user_details_decorator def get_flight_info( self, context: fl.ServerCallContext, descriptor: fl.FlightDescriptor ): @@ -49,6 +69,7 @@ def get_flight_info( return self._make_flight_info(key, descriptor) raise KeyError("Flight not found.") + @inject_user_details_decorator def list_flights(self, context: fl.ServerCallContext, criteria: bytes): for key, table in self.flights.items(): if key[1] is not None: @@ -60,6 +81,7 @@ def list_flights(self, context: fl.ServerCallContext, criteria: bytes): # Expects to receive request parameters and stores them in the flights dictionary # Indexed by the unique command + @inject_user_details_decorator def do_put( self, context: fl.ServerCallContext, @@ -156,6 +178,7 @@ def _validate_do_get_parameters(self, command: dict): # Extracts the API parameters from the flights dictionary, delegates the execution to the FeatureStore instance # and returns the stream of data + @inject_user_details_decorator def do_get(self, context: fl.ServerCallContext, ticket: fl.Ticket): key = ast.literal_eval(ticket.ticket.decode()) if key not in self.flights: @@ -217,7 +240,15 @@ def offline_write_batch(self, command: dict, key: str): assert len(feature_views) == 1, "incorrect feature view" table = self.flights[key] self.offline_store.offline_write_batch( - self.store.config, feature_views[0], table, command["progress"] + self.store.config, + cast( + FeatureView, + assert_permissions( + feature_views[0], actions=[AuthzedAction.WRITE_OFFLINE] + ), + ), + table, + command["progress"], ) def _validate_write_logged_features_parameters(self, command: dict): @@ -234,6 +265,10 @@ def write_logged_features(self, command: dict, key: str): feature_service.logging_config is not None ), "feature service must have logging_config set" + assert_permissions( + resource=feature_service, + actions=[AuthzedAction.WRITE_OFFLINE], + ) self.offline_store.write_logged_features( config=self.store.config, data=table, @@ -260,10 +295,12 @@ def _validate_pull_all_from_table_or_query_parameters(self, command: dict): def pull_all_from_table_or_query(self, command: dict): self._validate_pull_all_from_table_or_query_parameters(command) + data_source = self.store.get_data_source(command["data_source_name"]) + assert_permissions(data_source, actions=[AuthzedAction.READ_OFFLINE]) return self.offline_store.pull_all_from_table_or_query( self.store.config, - self.store.get_data_source(command["data_source_name"]), + data_source, command["join_key_columns"], command["feature_name_columns"], command["timestamp_field"], @@ -287,10 +324,11 @@ def _validate_pull_latest_from_table_or_query_parameters(self, command: dict): def pull_latest_from_table_or_query(self, command: dict): self._validate_pull_latest_from_table_or_query_parameters(command) - + data_source = self.store.get_data_source(command["data_source_name"]) + assert_permissions(resource=data_source, actions=[AuthzedAction.READ_OFFLINE]) return self.offline_store.pull_latest_from_table_or_query( self.store.config, - self.store.get_data_source(command["data_source_name"]), + data_source, command["join_key_columns"], command["feature_name_columns"], command["timestamp_field"], @@ -343,6 +381,11 @@ def get_historical_features(self, command: dict, key: str): project=project, ) + for feature_view in feature_views: + assert_permissions( + resource=feature_view, actions=[AuthzedAction.READ_OFFLINE] + ) + retJob = self.offline_store.get_historical_features( config=self.store.config, feature_views=feature_views, @@ -377,6 +420,10 @@ def persist(self, command: dict, key: str): raise NotImplementedError data_source = self.store.get_data_source(command["data_source_name"]) + assert_permissions( + resource=data_source, + actions=[AuthzedAction.WRITE_OFFLINE], + ) storage = SavedDatasetStorage.from_data_source(data_source) ret_job.persist(storage, command["allow_overwrite"], command["timeout"]) except Exception as e: @@ -401,11 +448,23 @@ def remove_dummies(fv: FeatureView) -> FeatureView: return fv +def _init_auth_manager(store: FeatureStore): + auth_type = str_to_auth_manager_type(store.config.auth_config.type) + init_security_manager(auth_type=auth_type, fs=store) + init_auth_manager( + auth_type=auth_type, + server_type=ServerType.ARROW, + auth_config=store.config.auth_config, + ) + + def start_server( store: FeatureStore, host: str, port: int, ): + _init_auth_manager(store) + location = "grpc+tcp://{}:{}".format(host, port) server = OfflineServer(store, location) logger.info(f"Offline store server serving on {location}") diff --git a/sdk/python/feast/permissions/__init__.py b/sdk/python/feast/permissions/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sdk/python/feast/permissions/action.py b/sdk/python/feast/permissions/action.py new file mode 100644 index 0000000000..0e85c1685f --- /dev/null +++ b/sdk/python/feast/permissions/action.py @@ -0,0 +1,40 @@ +import enum + + +class AuthzedAction(enum.Enum): + """ + Identify the type of action being secured by the permissions framework, according to the familiar CRUD and Feast terminology. + """ + + CREATE = "create" # Create an instance + DESCRIBE = "describe" # Access the instance state + UPDATE = "update" # Update the instance state + DELETE = "delete" # Delete an instance + READ_ONLINE = "read_online" # Read the online store only + READ_OFFLINE = "read_offline" # Read the offline store only + WRITE_ONLINE = "write_online" # Write to the online store only + WRITE_OFFLINE = "write_offline" # Write to the offline store only + + +# Alias for all available actions +ALL_ACTIONS = [a for a in AuthzedAction.__members__.values()] + +# Alias for all read actions +READ = [ + AuthzedAction.READ_OFFLINE, + AuthzedAction.READ_ONLINE, +] +# Alias for all write actions +WRITE = [ + AuthzedAction.WRITE_OFFLINE, + AuthzedAction.WRITE_ONLINE, +] + + +# Alias for CRUD actions +CRUD = [ + AuthzedAction.CREATE, + AuthzedAction.DESCRIBE, + AuthzedAction.UPDATE, + AuthzedAction.DELETE, +] diff --git a/sdk/python/feast/permissions/auth/__init__.py b/sdk/python/feast/permissions/auth/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sdk/python/feast/permissions/auth/auth_manager.py b/sdk/python/feast/permissions/auth/auth_manager.py new file mode 100644 index 0000000000..e608904567 --- /dev/null +++ b/sdk/python/feast/permissions/auth/auth_manager.py @@ -0,0 +1,68 @@ +from abc import ABC +from typing import Optional + +from .token_extractor import NoAuthTokenExtractor, TokenExtractor +from .token_parser import NoAuthTokenParser, TokenParser + + +class AuthManager(ABC): + """ + The authorization manager offers services to manage authorization tokens from client requests + to extract user details before injecting them in the security context. + """ + + _token_parser: TokenParser + _token_extractor: TokenExtractor + + def __init__(self, token_parser: TokenParser, token_extractor: TokenExtractor): + self._token_parser = token_parser + self._token_extractor = token_extractor + + @property + def token_parser(self) -> TokenParser: + return self._token_parser + + @property + def token_extractor(self) -> TokenExtractor: + return self._token_extractor + + +""" +The possibly empty global instance of `AuthManager`. +""" +_auth_manager: Optional[AuthManager] = None + + +def get_auth_manager() -> AuthManager: + """ + Return the global instance of `AuthManager`. + + Raises: + RuntimeError if the clobal instance is not set. + """ + global _auth_manager + if _auth_manager is None: + raise RuntimeError( + "AuthManager is not initialized. Call 'set_auth_manager' first." + ) + return _auth_manager + + +def set_auth_manager(auth_manager: AuthManager): + """ + Initialize the global instance of `AuthManager`. + """ + + global _auth_manager + _auth_manager = auth_manager + + +class AllowAll(AuthManager): + """ + An AuthManager not extracting nor parsing the authorization token. + """ + + def __init__(self): + super().__init__( + token_extractor=NoAuthTokenExtractor(), token_parser=NoAuthTokenParser() + ) diff --git a/sdk/python/feast/permissions/auth/auth_type.py b/sdk/python/feast/permissions/auth/auth_type.py new file mode 100644 index 0000000000..3fa34f97bd --- /dev/null +++ b/sdk/python/feast/permissions/auth/auth_type.py @@ -0,0 +1,11 @@ +import enum + + +class AuthType(enum.Enum): + """ + Identify the type of authorization. + """ + + NONE = "no_auth" + OIDC = "oidc" + KUBERNETES = "kubernetes" diff --git a/sdk/python/feast/permissions/auth/kubernetes_token_parser.py b/sdk/python/feast/permissions/auth/kubernetes_token_parser.py new file mode 100644 index 0000000000..c16e5232fb --- /dev/null +++ b/sdk/python/feast/permissions/auth/kubernetes_token_parser.py @@ -0,0 +1,107 @@ +import logging + +import jwt +from kubernetes import client, config +from starlette.authentication import ( + AuthenticationError, +) + +from feast.permissions.auth.token_parser import TokenParser +from feast.permissions.user import User + +logger = logging.getLogger(__name__) + + +class KubernetesTokenParser(TokenParser): + """ + A `TokenParser` implementation to use Kubernetes RBAC resources to retrieve the user details. + The assumption is that the request header includes an authorization bearer with the token of the + client `ServiceAccount`. + By inspecting the role bindings, this `TokenParser` extracts the associated `Role`s. + + The client `ServiceAccount` is instead used as the user name, together with the current namespace. + """ + + def __init__(self): + config.load_incluster_config() + self.v1 = client.CoreV1Api() + self.rbac_v1 = client.RbacAuthorizationV1Api() + + async def user_details_from_access_token(self, access_token: str) -> User: + """ + Extract the service account from the token and search the roles associated with it. + + Returns: + User: Current user, with associated roles. The `username` is the `:` separated concatenation of `namespace` and `service account name`. + + Raises: + AuthenticationError if any error happens. + """ + sa_namespace, sa_name = _decode_token(access_token) + current_user = f"{sa_namespace}:{sa_name}" + logging.info(f"Received request from {sa_name} in {sa_namespace}") + + roles = self.get_roles(sa_namespace, sa_name) + logging.info(f"SA roles are: {roles}") + + return User(username=current_user, roles=roles) + + def get_roles(self, namespace: str, service_account_name: str) -> list[str]: + """ + Fetches the Kubernetes `Role`s associated to the given `ServiceAccount` in the given `namespace`. + + The research also includes the `ClusterRole`s, so the running deployment must be granted enough permissions to query + for such instances in all the namespaces. + + Returns: + list[str]: Name of the `Role`s and `ClusterRole`s associated to the service account. No string manipulation is performed on the role name. + """ + role_bindings = self.rbac_v1.list_namespaced_role_binding(namespace) + cluster_role_bindings = self.rbac_v1.list_cluster_role_binding() + + roles: set[str] = set() + + for binding in role_bindings.items: + if binding.subjects is not None: + for subject in binding.subjects: + if ( + subject.kind == "ServiceAccount" + and subject.name == service_account_name + ): + roles.add(binding.role_ref.name) + + for binding in cluster_role_bindings.items: + if binding.subjects is not None: + for subject in binding.subjects: + if ( + subject.kind == "ServiceAccount" + and subject.name == service_account_name + and subject.namespace == namespace + ): + roles.add(binding.role_ref.name) + + return list(roles) + + +def _decode_token(access_token: str) -> tuple[str, str]: + """ + The `sub` portion of the decoded token includes the service account name in the format: `system:serviceaccount:NAMESPACE:SA_NAME` + + Returns: + str: the namespace name. + str: the `ServiceAccount` name. + """ + try: + decoded_token = jwt.decode(access_token, options={"verify_signature": False}) + if "sub" in decoded_token: + subject: str = decoded_token["sub"] + if len(subject.split(":")) != 4: + raise AuthenticationError( + f"Expecting 4 elements separated by : in th subject section, instead of {len(subject.split(':'))}." + ) + _, _, sa_namespace, sa_name = subject.split(":") + return (sa_namespace, sa_name) + else: + raise AuthenticationError("Missing sub section in received token.") + except jwt.DecodeError as e: + raise AuthenticationError(f"Error decoding JWT token: {e}") diff --git a/sdk/python/feast/permissions/auth/oidc_token_parser.py b/sdk/python/feast/permissions/auth/oidc_token_parser.py new file mode 100644 index 0000000000..921a585bc2 --- /dev/null +++ b/sdk/python/feast/permissions/auth/oidc_token_parser.py @@ -0,0 +1,105 @@ +import logging +from unittest.mock import Mock + +import jwt +from fastapi import Request +from fastapi.security import OAuth2AuthorizationCodeBearer +from jwt import PyJWKClient +from starlette.authentication import ( + AuthenticationError, +) + +from feast.permissions.auth.token_parser import TokenParser +from feast.permissions.auth_model import OidcAuthConfig +from feast.permissions.user import User + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + + +class OidcTokenParser(TokenParser): + """ + A `TokenParser` to use an OIDC server to retrieve the user details. + Server settings are retrieved from the `auth` configurationof the Feature store. + """ + + _auth_config: OidcAuthConfig + + def __init__(self, auth_config: OidcAuthConfig): + self._auth_config = auth_config + + async def _validate_token(self, access_token: str): + """ + Validate the token extracted from the headrer of the user request against the OAuth2 server. + """ + # FastAPI's OAuth2AuthorizationCodeBearer requires a Request type but actually uses only the headers field + # https://github.com/tiangolo/fastapi/blob/eca465f4c96acc5f6a22e92fd2211675ca8a20c8/fastapi/security/oauth2.py#L380 + request = Mock(spec=Request) + request.headers = {"Authorization": f"Bearer {access_token}"} + + oauth_2_scheme = OAuth2AuthorizationCodeBearer( + tokenUrl=f"{self._auth_config.auth_server_url}/realms/{self._auth_config.realm}/protocol/openid-connect/token", + authorizationUrl=f"{self._auth_config.auth_server_url}/realms/{self._auth_config.realm}/protocol/openid-connect/auth", + refreshUrl=f"{self._auth_config.auth_server_url}/realms/{self._auth_config.realm}/protocol/openid-connect/token", + ) + + await oauth_2_scheme(request=request) + + async def user_details_from_access_token(self, access_token: str) -> User: + """ + Validate the access token then decode it to extract the user credential and roles. + + Returns: + User: Current user, with associated roles. + + Raises: + AuthenticationError if any error happens. + """ + + try: + await self._validate_token(access_token) + logger.info("Validated token") + except Exception as e: + raise AuthenticationError(f"Invalid token: {e}") + + url = f"{self._auth_config.auth_server_url}/realms/{self._auth_config.realm}/protocol/openid-connect/certs" + optional_custom_headers = {"User-agent": "custom-user-agent"} + jwks_client = PyJWKClient(url, headers=optional_custom_headers) + + try: + signing_key = jwks_client.get_signing_key_from_jwt(access_token) + data = jwt.decode( + access_token, + signing_key.key, + algorithms=["RS256"], + audience="account", + options={ + "verify_aud": False, + "verify_signature": True, + "verify_exp": True, + }, + leeway=10, # accepts tokens generated up to 10 seconds in the past, in case of clock skew + ) + + if "preferred_username" not in data: + raise AuthenticationError( + "Missing preferred_username field in access token." + ) + current_user = data["preferred_username"] + + if "resource_access" not in data: + logger.warning("Missing resource_access field in access token.") + client_id = self._auth_config.client_id + if client_id not in data["resource_access"]: + logger.warning( + f"Missing resource_access.{client_id} field in access token. Defaulting to empty roles." + ) + roles = [] + else: + roles = data["resource_access"][client_id]["roles"] + + logger.info(f"Extracted user {current_user} and roles {roles}") + return User(username=current_user, roles=roles) + except jwt.exceptions.InvalidTokenError: + logger.exception("Exception while parsing the token:") + raise AuthenticationError("Invalid token.") diff --git a/sdk/python/feast/permissions/auth/token_extractor.py b/sdk/python/feast/permissions/auth/token_extractor.py new file mode 100644 index 0000000000..37779d7640 --- /dev/null +++ b/sdk/python/feast/permissions/auth/token_extractor.py @@ -0,0 +1,51 @@ +import re +from abc import ABC + +from starlette.authentication import ( + AuthenticationError, +) + + +class TokenExtractor(ABC): + """ + A class to extract the authorization token from a user request. + """ + + def extract_access_token(self, **kwargs) -> str: + """ + Extract the authorization token from a user request. + + The actual implementation has to specify what arguments have to be defined in the kwywork args `kwargs` + + Returns: + The extracted access token. + """ + raise NotImplementedError() + + def _extract_bearer_token(self, auth_header: str) -> str: + """ + Extract the bearer token from the authorization header value. + + Args: + auth_header: The full value of the authorization header. + + Returns: + str: The token value, without the `Bearer` part. + + Raises: + AuthenticationError if the authorization token does not match the `Bearer` scheme. + """ + pattern = r"(?i)Bearer .+" + if not bool(re.match(pattern, auth_header)): + raise AuthenticationError(f"Expected Bearer schema, found {auth_header}") + _, access_token = auth_header.split() + return access_token + + +class NoAuthTokenExtractor(TokenExtractor): + """ + A `TokenExtractor` always returning an empty token + """ + + def extract_access_token(self, **kwargs) -> str: + return "" diff --git a/sdk/python/feast/permissions/auth/token_parser.py b/sdk/python/feast/permissions/auth/token_parser.py new file mode 100644 index 0000000000..f8f2aee44a --- /dev/null +++ b/sdk/python/feast/permissions/auth/token_parser.py @@ -0,0 +1,28 @@ +from abc import ABC, abstractmethod + +from feast.permissions.user import User + + +class TokenParser(ABC): + """ + A class to parse an access token to extract the user credential and roles. + """ + + @abstractmethod + async def user_details_from_access_token(self, access_token: str) -> User: + """ + Parse the access token and return the current user and the list of associated roles. + + Returns: + User: Current user, with associated roles. + """ + raise NotImplementedError() + + +class NoAuthTokenParser(TokenParser): + """ + A `TokenParser` always returning an empty token + """ + + async def user_details_from_access_token(self, access_token: str, **kwargs) -> User: + return User(username="", roles=[]) diff --git a/sdk/python/feast/permissions/auth_model.py b/sdk/python/feast/permissions/auth_model.py new file mode 100644 index 0000000000..afb0a22bc9 --- /dev/null +++ b/sdk/python/feast/permissions/auth_model.py @@ -0,0 +1,25 @@ +from typing import Literal, Optional + +from feast.repo_config import FeastConfigBaseModel + + +class AuthConfig(FeastConfigBaseModel): + type: Literal["oidc", "kubernetes", "no_auth"] = "no_auth" + + +class OidcAuthConfig(AuthConfig): + auth_server_url: Optional[str] = None + auth_discovery_url: str + client_id: str + client_secret: Optional[str] = None + username: str + password: str + realm: str = "master" + + +class NoAuthConfig(AuthConfig): + pass + + +class KubernetesAuthConfig(AuthConfig): + pass diff --git a/sdk/python/feast/permissions/client/__init__.py b/sdk/python/feast/permissions/client/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sdk/python/feast/permissions/client/arrow_flight_auth_interceptor.py b/sdk/python/feast/permissions/client/arrow_flight_auth_interceptor.py new file mode 100644 index 0000000000..724c7df5ca --- /dev/null +++ b/sdk/python/feast/permissions/client/arrow_flight_auth_interceptor.py @@ -0,0 +1,38 @@ +import pyarrow.flight as fl + +from feast.permissions.auth.auth_type import AuthType +from feast.permissions.auth_model import AuthConfig +from feast.permissions.client.auth_client_manager_factory import get_auth_token + + +class FlightBearerTokenInterceptor(fl.ClientMiddleware): + def __init__(self, auth_config: AuthConfig): + super().__init__() + self.auth_config = auth_config + + def call_completed(self, exception): + pass + + def received_headers(self, headers): + pass + + def sending_headers(self): + access_token = get_auth_token(self.auth_config) + return {b"authorization": b"Bearer " + access_token.encode("utf-8")} + + +class FlightAuthInterceptorFactory(fl.ClientMiddlewareFactory): + def __init__(self, auth_config: AuthConfig): + super().__init__() + self.auth_config = auth_config + + def start_call(self, info): + return FlightBearerTokenInterceptor(self.auth_config) + + +def build_arrow_flight_client(host: str, port, auth_config: AuthConfig): + if auth_config.type != AuthType.NONE.value: + middleware_factory = FlightAuthInterceptorFactory(auth_config) + return fl.FlightClient(f"grpc://{host}:{port}", middleware=[middleware_factory]) + else: + return fl.FlightClient(f"grpc://{host}:{port}") diff --git a/sdk/python/feast/permissions/client/auth_client_manager.py b/sdk/python/feast/permissions/client/auth_client_manager.py new file mode 100644 index 0000000000..82f9b7433e --- /dev/null +++ b/sdk/python/feast/permissions/client/auth_client_manager.py @@ -0,0 +1,8 @@ +from abc import ABC, abstractmethod + + +class AuthenticationClientManager(ABC): + @abstractmethod + def get_token(self) -> str: + """Retrieves the token based on the authentication type configuration""" + pass diff --git a/sdk/python/feast/permissions/client/auth_client_manager_factory.py b/sdk/python/feast/permissions/client/auth_client_manager_factory.py new file mode 100644 index 0000000000..4e49802047 --- /dev/null +++ b/sdk/python/feast/permissions/client/auth_client_manager_factory.py @@ -0,0 +1,30 @@ +from feast.permissions.auth.auth_type import AuthType +from feast.permissions.auth_model import ( + AuthConfig, + KubernetesAuthConfig, + OidcAuthConfig, +) +from feast.permissions.client.auth_client_manager import AuthenticationClientManager +from feast.permissions.client.kubernetes_auth_client_manager import ( + KubernetesAuthClientManager, +) +from feast.permissions.client.oidc_authentication_client_manager import ( + OidcAuthClientManager, +) + + +def get_auth_client_manager(auth_config: AuthConfig) -> AuthenticationClientManager: + if auth_config.type == AuthType.OIDC.value: + assert isinstance(auth_config, OidcAuthConfig) + return OidcAuthClientManager(auth_config) + elif auth_config.type == AuthType.KUBERNETES.value: + assert isinstance(auth_config, KubernetesAuthConfig) + return KubernetesAuthClientManager(auth_config) + else: + raise RuntimeError( + f"No Auth client manager implemented for the auth type:${auth_config.type}" + ) + + +def get_auth_token(auth_config: AuthConfig) -> str: + return get_auth_client_manager(auth_config).get_token() diff --git a/sdk/python/feast/permissions/client/grpc_client_auth_interceptor.py b/sdk/python/feast/permissions/client/grpc_client_auth_interceptor.py new file mode 100644 index 0000000000..98cc445c7b --- /dev/null +++ b/sdk/python/feast/permissions/client/grpc_client_auth_interceptor.py @@ -0,0 +1,52 @@ +import logging + +import grpc + +from feast.permissions.auth_model import AuthConfig +from feast.permissions.client.auth_client_manager_factory import get_auth_token + +logger = logging.getLogger(__name__) + + +class GrpcClientAuthHeaderInterceptor( + grpc.UnaryUnaryClientInterceptor, + grpc.UnaryStreamClientInterceptor, + grpc.StreamUnaryClientInterceptor, + grpc.StreamStreamClientInterceptor, +): + def __init__(self, auth_type: AuthConfig): + self._auth_type = auth_type + + def intercept_unary_unary( + self, continuation, client_call_details, request_iterator + ): + client_call_details = self._append_auth_header_metadata(client_call_details) + return continuation(client_call_details, request_iterator) + + def intercept_unary_stream( + self, continuation, client_call_details, request_iterator + ): + client_call_details = self._append_auth_header_metadata(client_call_details) + return continuation(client_call_details, request_iterator) + + def intercept_stream_unary( + self, continuation, client_call_details, request_iterator + ): + client_call_details = self._append_auth_header_metadata(client_call_details) + return continuation(client_call_details, request_iterator) + + def intercept_stream_stream( + self, continuation, client_call_details, request_iterator + ): + client_call_details = self._append_auth_header_metadata(client_call_details) + return continuation(client_call_details, request_iterator) + + def _append_auth_header_metadata(self, client_call_details): + logger.debug( + "Intercepted the grpc api method call to inject Authorization header " + ) + metadata = client_call_details.metadata or [] + access_token = get_auth_token(self._auth_type) + metadata.append((b"authorization", b"Bearer " + access_token.encode("utf-8"))) + client_call_details = client_call_details._replace(metadata=metadata) + return client_call_details diff --git a/sdk/python/feast/permissions/client/http_auth_requests_wrapper.py b/sdk/python/feast/permissions/client/http_auth_requests_wrapper.py new file mode 100644 index 0000000000..3232e25025 --- /dev/null +++ b/sdk/python/feast/permissions/client/http_auth_requests_wrapper.py @@ -0,0 +1,22 @@ +import requests +from requests import Session + +from feast.permissions.auth.auth_type import AuthType +from feast.permissions.auth_model import ( + AuthConfig, +) +from feast.permissions.client.auth_client_manager_factory import get_auth_token + + +class AuthenticatedRequestsSession(Session): + def __init__(self, auth_token: str): + super().__init__() + self.headers.update({"Authorization": f"Bearer {auth_token}"}) + + +def get_http_auth_requests_session(auth_config: AuthConfig) -> Session: + if auth_config.type == AuthType.NONE.value: + request_session = requests.session() + else: + request_session = AuthenticatedRequestsSession(get_auth_token(auth_config)) + return request_session diff --git a/sdk/python/feast/permissions/client/kubernetes_auth_client_manager.py b/sdk/python/feast/permissions/client/kubernetes_auth_client_manager.py new file mode 100644 index 0000000000..1ca3c5a2ae --- /dev/null +++ b/sdk/python/feast/permissions/client/kubernetes_auth_client_manager.py @@ -0,0 +1,43 @@ +import logging +import os + +from feast.permissions.auth_model import KubernetesAuthConfig +from feast.permissions.client.auth_client_manager import AuthenticationClientManager + +logger = logging.getLogger(__name__) + + +class KubernetesAuthClientManager(AuthenticationClientManager): + def __init__(self, auth_config: KubernetesAuthConfig): + self.auth_config = auth_config + self.token_file_path = "/var/run/secrets/kubernetes.io/serviceaccount/token" + + def get_token(self): + try: + token = self._read_token_from_file() + return token + except Exception as e: + logger.info(f"Error reading token from file: {e}") + logger.info("Attempting to read token from environment variable.") + try: + token = self._read_token_from_env() + return token + except Exception as env_e: + logger.exception( + f"Error reading token from environment variable: {env_e}" + ) + raise env_e + + def _read_token_from_file(self): + try: + with open(self.token_file_path, "r") as file: + token = file.read().strip() + return token + except Exception as e: + raise e + + def _read_token_from_env(self): + token = os.getenv("LOCAL_K8S_TOKEN") + if not token: + raise KeyError("LOCAL_K8S_TOKEN environment variable is not set.") + return token diff --git a/sdk/python/feast/permissions/client/oidc_authentication_client_manager.py b/sdk/python/feast/permissions/client/oidc_authentication_client_manager.py new file mode 100644 index 0000000000..544764aae0 --- /dev/null +++ b/sdk/python/feast/permissions/client/oidc_authentication_client_manager.py @@ -0,0 +1,58 @@ +import logging + +import requests + +from feast.permissions.auth_model import OidcAuthConfig +from feast.permissions.client.auth_client_manager import AuthenticationClientManager + +logger = logging.getLogger(__name__) + + +class OidcAuthClientManager(AuthenticationClientManager): + def __init__(self, auth_config: OidcAuthConfig): + self.auth_config = auth_config + + def _get_token_endpoint(self): + response = requests.get(self.auth_config.auth_discovery_url) + if response.status_code == 200: + oidc_config = response.json() + if not oidc_config["token_endpoint"]: + raise RuntimeError( + " OIDC token_endpoint is not available from discovery url response." + ) + return oidc_config["token_endpoint"].replace( + "master", self.auth_config.realm + ) + else: + raise RuntimeError( + f"Error fetching OIDC token endpoint configuration: {response.status_code} - {response.text}" + ) + + def get_token(self): + # Fetch the token endpoint from the discovery URL + token_endpoint = self._get_token_endpoint() + + token_request_body = { + "grant_type": "password", + "client_id": self.auth_config.client_id, + "client_secret": self.auth_config.client_secret, + "username": self.auth_config.username, + "password": self.auth_config.password, + } + headers = {"Content-Type": "application/x-www-form-urlencoded"} + + token_response = requests.post( + token_endpoint, data=token_request_body, headers=headers + ) + if token_response.status_code == 200: + access_token = token_response.json()["access_token"] + if not access_token: + logger.debug( + f"access_token is empty for the client_id=${self.auth_config.client_id}" + ) + raise RuntimeError("access token is empty") + return access_token + else: + raise RuntimeError( + f"""Failed to obtain oidc access token:url=[{token_endpoint}] {token_response.status_code} - {token_response.text}""" + ) diff --git a/sdk/python/feast/permissions/decision.py b/sdk/python/feast/permissions/decision.py new file mode 100644 index 0000000000..963befe831 --- /dev/null +++ b/sdk/python/feast/permissions/decision.py @@ -0,0 +1,114 @@ +import enum +import logging +from typing import Optional + +logger = logging.getLogger(__name__) + + +class DecisionStrategy(enum.Enum): + """ + The strategy to be adopted in case multiple permissions match an execution request. + """ + + UNANIMOUS = "unanimous" # All policies must evaluate to a positive decision for the final decision to be also positive. + AFFIRMATIVE = ( + "affirmative" # At least one policy must evaluate to a positive decision + ) + # The number of positive decisions must be greater than the number of negative decisions. + # If the number of positive and negative decisions is the same, the final decision will be negative. + CONSENSUS = "consensus" + + +class DecisionEvaluator: + """ + A class to implement the decision logic, according to the selected strategy. + + Args: + decision_strategy: The associated `DecisionStrategy`. + num_of_voters: The expected number of votes to complete the decision. + + Examples: + Create the instance and specify the strategy and number of decisions: + `evaluator = DecisionEvaluator(DecisionStrategy.UNANIMOUS, 3) + + For each vote that you receivem, add a decision grant: `evaluator.add_grant(vote, message)` + and check if the decision process ended: `if evaluator.is_decided():` + Once decided, get the result and the failure explanations using: + `grant, explanations = evaluator.grant()` + """ + + def __init__( + self, + num_of_voters: int, + ): + # Only AFFIRMATIVE strategy is managed available + decision_strategy = DecisionStrategy.AFFIRMATIVE + self.num_of_voters = num_of_voters + + self.grant_count = 0 + self.deny_count = 0 + + self.grant_quorum = ( + 1 + if decision_strategy == DecisionStrategy.AFFIRMATIVE + else num_of_voters + if decision_strategy == DecisionStrategy.UNANIMOUS + else num_of_voters // 2 + 1 + ) + self.deny_quorum = ( + num_of_voters + if decision_strategy == DecisionStrategy.AFFIRMATIVE + else 1 + if decision_strategy == DecisionStrategy.UNANIMOUS + else num_of_voters // 2 + (num_of_voters % 2) + ) + self.grant_decision: Optional[bool] = None + self.explanations: list[str] = [] + logger.info( + f"Decision evaluation started with grant_quorum={self.grant_quorum}, deny_quorum={self.deny_quorum}" + ) + + def is_decided(self) -> bool: + """ + Returns: + bool: `True` when the decision process completed (e.g. we added as many votes as specified in the `num_of_voters` creation argument). + """ + return self.grant_decision is not None + + def grant(self) -> tuple[bool, list[str]]: + """ + Returns: + tuple[bool, list[str]]: The tuple of decision computation: a `bool` with the computation decision and a `list[str]` with the + denial explanations (possibly empty). + """ + logger.info( + f"Decided grant is {self.grant_decision}, explanations={self.explanations}" + ) + return bool(self.grant_decision), self.explanations + + def add_grant(self, grant: bool, explanation: str): + """ + Add a single vote to the decision computation, with a possible denial reason. + If the evaluation process already ended, additional votes are discarded. + + Args: + grant: `True` is the decision is accepted, `False` otherwise. + explanation: Denial reason (not considered when `vote` is `True`). + """ + + if self.is_decided(): + logger.warning("Grant decision already decided, discarding vote") + return + if grant: + self.grant_count += 1 + else: + self.deny_count += 1 + self.explanations.append(explanation) + + if self.grant_count >= self.grant_quorum: + self.grant_decision = True + if self.deny_count >= self.deny_quorum: + self.grant_decision = False + logger.debug( + f"After new grant: grants={self.grant_count}, deny_count={self.deny_count}, grant_decision={self.grant_decision}" + ) diff --git a/sdk/python/feast/permissions/decorator.py b/sdk/python/feast/permissions/decorator.py new file mode 100644 index 0000000000..3b9f7a4ae3 --- /dev/null +++ b/sdk/python/feast/permissions/decorator.py @@ -0,0 +1,42 @@ +import logging +from typing import Union + +from feast.permissions.action import AuthzedAction +from feast.permissions.matcher import is_a_feast_object +from feast.permissions.security_manager import assert_permissions + +logger = logging.getLogger(__name__) + + +def require_permissions(actions: Union[list[AuthzedAction], AuthzedAction]): + """ + A decorator to define the actions that are executed from the decorated class method and that must be protected + against unauthorized access. + + The first parameter of the protected method must be `self` + Args: + actions: The list of actions that must be permitted to the current user. + """ + + def require_permissions_decorator(func): + def permission_checker(*args, **kwargs): + logger.debug(f"permission_checker for {args}, {kwargs}") + resource = args[0] + if not is_a_feast_object(resource): + raise NotImplementedError( + f"The first argument is not of a managed type but {type(resource)}" + ) + + return assert_permissions( + resource=resource, + actions=actions, + ) + logger.debug( + f"Current User can invoke {actions} on {resource.name}:{type(resource)} " + ) + result = func(*args, **kwargs) + return result + + return permission_checker + + return require_permissions_decorator diff --git a/sdk/python/feast/permissions/enforcer.py b/sdk/python/feast/permissions/enforcer.py new file mode 100644 index 0000000000..af41d12a2c --- /dev/null +++ b/sdk/python/feast/permissions/enforcer.py @@ -0,0 +1,77 @@ +import logging + +from feast.feast_object import FeastObject +from feast.permissions.decision import DecisionEvaluator +from feast.permissions.permission import ( + AuthzedAction, + Permission, +) +from feast.permissions.user import User + +logger = logging.getLogger(__name__) + + +def enforce_policy( + permissions: list[Permission], + user: User, + resources: list[FeastObject], + actions: list[AuthzedAction], + filter_only: bool = False, +) -> list[FeastObject]: + """ + Define the logic to apply the configured permissions when a given action is requested on + a protected resource. + + If no permissions are defined, the result is to allow the execution. + + Args: + permissions: The configured set of `Permission`. + user: The current user. + resources: The resources for which we need to enforce authorized permission. + actions: The requested actions to be authorized. + filter_only: If `True`, it removes unauthorized resources from the returned value, otherwise it raises a `PermissionError` the + first unauthorized resource. Defaults to `False`. + + Returns: + list[FeastObject]: A filtered list of the permitted resources. + + Raises: + PermissionError: If the current user is not authorized to eecute the requested actions on the given resources (and `filter_only` is `False`). + """ + if not permissions: + return resources + + _permitted_resources: list[FeastObject] = [] + for resource in resources: + logger.debug( + f"Enforcing permission policies for {type(resource).__name__}:{resource.name} to execute {actions}" + ) + matching_permissions = [ + p + for p in permissions + if p.match_resource(resource) and p.match_actions(actions) + ] + + if matching_permissions: + evaluator = DecisionEvaluator(len(matching_permissions)) + for p in matching_permissions: + permission_grant, permission_explanation = p.policy.validate_user( + user=user + ) + evaluator.add_grant( + permission_grant, + f"Permission {p.name} denied execution of {[a.value.upper() for a in actions]} to {type(resource).__name__}:{resource.name}: {permission_explanation}", + ) + + if evaluator.is_decided(): + grant, explanations = evaluator.grant() + if not grant and not filter_only: + raise PermissionError(",".join(explanations)) + if grant: + _permitted_resources.append(resource) + break + else: + message = f"No permissions defined to manage {actions} on {type(resource)}/{resource.name}." + logger.exception(f"**PERMISSION NOT GRANTED**: {message}") + raise PermissionError(message) + return _permitted_resources diff --git a/sdk/python/feast/permissions/matcher.py b/sdk/python/feast/permissions/matcher.py new file mode 100644 index 0000000000..337bfd5c57 --- /dev/null +++ b/sdk/python/feast/permissions/matcher.py @@ -0,0 +1,129 @@ +""" +This module provides utility matching functions. +""" + +import logging +import re +from typing import TYPE_CHECKING, Any, Optional +from unittest.mock import Mock + +from feast.permissions.action import AuthzedAction + +if TYPE_CHECKING: + from feast.feast_object import FeastObject + +logger = logging.getLogger(__name__) + + +def is_a_feast_object(resource: Any): + """ + A matcher to verify that a given object is one of the Feast objects defined in the `FeastObject` type. + + Args: + resource: An object instance to verify. + Returns: + `True` if the given object is one of the types in the FeastObject alias or a subclass of one of them. + """ + from feast.feast_object import ALL_RESOURCE_TYPES + + for t in ALL_RESOURCE_TYPES: + # Use isinstance to pass Mock validation + if isinstance(resource, t): + return True + return False + + +def _get_type(resource: "FeastObject") -> Any: + is_mock = isinstance(resource, Mock) + if not is_mock: + return type(resource) + else: + return getattr(resource, "_spec_class", None) + + +def resource_match_config( + resource: "FeastObject", + expected_types: list["FeastObject"], + name_pattern: Optional[str] = None, + required_tags: Optional[dict[str, str]] = None, +) -> bool: + """ + Match a given Feast object against the configured type, name and tags in a permission configuration. + + Args: + resource: A FeastObject instance to match agains the permission. + expected_types: The list of object types configured in the permission. Type match also includes all the sub-classes. + name_pattern: The optional name pattern filter configured in the permission. + required_tags: The optional dictionary of required tags configured in the permission. + + Returns: + bool: `True` if the resource matches the configured permission filters. + """ + if resource is None: + logger.warning(f"None passed to {resource_match_config.__name__}") + return False + + _type = _get_type(resource) + if not is_a_feast_object(resource): + logger.warning(f"Given resource is not of a managed type but {_type}") + return False + + # mypy check ignored because of https://github.com/python/mypy/issues/11673, or it raises "Argument 2 to "isinstance" has incompatible type "tuple[Featu ..." + if not isinstance(resource, tuple(expected_types)): # type: ignore + logger.info( + f"Resource does not match any of the expected type {expected_types}" + ) + return False + + if name_pattern is not None: + if hasattr(resource, "name"): + if isinstance(resource.name, str): + match = bool(re.fullmatch(name_pattern, resource.name)) + if not match: + logger.info( + f"Resource name {resource.name} does not match pattern {name_pattern}" + ) + return False + else: + logger.warning( + f"Resource {resource} has no `name` attribute of unexpected type {type(resource.name)}" + ) + else: + logger.warning(f"Resource {resource} has no `name` attribute") + + if required_tags: + if hasattr(resource, "required_tags"): + if isinstance(resource.required_tags, dict): + for tag in required_tags.keys(): + required_value = required_tags.get(tag) + actual_value = resource.required_tags.get(tag) + if required_value != actual_value: + logger.info( + f"Unmatched value {actual_value} for required tag {tag}: expected {required_value}" + ) + return False + else: + logger.warning( + f"Resource {resource} has no `required_tags` attribute of unexpected type {type(resource.required_tags)}" + ) + else: + logger.warning(f"Resource {resource} has no `required_tags` attribute") + + return True + + +def actions_match_config( + requested_actions: list[AuthzedAction], + allowed_actions: list[AuthzedAction], +) -> bool: + """ + Match a list of actions against the actions defined in a permission configuration. + + Args: + requested_actions: A list of actions to be executed. + allowed_actions: The list of actions configured in the permission. + + Returns: + bool: `True` if all the given `requested_actions` are defined in the `allowed_actions`. + """ + return all(a in allowed_actions for a in requested_actions) diff --git a/sdk/python/feast/permissions/permission.py b/sdk/python/feast/permissions/permission.py new file mode 100644 index 0000000000..1117a3ee82 --- /dev/null +++ b/sdk/python/feast/permissions/permission.py @@ -0,0 +1,269 @@ +import logging +import re +from abc import ABC +from datetime import datetime +from typing import TYPE_CHECKING, Any, Dict, Optional, Union + +from google.protobuf.json_format import MessageToJson + +from feast.importer import import_class +from feast.permissions.action import ALL_ACTIONS, AuthzedAction +from feast.permissions.matcher import actions_match_config, resource_match_config +from feast.permissions.policy import AllowAll, Policy +from feast.protos.feast.core.Permission_pb2 import Permission as PermissionProto +from feast.protos.feast.core.Permission_pb2 import PermissionMeta as PermissionMetaProto +from feast.protos.feast.core.Permission_pb2 import PermissionSpec as PermissionSpecProto + +if TYPE_CHECKING: + from feast.feast_object import FeastObject + +logger = logging.getLogger(__name__) + +""" +Constant to refer to all the managed types. +""" + + +class Permission(ABC): + """ + The Permission class defines the authorization policy to be validated whenever the identified actions are + requested on the matching resources. + + Attributes: + name: The permission name (can be duplicated, used for logging troubleshooting). + types: The list of protected resource types as defined by the `FeastObject` type. The match includes all the sub-classes of the given types. + Defaults to all managed types (e.g. the `ALL_RESOURCE_TYPES` constant) + name_pattern: A regex to match the resource name. Defaults to None, meaning that no name filtering is applied + be present in a resource tags with the given value. Defaults to None, meaning that no tags filtering is applied. + actions: The actions authorized by this permission. Defaults to `ALL_ACTIONS`. + policy: The policy to be applied to validate a client request. + tags: A dictionary of key-value pairs to store arbitrary metadata. + required_tags: Dictionary of key-value pairs that must match the resource tags. All these tags must + """ + + _name: str + _types: list["FeastObject"] + _name_pattern: Optional[str] + _actions: list[AuthzedAction] + _policy: Policy + _tags: Dict[str, str] + _required_tags: dict[str, str] + created_timestamp: Optional[datetime] + last_updated_timestamp: Optional[datetime] + + def __init__( + self, + name: str, + types: Optional[Union[list["FeastObject"], "FeastObject"]] = None, + name_pattern: Optional[str] = None, + actions: Union[list[AuthzedAction], AuthzedAction] = ALL_ACTIONS, + policy: Policy = AllowAll, + tags: Optional[dict[str, str]] = None, + required_tags: Optional[dict[str, str]] = None, + ): + from feast.feast_object import ALL_RESOURCE_TYPES + + if not types: + types = ALL_RESOURCE_TYPES + for t in types if isinstance(types, list) else [types]: + if t not in ALL_RESOURCE_TYPES: + raise ValueError(f"{t} is not one of the managed types") + if actions is None or not actions: + raise ValueError("The list 'actions' must be non-empty.") + if not policy: + raise ValueError("The list 'policy' must be non-empty.") + self._name = name + self._types = types if isinstance(types, list) else [types] + self._name_pattern = _normalize_name_pattern(name_pattern) + self._actions = actions if isinstance(actions, list) else [actions] + self._policy = policy + self._tags = _normalize_tags(tags) + self._required_tags = _normalize_tags(required_tags) + self.created_timestamp = None + self.last_updated_timestamp = None + + def __eq__(self, other): + if not isinstance(other, Permission): + raise TypeError("Comparisons should only involve Permission class objects.") + + if ( + self.name != other.name + or self.name_pattern != other.name_pattern + or self.tags != other.tags + or self.policy != other.policy + or self.actions != other.actions + or self.required_tags != other.required_tags + ): + return False + + if set(self.types) != set(other.types): + return False + + return True + + def __hash__(self): + return hash(self.name) + + def __str__(self): + return str(MessageToJson(self.to_proto())) + + @property + def name(self) -> str: + return self._name + + @property + def types(self) -> list["FeastObject"]: + return self._types + + @property + def name_pattern(self) -> Optional[str]: + return self._name_pattern + + @property + def actions(self) -> list[AuthzedAction]: + return self._actions + + @property + def policy(self) -> Policy: + return self._policy + + @property + def tags(self) -> Dict[str, str]: + return self._tags + + @property + def required_tags(self) -> Dict[str, str]: + return self._required_tags + + def match_resource(self, resource: "FeastObject") -> bool: + """ + Returns: + `True` when the given resource matches the type, name and tags filters defined in the permission. + """ + return resource_match_config( + resource=resource, + expected_types=self.types, + name_pattern=self.name_pattern, + required_tags=self.required_tags, + ) + + def match_actions(self, requested_actions: list[AuthzedAction]) -> bool: + """ + Returns: + `True` when the given actions are included in the permitted actions. + """ + return actions_match_config( + allowed_actions=self.actions, + requested_actions=requested_actions, + ) + + @staticmethod + def from_proto(permission_proto: PermissionProto) -> Any: + """ + Converts permission config in protobuf spec to a Permission class object. + + Args: + permission_proto: A protobuf representation of a Permission. + + Returns: + A Permission class object. + """ + + types = [ + get_type_class_from_permission_type( + _PERMISSION_TYPES[PermissionSpecProto.Type.Name(t)] + ) + for t in permission_proto.spec.types + ] + actions = [ + AuthzedAction[PermissionSpecProto.AuthzedAction.Name(action)] + for action in permission_proto.spec.actions + ] + + permission = Permission( + permission_proto.spec.name, + types, + permission_proto.spec.name_pattern or None, + actions, + Policy.from_proto(permission_proto.spec.policy), + dict(permission_proto.spec.tags) or None, + dict(permission_proto.spec.required_tags) or None, + ) + + if permission_proto.meta.HasField("created_timestamp"): + permission.created_timestamp = ( + permission_proto.meta.created_timestamp.ToDatetime() + ) + if permission_proto.meta.HasField("last_updated_timestamp"): + permission.last_updated_timestamp = ( + permission_proto.meta.last_updated_timestamp.ToDatetime() + ) + + return permission + + def to_proto(self) -> PermissionProto: + """ + Converts a PermissionProto object to its protobuf representation. + """ + types = [ + PermissionSpecProto.Type.Value( + re.sub(r"([a-z])([A-Z])", r"\1_\2", t.__name__).upper() # type: ignore[union-attr] + ) + for t in self.types + ] + + actions = [ + PermissionSpecProto.AuthzedAction.Value(action.name) + for action in self.actions + ] + + permission_spec = PermissionSpecProto( + name=self.name, + types=types, + name_pattern=self.name_pattern if self.name_pattern is not None else "", + actions=actions, + policy=self.policy.to_proto(), + tags=self.tags, + required_tags=self.required_tags, + ) + + meta = PermissionMetaProto() + if self.created_timestamp: + meta.created_timestamp.FromDatetime(self.created_timestamp) + if self.last_updated_timestamp: + meta.last_updated_timestamp.FromDatetime(self.last_updated_timestamp) + + return PermissionProto(spec=permission_spec, meta=meta) + + +def _normalize_name_pattern(name_pattern: Optional[str]): + if name_pattern is not None: + return name_pattern.strip() + return None + + +def _normalize_tags(tags: Optional[dict[str, str]]): + if tags: + return { + k.strip(): v.strip() if isinstance(v, str) else v for k, v in tags.items() + } + return None + + +def get_type_class_from_permission_type(permission_type: str): + module_name, config_class_name = permission_type.rsplit(".", 1) + return import_class(module_name, config_class_name) + + +_PERMISSION_TYPES = { + "FEATURE_VIEW": "feast.feature_view.FeatureView", + "ON_DEMAND_FEATURE_VIEW": "feast.on_demand_feature_view.OnDemandFeatureView", + "BATCH_FEATURE_VIEW": "feast.batch_feature_view.BatchFeatureView", + "STREAM_FEATURE_VIEW": "feast.stream_feature_view.StreamFeatureView", + "ENTITY": "feast.entity.Entity", + "FEATURE_SERVICE": "feast.feature_service.FeatureService", + "DATA_SOURCE": "feast.data_source.DataSource", + "VALIDATION_REFERENCE": "feast.saved_dataset.ValidationReference", + "SAVED_DATASET": "feast.saved_dataset.SavedDataset", + "PERMISSION": "feast.permissions.permission.Permission", +} diff --git a/sdk/python/feast/permissions/policy.py b/sdk/python/feast/permissions/policy.py new file mode 100644 index 0000000000..271448422f --- /dev/null +++ b/sdk/python/feast/permissions/policy.py @@ -0,0 +1,129 @@ +from abc import ABC, abstractmethod +from typing import Any + +from feast.permissions.user import User +from feast.protos.feast.core.Policy_pb2 import Policy as PolicyProto +from feast.protos.feast.core.Policy_pb2 import RoleBasedPolicy as RoleBasedPolicyProto + + +class Policy(ABC): + """ + An abstract class to ensure that the current user matches the configured security policies. + """ + + @abstractmethod + def validate_user(self, user: User) -> tuple[bool, str]: + """ + Validate the given user against the configured policy. + + Args: + user: The current user. + + Returns: + bool: `True` if the user matches the policy criteria, `False` otherwise. + str: A possibly empty explanation of the reason for not matching the configured policy. + """ + raise NotImplementedError + + @staticmethod + def from_proto(policy_proto: PolicyProto) -> Any: + """ + Converts policy config in protobuf spec to a Policy class object. + + Args: + policy_proto: A protobuf representation of a Policy. + + Returns: + A Policy class object. + """ + policy_type = policy_proto.WhichOneof("policy_type") + if policy_type == "role_based_policy": + return RoleBasedPolicy.from_proto(policy_proto) + if policy_type is None: + return None + raise NotImplementedError(f"policy_type is unsupported: {policy_type}") + + @abstractmethod + def to_proto(self) -> PolicyProto: + """ + Converts a PolicyProto object to its protobuf representation. + """ + raise NotImplementedError + + +class RoleBasedPolicy(Policy): + """ + A `Policy` implementation where the user roles must be enforced to grant access to the requested action. + At least one of the configured roles must be granted to the current user in order to allow the execution of the secured operation. + + E.g., if the policy enforces roles `a` and `b`, the user must have at least one of them in order to satisfy the policy. + """ + + def __init__( + self, + roles: list[str], + ): + self.roles = roles + + def __eq__(self, other): + if not isinstance(other, RoleBasedPolicy): + raise TypeError( + "Comparisons should only involve RoleBasedPolicy class objects." + ) + + if sorted(self.roles) != sorted(other.roles): + return False + + return True + + def get_roles(self) -> list[str]: + return self.roles + + def validate_user(self, user: User) -> tuple[bool, str]: + """ + Validate the given `user` against the configured roles. + """ + result = user.has_matching_role(self.roles) + explain = "" if result else f"Requires roles {self.roles}" + return (result, explain) + + @staticmethod + def from_proto(policy_proto: PolicyProto) -> Any: + """ + Converts policy config in protobuf spec to a Policy class object. + + Args: + policy_proto: A protobuf representation of a Policy. + + Returns: + A RoleBasedPolicy class object. + """ + return RoleBasedPolicy(roles=list(policy_proto.role_based_policy.roles)) + + def to_proto(self) -> PolicyProto: + """ + Converts a PolicyProto object to its protobuf representation. + """ + + role_based_policy_proto = RoleBasedPolicyProto(roles=self.roles) + policy_proto = PolicyProto(role_based_policy=role_based_policy_proto) + + return policy_proto + + +def allow_all(self, user: User) -> tuple[bool, str]: + return True, "" + + +def empty_policy(self) -> PolicyProto: + return PolicyProto() + + +""" +A `Policy` instance to allow execution of any action to each user +""" +AllowAll = type( + "AllowAll", + (Policy,), + {Policy.validate_user.__name__: allow_all, Policy.to_proto.__name__: empty_policy}, +)() diff --git a/sdk/python/feast/permissions/security_manager.py b/sdk/python/feast/permissions/security_manager.py new file mode 100644 index 0000000000..178db6e6e9 --- /dev/null +++ b/sdk/python/feast/permissions/security_manager.py @@ -0,0 +1,163 @@ +import logging +from contextvars import ContextVar +from typing import List, Optional, Union + +from feast.feast_object import FeastObject +from feast.infra.registry.base_registry import BaseRegistry +from feast.permissions.action import AuthzedAction +from feast.permissions.enforcer import enforce_policy +from feast.permissions.permission import Permission +from feast.permissions.user import User + +logger = logging.getLogger(__name__) + + +class SecurityManager: + """ + The security manager it's the entry point to validate the configuration of the current user against the configured permission policies. + It is accessed and defined using the global functions `get_security_manager` and `set_security_manager` + """ + + def __init__( + self, + project: str, + registry: BaseRegistry, + ): + self._project = project + self._registry = registry + self._current_user: ContextVar[Optional[User]] = ContextVar( + "current_user", default=None + ) + + def set_current_user(self, current_user: User): + """ + Init the user for the current context. + """ + self._current_user.set(current_user) + + @property + def current_user(self) -> Optional[User]: + """ + Returns: + str: the possibly empty instance of the current user. `contextvars` module is used to ensure that each concurrent request has its own + individual user. + """ + return self._current_user.get() + + @property + def permissions(self) -> list[Permission]: + """ + Returns: + list[Permission]: the list of `Permission` configured in the Feast registry. + """ + return self._registry.list_permissions(project=self._project) + + def assert_permissions( + self, + resources: list[FeastObject], + actions: Union[AuthzedAction, List[AuthzedAction]], + filter_only: bool = False, + ) -> list[FeastObject]: + """ + Verify if the current user is authorized ro execute the requested actions on the given resources. + + If no permissions are defined, the result is to allow the execution. + + Args: + resources: The resources for which we need to enforce authorized permission. + actions: The requested actions to be authorized. + filter_only: If `True`, it removes unauthorized resources from the returned value, otherwise it raises a `PermissionError` the + first unauthorized resource. Defaults to `False`. + + Returns: + list[FeastObject]: A filtered list of the permitted resources, possibly empty. + + Raises: + PermissionError: If the current user is not authorized to eecute all the requested actions on the given resources. + """ + return enforce_policy( + permissions=self.permissions, + user=self.current_user if self.current_user is not None else User("", []), + resources=resources, + actions=actions if isinstance(actions, list) else [actions], + filter_only=filter_only, + ) + + +def assert_permissions( + resource: FeastObject, + actions: Union[AuthzedAction, List[AuthzedAction]], +) -> FeastObject: + """ + A utility function to invoke the `assert_permissions` method on the global security manager. + + If no global `SecurityManager` is defined, the execution is permitted. + + Args: + resource: The resource for which we need to enforce authorized permission. + actions: The requested actions to be authorized. + Returns: + FeastObject: The original `resource`, if permitted. + + Raises: + PermissionError: If the current user is not authorized to execute the requested actions on the given resources. + """ + sm = get_security_manager() + if sm is None: + return resource + return sm.assert_permissions( + resources=[resource], actions=actions, filter_only=False + )[0] + + +def permitted_resources( + resources: list[FeastObject], + actions: Union[AuthzedAction, List[AuthzedAction]], +) -> list[FeastObject]: + """ + A utility function to invoke the `assert_permissions` method on the global security manager. + + If no global `SecurityManager` is defined, the execution is permitted. + + Args: + resources: The resources for which we need to enforce authorized permission. + actions: The requested actions to be authorized. + Returns: + list[FeastObject]]: A filtered list of the permitted resources, possibly empty. + """ + sm = get_security_manager() + if sm is None: + return resources + return sm.assert_permissions(resources=resources, actions=actions, filter_only=True) + + +""" +The possibly empty global instance of `SecurityManager`. +""" +_sm: Optional[SecurityManager] = None + + +def get_security_manager() -> Optional[SecurityManager]: + """ + Return the global instance of `SecurityManager`. + """ + global _sm + return _sm + + +def set_security_manager(sm: SecurityManager): + """ + Initialize the global instance of `SecurityManager`. + """ + + global _sm + _sm = sm + + +def no_security_manager(): + """ + Initialize the empty global instance of `SecurityManager`. + """ + + global _sm + _sm = None diff --git a/sdk/python/feast/permissions/server/__init__.py b/sdk/python/feast/permissions/server/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sdk/python/feast/permissions/server/arrow.py b/sdk/python/feast/permissions/server/arrow.py new file mode 100644 index 0000000000..5eba7d0916 --- /dev/null +++ b/sdk/python/feast/permissions/server/arrow.py @@ -0,0 +1,111 @@ +""" +A module with utility functions and classes to support authorizing the Arrow Flight servers. +""" + +import asyncio +import functools +import logging +from typing import Optional, cast + +import pyarrow.flight as fl +from pyarrow.flight import ServerCallContext + +from feast.permissions.auth.auth_manager import ( + get_auth_manager, +) +from feast.permissions.security_manager import get_security_manager +from feast.permissions.server.utils import ( + AuthManagerType, +) +from feast.permissions.user import User + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + + +def arrowflight_middleware( + auth_type: AuthManagerType, +) -> Optional[dict[str, fl.ServerMiddlewareFactory]]: + """ + A dictionary with the configured middlewares to support extracting the user details when the authorization manager is defined. + The authorization middleware key is `auth`. + + Returns: + dict[str, fl.ServerMiddlewareFactory]: Optional dictionary of middlewares. If the authorization type is set to `NONE`, it returns `None`. + """ + + if auth_type == AuthManagerType.NONE: + return None + + return { + "auth": AuthorizationMiddlewareFactory(), + } + + +class AuthorizationMiddlewareFactory(fl.ServerMiddlewareFactory): + """ + A middleware factory to intercept the authorization header and propagate it to the authorization middleware. + """ + + def __init__(self): + pass + + def start_call(self, info, headers): + """ + Intercept the authorization header and propagate it to the authorization middleware. + """ + access_token = get_auth_manager().token_extractor.extract_access_token( + headers=headers + ) + return AuthorizationMiddleware(access_token=access_token) + + +class AuthorizationMiddleware(fl.ServerMiddleware): + """ + A server middleware holding the authorization header and offering a method to extract the user credentials. + """ + + def __init__(self, access_token: str): + self.access_token = access_token + + def call_completed(self, exception): + if exception: + print(f"{AuthorizationMiddleware.__name__} received {exception}") + + async def extract_user(self) -> User: + """ + Use the configured `TokenParser` to extract the user credentials. + """ + return await get_auth_manager().token_parser.user_details_from_access_token( + self.access_token + ) + + +def inject_user_details(context: ServerCallContext): + """ + Function to use in Arrow Flight endpoints (e.g. `do_get`, `do_put` and so on) to access the token extracted from the header, + extract the user details out of it and propagate them to the current security manager, if any. + + Args: + context: The endpoint context. + """ + if context.get_middleware("auth") is None: + logger.info("No `auth` middleware.") + return + + sm = get_security_manager() + if sm is not None: + auth_middleware = cast(AuthorizationMiddleware, context.get_middleware("auth")) + current_user = asyncio.run(auth_middleware.extract_user()) + print(f"extracted user: {current_user}") + + sm.set_current_user(current_user) + + +def inject_user_details_decorator(func): + @functools.wraps(func) + def wrapper(self, context, *args, **kwargs): + inject_user_details(context) + return func(self, context, *args, **kwargs) + + return wrapper diff --git a/sdk/python/feast/permissions/server/arrow_flight_token_extractor.py b/sdk/python/feast/permissions/server/arrow_flight_token_extractor.py new file mode 100644 index 0000000000..2378fa8b19 --- /dev/null +++ b/sdk/python/feast/permissions/server/arrow_flight_token_extractor.py @@ -0,0 +1,42 @@ +from starlette.authentication import ( + AuthenticationError, +) + +from feast.permissions.auth.token_extractor import TokenExtractor + + +class ArrowFlightTokenExtractor(TokenExtractor): + def extract_access_token(self, **kwargs) -> str: + """ + Token extractor for Arrow Flight requests. + + Requires a keyword argument called `headers` of type `dict`. + + Returns: + The extracted access token. + """ + + if "headers" not in kwargs: + raise ValueError("Missing keywork argument 'headers'") + if not isinstance(kwargs["headers"], dict): + raise ValueError( + f"The keywork argument 'headers' is not of the expected type {dict.__name__}" + ) + + access_token = None + headers = kwargs["headers"] + if isinstance(headers, dict): + for header in headers: + if header.lower() == "authorization": + # With Arrow Flight, the header value is a list and we take the 0-th element + if not isinstance(headers[header], list): + raise AuthenticationError( + f"Authorization header must be of type list, found {type(headers[header])}" + ) + + return self._extract_bearer_token(headers[header][0]) + + if access_token is None: + raise AuthenticationError("Missing authorization header") + + return access_token diff --git a/sdk/python/feast/permissions/server/grpc.py b/sdk/python/feast/permissions/server/grpc.py new file mode 100644 index 0000000000..3c94240869 --- /dev/null +++ b/sdk/python/feast/permissions/server/grpc.py @@ -0,0 +1,54 @@ +import asyncio +import logging +from typing import Optional + +import grpc + +from feast.permissions.auth.auth_manager import ( + get_auth_manager, +) +from feast.permissions.security_manager import get_security_manager +from feast.permissions.server.utils import ( + AuthManagerType, +) + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + + +def grpc_interceptors( + auth_type: AuthManagerType, +) -> Optional[list[grpc.ServerInterceptor]]: + """ + A list of the authorization interceptors. + + Args: + auth_type: The type of authorization manager, from the feature store configuration. + + Returns: + list[grpc.ServerInterceptor]: Optional list of interceptors. If the authorization type is set to `NONE`, it returns `None`. + """ + if auth_type == AuthManagerType.NONE: + return None + + return [AuthInterceptor()] + + +class AuthInterceptor(grpc.ServerInterceptor): + def intercept_service(self, continuation, handler_call_details): + sm = get_security_manager() + + if sm is not None: + auth_manager = get_auth_manager() + access_token = auth_manager.token_extractor.extract_access_token( + metadata=dict(handler_call_details.invocation_metadata) + ) + + print(f"Fetching user for token: {len(access_token)}") + current_user = asyncio.run( + auth_manager.token_parser.user_details_from_access_token(access_token) + ) + print(f"User is: {current_user}") + sm.set_current_user(current_user) + + return continuation(handler_call_details) diff --git a/sdk/python/feast/permissions/server/grpc_token_extractor.py b/sdk/python/feast/permissions/server/grpc_token_extractor.py new file mode 100644 index 0000000000..d75a18ded5 --- /dev/null +++ b/sdk/python/feast/permissions/server/grpc_token_extractor.py @@ -0,0 +1,36 @@ +from starlette.authentication import ( + AuthenticationError, +) + +from feast.permissions.auth.token_extractor import TokenExtractor + + +class GrpcTokenExtractor(TokenExtractor): + def extract_access_token(self, **kwargs) -> str: + """ + Token extractor for grpc server requests. + + Requires a keyword argument called `metadata` of type `dict`. + + Returns: + The extracted access token. + """ + + if "metadata" not in kwargs: + raise ValueError("Missing keywork argument 'metadata'") + if not isinstance(kwargs["metadata"], dict): + raise ValueError( + f"The keywork argument 'metadata' is not of the expected type {dict.__name__} but {type(kwargs['metadata'])}" + ) + + access_token = None + metadata = kwargs["metadata"] + if isinstance(metadata, dict): + for header in metadata: + if header.lower() == "authorization": + return self._extract_bearer_token(metadata[header]) + + if access_token is None: + raise AuthenticationError("Missing authorization header") + + return access_token diff --git a/sdk/python/feast/permissions/server/rest.py b/sdk/python/feast/permissions/server/rest.py new file mode 100644 index 0000000000..ecced3b34a --- /dev/null +++ b/sdk/python/feast/permissions/server/rest.py @@ -0,0 +1,33 @@ +""" +A module with utility functions to support authorizing the REST servers using the FastAPI framework. +""" + +from typing import Any + +from fastapi.requests import Request + +from feast.permissions.auth.auth_manager import ( + get_auth_manager, +) +from feast.permissions.security_manager import get_security_manager + + +async def inject_user_details(request: Request) -> Any: + """ + A function to extract the authorization token from a user request, extract the user details and propagate them to the + current security manager, if any. + """ + sm = get_security_manager() + current_user = None + if sm is not None: + auth_manager = get_auth_manager() + access_token = auth_manager.token_extractor.extract_access_token( + request=request + ) + current_user = await auth_manager.token_parser.user_details_from_access_token( + access_token=access_token + ) + + sm.set_current_user(current_user) + + return current_user diff --git a/sdk/python/feast/permissions/server/rest_token_extractor.py b/sdk/python/feast/permissions/server/rest_token_extractor.py new file mode 100644 index 0000000000..894c18eedb --- /dev/null +++ b/sdk/python/feast/permissions/server/rest_token_extractor.py @@ -0,0 +1,38 @@ +from fastapi.requests import Request +from starlette.authentication import ( + AuthenticationError, +) + +from feast.permissions.auth.token_extractor import TokenExtractor + + +class RestTokenExtractor(TokenExtractor): + def extract_access_token(self, **kwargs) -> str: + """ + Token extractor for REST requests. + + Requires a keyword argument called `request` of type `Request` + + Returns: + The extracted access token. + """ + + if "request" not in kwargs: + raise ValueError("Missing keywork argument 'request'") + if not isinstance(kwargs["request"], Request): + raise ValueError( + f"The keywork argument 'request' is not of the expected type {Request.__name__}" + ) + + access_token = None + request = kwargs["request"] + if isinstance(request, Request): + headers = request.headers + for header in headers: + if header.lower() == "authorization": + return self._extract_bearer_token(headers[header]) + + if access_token is None: + raise AuthenticationError("Missing authorization header") + + return access_token diff --git a/sdk/python/feast/permissions/server/utils.py b/sdk/python/feast/permissions/server/utils.py new file mode 100644 index 0000000000..34a2c0024a --- /dev/null +++ b/sdk/python/feast/permissions/server/utils.py @@ -0,0 +1,126 @@ +""" +A module with utility functions to support the authorization management in Feast servers. +""" + +import enum +import logging + +import feast +from feast.permissions.auth.auth_manager import ( + AllowAll, + AuthManager, + set_auth_manager, +) +from feast.permissions.auth.kubernetes_token_parser import KubernetesTokenParser +from feast.permissions.auth.oidc_token_parser import OidcTokenParser +from feast.permissions.auth.token_extractor import TokenExtractor +from feast.permissions.auth.token_parser import TokenParser +from feast.permissions.auth_model import AuthConfig, OidcAuthConfig +from feast.permissions.security_manager import ( + SecurityManager, + no_security_manager, + set_security_manager, +) +from feast.permissions.server.arrow_flight_token_extractor import ( + ArrowFlightTokenExtractor, +) +from feast.permissions.server.grpc_token_extractor import GrpcTokenExtractor +from feast.permissions.server.rest_token_extractor import RestTokenExtractor + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + + +class ServerType(enum.Enum): + """ + Identify the server type. + """ + + REST = "rest" + ARROW = "arrow" + GRPC = "grpc" # TODO RBAC: to be completed + + +class AuthManagerType(enum.Enum): + """ + Identify the type of authorization manager. + """ + + NONE = "no_auth" + OIDC = "oidc" + KUBERNETES = "kubernetes" + + +def str_to_auth_manager_type(value: str) -> AuthManagerType: + for t in AuthManagerType.__members__.values(): + if t.value.lower() == value.lower(): + return t + + logger.warning( + f"Requested unmanaged AuthManagerType of value {value}. Using NONE instead." + ) + return AuthManagerType.NONE + + +def init_security_manager(auth_type: AuthManagerType, fs: "feast.FeatureStore"): + """ + Initialize the global security manager. + Must be invoked at Feast server initialization time to create the `SecurityManager` instance. + + Args: + auth_type: The authorization manager type. + registry: The feature store registry. + """ + if auth_type == AuthManagerType.NONE: + no_security_manager() + else: + # TODO permissions from registry + set_security_manager( + SecurityManager( + project=fs.project, + registry=fs.registry, + ) + ) + + +def init_auth_manager( + server_type: ServerType, auth_type: AuthManagerType, auth_config: AuthConfig +): + """ + Initialize the global authorization manager. + Must be invoked at Feast server initialization time to create the `AuthManager` instance. + + Args: + server_type: The server type. + auth_type: The authorization manager type. + + Raises: + ValueError: If any input argument has an unmanaged value. + """ + if auth_type == AuthManagerType.NONE: + set_auth_manager(AllowAll()) + else: + token_extractor: TokenExtractor + token_parser: TokenParser + + if server_type == ServerType.REST: + token_extractor = RestTokenExtractor() + elif server_type == ServerType.ARROW: + token_extractor = ArrowFlightTokenExtractor() + elif server_type == ServerType.GRPC: + token_extractor = GrpcTokenExtractor() + else: + raise ValueError(f"Unmanaged server type {server_type}") + + if auth_type == AuthManagerType.KUBERNETES: + token_parser = KubernetesTokenParser() + elif auth_type == AuthManagerType.OIDC: + assert isinstance(auth_config, OidcAuthConfig) + token_parser = OidcTokenParser(auth_config=auth_config) + else: + raise ValueError(f"Unmanaged authorization manager type {auth_type}") + + auth_manager = AuthManager( + token_extractor=token_extractor, token_parser=token_parser + ) + set_auth_manager(auth_manager) diff --git a/sdk/python/feast/permissions/user.py b/sdk/python/feast/permissions/user.py new file mode 100644 index 0000000000..783b683de6 --- /dev/null +++ b/sdk/python/feast/permissions/user.py @@ -0,0 +1,38 @@ +import logging + +logger = logging.getLogger(__name__) + + +class User: + _username: str + _roles: list[str] + + def __init__(self, username: str, roles: list[str]): + self._username = username + self._roles = roles + + @property + def username(self): + return self._username + + @property + def roles(self): + return self._roles + + def has_matching_role(self, requested_roles: list[str]) -> bool: + """ + Verify that the user has at least one of the requested roles. + + Args: + requested_roles: The list of requested roles. + + Returns: + bool: `True` only if the user has any registered role and all the given roles are registered. + """ + logger.debug( + f"Check {self.username} has all {requested_roles}: currently {self.roles}" + ) + return any(role in self.roles for role in requested_roles) + + def __str__(self): + return f"{self.username} ({self.roles})" diff --git a/sdk/python/feast/registry_server.py b/sdk/python/feast/registry_server.py index 2ac968bed8..6b37aba08d 100644 --- a/sdk/python/feast/registry_server.py +++ b/sdk/python/feast/registry_server.py @@ -1,19 +1,31 @@ from concurrent import futures from datetime import datetime, timezone +from typing import Union, cast import grpc from google.protobuf.empty_pb2 import Empty from grpc_health.v1 import health, health_pb2, health_pb2_grpc from grpc_reflection.v1alpha import reflection -from feast import FeatureStore +from feast import FeatureService, FeatureStore from feast.data_source import DataSource from feast.entity import Entity -from feast.feature_service import FeatureService +from feast.errors import FeatureViewNotFoundException +from feast.feast_object import FeastObject from feast.feature_view import FeatureView from feast.infra.infra_object import Infra from feast.infra.registry.base_registry import BaseRegistry from feast.on_demand_feature_view import OnDemandFeatureView +from feast.permissions.action import CRUD, AuthzedAction +from feast.permissions.permission import Permission +from feast.permissions.security_manager import assert_permissions, permitted_resources +from feast.permissions.server.grpc import grpc_interceptors +from feast.permissions.server.utils import ( + ServerType, + init_auth_manager, + init_security_manager, + str_to_auth_manager_type, +) from feast.protos.feast.registry import RegistryServer_pb2, RegistryServer_pb2_grpc from feast.saved_dataset import SavedDataset, ValidationReference from feast.stream_feature_view import StreamFeatureView @@ -26,30 +38,55 @@ def __init__(self, registry: BaseRegistry) -> None: def ApplyEntity(self, request: RegistryServer_pb2.ApplyEntityRequest, context): self.proxied_registry.apply_entity( - entity=Entity.from_proto(request.entity), + entity=cast( + Entity, + assert_permissions( + resource=Entity.from_proto(request.entity), + actions=CRUD, + ), + ), project=request.project, commit=request.commit, ) + return Empty() def GetEntity(self, request: RegistryServer_pb2.GetEntityRequest, context): - return self.proxied_registry.get_entity( - name=request.name, project=request.project, allow_cache=request.allow_cache + return assert_permissions( + self.proxied_registry.get_entity( + name=request.name, + project=request.project, + allow_cache=request.allow_cache, + ), + actions=[AuthzedAction.DESCRIBE], ).to_proto() def ListEntities(self, request: RegistryServer_pb2.ListEntitiesRequest, context): return RegistryServer_pb2.ListEntitiesResponse( entities=[ entity.to_proto() - for entity in self.proxied_registry.list_entities( - project=request.project, - allow_cache=request.allow_cache, - tags=dict(request.tags), + for entity in permitted_resources( + resources=cast( + list[FeastObject], + self.proxied_registry.list_entities( + project=request.project, + allow_cache=request.allow_cache, + tags=dict(request.tags), + ), + ), + actions=AuthzedAction.DESCRIBE, ) ] ) def DeleteEntity(self, request: RegistryServer_pb2.DeleteEntityRequest, context): + assert_permissions( + resource=self.proxied_registry.get_entity( + name=request.name, project=request.project + ), + actions=AuthzedAction.DELETE, + ) + self.proxied_registry.delete_entity( name=request.name, project=request.project, commit=request.commit ) @@ -58,16 +95,30 @@ def DeleteEntity(self, request: RegistryServer_pb2.DeleteEntityRequest, context) def ApplyDataSource( self, request: RegistryServer_pb2.ApplyDataSourceRequest, context ): - self.proxied_registry.apply_data_source( - data_source=DataSource.from_proto(request.data_source), - project=request.project, - commit=request.commit, + ( + self.proxied_registry.apply_data_source( + data_source=cast( + DataSource, + assert_permissions( + resource=DataSource.from_proto(request.data_source), + actions=CRUD, + ), + ), + project=request.project, + commit=request.commit, + ), ) + return Empty() def GetDataSource(self, request: RegistryServer_pb2.GetDataSourceRequest, context): - return self.proxied_registry.get_data_source( - name=request.name, project=request.project, allow_cache=request.allow_cache + return assert_permissions( + resource=self.proxied_registry.get_data_source( + name=request.name, + project=request.project, + allow_cache=request.allow_cache, + ), + actions=AuthzedAction.DESCRIBE, ).to_proto() def ListDataSources( @@ -76,10 +127,16 @@ def ListDataSources( return RegistryServer_pb2.ListDataSourcesResponse( data_sources=[ data_source.to_proto() - for data_source in self.proxied_registry.list_data_sources( - project=request.project, - allow_cache=request.allow_cache, - tags=dict(request.tags), + for data_source in permitted_resources( + resources=cast( + list[FeastObject], + self.proxied_registry.list_data_sources( + project=request.project, + allow_cache=request.allow_cache, + tags=dict(request.tags), + ), + ), + actions=AuthzedAction.DESCRIBE, ) ] ) @@ -87,6 +144,14 @@ def ListDataSources( def DeleteDataSource( self, request: RegistryServer_pb2.DeleteDataSourceRequest, context ): + assert_permissions( + resource=self.proxied_registry.get_data_source( + name=request.name, + project=request.project, + ), + actions=AuthzedAction.DELETE, + ) + self.proxied_registry.delete_data_source( name=request.name, project=request.project, commit=request.commit ) @@ -95,8 +160,13 @@ def DeleteDataSource( def GetFeatureView( self, request: RegistryServer_pb2.GetFeatureViewRequest, context ): - return self.proxied_registry.get_feature_view( - name=request.name, project=request.project, allow_cache=request.allow_cache + return assert_permissions( + self.proxied_registry.get_feature_view( + name=request.name, + project=request.project, + allow_cache=request.allow_cache, + ), + actions=[AuthzedAction.DESCRIBE], ).to_proto() def ApplyFeatureView( @@ -112,9 +182,17 @@ def ApplyFeatureView( elif feature_view_type == "stream_feature_view": feature_view = StreamFeatureView.from_proto(request.stream_feature_view) - self.proxied_registry.apply_feature_view( - feature_view=feature_view, project=request.project, commit=request.commit + ( + self.proxied_registry.apply_feature_view( + feature_view=cast( + FeatureView, + assert_permissions(resource=feature_view, actions=CRUD), + ), + project=request.project, + commit=request.commit, + ), ) + return Empty() def ListFeatureViews( @@ -123,10 +201,16 @@ def ListFeatureViews( return RegistryServer_pb2.ListFeatureViewsResponse( feature_views=[ feature_view.to_proto() - for feature_view in self.proxied_registry.list_feature_views( - project=request.project, - allow_cache=request.allow_cache, - tags=dict(request.tags), + for feature_view in permitted_resources( + resources=cast( + list[FeastObject], + self.proxied_registry.list_feature_views( + project=request.project, + allow_cache=request.allow_cache, + tags=dict(request.tags), + ), + ), + actions=AuthzedAction.DESCRIBE, ) ] ) @@ -134,6 +218,21 @@ def ListFeatureViews( def DeleteFeatureView( self, request: RegistryServer_pb2.DeleteFeatureViewRequest, context ): + feature_view: Union[StreamFeatureView, FeatureView] + + try: + feature_view = self.proxied_registry.get_stream_feature_view( + name=request.name, project=request.project, allow_cache=False + ) + except FeatureViewNotFoundException: + feature_view = self.proxied_registry.get_feature_view( + name=request.name, project=request.project, allow_cache=False + ) + + assert_permissions( + resource=feature_view, + actions=[AuthzedAction.DELETE], + ) self.proxied_registry.delete_feature_view( name=request.name, project=request.project, commit=request.commit ) @@ -142,8 +241,13 @@ def DeleteFeatureView( def GetStreamFeatureView( self, request: RegistryServer_pb2.GetStreamFeatureViewRequest, context ): - return self.proxied_registry.get_stream_feature_view( - name=request.name, project=request.project, allow_cache=request.allow_cache + return assert_permissions( + resource=self.proxied_registry.get_stream_feature_view( + name=request.name, + project=request.project, + allow_cache=request.allow_cache, + ), + actions=[AuthzedAction.DESCRIBE], ).to_proto() def ListStreamFeatureViews( @@ -152,10 +256,16 @@ def ListStreamFeatureViews( return RegistryServer_pb2.ListStreamFeatureViewsResponse( stream_feature_views=[ stream_feature_view.to_proto() - for stream_feature_view in self.proxied_registry.list_stream_feature_views( - project=request.project, - allow_cache=request.allow_cache, - tags=dict(request.tags), + for stream_feature_view in permitted_resources( + resources=cast( + list[FeastObject], + self.proxied_registry.list_stream_feature_views( + project=request.project, + allow_cache=request.allow_cache, + tags=dict(request.tags), + ), + ), + actions=AuthzedAction.DESCRIBE, ) ] ) @@ -163,8 +273,13 @@ def ListStreamFeatureViews( def GetOnDemandFeatureView( self, request: RegistryServer_pb2.GetOnDemandFeatureViewRequest, context ): - return self.proxied_registry.get_on_demand_feature_view( - name=request.name, project=request.project, allow_cache=request.allow_cache + return assert_permissions( + resource=self.proxied_registry.get_on_demand_feature_view( + name=request.name, + project=request.project, + allow_cache=request.allow_cache, + ), + actions=[AuthzedAction.DESCRIBE], ).to_proto() def ListOnDemandFeatureViews( @@ -173,10 +288,16 @@ def ListOnDemandFeatureViews( return RegistryServer_pb2.ListOnDemandFeatureViewsResponse( on_demand_feature_views=[ on_demand_feature_view.to_proto() - for on_demand_feature_view in self.proxied_registry.list_on_demand_feature_views( - project=request.project, - allow_cache=request.allow_cache, - tags=dict(request.tags), + for on_demand_feature_view in permitted_resources( + resources=cast( + list[FeastObject], + self.proxied_registry.list_on_demand_feature_views( + project=request.project, + allow_cache=request.allow_cache, + tags=dict(request.tags), + ), + ), + actions=AuthzedAction.DESCRIBE, ) ] ) @@ -185,17 +306,29 @@ def ApplyFeatureService( self, request: RegistryServer_pb2.ApplyFeatureServiceRequest, context ): self.proxied_registry.apply_feature_service( - feature_service=FeatureService.from_proto(request.feature_service), + feature_service=cast( + FeatureService, + assert_permissions( + resource=FeatureService.from_proto(request.feature_service), + actions=CRUD, + ), + ), project=request.project, commit=request.commit, ) + return Empty() def GetFeatureService( self, request: RegistryServer_pb2.GetFeatureServiceRequest, context ): - return self.proxied_registry.get_feature_service( - name=request.name, project=request.project, allow_cache=request.allow_cache + return assert_permissions( + resource=self.proxied_registry.get_feature_service( + name=request.name, + project=request.project, + allow_cache=request.allow_cache, + ), + actions=[AuthzedAction.DESCRIBE], ).to_proto() def ListFeatureServices( @@ -204,10 +337,16 @@ def ListFeatureServices( return RegistryServer_pb2.ListFeatureServicesResponse( feature_services=[ feature_service.to_proto() - for feature_service in self.proxied_registry.list_feature_services( - project=request.project, - allow_cache=request.allow_cache, - tags=dict(request.tags), + for feature_service in permitted_resources( + resources=cast( + list[FeastObject], + self.proxied_registry.list_feature_services( + project=request.project, + allow_cache=request.allow_cache, + tags=dict(request.tags), + ), + ), + actions=AuthzedAction.DESCRIBE, ) ] ) @@ -215,6 +354,15 @@ def ListFeatureServices( def DeleteFeatureService( self, request: RegistryServer_pb2.DeleteFeatureServiceRequest, context ): + ( + assert_permissions( + resource=self.proxied_registry.get_feature_service( + name=request.name, project=request.project + ), + actions=[AuthzedAction.DELETE], + ), + ) + self.proxied_registry.delete_feature_service( name=request.name, project=request.project, commit=request.commit ) @@ -223,18 +371,32 @@ def DeleteFeatureService( def ApplySavedDataset( self, request: RegistryServer_pb2.ApplySavedDatasetRequest, context ): - self.proxied_registry.apply_saved_dataset( - saved_dataset=SavedDataset.from_proto(request.saved_dataset), - project=request.project, - commit=request.commit, + ( + self.proxied_registry.apply_saved_dataset( + saved_dataset=cast( + SavedDataset, + assert_permissions( + resource=SavedDataset.from_proto(request.saved_dataset), + actions=CRUD, + ), + ), + project=request.project, + commit=request.commit, + ), ) + return Empty() def GetSavedDataset( self, request: RegistryServer_pb2.GetSavedDatasetRequest, context ): - return self.proxied_registry.get_saved_dataset( - name=request.name, project=request.project, allow_cache=request.allow_cache + return assert_permissions( + self.proxied_registry.get_saved_dataset( + name=request.name, + project=request.project, + allow_cache=request.allow_cache, + ), + actions=[AuthzedAction.DESCRIBE], ).to_proto() def ListSavedDatasets( @@ -243,8 +405,16 @@ def ListSavedDatasets( return RegistryServer_pb2.ListSavedDatasetsResponse( saved_datasets=[ saved_dataset.to_proto() - for saved_dataset in self.proxied_registry.list_saved_datasets( - project=request.project, allow_cache=request.allow_cache + for saved_dataset in permitted_resources( + resources=cast( + list[FeastObject], + self.proxied_registry.list_saved_datasets( + project=request.project, + allow_cache=request.allow_cache, + tags=dict(request.tags), + ), + ), + actions=AuthzedAction.DESCRIBE, ) ] ) @@ -252,6 +422,13 @@ def ListSavedDatasets( def DeleteSavedDataset( self, request: RegistryServer_pb2.DeleteSavedDatasetRequest, context ): + assert_permissions( + resource=self.proxied_registry.get_saved_dataset( + name=request.name, project=request.project + ), + actions=[AuthzedAction.DELETE], + ) + self.proxied_registry.delete_saved_dataset( name=request.name, project=request.project, commit=request.commit ) @@ -261,19 +438,29 @@ def ApplyValidationReference( self, request: RegistryServer_pb2.ApplyValidationReferenceRequest, context ): self.proxied_registry.apply_validation_reference( - validation_reference=ValidationReference.from_proto( - request.validation_reference + validation_reference=cast( + ValidationReference, + assert_permissions( + ValidationReference.from_proto(request.validation_reference), + actions=CRUD, + ), ), project=request.project, commit=request.commit, ) + return Empty() def GetValidationReference( self, request: RegistryServer_pb2.GetValidationReferenceRequest, context ): - return self.proxied_registry.get_validation_reference( - name=request.name, project=request.project, allow_cache=request.allow_cache + return assert_permissions( + self.proxied_registry.get_validation_reference( + name=request.name, + project=request.project, + allow_cache=request.allow_cache, + ), + actions=[AuthzedAction.DESCRIBE], ).to_proto() def ListValidationReferences( @@ -282,8 +469,16 @@ def ListValidationReferences( return RegistryServer_pb2.ListValidationReferencesResponse( validation_references=[ validation_reference.to_proto() - for validation_reference in self.proxied_registry.list_validation_references( - project=request.project, allow_cache=request.allow_cache + for validation_reference in permitted_resources( + resources=cast( + list[FeastObject], + self.proxied_registry.list_validation_references( + project=request.project, + allow_cache=request.allow_cache, + tags=dict(request.tags), + ), + ), + actions=AuthzedAction.DESCRIBE, ) ] ) @@ -291,6 +486,12 @@ def ListValidationReferences( def DeleteValidationReference( self, request: RegistryServer_pb2.DeleteValidationReferenceRequest, context ): + assert_permissions( + resource=self.proxied_registry.get_validation_reference( + name=request.name, project=request.project + ), + actions=[AuthzedAction.DELETE], + ) self.proxied_registry.delete_validation_reference( name=request.name, project=request.project, commit=request.commit ) @@ -311,6 +512,11 @@ def ListProjectMetadata( def ApplyMaterialization( self, request: RegistryServer_pb2.ApplyMaterializationRequest, context ): + assert_permissions( + resource=FeatureView.from_proto(request.feature_view), + actions=[AuthzedAction.WRITE_ONLINE], + ) + self.proxied_registry.apply_materialization( feature_view=FeatureView.from_proto(request.feature_view), project=request.project, @@ -338,6 +544,67 @@ def GetInfra(self, request: RegistryServer_pb2.GetInfraRequest, context): project=request.project, allow_cache=request.allow_cache ).to_proto() + def ApplyPermission( + self, request: RegistryServer_pb2.ApplyPermissionRequest, context + ): + self.proxied_registry.apply_permission( + permission=cast( + Permission, + assert_permissions( + Permission.from_proto(request.permission), actions=CRUD + ), + ), + project=request.project, + commit=request.commit, + ) + return Empty() + + def GetPermission(self, request: RegistryServer_pb2.GetPermissionRequest, context): + permission = self.proxied_registry.get_permission( + name=request.name, project=request.project, allow_cache=request.allow_cache + ) + assert_permissions( + resource=permission, + actions=[AuthzedAction.DESCRIBE], + ) + permission.to_proto().spec.project = request.project + + return permission.to_proto() + + def ListPermissions( + self, request: RegistryServer_pb2.ListPermissionsRequest, context + ): + return RegistryServer_pb2.ListPermissionsResponse( + permissions=[ + permission.to_proto() + for permission in permitted_resources( + resources=cast( + list[FeastObject], + self.proxied_registry.list_permissions( + project=request.project, allow_cache=request.allow_cache + ), + ), + actions=AuthzedAction.DESCRIBE, + ) + ] + ) + + def DeletePermission( + self, request: RegistryServer_pb2.DeletePermissionRequest, context + ): + assert_permissions( + resource=self.proxied_registry.get_permission( + name=request.name, + project=request.project, + ), + actions=[AuthzedAction.DELETE], + ) + + self.proxied_registry.delete_permission( + name=request.name, project=request.project, commit=request.commit + ) + return Empty() + def Commit(self, request, context): self.proxied_registry.commit() return Empty() @@ -350,8 +617,19 @@ def Proto(self, request, context): return self.proxied_registry.proto() -def start_server(store: FeatureStore, port: int): - server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) +def start_server(store: FeatureStore, port: int, wait_for_termination: bool = True): + auth_manager_type = str_to_auth_manager_type(store.config.auth_config.type) + init_security_manager(auth_type=auth_manager_type, fs=store) + init_auth_manager( + auth_type=auth_manager_type, + server_type=ServerType.GRPC, + auth_config=store.config.auth_config, + ) + + server = grpc.server( + futures.ThreadPoolExecutor(max_workers=10), + interceptors=grpc_interceptors(auth_manager_type), + ) RegistryServer_pb2_grpc.add_RegistryServerServicer_to_server( RegistryServer(store.registry), server ) @@ -369,4 +647,7 @@ def start_server(store: FeatureStore, port: int): server.add_insecure_port(f"[::]:{port}") server.start() - server.wait_for_termination() + if wait_for_termination: + server.wait_for_termination() + else: + return server diff --git a/sdk/python/feast/repo_config.py b/sdk/python/feast/repo_config.py index fc2792e323..069b579999 100644 --- a/sdk/python/feast/repo_config.py +++ b/sdk/python/feast/repo_config.py @@ -19,12 +19,14 @@ from feast.errors import ( FeastFeatureServerTypeInvalidError, + FeastInvalidAuthConfigClass, FeastOfflineStoreInvalidName, FeastOnlineStoreInvalidName, FeastRegistryNotSetError, FeastRegistryTypeInvalidError, ) from feast.importer import import_class +from feast.permissions.auth.auth_type import AuthType warnings.simplefilter("once", RuntimeWarning) @@ -86,6 +88,12 @@ "local": "feast.infra.feature_servers.local_process.config.LocalFeatureServerConfig", } +AUTH_CONFIGS_CLASS_FOR_TYPE = { + "no_auth": "feast.permissions.auth_model.NoAuthConfig", + "kubernetes": "feast.permissions.auth_model.KubernetesAuthConfig", + "oidc": "feast.permissions.auth_model.OidcAuthConfig", +} + class FeastBaseModel(BaseModel): """Feast Pydantic Configuration Class""" @@ -167,6 +175,9 @@ class RepoConfig(FeastBaseModel): online_config: Any = Field(None, alias="online_store") """ OnlineStoreConfig: Online store configuration (optional depending on provider) """ + auth: Any = Field(None, alias="auth") + """ auth: Optional if the services needs the authentication against IDPs (optional depending on provider) """ + offline_config: Any = Field(None, alias="offline_store") """ OfflineStoreConfig: Offline store configuration (optional depending on provider) """ @@ -211,6 +222,13 @@ def __init__(self, **data: Any): self._online_store = None self.online_config = data.get("online_store", "sqlite") + self._auth = None + if "auth" not in data: + self.auth = dict() + self.auth["type"] = AuthType.NONE.value + else: + self.auth = data.get("auth") + self._batch_engine = None if "batch_engine" in data: self.batch_engine_config = data["batch_engine"] @@ -270,6 +288,20 @@ def offline_store(self): self._offline_store = self.offline_config return self._offline_store + @property + def auth_config(self): + if not self._auth: + if isinstance(self.auth, Dict): + self._auth = get_auth_config_from_type(self.auth.get("type"))( + **self.auth + ) + elif isinstance(self.auth, str): + self._auth = get_auth_config_from_type(self.auth.get("type"))() + elif self.auth: + self._auth = self.auth + + return self._auth + @property def online_store(self): if not self._online_store: @@ -300,6 +332,30 @@ def batch_engine(self): return self._batch_engine + @model_validator(mode="before") + def _validate_auth_config(cls, values: Any) -> Any: + from feast.permissions.auth_model import AuthConfig + + if "auth" in values: + allowed_auth_types = AUTH_CONFIGS_CLASS_FOR_TYPE.keys() + if isinstance(values["auth"], Dict): + if values["auth"].get("type") is None: + raise ValueError( + f"auth configuration is missing authentication type. Possible values={allowed_auth_types}" + ) + elif values["auth"]["type"] not in allowed_auth_types: + raise ValueError( + f'auth configuration has invalid authentication type={values["auth"]["type"]}. Possible ' + f'values={allowed_auth_types}' + ) + elif isinstance(values["auth"], AuthConfig): + if values["auth"].type not in allowed_auth_types: + raise ValueError( + f'auth configuration has invalid authentication type={values["auth"].type}. Possible ' + f'values={allowed_auth_types}' + ) + return values + @model_validator(mode="before") def _validate_online_store_config(cls, values: Any) -> Any: # This method will validate whether the online store configurations are set correctly. This explicit validation @@ -480,6 +536,17 @@ def get_online_config_from_type(online_store_type: str): return import_class(module_name, config_class_name, config_class_name) +def get_auth_config_from_type(auth_config_type: str): + if auth_config_type in AUTH_CONFIGS_CLASS_FOR_TYPE: + auth_config_type = AUTH_CONFIGS_CLASS_FOR_TYPE[auth_config_type] + elif not auth_config_type.endswith("AuthConfig"): + raise FeastInvalidAuthConfigClass(auth_config_type) + module_name, online_store_class_type = auth_config_type.rsplit(".", 1) + config_class_name = f"{online_store_class_type}" + + return import_class(module_name, config_class_name, config_class_name) + + def get_offline_config_from_type(offline_store_type: str): if offline_store_type in OFFLINE_STORE_CLASS_FOR_TYPE: offline_store_type = OFFLINE_STORE_CLASS_FOR_TYPE[offline_store_type] diff --git a/sdk/python/feast/repo_contents.py b/sdk/python/feast/repo_contents.py index 33b99f29b2..9893d5be4e 100644 --- a/sdk/python/feast/repo_contents.py +++ b/sdk/python/feast/repo_contents.py @@ -18,6 +18,7 @@ from feast.feature_service import FeatureService from feast.feature_view import FeatureView from feast.on_demand_feature_view import OnDemandFeatureView +from feast.permissions.permission import Permission from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto from feast.stream_feature_view import StreamFeatureView @@ -33,6 +34,7 @@ class RepoContents(NamedTuple): stream_feature_views: List[StreamFeatureView] entities: List[Entity] feature_services: List[FeatureService] + permissions: List[Permission] def to_registry_proto(self) -> RegistryProto: registry_proto = RegistryProto() @@ -50,4 +52,6 @@ def to_registry_proto(self) -> RegistryProto: registry_proto.stream_feature_views.extend( [fv.to_proto() for fv in self.stream_feature_views] ) + registry_proto.permissions.extend([p.to_proto() for p in self.permissions]) + return registry_proto diff --git a/sdk/python/feast/repo_operations.py b/sdk/python/feast/repo_operations.py index 0a89ab72ca..cb27568957 100644 --- a/sdk/python/feast/repo_operations.py +++ b/sdk/python/feast/repo_operations.py @@ -27,6 +27,7 @@ from feast.infra.registry.registry import FEAST_OBJECT_TYPES, FeastObjectType, Registry from feast.names import adjectives, animals from feast.on_demand_feature_view import OnDemandFeatureView +from feast.permissions.permission import Permission from feast.repo_config import RepoConfig from feast.repo_contents import RepoContents from feast.stream_feature_view import StreamFeatureView @@ -120,6 +121,7 @@ def parse_repo(repo_root: Path) -> RepoContents: feature_services=[], on_demand_feature_views=[], stream_feature_views=[], + permissions=[], ) for repo_file in get_repo_files(repo_root): @@ -201,6 +203,10 @@ def parse_repo(repo_root: Path) -> RepoContents: (obj is odfv) for odfv in res.on_demand_feature_views ): res.on_demand_feature_views.append(obj) + elif isinstance(obj, Permission) and not any( + (obj is p) for p in res.permissions + ): + res.permissions.append(obj) res.entities.append(DUMMY_ENTITY) return res @@ -367,7 +373,12 @@ def registry_dump(repo_config: RepoConfig, repo_path: Path) -> str: """For debugging only: output contents of the metadata registry""" registry_config = repo_config.registry project = repo_config.project - registry = Registry(project, registry_config=registry_config, repo_path=repo_path) + registry = Registry( + project, + registry_config=registry_config, + repo_path=repo_path, + auth_config=repo_config.auth_config, + ) registry_dict = registry.to_dict(project=project) return json.dumps(registry_dict, indent=2, sort_keys=True) diff --git a/sdk/python/feast/templates/local/feature_repo/feature_store.yaml b/sdk/python/feast/templates/local/feature_repo/feature_store.yaml index 3e6a360316..11b339583e 100644 --- a/sdk/python/feast/templates/local/feature_repo/feature_store.yaml +++ b/sdk/python/feast/templates/local/feature_repo/feature_store.yaml @@ -7,3 +7,6 @@ online_store: type: sqlite path: data/online_store.db entity_key_serialization_version: 2 +# By default, no_auth for authentication and authorization, other possible values kubernetes and oidc. Refer the documentation for more details. +auth: + type: no_auth diff --git a/sdk/python/feast/templates/minimal/feature_repo/feature_store.yaml b/sdk/python/feast/templates/minimal/feature_repo/feature_store.yaml index 9808690005..45a0ce7718 100644 --- a/sdk/python/feast/templates/minimal/feature_repo/feature_store.yaml +++ b/sdk/python/feast/templates/minimal/feature_repo/feature_store.yaml @@ -3,4 +3,4 @@ registry: /path/to/registry.db provider: local online_store: path: /path/to/online_store.db -entity_key_serialization_version: 2 +entity_key_serialization_version: 2 \ No newline at end of file diff --git a/sdk/python/requirements/py3.10-ci-requirements.txt b/sdk/python/requirements/py3.10-ci-requirements.txt index 785342550a..89459d1a69 100644 --- a/sdk/python/requirements/py3.10-ci-requirements.txt +++ b/sdk/python/requirements/py3.10-ci-requirements.txt @@ -40,6 +40,8 @@ async-timeout==4.0.3 # via # aiohttp # redis +async-property==0.2.2 + # via python-keycloak atpublic==4.1.0 # via ibis-framework attrs==23.2.0 @@ -63,6 +65,8 @@ beautifulsoup4==4.12.3 # via nbconvert bidict==0.23.1 # via ibis-framework +bigtree==0.19.2 + # via feast (setup.py) bleach==6.1.0 # via nbconvert boto3==1.34.131 @@ -131,6 +135,7 @@ cryptography==42.0.8 # azure-identity # azure-storage-blob # great-expectations + # jwcrypto # moto # msal # pyjwt @@ -154,6 +159,8 @@ defusedxml==0.7.1 # via nbconvert deltalake==0.18.1 # via feast (setup.py) +deprecation==2.1.0 + # via python-keycloak dill==0.3.8 # via feast (setup.py) distlib==0.3.8 @@ -300,6 +307,7 @@ httpx==0.27.0 # feast (setup.py) # fastapi # jupyterlab + # python-keycloak ibis-framework[duckdb]==9.1.0 # via # feast (setup.py) @@ -409,6 +417,8 @@ jupyterlab-server==2.27.2 # notebook jupyterlab-widgets==3.0.11 # via ipywidgets +jwcrypto==1.5.6 + # via python-keycloak kubernetes==20.13.0 # via feast (setup.py) locket==1.0.0 @@ -502,6 +512,7 @@ packaging==24.1 # build # dask # db-dtypes + # deprecation # google-cloud-bigquery # great-expectations # gunicorn @@ -712,6 +723,8 @@ python-dotenv==1.0.1 # via uvicorn python-json-logger==2.0.7 # via jupyter-events +python-keycloak==4.2.2 + # via feast (setup.py) python-multipart==0.0.9 # via fastapi pytz==2024.1 @@ -760,7 +773,9 @@ requests==2.32.3 # kubernetes # moto # msal + # python-keycloak # requests-oauthlib + # requests-toolbelt # responses # singlestoredb # snowflake-connector-python @@ -768,6 +783,8 @@ requests==2.32.3 # trino requests-oauthlib==2.0.0 # via kubernetes +requests-toolbelt==1.0.0 + # via python-keycloak responses==0.25.3 # via moto rfc3339-validator==0.1.4 @@ -972,6 +989,7 @@ typing-extensions==4.12.2 # great-expectations # ibis-framework # ipython + # jwcrypto # mypy # psycopg # psycopg-pool @@ -1050,3 +1068,4 @@ yarl==1.9.4 # via aiohttp zipp==3.19.1 # via importlib-metadata +bigtree==0.19.2 diff --git a/sdk/python/requirements/py3.10-requirements.txt b/sdk/python/requirements/py3.10-requirements.txt index 250e617b85..f1ec2b16ab 100644 --- a/sdk/python/requirements/py3.10-requirements.txt +++ b/sdk/python/requirements/py3.10-requirements.txt @@ -222,3 +222,4 @@ websockets==12.0 # via uvicorn zipp==3.19.1 # via importlib-metadata +bigtree==0.19.2 diff --git a/sdk/python/requirements/py3.11-ci-requirements.txt b/sdk/python/requirements/py3.11-ci-requirements.txt index f16b486aa5..fd0b5a6d26 100644 --- a/sdk/python/requirements/py3.11-ci-requirements.txt +++ b/sdk/python/requirements/py3.11-ci-requirements.txt @@ -36,6 +36,8 @@ asttokens==2.4.1 # via stack-data async-lru==2.0.4 # via jupyterlab +async-property==0.2.2 + # via python-keycloak atpublic==4.1.0 # via ibis-framework attrs==23.2.0 @@ -59,6 +61,8 @@ beautifulsoup4==4.12.3 # via nbconvert bidict==0.23.1 # via ibis-framework +bigtree==0.19.2 + # via feast (setup.py) bleach==6.1.0 # via nbconvert boto3==1.34.131 @@ -127,6 +131,7 @@ cryptography==42.0.8 # azure-identity # azure-storage-blob # great-expectations + # jwcrypto # moto # msal # pyjwt @@ -150,6 +155,8 @@ defusedxml==0.7.1 # via nbconvert deltalake==0.18.1 # via feast (setup.py) +deprecation==2.1.0 + # via python-keycloak dill==0.3.8 # via feast (setup.py) distlib==0.3.8 @@ -291,6 +298,7 @@ httpx==0.27.0 # feast (setup.py) # fastapi # jupyterlab + # python-keycloak ibis-framework[duckdb]==9.1.0 # via # feast (setup.py) @@ -400,6 +408,8 @@ jupyterlab-server==2.27.2 # notebook jupyterlab-widgets==3.0.11 # via ipywidgets +jwcrypto==1.5.6 + # via python-keycloak kubernetes==20.13.0 # via feast (setup.py) locket==1.0.0 @@ -493,6 +503,7 @@ packaging==24.1 # build # dask # db-dtypes + # deprecation # google-cloud-bigquery # great-expectations # gunicorn @@ -703,6 +714,8 @@ python-dotenv==1.0.1 # via uvicorn python-json-logger==2.0.7 # via jupyter-events +python-keycloak==4.2.2 + # via feast (setup.py) python-multipart==0.0.9 # via fastapi pytz==2024.1 @@ -751,7 +764,9 @@ requests==2.32.3 # kubernetes # moto # msal + # python-keycloak # requests-oauthlib + # requests-toolbelt # responses # singlestoredb # snowflake-connector-python @@ -759,6 +774,8 @@ requests==2.32.3 # trino requests-oauthlib==2.0.0 # via kubernetes +requests-toolbelt==1.0.0 + # via python-keycloak responses==0.25.3 # via moto rfc3339-validator==0.1.4 @@ -951,6 +968,7 @@ typing-extensions==4.12.2 # great-expectations # ibis-framework # ipython + # jwcrypto # mypy # psycopg # psycopg-pool diff --git a/sdk/python/requirements/py3.11-requirements.txt b/sdk/python/requirements/py3.11-requirements.txt index 4f1655de09..e51452a594 100644 --- a/sdk/python/requirements/py3.11-requirements.txt +++ b/sdk/python/requirements/py3.11-requirements.txt @@ -216,3 +216,4 @@ websockets==12.0 # via uvicorn zipp==3.19.1 # via importlib-metadata +bigtree==0.19.2 diff --git a/sdk/python/requirements/py3.9-ci-requirements.txt b/sdk/python/requirements/py3.9-ci-requirements.txt index 94bfa82058..be30f032a9 100644 --- a/sdk/python/requirements/py3.9-ci-requirements.txt +++ b/sdk/python/requirements/py3.9-ci-requirements.txt @@ -40,6 +40,8 @@ async-timeout==4.0.3 # via # aiohttp # redis +async-property==0.2.2 + # via python-keycloak atpublic==4.1.0 # via ibis-framework attrs==23.2.0 @@ -63,6 +65,8 @@ beautifulsoup4==4.12.3 # via nbconvert bidict==0.23.1 # via ibis-framework +bigtree==0.19.2 + # via feast (setup.py) bleach==6.1.0 # via nbconvert boto3==1.34.131 @@ -131,6 +135,7 @@ cryptography==42.0.8 # azure-identity # azure-storage-blob # great-expectations + # jwcrypto # moto # msal # pyjwt @@ -154,6 +159,8 @@ defusedxml==0.7.1 # via nbconvert deltalake==0.18.1 # via feast (setup.py) +deprecation==2.1.0 + # via python-keycloak dill==0.3.8 # via feast (setup.py) distlib==0.3.8 @@ -300,6 +307,7 @@ httpx==0.27.0 # feast (setup.py) # fastapi # jupyterlab + # python-keycloak ibis-framework[duckdb]==9.0.0 # via # feast (setup.py) @@ -418,6 +426,8 @@ jupyterlab-server==2.27.2 # notebook jupyterlab-widgets==3.0.11 # via ipywidgets +jwcrypto==1.5.6 + # via python-keycloak kubernetes==20.13.0 # via feast (setup.py) locket==1.0.0 @@ -511,6 +521,7 @@ packaging==24.1 # build # dask # db-dtypes + # deprecation # google-cloud-bigquery # great-expectations # gunicorn @@ -721,6 +732,8 @@ python-dotenv==1.0.1 # via uvicorn python-json-logger==2.0.7 # via jupyter-events +python-keycloak==4.2.2 + # via feast (setup.py) python-multipart==0.0.9 # via fastapi pytz==2024.1 @@ -769,7 +782,9 @@ requests==2.32.3 # kubernetes # moto # msal + # python-keycloak # requests-oauthlib + # requests-toolbelt # responses # singlestoredb # snowflake-connector-python @@ -777,6 +792,8 @@ requests==2.32.3 # trino requests-oauthlib==2.0.0 # via kubernetes +requests-toolbelt==1.0.0 + # via python-keycloak responses==0.25.3 # via moto rfc3339-validator==0.1.4 @@ -984,6 +1001,7 @@ typing-extensions==4.12.2 # great-expectations # ibis-framework # ipython + # jwcrypto # mypy # psycopg # psycopg-pool diff --git a/sdk/python/requirements/py3.9-requirements.txt b/sdk/python/requirements/py3.9-requirements.txt index f9fa856a0e..0b3c8a33c9 100644 --- a/sdk/python/requirements/py3.9-requirements.txt +++ b/sdk/python/requirements/py3.9-requirements.txt @@ -225,3 +225,4 @@ websockets==12.0 # via uvicorn zipp==3.19.2 # via importlib-metadata +bigtree==0.19.2 diff --git a/sdk/python/tests/conftest.py b/sdk/python/tests/conftest.py index 1fd510d104..74aa68e984 100644 --- a/sdk/python/tests/conftest.py +++ b/sdk/python/tests/conftest.py @@ -15,9 +15,11 @@ import multiprocessing import os import random +import tempfile from datetime import timedelta from multiprocessing import Process from sys import platform +from textwrap import dedent from typing import Any, Dict, List, Tuple, no_type_check from unittest import mock @@ -29,8 +31,8 @@ from feast.feature_store import FeatureStore # noqa: E402 from feast.utils import _utc_now from feast.wait import wait_retry_backoff # noqa: E402 -from tests.data.data_creator import ( # noqa: E402 - create_basic_driver_dataset, +from tests.data.data_creator import ( + create_basic_driver_dataset, # noqa: E402 create_document_dataset, ) from tests.integration.feature_repos.integration_test_repo_config import ( @@ -54,6 +56,7 @@ driver, location, ) +from tests.utils.auth_permissions_util import default_store from tests.utils.http_server import check_port_open, free_port # noqa: E402 logger = logging.getLogger(__name__) @@ -406,3 +409,75 @@ def fake_document_data(environment: Environment) -> Tuple[pd.DataFrame, DataSour environment.feature_store.project, ) return df, data_source + + +@pytest.fixture +def temp_dir(): + with tempfile.TemporaryDirectory() as temp_dir: + print(f"Created {temp_dir}") + yield temp_dir + + +@pytest.fixture +def server_port(): + return free_port() + + +@pytest.fixture +def feature_store(temp_dir, auth_config, applied_permissions): + print(f"Creating store at {temp_dir}") + return default_store(str(temp_dir), auth_config, applied_permissions) + + +@pytest.fixture(scope="module") +def all_markers_from_module(request): + markers = set() + for item in request.session.items: + for marker in item.iter_markers(): + markers.add(marker.name) + + return markers + + +@pytest.fixture(scope="module") +def is_integration_test(all_markers_from_module): + return "integration" in all_markers_from_module + + +@pytest.fixture( + scope="module", + params=[ + dedent(""" + auth: + type: no_auth + """), + dedent(""" + auth: + type: kubernetes + """), + dedent(""" + auth: + type: oidc + client_id: feast-integration-client + client_secret: feast-integration-client-secret + username: reader_writer + password: password + realm: master + auth_server_url: KEYCLOAK_URL_PLACE_HOLDER + auth_discovery_url: KEYCLOAK_URL_PLACE_HOLDER/realms/master/.well-known/openid-configuration + """), + ], +) +def auth_config(request, is_integration_test): + auth_configuration = request.param + + if is_integration_test: + if "kubernetes" in auth_configuration: + pytest.skip( + "skipping integration tests for kubernetes platform, unit tests are covering this functionality." + ) + elif "oidc" in auth_configuration: + keycloak_url = request.getfixturevalue("start_keycloak_server") + return auth_configuration.replace("KEYCLOAK_URL_PLACE_HOLDER", keycloak_url) + + return auth_configuration diff --git a/sdk/python/tests/integration/conftest.py b/sdk/python/tests/integration/conftest.py new file mode 100644 index 0000000000..5c34a448e2 --- /dev/null +++ b/sdk/python/tests/integration/conftest.py @@ -0,0 +1,16 @@ +import logging + +import pytest +from testcontainers.keycloak import KeycloakContainer + +from tests.utils.auth_permissions_util import setup_permissions_on_keycloak + +logger = logging.getLogger(__name__) + + +@pytest.fixture(scope="session") +def start_keycloak_server(): + logger.info("Starting keycloak instance") + with KeycloakContainer("quay.io/keycloak/keycloak:24.0.1") as keycloak_container: + setup_permissions_on_keycloak(keycloak_container.get_client()) + yield keycloak_container.get_url() diff --git a/sdk/python/tests/integration/feature_repos/repo_configuration.py b/sdk/python/tests/integration/feature_repos/repo_configuration.py index 48f5070f1e..235c909d5f 100644 --- a/sdk/python/tests/integration/feature_repos/repo_configuration.py +++ b/sdk/python/tests/integration/feature_repos/repo_configuration.py @@ -11,15 +11,26 @@ import pandas as pd import pytest -from feast import FeatureStore, FeatureView, OnDemandFeatureView, driver_test_data +from feast import ( + FeatureStore, + FeatureView, + OnDemandFeatureView, + StreamFeatureView, + driver_test_data, +) from feast.constants import FULL_REPO_CONFIGS_MODULE_ENV_NAME from feast.data_source import DataSource from feast.errors import FeastModuleImportError +from feast.feature_service import FeatureService from feast.infra.feature_servers.base_config import ( BaseFeatureServerConfig, FeatureLoggingConfig, ) from feast.infra.feature_servers.local_process.config import LocalFeatureServerConfig +from feast.permissions.action import AuthzedAction +from feast.permissions.auth_model import OidcAuthConfig +from feast.permissions.permission import Permission +from feast.permissions.policy import RoleBasedPolicy from feast.repo_config import RegistryConfig, RepoConfig from feast.utils import _utc_now from tests.integration.feature_repos.integration_test_repo_config import ( @@ -36,6 +47,7 @@ DuckDBDataSourceCreator, DuckDBDeltaDataSourceCreator, FileDataSourceCreator, + RemoteOfflineOidcAuthStoreDataSourceCreator, RemoteOfflineStoreDataSourceCreator, ) from tests.integration.feature_repos.universal.data_sources.redshift import ( @@ -124,6 +136,7 @@ ("local", DuckDBDataSourceCreator), ("local", DuckDBDeltaDataSourceCreator), ("local", RemoteOfflineStoreDataSourceCreator), + ("local", RemoteOfflineOidcAuthStoreDataSourceCreator), ] if os.getenv("FEAST_IS_LOCAL_TEST", "False") == "True": @@ -134,7 +147,6 @@ ] ) - AVAILABLE_ONLINE_STORES: Dict[ str, Tuple[Union[str, Dict[Any, Any]], Optional[Type[OnlineStoreCreator]]] ] = {"sqlite": ({"type": "sqlite"}, None)} @@ -164,7 +176,6 @@ # containerized version of IKV. # AVAILABLE_ONLINE_STORES["ikv"] = (IKV_CONFIG, None) - full_repo_configs_module = os.environ.get(FULL_REPO_CONFIGS_MODULE_ENV_NAME) if full_repo_configs_module is not None: try: @@ -200,7 +211,6 @@ for c in FULL_REPO_CONFIGS } - # Replace online stores with emulated online stores if we're running local integration tests if os.getenv("FEAST_LOCAL_ONLINE_CONTAINER", "False").lower() == "true": replacements: Dict[ @@ -432,6 +442,7 @@ def setup(self): feature_server=self.feature_server, entity_key_serialization_version=self.entity_key_serialization_version, ) + self.feature_store = FeatureStore(config=self.config) def teardown(self): @@ -441,6 +452,73 @@ def teardown(self): self.online_store_creator.teardown() +@dataclass +class OfflineServerPermissionsEnvironment(Environment): + def setup(self): + self.data_source_creator.setup(self.registry) + keycloak_url = self.data_source_creator.get_keycloak_url() + auth_config = OidcAuthConfig( + client_id="feast-integration-client", + client_secret="feast-integration-client-secret", + username="reader_writer", + password="password", + realm="master", + type="oidc", + auth_server_url=keycloak_url, + auth_discovery_url=f"{keycloak_url}/realms/master/.well-known" + f"/openid-configuration", + ) + self.config = RepoConfig( + registry=self.registry, + project=self.project, + provider=self.provider, + offline_store=self.data_source_creator.create_offline_store_config(), + online_store=self.online_store_creator.create_online_store() + if self.online_store_creator + else self.online_store, + batch_engine=self.batch_engine, + repo_path=self.repo_dir_name, + feature_server=self.feature_server, + entity_key_serialization_version=self.entity_key_serialization_version, + auth=auth_config, + ) + + self.feature_store = FeatureStore(config=self.config) + permissions_list = [ + Permission( + name="offline_fv_perm", + types=FeatureView, + policy=RoleBasedPolicy(roles=["writer"]), + actions=[AuthzedAction.READ_OFFLINE, AuthzedAction.WRITE_OFFLINE], + ), + Permission( + name="offline_odfv_perm", + types=OnDemandFeatureView, + policy=RoleBasedPolicy(roles=["writer"]), + actions=[AuthzedAction.READ_OFFLINE, AuthzedAction.WRITE_OFFLINE], + ), + Permission( + name="offline_sfv_perm", + types=StreamFeatureView, + policy=RoleBasedPolicy(roles=["writer"]), + actions=[AuthzedAction.READ_OFFLINE, AuthzedAction.WRITE_OFFLINE], + ), + Permission( + name="offline_fs_perm", + types=FeatureService, + policy=RoleBasedPolicy(roles=["writer"]), + actions=[AuthzedAction.READ_OFFLINE, AuthzedAction.WRITE_OFFLINE], + ), + Permission( + name="offline_datasource_perm", + types=DataSource, + policy=RoleBasedPolicy(roles=["writer"]), + actions=[AuthzedAction.READ_OFFLINE, AuthzedAction.WRITE_OFFLINE], + ), + ] + self.feature_store.apply(permissions_list) + + def table_name_from_data_source(ds: DataSource) -> Optional[str]: if hasattr(ds, "table_ref"): return ds.table_ref # type: ignore @@ -491,23 +569,27 @@ def construct_test_environment( cache_ttl_seconds=1, ) - environment = Environment( - name=project, - provider=test_repo_config.provider, - data_source_creator=offline_creator, - python_feature_server=test_repo_config.python_feature_server, - worker_id=worker_id, - online_store_creator=online_creator, - fixture_request=fixture_request, - project=project, - registry=registry, - feature_server=feature_server, - entity_key_serialization_version=entity_key_serialization_version, - repo_dir_name=repo_dir_name, - batch_engine=test_repo_config.batch_engine, - online_store=test_repo_config.online_store, - ) + environment_params = { + "name": project, + "provider": test_repo_config.provider, + "data_source_creator": offline_creator, + "python_feature_server": test_repo_config.python_feature_server, + "worker_id": worker_id, + "online_store_creator": online_creator, + "fixture_request": fixture_request, + "project": project, + "registry": registry, + "feature_server": feature_server, + "entity_key_serialization_version": entity_key_serialization_version, + "repo_dir_name": repo_dir_name, + "batch_engine": test_repo_config.batch_engine, + "online_store": test_repo_config.online_store, + } + if not isinstance(offline_creator, RemoteOfflineOidcAuthStoreDataSourceCreator): + environment = Environment(**environment_params) + else: + environment = OfflineServerPermissionsEnvironment(**environment_params) return environment diff --git a/sdk/python/tests/integration/feature_repos/universal/data_sources/file.py b/sdk/python/tests/integration/feature_repos/universal/data_sources/file.py index e505986350..b600699f81 100644 --- a/sdk/python/tests/integration/feature_repos/universal/data_sources/file.py +++ b/sdk/python/tests/integration/feature_repos/universal/data_sources/file.py @@ -33,6 +33,7 @@ from tests.integration.feature_repos.universal.data_source_creator import ( DataSourceCreator, ) +from tests.utils.auth_permissions_util import include_auth_config from tests.utils.http_server import check_port_open, free_port # noqa: E402 logger = logging.getLogger(__name__) @@ -428,3 +429,95 @@ def teardown(self): ), timeout_secs=30, ) + + +class RemoteOfflineOidcAuthStoreDataSourceCreator(FileDataSourceCreator): + def __init__(self, project_name: str, *args, **kwargs): + super().__init__(project_name) + if "fixture_request" in kwargs: + request = kwargs["fixture_request"] + self.keycloak_url = request.getfixturevalue("start_keycloak_server") + else: + raise RuntimeError( + "fixture_request object is not passed to inject keycloak fixture dynamically." + ) + auth_config_template = """ +auth: + type: oidc + client_id: feast-integration-client + client_secret: feast-integration-client-secret + username: reader_writer + password: password + realm: master + auth_server_url: {keycloak_url} + auth_discovery_url: {keycloak_url}/realms/master/.well-known/openid-configuration +""" + self.auth_config = auth_config_template.format(keycloak_url=self.keycloak_url) + self.server_port: int = 0 + self.proc = None + + def setup(self, registry: RegistryConfig): + parent_offline_config = super().create_offline_store_config() + config = RepoConfig( + project=self.project_name, + provider="local", + offline_store=parent_offline_config, + registry=registry.path, + entity_key_serialization_version=2, + ) + + repo_path = Path(tempfile.mkdtemp()) + with open(repo_path / "feature_store.yaml", "w") as outfile: + yaml.dump(config.model_dump(by_alias=True), outfile) + repo_path = str(repo_path.resolve()) + + include_auth_config( + file_path=f"{repo_path}/feature_store.yaml", auth_config=self.auth_config + ) + + self.server_port = free_port() + host = "0.0.0.0" + cmd = [ + "feast", + "-c" + repo_path, + "serve_offline", + "--host", + host, + "--port", + str(self.server_port), + ] + self.proc = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL + ) + + _time_out_sec: int = 60 + # Wait for server to start + wait_retry_backoff( + lambda: (None, check_port_open(host, self.server_port)), + timeout_secs=_time_out_sec, + timeout_msg=f"Unable to start the feast remote offline server in {_time_out_sec} seconds at port={self.server_port}", + ) + return "grpc+tcp://{}:{}".format(host, self.server_port) + + def create_offline_store_config(self) -> FeastConfigBaseModel: + self.remote_offline_store_config = RemoteOfflineStoreConfig( + type="remote", host="0.0.0.0", port=self.server_port + ) + return self.remote_offline_store_config + + def get_keycloak_url(self): + return self.keycloak_url + + def teardown(self): + super().teardown() + if self.proc is not None: + self.proc.kill() + + # wait server to free the port + wait_retry_backoff( + lambda: ( + None, + not check_port_open("localhost", self.server_port), + ), + timeout_secs=30, + ) diff --git a/sdk/python/tests/integration/offline_store/test_universal_historical_retrieval.py b/sdk/python/tests/integration/offline_store/test_universal_historical_retrieval.py index ecaa5f40db..97ad54251f 100644 --- a/sdk/python/tests/integration/offline_store/test_universal_historical_retrieval.py +++ b/sdk/python/tests/integration/offline_store/test_universal_historical_retrieval.py @@ -21,6 +21,7 @@ table_name_from_data_source, ) from tests.integration.feature_repos.universal.data_sources.file import ( + RemoteOfflineOidcAuthStoreDataSourceCreator, RemoteOfflineStoreDataSourceCreator, ) from tests.integration.feature_repos.universal.data_sources.snowflake import ( @@ -162,7 +163,11 @@ def test_historical_features_main( ) if not isinstance( - environment.data_source_creator, RemoteOfflineStoreDataSourceCreator + environment.data_source_creator, + ( + RemoteOfflineStoreDataSourceCreator, + RemoteOfflineOidcAuthStoreDataSourceCreator, + ), ): assert_feature_service_correctness( store, diff --git a/sdk/python/tests/integration/online_store/test_remote_online_store.py b/sdk/python/tests/integration/online_store/test_remote_online_store.py index 21ac00583b..f74fb14a86 100644 --- a/sdk/python/tests/integration/online_store/test_remote_online_store.py +++ b/sdk/python/tests/integration/online_store/test_remote_online_store.py @@ -1,28 +1,59 @@ import os -import subprocess import tempfile from textwrap import dedent import pytest +from feast import FeatureView, OnDemandFeatureView, StreamFeatureView from feast.feature_store import FeatureStore -from feast.utils import _utc_now -from feast.wait import wait_retry_backoff +from feast.permissions.action import AuthzedAction +from feast.permissions.permission import Permission +from feast.permissions.policy import RoleBasedPolicy +from tests.utils.auth_permissions_util import ( + PROJECT_NAME, + default_store, + start_feature_server, +) from tests.utils.cli_repo_creator import CliRunner -from tests.utils.http_server import check_port_open, free_port +from tests.utils.http_server import free_port @pytest.mark.integration -def test_remote_online_store_read(): +def test_remote_online_store_read(auth_config): with tempfile.TemporaryDirectory() as remote_server_tmp_dir, tempfile.TemporaryDirectory() as remote_client_tmp_dir: + permissions_list = [ + Permission( + name="online_list_fv_perm", + types=FeatureView, + policy=RoleBasedPolicy(roles=["reader"]), + actions=[AuthzedAction.READ_ONLINE], + ), + Permission( + name="online_list_odfv_perm", + types=OnDemandFeatureView, + policy=RoleBasedPolicy(roles=["reader"]), + actions=[AuthzedAction.READ_ONLINE], + ), + Permission( + name="online_list_sfv_perm", + types=StreamFeatureView, + policy=RoleBasedPolicy(roles=["reader"]), + actions=[AuthzedAction.READ_ONLINE], + ), + ] server_store, server_url, registry_path = ( - _create_server_store_spin_feature_server(temp_dir=remote_server_tmp_dir) + _create_server_store_spin_feature_server( + temp_dir=remote_server_tmp_dir, + auth_config=auth_config, + permissions_list=permissions_list, + ) ) assert None not in (server_store, server_url, registry_path) client_store = _create_remote_client_feature_store( temp_dir=remote_client_tmp_dir, server_registry_path=str(registry_path), feature_server_url=server_url, + auth_config=auth_config, ) assert client_store is not None _assert_non_existing_entity_feature_views_entity( @@ -127,11 +158,13 @@ def _assert_client_server_online_stores_are_matching( assert online_features_from_client == online_features_from_server -def _create_server_store_spin_feature_server(temp_dir): +def _create_server_store_spin_feature_server( + temp_dir, auth_config: str, permissions_list +): + store = default_store(str(temp_dir), auth_config, permissions_list) feast_server_port = free_port() - store = _default_store(str(temp_dir), "REMOTE_ONLINE_SERVER_PROJECT") server_url = next( - _start_feature_server( + start_feature_server( repo_path=str(store.repo_path), server_port=feast_server_port ) ) @@ -139,24 +172,8 @@ def _create_server_store_spin_feature_server(temp_dir): return store, server_url, os.path.join(store.repo_path, "data", "registry.db") -def _default_store(temp_dir, project_name) -> FeatureStore: - runner = CliRunner() - result = runner.run(["init", project_name], cwd=temp_dir) - repo_path = os.path.join(temp_dir, project_name, "feature_repo") - assert result.returncode == 0 - - result = runner.run(["--chdir", repo_path, "apply"], cwd=temp_dir) - assert result.returncode == 0 - - fs = FeatureStore(repo_path=repo_path) - fs.materialize_incremental( - end_date=_utc_now(), feature_views=["driver_hourly_stats"] - ) - return fs - - def _create_remote_client_feature_store( - temp_dir, server_registry_path: str, feature_server_url: str + temp_dir, server_registry_path: str, feature_server_url: str, auth_config: str ) -> FeatureStore: project_name = "REMOTE_ONLINE_CLIENT_PROJECT" runner = CliRunner() @@ -167,6 +184,7 @@ def _create_remote_client_feature_store( repo_path=str(repo_path), registry_path=server_registry_path, feature_server_url=feature_server_url, + auth_config=auth_config, ) result = runner.run(["--chdir", repo_path, "apply"], cwd=temp_dir) @@ -176,14 +194,14 @@ def _create_remote_client_feature_store( def _overwrite_remote_client_feature_store_yaml( - repo_path: str, registry_path: str, feature_server_url: str + repo_path: str, registry_path: str, feature_server_url: str, auth_config: str ): repo_config = os.path.join(repo_path, "feature_store.yaml") with open(repo_config, "w") as repo_config: repo_config.write( dedent( f""" - project: REMOTE_ONLINE_CLIENT_PROJECT + project: {PROJECT_NAME} registry: {registry_path} provider: local online_store: @@ -192,57 +210,5 @@ def _overwrite_remote_client_feature_store_yaml( entity_key_serialization_version: 2 """ ) - ) - - -def _start_feature_server(repo_path: str, server_port: int, metrics: bool = False): - host = "0.0.0.0" - cmd = [ - "feast", - "-c" + repo_path, - "serve", - "--host", - host, - "--port", - str(server_port), - ] - feast_server_process = subprocess.Popen( - cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL - ) - _time_out_sec: int = 60 - # Wait for server to start - wait_retry_backoff( - lambda: (None, check_port_open(host, server_port)), - timeout_secs=_time_out_sec, - timeout_msg=f"Unable to start the feast server in {_time_out_sec} seconds for remote online store type, port={server_port}", - ) - - if metrics: - cmd.append("--metrics") - - # Check if metrics are enabled and Prometheus server is running - if metrics: - wait_retry_backoff( - lambda: (None, check_port_open("localhost", 8000)), - timeout_secs=_time_out_sec, - timeout_msg="Unable to start the Prometheus server in 60 seconds.", - ) - else: - assert not check_port_open( - "localhost", 8000 - ), "Prometheus server is running when it should be disabled." - - yield f"http://localhost:{server_port}" - - if feast_server_process is not None: - feast_server_process.kill() - - # wait server to free the port - wait_retry_backoff( - lambda: ( - None, - not check_port_open("localhost", server_port), - ), - timeout_msg=f"Unable to stop the feast server in {_time_out_sec} seconds for remote online store type, port={server_port}", - timeout_secs=_time_out_sec, + + auth_config ) diff --git a/sdk/python/tests/integration/registration/test_universal_cli.py b/sdk/python/tests/integration/registration/test_universal_cli.py index fc90108d78..9e02ded4e4 100644 --- a/sdk/python/tests/integration/registration/test_universal_cli.py +++ b/sdk/python/tests/integration/registration/test_universal_cli.py @@ -61,6 +61,8 @@ def test_universal_cli(): assertpy.assert_that(result.returncode).is_equal_to(0) result = runner.run(["data-sources", "list"], cwd=repo_path) assertpy.assert_that(result.returncode).is_equal_to(0) + result = runner.run(["permissions", "list"], cwd=repo_path) + assertpy.assert_that(result.returncode).is_equal_to(0) # entity & feature view describe commands should succeed when objects exist result = runner.run(["entities", "describe", "driver"], cwd=repo_path) @@ -91,6 +93,8 @@ def test_universal_cli(): assertpy.assert_that(result.returncode).is_equal_to(1) result = runner.run(["data-sources", "describe", "foo"], cwd=repo_path) assertpy.assert_that(result.returncode).is_equal_to(1) + result = runner.run(["permissions", "describe", "foo"], cwd=repo_path) + assertpy.assert_that(result.returncode).is_equal_to(1) # Doing another apply should be a no op, and should not cause errors result = runner.run(["apply"], cwd=repo_path) diff --git a/sdk/python/tests/integration/registration/test_universal_registry.py b/sdk/python/tests/integration/registration/test_universal_registry.py index 9dcd1b5b91..c528cee4a8 100644 --- a/sdk/python/tests/integration/registration/test_universal_registry.py +++ b/sdk/python/tests/integration/registration/test_universal_registry.py @@ -40,6 +40,9 @@ from feast.infra.registry.remote import RemoteRegistry, RemoteRegistryConfig from feast.infra.registry.sql import SqlRegistry from feast.on_demand_feature_view import on_demand_feature_view +from feast.permissions.action import AuthzedAction +from feast.permissions.permission import Permission +from feast.permissions.policy import RoleBasedPolicy from feast.protos.feast.registry import RegistryServer_pb2, RegistryServer_pb2_grpc from feast.registry_server import RegistryServer from feast.repo_config import RegistryConfig @@ -270,7 +273,9 @@ def mock_remote_registry(): proxied_registry = Registry("project", registry_config, None) registry = RemoteRegistry( - registry_config=RemoteRegistryConfig(path=""), project=None, repo_path=None + registry_config=RemoteRegistryConfig(path=""), + project=None, + repo_path=None, ) mock_channel = GrpcMockChannel( RegistryServer_pb2.DESCRIPTOR.services_by_name["RegistryServer"], @@ -1154,7 +1159,9 @@ def simple_udf(x: int): assert stream_feature_views[0] == sfv test_registry.delete_feature_view("test kafka stream feature view", project) - stream_feature_views = test_registry.list_stream_feature_views(project) + stream_feature_views = test_registry.list_stream_feature_views( + project, tags=sfv.tags + ) assert len(stream_feature_views) == 0 test_registry.teardown() @@ -1343,3 +1350,138 @@ def validate_project_uuid(project_uuid, test_registry): assert len(test_registry.cached_registry_proto.project_metadata) == 1 project_metadata = test_registry.cached_registry_proto.project_metadata[0] assert project_metadata.project_uuid == project_uuid + + +@pytest.mark.integration +@pytest.mark.parametrize("test_registry", all_fixtures) +def test_apply_permission_success(test_registry): + permission = Permission( + name="read_permission", + actions=AuthzedAction.DESCRIBE, + policy=RoleBasedPolicy(roles=["reader"]), + types=FeatureView, + ) + + project = "project" + + # Register Permission + test_registry.apply_permission(permission, project) + project_metadata = test_registry.list_project_metadata(project=project) + assert len(project_metadata) == 1 + project_uuid = project_metadata[0].project_uuid + assert len(project_metadata[0].project_uuid) == 36 + assert_project_uuid(project, project_uuid, test_registry) + + permissions = test_registry.list_permissions(project) + assert_project_uuid(project, project_uuid, test_registry) + + permission = permissions[0] + assert ( + len(permissions) == 1 + and permission.name == "read_permission" + and len(permission.types) == 1 + and permission.types[0] == FeatureView + and len(permission.actions) == 1 + and permission.actions[0] == AuthzedAction.DESCRIBE + and isinstance(permission.policy, RoleBasedPolicy) + and len(permission.policy.roles) == 1 + and permission.policy.roles[0] == "reader" + and permission.name_pattern is None + and permission.tags is None + and permission.required_tags is None + ) + + # After the first apply, the created_timestamp should be the same as the last_update_timestamp. + assert permission.created_timestamp == permission.last_updated_timestamp + + permission = test_registry.get_permission("read_permission", project) + assert ( + permission.name == "read_permission" + and len(permission.types) == 1 + and permission.types[0] == FeatureView + and len(permission.actions) == 1 + and permission.actions[0] == AuthzedAction.DESCRIBE + and isinstance(permission.policy, RoleBasedPolicy) + and len(permission.policy.roles) == 1 + and permission.policy.roles[0] == "reader" + and permission.name_pattern is None + and permission.tags is None + and permission.required_tags is None + ) + + # Update permission + updated_permission = Permission( + name="read_permission", + actions=[AuthzedAction.DESCRIBE, AuthzedAction.WRITE_ONLINE], + policy=RoleBasedPolicy(roles=["reader", "writer"]), + types=FeatureView, + ) + test_registry.apply_permission(updated_permission, project) + + permissions = test_registry.list_permissions(project) + assert_project_uuid(project, project_uuid, test_registry) + assert len(permissions) == 1 + + updated_permission = test_registry.get_permission("read_permission", project) + assert ( + updated_permission.name == "read_permission" + and len(updated_permission.types) == 1 + and updated_permission.types[0] == FeatureView + and len(updated_permission.actions) == 2 + and AuthzedAction.DESCRIBE in updated_permission.actions + and AuthzedAction.WRITE_ONLINE in updated_permission.actions + and isinstance(updated_permission.policy, RoleBasedPolicy) + and len(updated_permission.policy.roles) == 2 + and "reader" in updated_permission.policy.roles + and "writer" in updated_permission.policy.roles + and updated_permission.name_pattern is None + and updated_permission.tags is None + and updated_permission.required_tags is None + ) + + # The created_timestamp for the entity should be set to the created_timestamp value stored from the previous apply + assert ( + updated_permission.created_timestamp is not None + and updated_permission.created_timestamp == permission.created_timestamp + ) + + updated_permission = Permission( + name="read_permission", + actions=[AuthzedAction.DESCRIBE, AuthzedAction.WRITE_ONLINE], + policy=RoleBasedPolicy(roles=["reader", "writer"]), + types=FeatureView, + name_pattern="aaa", + tags={"team": "matchmaking"}, + required_tags={"tag1": "tag1-value"}, + ) + test_registry.apply_permission(updated_permission, project) + + permissions = test_registry.list_permissions(project) + assert_project_uuid(project, project_uuid, test_registry) + assert len(permissions) == 1 + + updated_permission = test_registry.get_permission("read_permission", project) + assert ( + updated_permission.name == "read_permission" + and len(updated_permission.types) == 1 + and updated_permission.types[0] == FeatureView + and len(updated_permission.actions) == 2 + and AuthzedAction.DESCRIBE in updated_permission.actions + and AuthzedAction.WRITE_ONLINE in updated_permission.actions + and isinstance(updated_permission.policy, RoleBasedPolicy) + and len(updated_permission.policy.roles) == 2 + and "reader" in updated_permission.policy.roles + and "writer" in updated_permission.policy.roles + and updated_permission.name_pattern == "aaa" + and "team" in updated_permission.tags + and updated_permission.tags["team"] == "matchmaking" + and updated_permission.required_tags["tag1"] == "tag1-value" + ) + + test_registry.delete_permission("read_permission", project) + assert_project_uuid(project, project_uuid, test_registry) + permissions = test_registry.list_permissions(project) + assert_project_uuid(project, project_uuid, test_registry) + assert len(permissions) == 0 + + test_registry.teardown() diff --git a/sdk/python/tests/unit/diff/test_registry_diff.py b/sdk/python/tests/unit/diff/test_registry_diff.py index c209f1e0e0..2834c57800 100644 --- a/sdk/python/tests/unit/diff/test_registry_diff.py +++ b/sdk/python/tests/unit/diff/test_registry_diff.py @@ -6,8 +6,12 @@ tag_objects_for_keep_delete_update_add, ) from feast.entity import Entity +from feast.feast_object import ALL_RESOURCE_TYPES from feast.feature_view import FeatureView from feast.on_demand_feature_view import on_demand_feature_view +from feast.permissions.action import AuthzedAction +from feast.permissions.permission import Permission +from feast.permissions.policy import RoleBasedPolicy from feast.types import String from tests.utils.data_source_test_creator import prep_file_source @@ -170,3 +174,22 @@ def test_diff_registry_objects_batch_to_push_source(simple_dataset_1): feast_object_diffs.feast_object_property_diffs[0].property_name == "stream_source" ) + + +def test_diff_registry_objects_permissions(): + pre_changed = Permission( + name="reader", + types=ALL_RESOURCE_TYPES, + policy=RoleBasedPolicy(roles=["reader"]), + actions=[AuthzedAction.DESCRIBE], + ) + post_changed = Permission( + name="reader", + types=ALL_RESOURCE_TYPES, + policy=RoleBasedPolicy(roles=["reader"]), + actions=[AuthzedAction.CREATE], + ) + + feast_object_diffs = diff_registry_objects(pre_changed, post_changed, "permission") + assert len(feast_object_diffs.feast_object_property_diffs) == 1 + assert feast_object_diffs.feast_object_property_diffs[0].property_name == "actions" diff --git a/sdk/python/tests/unit/infra/scaffolding/test_repo_config.py b/sdk/python/tests/unit/infra/scaffolding/test_repo_config.py index 98d82ce357..0725d6d261 100644 --- a/sdk/python/tests/unit/infra/scaffolding/test_repo_config.py +++ b/sdk/python/tests/unit/infra/scaffolding/test_repo_config.py @@ -4,6 +4,12 @@ from typing import Optional from feast.infra.online_stores.sqlite import SqliteOnlineStoreConfig +from feast.permissions.auth.auth_type import AuthType +from feast.permissions.auth_model import ( + KubernetesAuthConfig, + NoAuthConfig, + OidcAuthConfig, +) from feast.repo_config import FeastConfigError, load_repo_config @@ -195,3 +201,119 @@ def test_no_provider(): ), expect_error=None, ) + + +def test_auth_config(): + _test_config( + dedent( + """ + project: foo + auth: + client_id: test_client_id + client_secret: test_client_secret + username: test_user_name + password: test_password + realm: master + auth_server_url: http://localhost:8712 + auth_discovery_url: http://localhost:8080/realms/master/.well-known/openid-configuration + registry: "registry.db" + provider: local + online_store: + path: foo + entity_key_serialization_version: 2 + """ + ), + expect_error="missing authentication type", + ) + + _test_config( + dedent( + """ + project: foo + auth: + type: not_valid_auth_type + client_id: test_client_id + client_secret: test_client_secret + username: test_user_name + password: test_password + realm: master + auth_server_url: http://localhost:8712 + auth_discovery_url: http://localhost:8080/realms/master/.well-known/openid-configuration + registry: "registry.db" + provider: local + online_store: + path: foo + entity_key_serialization_version: 2 + """ + ), + expect_error="invalid authentication type=not_valid_auth_type", + ) + + oidc_repo_config = _test_config( + dedent( + """ + project: foo + auth: + type: oidc + client_id: test_client_id + client_secret: test_client_secret + username: test_user_name + password: test_password + realm: master + auth_server_url: http://localhost:8080 + auth_discovery_url: http://localhost:8080/realms/master/.well-known/openid-configuration + registry: "registry.db" + provider: local + online_store: + path: foo + entity_key_serialization_version: 2 + """ + ), + expect_error=None, + ) + assert oidc_repo_config.auth["type"] == AuthType.OIDC.value + assert isinstance(oidc_repo_config.auth_config, OidcAuthConfig) + assert oidc_repo_config.auth_config.client_id == "test_client_id" + assert oidc_repo_config.auth_config.client_secret == "test_client_secret" + assert oidc_repo_config.auth_config.username == "test_user_name" + assert oidc_repo_config.auth_config.password == "test_password" + assert oidc_repo_config.auth_config.realm == "master" + assert oidc_repo_config.auth_config.auth_server_url == "http://localhost:8080" + assert ( + oidc_repo_config.auth_config.auth_discovery_url + == "http://localhost:8080/realms/master/.well-known/openid-configuration" + ) + + no_auth_repo_config = _test_config( + dedent( + """ + project: foo + registry: "registry.db" + provider: local + online_store: + path: foo + entity_key_serialization_version: 2 + """ + ), + expect_error=None, + ) + assert no_auth_repo_config.auth.get("type") == AuthType.NONE.value + assert isinstance(no_auth_repo_config.auth_config, NoAuthConfig) + + k8_repo_config = _test_config( + dedent( + """ + auth: + type: kubernetes + project: foo + registry: "registry.db" + provider: local + online_store: + path: foo + entity_key_serialization_version: 2 + """ + ), + expect_error=None, + ) + assert k8_repo_config.auth.get("type") == AuthType.KUBERNETES.value + assert isinstance(k8_repo_config.auth_config, KubernetesAuthConfig) diff --git a/sdk/python/tests/unit/local_feast_tests/test_local_feature_store.py b/sdk/python/tests/unit/local_feast_tests/test_local_feature_store.py index 0e834e314b..c86441d56c 100644 --- a/sdk/python/tests/unit/local_feast_tests/test_local_feature_store.py +++ b/sdk/python/tests/unit/local_feast_tests/test_local_feature_store.py @@ -9,11 +9,15 @@ from feast.data_format import AvroFormat, ParquetFormat from feast.data_source import KafkaSource from feast.entity import Entity +from feast.feast_object import ALL_RESOURCE_TYPES from feast.feature_store import FeatureStore from feast.feature_view import FeatureView from feast.field import Field from feast.infra.offline_stores.file_source import FileSource from feast.infra.online_stores.sqlite import SqliteOnlineStoreConfig +from feast.permissions.action import AuthzedAction +from feast.permissions.permission import Permission +from feast.permissions.policy import RoleBasedPolicy from feast.repo_config import RepoConfig from feast.stream_feature_view import stream_feature_view from feast.types import Array, Bytes, Float32, Int64, String @@ -338,6 +342,36 @@ def test_apply_entities_and_feature_views(test_feature_store): test_feature_store.teardown() +@pytest.mark.parametrize( + "test_feature_store", + [lazy_fixture("feature_store_with_local_registry")], +) +def test_apply_permissions(test_feature_store): + assert isinstance(test_feature_store, FeatureStore) + + permission = Permission( + name="reader", + types=ALL_RESOURCE_TYPES, + policy=RoleBasedPolicy(roles=["reader"]), + actions=[AuthzedAction.DESCRIBE], + ) + + # Register Permission + test_feature_store.apply([permission]) + + permissions = test_feature_store.list_permissions() + assert len(permissions) == 1 + assert permissions[0] == permission + + # delete Permission + test_feature_store.apply(objects=[], objects_to_delete=[permission], partial=False) + + permissions = test_feature_store.list_permissions() + assert len(permissions) == 0 + + test_feature_store.teardown() + + @pytest.mark.parametrize( "test_feature_store", [lazy_fixture("feature_store_with_local_registry")], diff --git a/sdk/python/tests/unit/permissions/__init__.py b/sdk/python/tests/unit/permissions/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sdk/python/tests/unit/permissions/auth/conftest.py b/sdk/python/tests/unit/permissions/auth/conftest.py new file mode 100644 index 0000000000..dc71aba23b --- /dev/null +++ b/sdk/python/tests/unit/permissions/auth/conftest.py @@ -0,0 +1,101 @@ +import pytest +from kubernetes import client + +from feast.permissions.auth_model import OidcAuthConfig +from tests.unit.permissions.auth.server.test_utils import ( + invalid_list_entities_perm, + read_entities_perm, + read_fv_perm, + read_odfv_perm, + read_permissions_perm, + read_sfv_perm, +) +from tests.unit.permissions.auth.test_token_parser import _CLIENT_ID + + +@pytest.fixture +def sa_name(): + return "my-name" + + +@pytest.fixture +def namespace(): + return "my-ns" + + +@pytest.fixture +def rolebindings(sa_name, namespace) -> dict: + roles = ["reader", "writer"] + items = [] + for r in roles: + items.append( + client.V1RoleBinding( + metadata=client.V1ObjectMeta(name=r, namespace=namespace), + subjects=[ + client.V1Subject( + kind="ServiceAccount", + name=sa_name, + api_group="rbac.authorization.k8s.io", + ) + ], + role_ref=client.V1RoleRef( + kind="Role", name=r, api_group="rbac.authorization.k8s.io" + ), + ) + ) + return {"items": client.V1RoleBindingList(items=items), "roles": roles} + + +@pytest.fixture +def clusterrolebindings(sa_name, namespace) -> dict: + roles = ["updater"] + items = [] + for r in roles: + items.append( + client.V1ClusterRoleBinding( + metadata=client.V1ObjectMeta(name=r, namespace=namespace), + subjects=[ + client.V1Subject( + kind="ServiceAccount", + name=sa_name, + namespace=namespace, + api_group="rbac.authorization.k8s.io", + ) + ], + role_ref=client.V1RoleRef( + kind="Role", name=r, api_group="rbac.authorization.k8s.io" + ), + ) + ) + return {"items": client.V1RoleBindingList(items=items), "roles": roles} + + +@pytest.fixture +def oidc_config() -> OidcAuthConfig: + return OidcAuthConfig( + auth_server_url="", + auth_discovery_url="", + client_id=_CLIENT_ID, + client_secret="", + username="", + password="", + realm="", + ) + + +@pytest.fixture( + scope="module", + params=[ + [], + [invalid_list_entities_perm], + [ + read_entities_perm, + read_permissions_perm, + read_fv_perm, + read_odfv_perm, + read_sfv_perm, + ], + ], +) +def applied_permissions(request): + return request.param diff --git a/sdk/python/tests/unit/permissions/auth/server/mock_utils.py b/sdk/python/tests/unit/permissions/auth/server/mock_utils.py new file mode 100644 index 0000000000..8f598774ee --- /dev/null +++ b/sdk/python/tests/unit/permissions/auth/server/mock_utils.py @@ -0,0 +1,72 @@ +from unittest.mock import MagicMock, Mock + +from requests import Response + + +def mock_oidc(request, monkeypatch, client_id): + async def mock_oath2(self, request): + return "OK" + + monkeypatch.setattr( + "feast.permissions.auth.oidc_token_parser.OAuth2AuthorizationCodeBearer.__call__", + mock_oath2, + ) + signing_key = MagicMock() + signing_key.key = "a-key" + monkeypatch.setattr( + "feast.permissions.auth.oidc_token_parser.PyJWKClient.get_signing_key_from_jwt", + lambda self, access_token: signing_key, + ) + user_data = { + "preferred_username": "my-name", + "resource_access": {client_id: {"roles": ["reader", "writer"]}}, + } + monkeypatch.setattr( + "feast.permissions.auth.oidc_token_parser.jwt.decode", + lambda self, *args, **kwargs: user_data, + ) + discovery_response = Mock(spec=Response) + discovery_response.status_code = 200 + discovery_response.json.return_value = { + "token_endpoint": "http://localhost:8080/realms/master/protocol/openid-connect/token" + } + monkeypatch.setattr( + "feast.permissions.client.oidc_authentication_client_manager.requests.get", + lambda url: discovery_response, + ) + token_response = Mock(spec=Response) + token_response.status_code = 200 + token_response.json.return_value = {"access_token": "my-token"} + monkeypatch.setattr( + "feast.permissions.client.oidc_authentication_client_manager.requests.post", + lambda url, data, headers: token_response, + ) + + +def mock_kubernetes(request, monkeypatch): + sa_name = request.getfixturevalue("sa_name") + namespace = request.getfixturevalue("namespace") + subject = f"system:serviceaccount:{namespace}:{sa_name}" + rolebindings = request.getfixturevalue("rolebindings") + clusterrolebindings = request.getfixturevalue("clusterrolebindings") + + monkeypatch.setattr( + "feast.permissions.auth.kubernetes_token_parser.config.load_incluster_config", + lambda: None, + ) + monkeypatch.setattr( + "feast.permissions.auth.kubernetes_token_parser.jwt.decode", + lambda *args, **kwargs: {"sub": subject}, + ) + monkeypatch.setattr( + "feast.permissions.auth.kubernetes_token_parser.client.RbacAuthorizationV1Api.list_namespaced_role_binding", + lambda *args, **kwargs: rolebindings["items"], + ) + monkeypatch.setattr( + "feast.permissions.auth.kubernetes_token_parser.client.RbacAuthorizationV1Api.list_cluster_role_binding", + lambda *args, **kwargs: clusterrolebindings["items"], + ) + monkeypatch.setattr( + "feast.permissions.client.kubernetes_auth_client_manager.KubernetesAuthClientManager.get_token", + lambda self: "my-token", + ) diff --git a/sdk/python/tests/unit/permissions/auth/server/test_auth_registry_server.py b/sdk/python/tests/unit/permissions/auth/server/test_auth_registry_server.py new file mode 100644 index 0000000000..bc16bdac3b --- /dev/null +++ b/sdk/python/tests/unit/permissions/auth/server/test_auth_registry_server.py @@ -0,0 +1,239 @@ +from datetime import datetime + +import assertpy +import pandas as pd +import pytest +import yaml + +from feast import ( + FeatureStore, +) +from feast.permissions.permission import Permission +from feast.registry_server import start_server +from feast.wait import wait_retry_backoff # noqa: E402 +from tests.unit.permissions.auth.server import mock_utils +from tests.unit.permissions.auth.server.test_utils import ( + invalid_list_entities_perm, + read_entities_perm, + read_fv_perm, + read_odfv_perm, + read_permissions_perm, + read_sfv_perm, +) +from tests.utils.auth_permissions_util import get_remote_registry_store +from tests.utils.http_server import check_port_open # noqa: E402 + + +@pytest.fixture +def start_registry_server( + request, + auth_config, + server_port, + feature_store, + monkeypatch, +): + if "kubernetes" in auth_config: + mock_utils.mock_kubernetes(request=request, monkeypatch=monkeypatch) + elif "oidc" in auth_config: + auth_config_yaml = yaml.safe_load(auth_config) + mock_utils.mock_oidc( + request=request, + monkeypatch=monkeypatch, + client_id=auth_config_yaml["auth"]["client_id"], + ) + + assertpy.assert_that(server_port).is_not_equal_to(0) + + print(f"Starting Registry at {server_port}") + server = start_server(feature_store, server_port, wait_for_termination=False) + print("Waiting server availability") + wait_retry_backoff( + lambda: (None, check_port_open("localhost", server_port)), + timeout_secs=10, + ) + print("Server started") + + yield server + + print("Stopping server") + server.stop(grace=None) # Teardown server + + +def test_registry_apis( + auth_config, + temp_dir, + server_port, + start_registry_server, + feature_store, + applied_permissions, +): + print(f"Running for\n:{auth_config}") + remote_feature_store = get_remote_registry_store(server_port, feature_store) + permissions = _test_list_permissions(remote_feature_store, applied_permissions) + _test_list_entities(remote_feature_store, applied_permissions) + _test_list_fvs(remote_feature_store, applied_permissions) + + if _permissions_exist_in_permission_list( + [ + read_entities_perm, + read_permissions_perm, + read_fv_perm, + read_odfv_perm, + read_sfv_perm, + ], + permissions, + ): + _test_get_historical_features(remote_feature_store) + + +def _test_get_historical_features(client_fs: FeatureStore): + entity_df = pd.DataFrame.from_dict( + { + # entity's join key -> entity values + "driver_id": [1001, 1002, 1003], + # "event_timestamp" (reserved key) -> timestamps + "event_timestamp": [ + datetime(2021, 4, 12, 10, 59, 42), + datetime(2021, 4, 12, 8, 12, 10), + datetime(2021, 4, 12, 16, 40, 26), + ], + # (optional) label name -> label values. Feast does not process these + "label_driver_reported_satisfaction": [1, 5, 3], + # values we're using for an on-demand transformation + "val_to_add": [1, 2, 3], + "val_to_add_2": [10, 20, 30], + } + ) + + training_df = client_fs.get_historical_features( + entity_df=entity_df, + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "driver_hourly_stats:avg_daily_trips", + "transformed_conv_rate:conv_rate_plus_val1", + "transformed_conv_rate:conv_rate_plus_val2", + ], + ).to_df() + assertpy.assert_that(training_df).is_not_none() + + +def _test_list_entities(client_fs: FeatureStore, permissions: list[Permission]): + entities = client_fs.list_entities() + + if not _is_auth_enabled(client_fs) or _is_permission_enabled( + client_fs, permissions, read_entities_perm + ): + assertpy.assert_that(entities).is_not_none() + assertpy.assert_that(len(entities)).is_equal_to(1) + assertpy.assert_that(entities[0].name).is_equal_to("driver") + else: + assertpy.assert_that(entities).is_not_none() + assertpy.assert_that(len(entities)).is_equal_to(0) + + +def _no_permission_retrieved(permissions: list[Permission]) -> bool: + return len(permissions) == 0 + + +def _test_list_permissions( + client_fs: FeatureStore, applied_permissions: list[Permission] +) -> list[Permission]: + if _is_auth_enabled(client_fs) and _permissions_exist_in_permission_list( + [invalid_list_entities_perm], applied_permissions + ): + with pytest.raises(Exception): + client_fs.list_permissions() + return [] + else: + permissions = client_fs.list_permissions() + + if not _is_auth_enabled(client_fs): + assertpy.assert_that(permissions).is_not_none() + assertpy.assert_that(len(permissions)).is_equal_to(len(applied_permissions)) + elif _is_auth_enabled(client_fs) and _permissions_exist_in_permission_list( + [ + read_entities_perm, + read_permissions_perm, + read_fv_perm, + read_odfv_perm, + read_sfv_perm, + ], + permissions, + ): + assertpy.assert_that(permissions).is_not_none() + assertpy.assert_that(len(permissions)).is_equal_to( + len( + [ + read_entities_perm, + read_permissions_perm, + read_fv_perm, + read_odfv_perm, + read_sfv_perm, + ] + ) + ) + elif _is_auth_enabled(client_fs) and _is_listing_permissions_allowed(permissions): + assertpy.assert_that(permissions).is_not_none() + assertpy.assert_that(len(permissions)).is_equal_to(1) + + return permissions + + +def _is_listing_permissions_allowed(permissions: list[Permission]) -> bool: + return read_permissions_perm in permissions + + +def _is_auth_enabled(client_fs: FeatureStore) -> bool: + return client_fs.config.auth_config.type != "no_auth" + + +def _test_list_fvs(client_fs: FeatureStore, permissions: list[Permission]): + if _is_auth_enabled(client_fs) and _permissions_exist_in_permission_list( + [invalid_list_entities_perm], permissions + ): + with pytest.raises(Exception): + client_fs.list_feature_views() + return [] + else: + fvs = client_fs.list_feature_views() + for fv in fvs: + print(f"{fv.name}, {type(fv).__name__}") + + if not _is_auth_enabled(client_fs) or _is_permission_enabled( + client_fs, permissions, read_fv_perm + ): + assertpy.assert_that(fvs).is_not_none() + assertpy.assert_that(len(fvs)).is_equal_to(2) + + names = _to_names(fvs) + assertpy.assert_that(names).contains("driver_hourly_stats") + assertpy.assert_that(names).contains("driver_hourly_stats_fresh") + else: + assertpy.assert_that(fvs).is_not_none() + assertpy.assert_that(len(fvs)).is_equal_to(0) + + +def _permissions_exist_in_permission_list( + permission_to_test: list[Permission], permission_list: list[Permission] +) -> bool: + return all(e in permission_list for e in permission_to_test) + + +def _is_permission_enabled( + client_fs: FeatureStore, + permissions: list[Permission], + permission: Permission, +): + return _is_auth_enabled(client_fs) and ( + _no_permission_retrieved(permissions) + or ( + _permissions_exist_in_permission_list( + [read_permissions_perm, permission], permissions + ) + ) + ) + + +def _to_names(items): + return [i.name for i in items] diff --git a/sdk/python/tests/unit/permissions/auth/server/test_utils.py b/sdk/python/tests/unit/permissions/auth/server/test_utils.py new file mode 100644 index 0000000000..5d781919a0 --- /dev/null +++ b/sdk/python/tests/unit/permissions/auth/server/test_utils.py @@ -0,0 +1,61 @@ +import assertpy +import pytest + +from feast import Entity, FeatureView, OnDemandFeatureView, StreamFeatureView +from feast.permissions.action import AuthzedAction +from feast.permissions.permission import Permission +from feast.permissions.policy import RoleBasedPolicy +from feast.permissions.server.utils import AuthManagerType, str_to_auth_manager_type + +read_permissions_perm = Permission( + name="read_permissions_perm", + types=Permission, + policy=RoleBasedPolicy(roles=["reader"]), + actions=[AuthzedAction.DESCRIBE], +) + +read_entities_perm = Permission( + name="read_entities_perm", + types=Entity, + policy=RoleBasedPolicy(roles=["reader"]), + actions=[AuthzedAction.DESCRIBE], +) + +read_fv_perm = Permission( + name="read_fv_perm", + types=FeatureView, + policy=RoleBasedPolicy(roles=["reader"]), + actions=[AuthzedAction.DESCRIBE], +) + +read_odfv_perm = Permission( + name="read_odfv_perm", + types=OnDemandFeatureView, + policy=RoleBasedPolicy(roles=["reader"]), + actions=[AuthzedAction.DESCRIBE], +) + +read_sfv_perm = Permission( + name="read_sfv_perm", + types=StreamFeatureView, + policy=RoleBasedPolicy(roles=["reader"]), + actions=[AuthzedAction.DESCRIBE], +) + +invalid_list_entities_perm = Permission( + name="invalid_list_entity_perm", + types=Entity, + policy=RoleBasedPolicy(roles=["dancer"]), + actions=[AuthzedAction.DESCRIBE], +) + + +@pytest.mark.parametrize( + "label, value", + [(t.value, t) for t in AuthManagerType] + + [(t.value.upper(), t) for t in AuthManagerType] + + [(t.value.lower(), t) for t in AuthManagerType] + + [("none", AuthManagerType.NONE)], +) +def test_str_to_auth_type(label, value): + assertpy.assert_that(str_to_auth_manager_type(label)).is_equal_to(value) diff --git a/sdk/python/tests/unit/permissions/auth/test_token_extractor.py b/sdk/python/tests/unit/permissions/auth/test_token_extractor.py new file mode 100644 index 0000000000..a6fcd89e5b --- /dev/null +++ b/sdk/python/tests/unit/permissions/auth/test_token_extractor.py @@ -0,0 +1,140 @@ +from unittest.mock import Mock + +import assertpy +import pytest +from fastapi.requests import Request +from starlette.authentication import ( + AuthenticationError, +) + +from feast.permissions.server.arrow_flight_token_extractor import ( + ArrowFlightTokenExtractor, +) +from feast.permissions.server.grpc_token_extractor import GrpcTokenExtractor +from feast.permissions.server.rest_token_extractor import RestTokenExtractor + + +@pytest.mark.parametrize( + "error_type, dict, header", + [ + (ValueError, {}, None), + (ValueError, {"other": 123}, None), + (AuthenticationError, {}, ""), + (AuthenticationError, {}, "abcd"), + (AuthenticationError, {}, "other-scheme abcd"), + ], +) +def test_rest_token_extractor_failures(error_type, dict, header): + token_extractor = RestTokenExtractor() + + request = None + if header is not None: + request = Mock(spec=Request) + if header != "": + request.headers = {"authorization": header} + else: + request.headers = {} + with pytest.raises(error_type): + if request is None: + token_extractor.extract_access_token(**dict) + else: + token_extractor.extract_access_token(request=request) + + +@pytest.mark.parametrize( + "error_type, dict, header", + [ + (ValueError, {}, None), + (ValueError, {"other": 123}, None), + (AuthenticationError, {}, ""), + (AuthenticationError, {}, "abcd"), + (AuthenticationError, {}, "other-scheme abcd"), + ], +) +def test_grpc_token_extractor_failures(error_type, dict, header): + token_extractor = GrpcTokenExtractor() + + metadata = None + if header is not None: + metadata = {} + if metadata != "": + metadata["authorization"] = header + with pytest.raises(error_type): + if metadata is None: + token_extractor.extract_access_token(**dict) + else: + token_extractor.extract_access_token(metadata=metadata) + + +def test_rest_token_extractor(): + token_extractor = RestTokenExtractor() + request: Request = Mock(spec=Request) + token = "abcd" + + request.headers = {"authorization": f"Bearer {token}"} + assertpy.assert_that( + token_extractor.extract_access_token(request=request) + ).is_equal_to(token) + + request.headers = {"authorization": f"bearer {token}"} + assertpy.assert_that( + token_extractor.extract_access_token(request=request) + ).is_equal_to(token) + + +def test_grpc_token_extractor(): + token_extractor = GrpcTokenExtractor() + metadata = {} + token = "abcd" + + metadata["authorization"] = f"Bearer {token}" + assertpy.assert_that( + token_extractor.extract_access_token(metadata=metadata) + ).is_equal_to(token) + + metadata["authorization"] = f"bearer {token}" + assertpy.assert_that( + token_extractor.extract_access_token(metadata=metadata) + ).is_equal_to(token) + + +@pytest.mark.parametrize( + "error_type, dict, header", + [ + (ValueError, {}, None), + (ValueError, {"other": 123}, None), + (AuthenticationError, {}, ""), + (AuthenticationError, {}, "abcd"), + (AuthenticationError, {}, ["abcd"]), + (AuthenticationError, {}, ["other-scheme abcd"]), + ], +) +def test_arrow_flight_token_extractor_failures(error_type, dict, header): + token_extractor = ArrowFlightTokenExtractor() + + headers = None + if header is not None: + if header != "": + headers = {"authorization": header} + else: + headers = {} + with pytest.raises(error_type): + if headers is None: + token_extractor.extract_access_token(**dict) + else: + token_extractor.extract_access_token(headers=headers) + + +def test_arrow_flight_token_extractor(): + token_extractor = ArrowFlightTokenExtractor() + token = "abcd" + + headers = {"authorization": [f"Bearer {token}"]} + assertpy.assert_that( + token_extractor.extract_access_token(headers=headers) + ).is_equal_to(token) + + headers = {"authorization": [f"bearer {token}"]} + assertpy.assert_that( + token_extractor.extract_access_token(headers=headers) + ).is_equal_to(token) diff --git a/sdk/python/tests/unit/permissions/auth/test_token_parser.py b/sdk/python/tests/unit/permissions/auth/test_token_parser.py new file mode 100644 index 0000000000..6ae9094f81 --- /dev/null +++ b/sdk/python/tests/unit/permissions/auth/test_token_parser.py @@ -0,0 +1,122 @@ +# test_token_validator.py + +import asyncio +from unittest.mock import MagicMock, patch + +import assertpy +import pytest +from starlette.authentication import ( + AuthenticationError, +) + +from feast.permissions.auth.kubernetes_token_parser import KubernetesTokenParser +from feast.permissions.auth.oidc_token_parser import OidcTokenParser +from feast.permissions.user import User + +_CLIENT_ID = "test" + + +@patch( + "feast.permissions.auth.oidc_token_parser.OAuth2AuthorizationCodeBearer.__call__" +) +@patch("feast.permissions.auth.oidc_token_parser.PyJWKClient.get_signing_key_from_jwt") +@patch("feast.permissions.auth.oidc_token_parser.jwt.decode") +def test_oidc_token_validation_success( + mock_jwt, mock_signing_key, mock_oauth2, oidc_config +): + signing_key = MagicMock() + signing_key.key = "a-key" + mock_signing_key.return_value = signing_key + + user_data = { + "preferred_username": "my-name", + "resource_access": {_CLIENT_ID: {"roles": ["reader", "writer"]}}, + } + mock_jwt.return_value = user_data + + access_token = "aaa-bbb-ccc" + token_parser = OidcTokenParser(auth_config=oidc_config) + user = asyncio.run( + token_parser.user_details_from_access_token(access_token=access_token) + ) + + assertpy.assert_that(user).is_type_of(User) + if isinstance(user, User): + assertpy.assert_that(user.username).is_equal_to("my-name") + assertpy.assert_that(user.roles.sort()).is_equal_to(["reader", "writer"].sort()) + assertpy.assert_that(user.has_matching_role(["reader"])).is_true() + assertpy.assert_that(user.has_matching_role(["writer"])).is_true() + assertpy.assert_that(user.has_matching_role(["updater"])).is_false() + + +@patch( + "feast.permissions.auth.oidc_token_parser.OAuth2AuthorizationCodeBearer.__call__" +) +def test_oidc_token_validation_failure(mock_oauth2, oidc_config): + mock_oauth2.side_effect = AuthenticationError("wrong token") + + access_token = "aaa-bbb-ccc" + token_parser = OidcTokenParser(auth_config=oidc_config) + with pytest.raises(AuthenticationError): + asyncio.run( + token_parser.user_details_from_access_token(access_token=access_token) + ) + + +# TODO RBAC: Move role bindings to a reusable fixture +@patch("feast.permissions.auth.kubernetes_token_parser.config.load_incluster_config") +@patch("feast.permissions.auth.kubernetes_token_parser.jwt.decode") +@patch( + "feast.permissions.auth.kubernetes_token_parser.client.RbacAuthorizationV1Api.list_namespaced_role_binding" +) +@patch( + "feast.permissions.auth.kubernetes_token_parser.client.RbacAuthorizationV1Api.list_cluster_role_binding" +) +def test_k8s_token_validation_success( + mock_crb, + mock_rb, + mock_jwt, + mock_config, + rolebindings, + clusterrolebindings, +): + sa_name = "my-name" + namespace = "my-ns" + subject = f"system:serviceaccount:{namespace}:{sa_name}" + mock_jwt.return_value = {"sub": subject} + + mock_rb.return_value = rolebindings["items"] + mock_crb.return_value = clusterrolebindings["items"] + + roles = rolebindings["roles"] + croles = clusterrolebindings["roles"] + + access_token = "aaa-bbb-ccc" + token_parser = KubernetesTokenParser() + user = asyncio.run( + token_parser.user_details_from_access_token(access_token=access_token) + ) + + assertpy.assert_that(user).is_type_of(User) + if isinstance(user, User): + assertpy.assert_that(user.username).is_equal_to(f"{namespace}:{sa_name}") + assertpy.assert_that(user.roles.sort()).is_equal_to((roles + croles).sort()) + for r in roles: + assertpy.assert_that(user.has_matching_role([r])).is_true() + for cr in croles: + assertpy.assert_that(user.has_matching_role([cr])).is_true() + assertpy.assert_that(user.has_matching_role(["foo"])).is_false() + + +@patch("feast.permissions.auth.kubernetes_token_parser.config.load_incluster_config") +@patch("feast.permissions.auth.kubernetes_token_parser.jwt.decode") +def test_k8s_token_validation_failure(mock_jwt, mock_config): + subject = "wrong-subject" + mock_jwt.return_value = {"sub": subject} + + access_token = "aaa-bbb-ccc" + token_parser = KubernetesTokenParser() + with pytest.raises(AuthenticationError): + asyncio.run( + token_parser.user_details_from_access_token(access_token=access_token) + ) diff --git a/sdk/python/tests/unit/permissions/conftest.py b/sdk/python/tests/unit/permissions/conftest.py new file mode 100644 index 0000000000..7cd944fb47 --- /dev/null +++ b/sdk/python/tests/unit/permissions/conftest.py @@ -0,0 +1,88 @@ +from unittest.mock import Mock + +import pytest + +from feast import FeatureView +from feast.infra.registry.base_registry import BaseRegistry +from feast.permissions.decorator import require_permissions +from feast.permissions.permission import AuthzedAction, Permission +from feast.permissions.policy import RoleBasedPolicy +from feast.permissions.security_manager import ( + SecurityManager, + set_security_manager, +) +from feast.permissions.user import User + + +class SecuredFeatureView(FeatureView): + def __init__(self, name, tags): + super().__init__( + name=name, + source=Mock(), + tags=tags, + ) + + @require_permissions(actions=[AuthzedAction.DESCRIBE]) + def read_protected(self) -> bool: + return True + + @require_permissions(actions=[AuthzedAction.UPDATE]) + def write_protected(self) -> bool: + return True + + def unprotected(self) -> bool: + return True + + +@pytest.fixture +def feature_views() -> list[FeatureView]: + return [ + SecuredFeatureView("secured", {}), + SecuredFeatureView("special-secured", {}), + ] + + +@pytest.fixture +def users() -> list[User]: + users = [] + users.append(User("r", ["reader"])) + users.append(User("w", ["writer"])) + users.append(User("rw", ["reader", "writer"])) + users.append(User("admin", ["reader", "writer", "admin"])) + return dict([(u.username, u) for u in users]) + + +@pytest.fixture +def security_manager() -> SecurityManager: + permissions = [] + permissions.append( + Permission( + name="reader", + types=FeatureView, + policy=RoleBasedPolicy(roles=["reader"]), + actions=[AuthzedAction.DESCRIBE], + ) + ) + permissions.append( + Permission( + name="writer", + types=FeatureView, + policy=RoleBasedPolicy(roles=["writer"]), + actions=[AuthzedAction.UPDATE], + ) + ) + permissions.append( + Permission( + name="special", + types=FeatureView, + name_pattern="special.*", + policy=RoleBasedPolicy(roles=["admin", "special-reader"]), + actions=[AuthzedAction.DESCRIBE, AuthzedAction.UPDATE], + ) + ) + + registry = Mock(spec=BaseRegistry) + registry.list_permissions = Mock(return_value=permissions) + sm = SecurityManager(project="any", registry=registry) + set_security_manager(sm) + return sm diff --git a/sdk/python/tests/unit/permissions/test_decision.py b/sdk/python/tests/unit/permissions/test_decision.py new file mode 100644 index 0000000000..23bafedeab --- /dev/null +++ b/sdk/python/tests/unit/permissions/test_decision.py @@ -0,0 +1,34 @@ +import assertpy +import pytest + +from feast.permissions.decision import DecisionEvaluator + +# Each vote is a tuple of `current_vote` and expected output of `is_decided` + + +@pytest.mark.parametrize( + "evaluator, votes, decision, no_of_explanations", + [ + (DecisionEvaluator(3), [(True, True)], True, 0), + (DecisionEvaluator(3), [(True, True)], True, 0), + ( + DecisionEvaluator(3), + [(False, False), (False, False), (False, True)], + False, + 3, + ), + ], +) +def test_decision_evaluator(evaluator, votes, decision, no_of_explanations): + for v in votes: + vote = v[0] + decided = v[1] + evaluator.add_grant(vote, "" if vote else "a message") + if decided: + assertpy.assert_that(evaluator.is_decided()).is_true() + else: + assertpy.assert_that(evaluator.is_decided()).is_false() + + grant, explanations = evaluator.grant() + assertpy.assert_that(grant).is_equal_to(decision) + assertpy.assert_that(explanations).is_length(no_of_explanations) diff --git a/sdk/python/tests/unit/permissions/test_decorator.py b/sdk/python/tests/unit/permissions/test_decorator.py new file mode 100644 index 0000000000..8f6c2c420b --- /dev/null +++ b/sdk/python/tests/unit/permissions/test_decorator.py @@ -0,0 +1,32 @@ +import assertpy +import pytest + + +@pytest.mark.parametrize( + "username, can_read, can_write", + [ + (None, False, False), + ("r", True, False), + ("w", False, True), + ("rw", True, True), + ], +) +def test_access_SecuredFeatureView( + security_manager, feature_views, users, username, can_read, can_write +): + sm = security_manager + fv = feature_views[0] + user = users.get(username) + + sm.set_current_user(user) + if can_read: + fv.read_protected() + else: + with pytest.raises(PermissionError): + fv.read_protected() + if can_write: + fv.write_protected() + else: + with pytest.raises(PermissionError): + fv.write_protected() + assertpy.assert_that(fv.unprotected()).is_true() diff --git a/sdk/python/tests/unit/permissions/test_oidc_auth_client.py b/sdk/python/tests/unit/permissions/test_oidc_auth_client.py new file mode 100644 index 0000000000..22ed5b6f87 --- /dev/null +++ b/sdk/python/tests/unit/permissions/test_oidc_auth_client.py @@ -0,0 +1,62 @@ +from unittest.mock import patch + +from requests import Session + +from feast.permissions.auth_model import ( + KubernetesAuthConfig, + NoAuthConfig, + OidcAuthConfig, +) +from feast.permissions.client.http_auth_requests_wrapper import ( + AuthenticatedRequestsSession, + get_http_auth_requests_session, +) +from feast.permissions.client.kubernetes_auth_client_manager import ( + KubernetesAuthClientManager, +) +from feast.permissions.client.oidc_authentication_client_manager import ( + OidcAuthClientManager, +) + +MOCKED_TOKEN_VALUE: str = "dummy_token" + + +def _get_dummy_oidc_auth_type() -> OidcAuthConfig: + oidc_config = OidcAuthConfig( + auth_discovery_url="http://localhost:8080/realms/master/.well-known/openid-configuration", + type="oidc", + username="admin_test", + password="password_test", + client_id="dummy_client_id", + ) + return oidc_config + + +@patch.object(KubernetesAuthClientManager, "get_token", return_value=MOCKED_TOKEN_VALUE) +@patch.object(OidcAuthClientManager, "get_token", return_value=MOCKED_TOKEN_VALUE) +def test_http_auth_requests_session(mock_kubernetes_token, mock_oidc_token): + no_auth_config = NoAuthConfig() + assert isinstance(get_http_auth_requests_session(no_auth_config), Session) + + oidc_auth_config = _get_dummy_oidc_auth_type() + oidc_auth_requests_session = get_http_auth_requests_session(oidc_auth_config) + _assert_auth_requests_session(oidc_auth_requests_session, MOCKED_TOKEN_VALUE) + + kubernetes_auth_config = KubernetesAuthConfig(type="kubernetes") + kubernetes_auth_requests_session = get_http_auth_requests_session( + kubernetes_auth_config + ) + _assert_auth_requests_session(kubernetes_auth_requests_session, MOCKED_TOKEN_VALUE) + + +def _assert_auth_requests_session( + auth_req_session: AuthenticatedRequestsSession, expected_token: str +): + assert isinstance(auth_req_session, AuthenticatedRequestsSession) + assert "Authorization" in auth_req_session.headers, ( + "Authorization header is missing in object of class: " + "AuthenticatedRequestsSession " + ) + assert ( + auth_req_session.headers["Authorization"] == f"Bearer {expected_token}" + ), "Authorization token is incorrect" diff --git a/sdk/python/tests/unit/permissions/test_permission.py b/sdk/python/tests/unit/permissions/test_permission.py new file mode 100644 index 0000000000..606d750d81 --- /dev/null +++ b/sdk/python/tests/unit/permissions/test_permission.py @@ -0,0 +1,205 @@ +from unittest.mock import Mock + +import assertpy +import pytest + +from feast.batch_feature_view import BatchFeatureView +from feast.data_source import DataSource +from feast.entity import Entity +from feast.feast_object import ALL_RESOURCE_TYPES +from feast.feature_service import FeatureService +from feast.feature_view import FeatureView +from feast.on_demand_feature_view import OnDemandFeatureView +from feast.permissions.action import ALL_ACTIONS, AuthzedAction +from feast.permissions.permission import ( + Permission, +) +from feast.permissions.policy import AllowAll, Policy +from feast.saved_dataset import ValidationReference +from feast.stream_feature_view import StreamFeatureView + + +def test_defaults(): + p = Permission(name="test") + assertpy.assert_that(type(p.types)).is_equal_to(list) + assertpy.assert_that(p.types).is_equal_to(ALL_RESOURCE_TYPES) + assertpy.assert_that(p.name_pattern).is_none() + assertpy.assert_that(p.tags).is_none() + assertpy.assert_that(type(p.actions)).is_equal_to(list) + assertpy.assert_that(p.actions).is_equal_to(ALL_ACTIONS) + assertpy.assert_that(type(p.actions)).is_equal_to(list) + assertpy.assert_that(isinstance(p.policy, Policy)).is_true() + assertpy.assert_that(p.policy).is_equal_to(AllowAll) + + +@pytest.mark.parametrize( + "dict, result", + [ + ({"types": None}, True), + ({"types": []}, True), + ({"types": ALL_RESOURCE_TYPES}, True), + ({"types": [FeatureView, FeatureService]}, True), + ({"actions": None}, False), + ({"actions": []}, False), + ({"actions": ALL_ACTIONS}, True), + ({"actions": ALL_ACTIONS}, True), + ({"actions": [AuthzedAction.CREATE, AuthzedAction.DELETE]}, True), + ({"policy": None}, False), + ({"policy": []}, False), + ({"policy": Mock(spec=Policy)}, True), + ], +) +def test_validity(dict, result): + if not result: + with pytest.raises(ValueError): + Permission(name="test", **dict) + else: + Permission(name="test", **dict) + + +def test_normalized_args(): + p = Permission(name="test") + assertpy.assert_that(type(p.types)).is_equal_to(list) + assertpy.assert_that(p.types).is_equal_to(ALL_RESOURCE_TYPES) + + p = Permission(name="test", actions=AuthzedAction.CREATE) + assertpy.assert_that(type(p.actions)).is_equal_to(list) + assertpy.assert_that(p.actions).is_equal_to([AuthzedAction.CREATE]) + + +@pytest.mark.parametrize( + "resource, types, result", + [ + (None, ALL_RESOURCE_TYPES, False), + ("invalid string", ALL_RESOURCE_TYPES, False), + ("ALL", ALL_RESOURCE_TYPES, False), + ("ALL", ALL_RESOURCE_TYPES, False), + ( + Mock(spec=FeatureView), + [t for t in ALL_RESOURCE_TYPES if t not in [FeatureView]], + False, + ), + ( + Mock(spec=OnDemandFeatureView), + [t for t in ALL_RESOURCE_TYPES if t not in [OnDemandFeatureView]], + False, + ), # OnDemandFeatureView is a BaseFeatureView + ( + Mock(spec=BatchFeatureView), + FeatureView, + True, + ), # BatchFeatureView is a FeatureView + ( + Mock(spec=BatchFeatureView), + [t for t in ALL_RESOURCE_TYPES if t not in [FeatureView, BatchFeatureView]], + False, + ), + ( + Mock(spec=StreamFeatureView), + FeatureView, + True, + ), # StreamFeatureView is a FeatureView + ( + Mock(spec=StreamFeatureView), + [ + t + for t in ALL_RESOURCE_TYPES + if t not in [FeatureView, StreamFeatureView] + ], + False, + ), + ( + Mock(spec=Entity), + [t for t in ALL_RESOURCE_TYPES if t not in [Entity]], + False, + ), + ( + Mock(spec=FeatureService), + [t for t in ALL_RESOURCE_TYPES if t not in [FeatureService]], + False, + ), + ( + Mock(spec=DataSource), + [t for t in ALL_RESOURCE_TYPES if t not in [DataSource]], + False, + ), + ( + Mock(spec=ValidationReference), + [t for t in ALL_RESOURCE_TYPES if t not in [ValidationReference]], + False, + ), + ( + Mock(spec=Permission), + [t for t in ALL_RESOURCE_TYPES if t not in [Permission]], + False, + ), + ] + + [(Mock(spec=t), ALL_RESOURCE_TYPES, True) for t in ALL_RESOURCE_TYPES] + + [(Mock(spec=t), [t], True) for t in ALL_RESOURCE_TYPES], +) +def test_match_resource_with_subclasses(resource, types, result): + p = Permission(name="test", types=types) + assertpy.assert_that(p.match_resource(resource)).is_equal_to(result) + + +@pytest.mark.parametrize( + "pattern, name, match", + [ + ("test.*", "test", True), + ("test.*", "test1", True), + ("test.*", "wrongtest", False), + (".*test.*", "wrongtest", True), + ], +) +def test_resource_match_with_name_filter(pattern, name, match): + p = Permission(name="test", name_pattern=pattern) + for t in ALL_RESOURCE_TYPES: + resource = Mock(spec=t) + resource.name = name + assertpy.assert_that(p.match_resource(resource)).is_equal_to(match) + + +@pytest.mark.parametrize( + ("required_tags, tags, result"), + [ + ({"owner": "dev"}, {}, False), + ({"owner": "dev"}, {"owner": "master"}, False), + ({"owner": "dev"}, {"owner": "dev", "other": 1}, True), + ({"owner": "dev", "dep": 1}, {"owner": "dev", "other": 1}, False), + ({"owner": "dev", "dep": 1}, {"owner": "dev", "other": 1, "dep": 1}, True), + ], +) +def test_resource_match_with_tags(required_tags, tags, result): + # Missing tags + p = Permission(name="test", required_tags=required_tags) + for t in ALL_RESOURCE_TYPES: + resource = Mock(spec=t) + resource.name = "test" + resource.required_tags = tags + assertpy.assert_that(p.match_resource(resource)).is_equal_to(result) + + +@pytest.mark.parametrize( + ("permitted_actions, requested_actions, result"), + [(ALL_ACTIONS, [a], True) for a in AuthzedAction.__members__.values()] + + [ + ( + [AuthzedAction.CREATE, AuthzedAction.DELETE], + [AuthzedAction.CREATE, AuthzedAction.DELETE], + True, + ), + ([AuthzedAction.CREATE, AuthzedAction.DELETE], [AuthzedAction.CREATE], True), + ([AuthzedAction.CREATE, AuthzedAction.DELETE], [AuthzedAction.DELETE], True), + ([AuthzedAction.CREATE, AuthzedAction.DELETE], [AuthzedAction.UPDATE], False), + ( + [AuthzedAction.CREATE, AuthzedAction.DELETE], + [AuthzedAction.CREATE, AuthzedAction.DELETE, AuthzedAction.UPDATE], + False, + ), + ], +) +def test_match_actions(permitted_actions, requested_actions, result): + p = Permission(name="test", actions=permitted_actions) + assertpy.assert_that( + p.match_actions(requested_actions=requested_actions) + ).is_equal_to(result) diff --git a/sdk/python/tests/unit/permissions/test_policy.py b/sdk/python/tests/unit/permissions/test_policy.py new file mode 100644 index 0000000000..4e78282d4f --- /dev/null +++ b/sdk/python/tests/unit/permissions/test_policy.py @@ -0,0 +1,44 @@ +import assertpy +import pytest + +from feast.permissions.policy import AllowAll, RoleBasedPolicy +from feast.permissions.user import User + + +@pytest.mark.parametrize( + "username", + [("r"), ("w"), ("rw"), ("missing")], +) +def test_allow_all(users, username): + user = users.get(username, User(username, [])) + assertpy.assert_that(AllowAll.validate_user(user)).is_true() + + +@pytest.mark.parametrize( + "required_roles, username, result", + [ + (["reader"], "r", True), + (["writer"], "r", False), + (["reader", "writer"], "r", True), + (["writer", "updater"], "r", False), + (["reader"], "w", False), + (["writer"], "w", True), + (["reader", "writer"], "w", True), + (["reader", "updater"], "w", False), + (["reader"], "rw", True), + (["writer"], "rw", True), + (["reader", "writer"], "rw", True), + (["updater"], "rw", False), + ], +) +def test_role_based_policy(users, required_roles, username, result): + user = users.get(username) + policy = RoleBasedPolicy(roles=required_roles) + + validate_result, explain = policy.validate_user(user) + assertpy.assert_that(validate_result).is_equal_to(result) + + if result is True: + assertpy.assert_that(explain).is_equal_to("") + else: + assertpy.assert_that(len(explain)).is_greater_than(0) diff --git a/sdk/python/tests/unit/permissions/test_security_manager.py b/sdk/python/tests/unit/permissions/test_security_manager.py new file mode 100644 index 0000000000..192542da78 --- /dev/null +++ b/sdk/python/tests/unit/permissions/test_security_manager.py @@ -0,0 +1,83 @@ +import assertpy +import pytest + +from feast.permissions.action import READ, AuthzedAction +from feast.permissions.security_manager import assert_permissions, permitted_resources + + +@pytest.mark.parametrize( + "username, requested_actions, allowed, allowed_single, raise_error_in_assert, raise_error_in_permit", + [ + (None, [], False, [False, False], [True, True], False), + ("r", [AuthzedAction.DESCRIBE], True, [True, True], [False, False], False), + ("r", [AuthzedAction.UPDATE], False, [False, False], [True, True], False), + ("w", [AuthzedAction.DESCRIBE], False, [False, False], [True, True], False), + ("w", [AuthzedAction.UPDATE], False, [True, True], [False, False], False), + ("rw", [AuthzedAction.DESCRIBE], False, [True, True], [False, False], False), + ("rw", [AuthzedAction.UPDATE], False, [True, True], [False, False], False), + ( + "rw", + [AuthzedAction.DESCRIBE, AuthzedAction.UPDATE], + False, + [False, False], + [True, True], + True, + ), + ( + "admin", + [AuthzedAction.DESCRIBE, AuthzedAction.UPDATE], + False, + [False, True], + [True, False], + True, + ), + ( + "admin", + READ + [AuthzedAction.UPDATE], + False, + [False, False], + [True, True], + True, + ), + ], +) +def test_access_SecuredFeatureView( + security_manager, + feature_views, + users, + username, + requested_actions, + allowed, + allowed_single, + raise_error_in_assert, + raise_error_in_permit, +): + sm = security_manager + resources = feature_views + + user = users.get(username) + sm.set_current_user(user) + + result = [] + if raise_error_in_permit: + with pytest.raises(PermissionError): + result = permitted_resources(resources=resources, actions=requested_actions) + else: + result = permitted_resources(resources=resources, actions=requested_actions) + + if allowed: + assertpy.assert_that(result).is_equal_to(resources) + elif not raise_error_in_permit: + filtered = [r for i, r in enumerate(resources) if allowed_single[i]] + assertpy.assert_that(result).is_equal_to(filtered) + + for i, r in enumerate(resources): + if allowed_single[i]: + result = assert_permissions(resource=r, actions=requested_actions) + assertpy.assert_that(result).is_equal_to(r) + elif raise_error_in_assert[i]: + with pytest.raises(PermissionError): + assert_permissions(resource=r, actions=requested_actions) + else: + result = assert_permissions(resource=r, actions=requested_actions) + assertpy.assert_that(result).is_none() diff --git a/sdk/python/tests/unit/permissions/test_user.py b/sdk/python/tests/unit/permissions/test_user.py new file mode 100644 index 0000000000..cce318cba7 --- /dev/null +++ b/sdk/python/tests/unit/permissions/test_user.py @@ -0,0 +1,34 @@ +import assertpy +import pytest + +from feast.permissions.user import User + + +@pytest.fixture(scope="module") +def users(): + users = [] + users.append(User("a", ["a1", "a2"])) + users.append(User("b", ["b1", "b2"])) + return dict([(u.username, u) for u in users]) + + +@pytest.mark.parametrize( + "username, roles, result", + [ + ("c", [], False), + ("a", ["b1"], False), + ("a", ["a1", "b1"], True), + ("a", ["a1"], True), + ("a", ["a1", "a2"], True), + ("a", ["a1", "a2", "a3"], True), + ("b", ["a1", "a3"], False), + ("b", ["a1", "b1"], True), + ("b", ["b1", "b2"], True), + ("b", ["b1", "b2", "b3"], True), + ], +) +def test_user_has_matching_role(users, username, roles, result): + user = users.get(username, User(username, [])) + assertpy.assert_that(user.has_matching_role(requested_roles=roles)).is_equal_to( + result + ) diff --git a/sdk/python/tests/unit/test_offline_server.py b/sdk/python/tests/unit/test_offline_server.py index 5991e7450d..237e2ecad4 100644 --- a/sdk/python/tests/unit/test_offline_server.py +++ b/sdk/python/tests/unit/test_offline_server.py @@ -14,7 +14,7 @@ RemoteOfflineStore, RemoteOfflineStoreConfig, ) -from feast.offline_server import OfflineServer +from feast.offline_server import OfflineServer, _init_auth_manager from feast.repo_config import RepoConfig from tests.utils.cli_repo_creator import CliRunner @@ -26,6 +26,7 @@ def empty_offline_server(environment): store = environment.feature_store location = "grpc+tcp://localhost:0" + _init_auth_manager(store=store) return OfflineServer(store=store, location=location) @@ -102,6 +103,8 @@ def test_remote_offline_store_apis(): with tempfile.TemporaryDirectory() as temp_dir: store = default_store(str(temp_dir)) location = "grpc+tcp://localhost:0" + + _init_auth_manager(store=store) server = OfflineServer(store=store, location=location) assertpy.assert_that(server).is_not_none diff --git a/sdk/python/tests/utils/auth_permissions_util.py b/sdk/python/tests/utils/auth_permissions_util.py new file mode 100644 index 0000000000..3b5e589812 --- /dev/null +++ b/sdk/python/tests/utils/auth_permissions_util.py @@ -0,0 +1,245 @@ +import os +import subprocess + +import yaml +from keycloak import KeycloakAdmin + +from feast import ( + FeatureStore, + RepoConfig, +) +from feast.infra.registry.remote import RemoteRegistryConfig +from feast.permissions.permission import Permission +from feast.wait import wait_retry_backoff +from tests.utils.cli_repo_creator import CliRunner +from tests.utils.http_server import check_port_open + +PROJECT_NAME = "feast_test_project" + + +def include_auth_config(file_path, auth_config: str): + with open(file_path, "r") as file: + existing_content = yaml.safe_load(file) + new_section = yaml.safe_load(auth_config) + if isinstance(existing_content, dict) and isinstance(new_section, dict): + existing_content.update(new_section) + else: + raise ValueError("Both existing content and new section must be dictionaries.") + with open(file_path, "w") as file: + yaml.safe_dump(existing_content, file, default_flow_style=False) + print(f"Updated auth section at {file_path}") + + +def default_store( + temp_dir, + auth_config: str, + permissions: list[Permission], +): + runner = CliRunner() + result = runner.run(["init", PROJECT_NAME], cwd=temp_dir) + repo_path = os.path.join(temp_dir, PROJECT_NAME, "feature_repo") + assert result.returncode == 0 + + include_auth_config( + file_path=f"{repo_path}/feature_store.yaml", auth_config=auth_config + ) + + result = runner.run(["--chdir", repo_path, "apply"], cwd=temp_dir) + assert result.returncode == 0 + + fs = FeatureStore(repo_path=repo_path) + + fs.apply(permissions) + + return fs + + +def start_feature_server(repo_path: str, server_port: int, metrics: bool = False): + host = "0.0.0.0" + cmd = [ + "feast", + "-c" + repo_path, + "serve", + "--host", + host, + "--port", + str(server_port), + ] + feast_server_process = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + _time_out_sec: int = 60 + # Wait for server to start + wait_retry_backoff( + lambda: (None, check_port_open(host, server_port)), + timeout_secs=_time_out_sec, + timeout_msg=f"Unable to start the feast server in {_time_out_sec} seconds for remote online store type, port={server_port}", + ) + + if metrics: + cmd.append("--metrics") + + # Check if metrics are enabled and Prometheus server is running + if metrics: + wait_retry_backoff( + lambda: (None, check_port_open("localhost", 8000)), + timeout_secs=_time_out_sec, + timeout_msg="Unable to start the Prometheus server in 60 seconds.", + ) + else: + assert not check_port_open( + "localhost", 8000 + ), "Prometheus server is running when it should be disabled." + + yield f"http://localhost:{server_port}" + + if feast_server_process is not None: + feast_server_process.kill() + + # wait server to free the port + wait_retry_backoff( + lambda: ( + None, + not check_port_open("localhost", server_port), + ), + timeout_msg=f"Unable to stop the feast server in {_time_out_sec} seconds for remote online store type, port={server_port}", + timeout_secs=_time_out_sec, + ) + + +def get_remote_registry_store(server_port, feature_store): + registry_config = RemoteRegistryConfig( + registry_type="remote", path=f"localhost:{server_port}" + ) + + store = FeatureStore( + config=RepoConfig( + project=PROJECT_NAME, + auth=feature_store.config.auth, + registry=registry_config, + provider="local", + entity_key_serialization_version=2, + ) + ) + return store + + +def setup_permissions_on_keycloak(keycloak_admin: KeycloakAdmin): + new_client_id = "feast-integration-client" + new_client_secret = "feast-integration-client-secret" + # Create a new client + client_representation = { + "clientId": new_client_id, + "secret": new_client_secret, + "enabled": True, + "directAccessGrantsEnabled": True, + "publicClient": False, + "redirectUris": ["*"], + "serviceAccountsEnabled": True, + "standardFlowEnabled": True, + } + keycloak_admin.create_client(client_representation) + + # Get the client ID + client_id = keycloak_admin.get_client_id(new_client_id) + + # Role representation + reader_role_rep = { + "name": "reader", + "description": "feast reader client role", + "composite": False, + "clientRole": True, + "containerId": client_id, + } + keycloak_admin.create_client_role(client_id, reader_role_rep, True) + reader_role_id = keycloak_admin.get_client_role( + client_id=client_id, role_name="reader" + ) + + # Role representation + writer_role_rep = { + "name": "writer", + "description": "feast writer client role", + "composite": False, + "clientRole": True, + "containerId": client_id, + } + keycloak_admin.create_client_role(client_id, writer_role_rep, True) + writer_role_id = keycloak_admin.get_client_role( + client_id=client_id, role_name="writer" + ) + + # Mapper representation + mapper_representation = { + "name": "client-roles-mapper", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": False, + "config": { + "multivalued": "true", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "roles", + "jsonType.label": "String", + "client.id": client_id, + }, + } + + # Add predefined client roles mapper to the client + keycloak_admin.add_mapper_to_client(client_id, mapper_representation) + + reader_writer_user = { + "username": "reader_writer", + "enabled": True, + "firstName": "reader_writer fn", + "lastName": "reader_writer ln", + "email": "reader_writer@email.com", + "emailVerified": True, + "credentials": [{"value": "password", "type": "password", "temporary": False}], + } + reader_writer_user_id = keycloak_admin.create_user(reader_writer_user) + keycloak_admin.assign_client_role( + user_id=reader_writer_user_id, + client_id=client_id, + roles=[reader_role_id, writer_role_id], + ) + + reader_user = { + "username": "reader", + "enabled": True, + "firstName": "reader fn", + "lastName": "reader ln", + "email": "reader@email.com", + "emailVerified": True, + "credentials": [{"value": "password", "type": "password", "temporary": False}], + } + reader_user_id = keycloak_admin.create_user(reader_user) + keycloak_admin.assign_client_role( + user_id=reader_user_id, client_id=client_id, roles=[reader_role_id] + ) + + writer_user = { + "username": "writer", + "enabled": True, + "firstName": "writer fn", + "lastName": "writer ln", + "email": "writer@email.com", + "emailVerified": True, + "credentials": [{"value": "password", "type": "password", "temporary": False}], + } + writer_user_id = keycloak_admin.create_user(writer_user) + keycloak_admin.assign_client_role( + user_id=writer_user_id, client_id=client_id, roles=[writer_role_id] + ) + + no_roles_user = { + "username": "no_roles_user", + "enabled": True, + "firstName": "no_roles_user fn", + "lastName": "no_roles_user ln", + "email": "no_roles_user@email.com", + "emailVerified": True, + "credentials": [{"value": "password", "type": "password", "temporary": False}], + } + keycloak_admin.create_user(no_roles_user) diff --git a/setup.py b/setup.py index 6fb5bfee61..d53aee1002 100644 --- a/setup.py +++ b/setup.py @@ -61,6 +61,9 @@ "dask[dataframe]>=2024.2.1", "prometheus_client", "psutil", + "bigtree>=0.19.2", + "pyjwt", + "kubernetes<=20.13.0", ] GCP_REQUIRED = [ @@ -183,6 +186,7 @@ "pytest-env", "Sphinx>4.0.0,<7", "testcontainers==4.4.0", + "python-keycloak==4.2.2", "pre-commit<3.3.2", "assertpy==1.1", "pip-tools", From 75983f2b84dae125ada63193afd3d3946fd1114f Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Wed, 21 Aug 2024 11:37:16 -0400 Subject: [PATCH 029/185] chore: Fix rbac url. --- docs/SUMMARY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index b176eec3c1..4f8379b8ed 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -15,6 +15,7 @@ * [Write Patterns](getting-started/architecture/write-patterns.md) * [Feature Transformation](getting-started/architecture/feature-transformation.md) * [Feature Serving and Model Inference](getting-started/architecture/model-inference.md) + * [Role-Based Access Control (RBAC)](getting-started/architecture/rbac.md) * [Concepts](getting-started/concepts/README.md) * [Overview](getting-started/concepts/overview.md) * [Data ingestion](getting-started/concepts/data-ingestion.md) @@ -23,7 +24,6 @@ * [Feature retrieval](getting-started/concepts/feature-retrieval.md) * [Point-in-time joins](getting-started/concepts/point-in-time-joins.md) * [Registry](getting-started/concepts/registry.md) - * [Role-Based Access Control (RBAC)](getting-started/architecture/rbac.md) * [\[Alpha\] Saved dataset](getting-started/concepts/dataset.md) * [Components](getting-started/components/README.md) * [Overview](getting-started/components/overview.md) From 0a48f7bb436febb0171c78a559a577eedeff421f Mon Sep 17 00:00:00 2001 From: Abdul Hameed Date: Wed, 21 Aug 2024 14:34:17 -0400 Subject: [PATCH 030/185] fix: Links to the RBAC documentation under Concepts and Components (#4430) * fix the rbac docs links Signed-off-by: Abdul Hameed * fix: links to the RBAC documentation under Concepts and Components sections Signed-off-by: Abdul Hameed --------- Signed-off-by: Abdul Hameed --- docs/SUMMARY.md | 2 ++ docs/getting-started/components/overview.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 4f8379b8ed..a5d02c4f64 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -24,6 +24,7 @@ * [Feature retrieval](getting-started/concepts/feature-retrieval.md) * [Point-in-time joins](getting-started/concepts/point-in-time-joins.md) * [Registry](getting-started/concepts/registry.md) + * [Permission](getting-started/concepts/permission.md) * [\[Alpha\] Saved dataset](getting-started/concepts/dataset.md) * [Components](getting-started/components/README.md) * [Overview](getting-started/components/overview.md) @@ -32,6 +33,7 @@ * [Online store](getting-started/components/online-store.md) * [Batch Materialization Engine](getting-started/components/batch-materialization-engine.md) * [Provider](getting-started/components/provider.md) + * [Authorization Manager](getting-started/components/authz_manager.md) * [Third party integrations](getting-started/third-party-integrations.md) * [FAQ](getting-started/faq.md) diff --git a/docs/getting-started/components/overview.md b/docs/getting-started/components/overview.md index 0ee3835de6..ac0b99de8a 100644 --- a/docs/getting-started/components/overview.md +++ b/docs/getting-started/components/overview.md @@ -28,4 +28,4 @@ A complete Feast deployment contains the following components: * **Batch Materialization Engine:** The [Batch Materialization Engine](batch-materialization-engine.md) component launches a process which loads data into the online store from the offline store. By default, Feast uses a local in-process engine implementation to materialize data. However, additional infrastructure can be used for a more scalable materialization process. * **Online Store:** The online store is a database that stores only the latest feature values for each entity. The online store is either populated through materialization jobs or through [stream ingestion](../../reference/data-sources/push.md). * **Offline Store:** The offline store persists batch data that has been ingested into Feast. This data is used for producing training datasets. For feature retrieval and materialization, Feast does not manage the offline store directly, but runs queries against it. However, offline stores can be configured to support writes if Feast configures logging functionality of served features. -* **Authorization manager**: The authorization manager detects authentication tokens from client requests to Feast servers and uses this information to enforce permission policies on the requested services. +* **Authorization Manager**: The authorization manager detects authentication tokens from client requests to Feast servers and uses this information to enforce permission policies on the requested services. From 42d659f2aeb7f07731d2815b5068a63fff472504 Mon Sep 17 00:00:00 2001 From: Tornike Gurgenidze Date: Fri, 23 Aug 2024 14:02:43 +0400 Subject: [PATCH 031/185] docs: Reorganize registry docs (#4407) * reorganize registry docs Signed-off-by: tokoko * remove commented out text Signed-off-by: tokoko * changes in registry.md Signed-off-by: tokoko --------- Signed-off-by: tokoko Co-authored-by: tokoko --- docs/SUMMARY.md | 8 +- docs/getting-started/components/registry.md | 52 ++++++--- docs/getting-started/concepts/README.md | 4 - docs/getting-started/concepts/registry.md | 107 ------------------ docs/reference/registries/README.md | 23 ++++ docs/reference/registries/gcs.md | 23 ++++ docs/reference/registries/local.md | 23 ++++ docs/reference/registries/s3.md | 23 ++++ .../{registry => registries}/snowflake.md | 2 +- .../registries/sql.md} | 7 +- 10 files changed, 136 insertions(+), 136 deletions(-) delete mode 100644 docs/getting-started/concepts/registry.md create mode 100644 docs/reference/registries/README.md create mode 100644 docs/reference/registries/gcs.md create mode 100644 docs/reference/registries/local.md create mode 100644 docs/reference/registries/s3.md rename docs/reference/{registry => registries}/snowflake.md (97%) rename docs/{tutorials/using-scalable-registry.md => reference/registries/sql.md} (97%) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index a5d02c4f64..c8e313850f 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -23,7 +23,6 @@ * [Feature view](getting-started/concepts/feature-view.md) * [Feature retrieval](getting-started/concepts/feature-retrieval.md) * [Point-in-time joins](getting-started/concepts/point-in-time-joins.md) - * [Registry](getting-started/concepts/registry.md) * [Permission](getting-started/concepts/permission.md) * [\[Alpha\] Saved dataset](getting-started/concepts/dataset.md) * [Components](getting-started/components/README.md) @@ -45,7 +44,6 @@ * [Real-time credit scoring on AWS](tutorials/tutorials-overview/real-time-credit-scoring-on-aws.md) * [Driver stats on Snowflake](tutorials/tutorials-overview/driver-stats-on-snowflake.md) * [Validating historical features with Great Expectations](tutorials/validating-historical-features.md) -* [Using Scalable Registry](tutorials/using-scalable-registry.md) * [Building streaming features](tutorials/building-streaming-features.md) ## How-to Guides @@ -114,6 +112,12 @@ * [Hazelcast (contrib)](reference/online-stores/hazelcast.md) * [ScyllaDB (contrib)](reference/online-stores/scylladb.md) * [SingleStore (contrib)](reference/online-stores/singlestore.md) +* [Registries](reference/registries/README.md) + * [Local](reference/registries/local.md) + * [S3](reference/registries/s3.md) + * [GCS](reference/registries/gcs.md) + * [SQL](reference/registries/sql.md) + * [Snowflake](reference/registries/snowflake.md) * [Providers](reference/providers/README.md) * [Local](reference/providers/local.md) * [Google Cloud Platform](reference/providers/google-cloud-platform.md) diff --git a/docs/getting-started/components/registry.md b/docs/getting-started/components/registry.md index 0939fb53fc..0c85c5ad36 100644 --- a/docs/getting-started/components/registry.md +++ b/docs/getting-started/components/registry.md @@ -1,31 +1,51 @@ # Registry -The Feast feature registry is a central catalog of all the feature definitions and their related metadata. It allows data scientists to search, discover, and collaborate on new features. +The Feast feature registry is a central catalog of all feature definitions and their related metadata. Feast uses the registry to store all applied Feast objects (e.g. Feature views, entities, etc). It allows data scientists to search, discover, and collaborate on new features. The registry exposes methods to apply, list, retrieve and delete these objects, and is an abstraction with multiple implementations. -Each Feast deployment has a single feature registry. Feast only supports file-based registries today, but supports four different backends. +Feast comes with built-in file-based and sql-based registry implementations. By default, Feast uses a file-based registry, which stores the protobuf representation of the registry as a serialized file in the local file system. For more details on which registries are supported, please see [Registries](../../reference/registries/). -* `Local`: Used as a local backend for storing the registry during development -* `S3`: Used as a centralized backend for storing the registry on AWS -* `GCS`: Used as a centralized backend for storing the registry on GCP -* `[Alpha] Azure`: Used as centralized backend for storing the registry on Azure Blob storage. +## Updating the registry -The feature registry is updated during different operations when using Feast. More specifically, objects within the registry \(entities, feature views, feature services\) are updated when running `apply` from the Feast CLI, but metadata about objects can also be updated during operations like materialization. +We recommend users store their Feast feature definitions in a version controlled repository, which then via CI/CD +automatically stays synced with the registry. Users will often also want multiple registries to correspond to +different environments (e.g. dev vs staging vs prod), with staging and production registries with locked down write +access since they can impact real user traffic. See [Running Feast in Production](../../how-to-guides/running-feast-in-production.md#1.-automatically-deploying-changes-to-your-feature-definitions) for details on how to set this up. -Users interact with a feature registry through the Feast SDK. Listing all feature views: +## Accessing the registry from clients + +Users can specify the registry through a `feature_store.yaml` config file, or programmatically. We often see teams +preferring the programmatic approach because it makes notebook driven development very easy: + +### Option 1: programmatically specifying the registry ```python -fs = FeatureStore("my_feature_repo/") -print(fs.list_feature_views()) +repo_config = RepoConfig( + registry=RegistryConfig(path="gs://feast-test-gcs-bucket/registry.pb"), + project="feast_demo_gcp", + provider="gcp", + offline_store="file", # Could also be the OfflineStoreConfig e.g. FileOfflineStoreConfig + online_store="null", # Could also be the OnlineStoreConfig e.g. RedisOnlineStoreConfig +) +store = FeatureStore(config=repo_config) +``` + +### Option 2: specifying the registry in the project's `feature_store.yaml` file + +```yaml +project: feast_demo_aws +provider: aws +registry: s3://feast-test-s3-bucket/registry.pb +online_store: null +offline_store: + type: file ``` -Or retrieving a specific feature view: +Instantiating a `FeatureStore` object can then point to this: ```python -fs = FeatureStore("my_feature_repo/") -fv = fs.get_feature_view(“my_fv1”) +store = FeatureStore(repo_path=".") ``` {% hint style="info" %} -The feature registry is a [Protobuf representation](https://github.com/feast-dev/feast/blob/master/protos/feast/core/Registry.proto) of Feast metadata. This Protobuf file can be read programmatically from other programming languages, but no compatibility guarantees are made on the internal structure of the registry. -{% endhint %} - +The file-based feature registry is a [Protobuf representation](https://github.com/feast-dev/feast/blob/master/protos/feast/core/Registry.proto) of Feast metadata. This Protobuf file can be read programmatically from other programming languages, but no compatibility guarantees are made on the internal structure of the registry. +{% endhint %} \ No newline at end of file diff --git a/docs/getting-started/concepts/README.md b/docs/getting-started/concepts/README.md index 1769a2d741..9b967fb5af 100644 --- a/docs/getting-started/concepts/README.md +++ b/docs/getting-started/concepts/README.md @@ -24,10 +24,6 @@ [point-in-time-joins.md](point-in-time-joins.md) {% endcontent-ref %} -{% content-ref url="registry.md" %} -[registry.md](registry.md) -{% endcontent-ref %} - {% content-ref url="dataset.md" %} [dataset.md](dataset.md) {% endcontent-ref %} diff --git a/docs/getting-started/concepts/registry.md b/docs/getting-started/concepts/registry.md deleted file mode 100644 index 8ac32ce87b..0000000000 --- a/docs/getting-started/concepts/registry.md +++ /dev/null @@ -1,107 +0,0 @@ -# Registry - -Feast uses a registry to store all applied Feast objects (e.g. Feature views, entities, etc). The registry exposes -methods to apply, list, retrieve and delete these objects, and is an abstraction with multiple implementations. - -### Options for registry implementations - -#### File-based registry -By default, Feast uses a file-based registry implementation, which stores the protobuf representation of the registry as -a serialized file. This registry file can be stored in a local file system, or in cloud storage (in, say, S3 or GCS, or Azure). - -The quickstart guides that use `feast init` will use a registry on a local file system. To allow Feast to configure -a remote file registry, you need to create a GCS / S3 bucket that Feast can understand: -{% tabs %} -{% tab title="Example S3 file registry" %} -```yaml -project: feast_demo_aws -provider: aws -registry: - path: s3://[YOUR BUCKET YOU CREATED]/registry.pb - cache_ttl_seconds: 60 -online_store: null -offline_store: - type: file -``` -{% endtab %} - -{% tab title="Example GCS file registry" %} -```yaml -project: feast_demo_gcp -provider: gcp -registry: - path: gs://[YOUR BUCKET YOU CREATED]/registry.pb - cache_ttl_seconds: 60 -online_store: null -offline_store: - type: file -``` -{% endtab %} -{% endtabs %} - -However, there are inherent limitations with a file-based registry, since changing a single field in the registry -requires re-writing the whole registry file. With multiple concurrent writers, this presents a risk of data loss, or -bottlenecks writes to the registry since all changes have to be serialized (e.g. when running materialization for -multiple feature views or time ranges concurrently). - -#### SQL Registry -Alternatively, a [SQL Registry](../../tutorials/using-scalable-registry.md) can be used for a more scalable registry. - -The configuration roughly looks like: -```yaml -project: -provider: -online_store: redis -offline_store: file -registry: - registry_type: sql - path: postgresql://postgres:mysecretpassword@127.0.0.1:55001/feast - cache_ttl_seconds: 60 - sqlalchemy_config_kwargs: - echo: false - pool_pre_ping: true -``` - -This supports any SQLAlchemy compatible database as a backend. The exact schema can be seen in [sql.py](https://github.com/feast-dev/feast/blob/master/sdk/python/feast/infra/registry/sql.py) - -### Updating the registry - -We recommend users store their Feast feature definitions in a version controlled repository, which then via CI/CD -automatically stays synced with the registry. Users will often also want multiple registries to correspond to -different environments (e.g. dev vs staging vs prod), with staging and production registries with locked down write -access since they can impact real user traffic. See [Running Feast in Production](../../how-to-guides/running-feast-in-production.md#1.-automatically-deploying-changes-to-your-feature-definitions) for details on how to set this up. - -### Accessing the registry from clients - -Users can specify the registry through a `feature_store.yaml` config file, or programmatically. We often see teams -preferring the programmatic approach because it makes notebook driven development very easy: - -#### Option 1: programmatically specifying the registry - -```python -repo_config = RepoConfig( - registry=RegistryConfig(path="gs://feast-test-gcs-bucket/registry.pb"), - project="feast_demo_gcp", - provider="gcp", - offline_store="file", # Could also be the OfflineStoreConfig e.g. FileOfflineStoreConfig - online_store="null", # Could also be the OnlineStoreConfig e.g. RedisOnlineStoreConfig -) -store = FeatureStore(config=repo_config) -``` - -#### Option 2: specifying the registry in the project's `feature_store.yaml` file - -```yaml -project: feast_demo_aws -provider: aws -registry: s3://feast-test-s3-bucket/registry.pb -online_store: null -offline_store: - type: file -``` - -Instantiating a `FeatureStore` object can then point to this: - -```python -store = FeatureStore(repo_path=".") -``` \ No newline at end of file diff --git a/docs/reference/registries/README.md b/docs/reference/registries/README.md new file mode 100644 index 0000000000..1310506f1d --- /dev/null +++ b/docs/reference/registries/README.md @@ -0,0 +1,23 @@ +# Registies + +Please see [Registry](../../getting-started/architecture-and-components/registry.md) for a conceptual explanation of registries. + +{% content-ref url="local.md" %} +[local.md](local.md) +{% endcontent-ref %} + +{% content-ref url="s3.md" %} +[s3.md](s3.md) +{% endcontent-ref %} + +{% content-ref url="gcs.md" %} +[gcs.md](gcs.md) +{% endcontent-ref %} + +{% content-ref url="sql.md" %} +[sql.md](sql.md) +{% endcontent-ref %} + +{% content-ref url="snowflake.md" %} +[snowflake.md](snowflake.md) +{% endcontent-ref %} diff --git a/docs/reference/registries/gcs.md b/docs/reference/registries/gcs.md new file mode 100644 index 0000000000..13c9657aa1 --- /dev/null +++ b/docs/reference/registries/gcs.md @@ -0,0 +1,23 @@ +# GCS Registry + +## Description + +GCS registry provides support for storing the protobuf representation of your feature store objects (data sources, feature views, feature services, etc.) uing Google Cloud Storage. + +While it can be used in production, there are still inherent limitations with a file-based registries, since changing a single field in the registry requires re-writing the whole registry file. With multiple concurrent writers, this presents a risk of data loss, or bottlenecks writes to the registry since all changes have to be serialized (e.g. when running materialization for multiple feature views or time ranges concurrently). + +An example of how to configure this would be: + +## Example + +{% code title="feature_store.yaml" %} +```yaml +project: feast_gcp +registry: + path: gs://[YOUR BUCKET YOU CREATED]/registry.pb + cache_ttl_seconds: 60 +online_store: null +offline_store: + type: dask +``` +{% endcode %} \ No newline at end of file diff --git a/docs/reference/registries/local.md b/docs/reference/registries/local.md new file mode 100644 index 0000000000..ad1d98cea9 --- /dev/null +++ b/docs/reference/registries/local.md @@ -0,0 +1,23 @@ +# Local Registry + +## Description + +Local registry provides support for storing the protobuf representation of your feature store objects (data sources, feature views, feature services, etc.) in local file system. It is only intended to be used for experimentation with Feast and should not be used in production. + +There are inherent limitations with a file-based registries, since changing a single field in the registry requires re-writing the whole registry file. With multiple concurrent writers, this presents a risk of data loss, or bottlenecks writes to the registry since all changes have to be serialized (e.g. when running materialization for multiple feature views or time ranges concurrently). + +An example of how to configure this would be: + +## Example + +{% code title="feature_store.yaml" %} +```yaml +project: feast_local +registry: + path: registry.pb + cache_ttl_seconds: 60 +online_store: null +offline_store: + type: dask +``` +{% endcode %} \ No newline at end of file diff --git a/docs/reference/registries/s3.md b/docs/reference/registries/s3.md new file mode 100644 index 0000000000..65069c415c --- /dev/null +++ b/docs/reference/registries/s3.md @@ -0,0 +1,23 @@ +# S3 Registry + +## Description + +S3 registry provides support for storing the protobuf representation of your feature store objects (data sources, feature views, feature services, etc.) in S3 file system. + +While it can be used in production, there are still inherent limitations with a file-based registries, since changing a single field in the registry requires re-writing the whole registry file. With multiple concurrent writers, this presents a risk of data loss, or bottlenecks writes to the registry since all changes have to be serialized (e.g. when running materialization for multiple feature views or time ranges concurrently). + +An example of how to configure this would be: + +## Example + +{% code title="feature_store.yaml" %} +```yaml +project: feast_aws_s3 +registry: + path: s3://[YOUR BUCKET YOU CREATED]/registry.pb + cache_ttl_seconds: 60 +online_store: null +offline_store: + type: dask +``` +{% endcode %} \ No newline at end of file diff --git a/docs/reference/registry/snowflake.md b/docs/reference/registries/snowflake.md similarity index 97% rename from docs/reference/registry/snowflake.md rename to docs/reference/registries/snowflake.md index 31b0db9582..00d87b1977 100644 --- a/docs/reference/registry/snowflake.md +++ b/docs/reference/registries/snowflake.md @@ -1,4 +1,4 @@ -# Snowflake registry +# Snowflake Registry ## Description diff --git a/docs/tutorials/using-scalable-registry.md b/docs/reference/registries/sql.md similarity index 97% rename from docs/tutorials/using-scalable-registry.md rename to docs/reference/registries/sql.md index 25746f60e2..631a20cbe3 100644 --- a/docs/tutorials/using-scalable-registry.md +++ b/docs/reference/registries/sql.md @@ -1,9 +1,4 @@ ---- -description: >- - Tutorial on how to use the SQL registry for scalable registry updates ---- - -# Using Scalable Registry +# SQL Registry ## Overview From 7d744ad3aa4a0057f136aa5bc2a8b416ab827398 Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Fri, 23 Aug 2024 12:14:49 -0400 Subject: [PATCH 032/185] chore: Update Slack link --- infra/templates/README.md.jinja2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/templates/README.md.jinja2 b/infra/templates/README.md.jinja2 index 4e71ac2900..9c7df17da9 100644 --- a/infra/templates/README.md.jinja2 +++ b/infra/templates/README.md.jinja2 @@ -15,7 +15,7 @@ [![GitHub Release](https://img.shields.io/github/v/release/feast-dev/feast.svg?style=flat&sort=semver&color=blue)](https://github.com/feast-dev/feast/releases) ## Join us on Slack! -👋👋👋 [Come say hi on Slack!](https://join.slack.com/t/feastopensource/signup) +👋👋👋 [Come say hi on Slack!](https://communityinviter.com/apps/feastopensource/feast-the-open-source-feature-store) ## Overview From a2460d9e65a406cc3641552e0c2f497eeb2fc735 Mon Sep 17 00:00:00 2001 From: "Yang, Bo" Date: Fri, 23 Aug 2024 09:23:34 -0700 Subject: [PATCH 033/185] build: Set a proper build-system protobuf version (#4438) build: force the protobuf version in the build system so that it is compatible with the runtime dependency Signed-off-by: Yang, Bo --- pyproject.toml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 00170ab443..af44861502 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,14 @@ [build-system] -requires = ["setuptools>=60", "wheel", "setuptools_scm>=6.2", "grpcio", "grpcio-tools>=1.47.0", "mypy-protobuf==3.1", "sphinx!=4.0.0"] +requires = [ + "setuptools>=60", + "wheel", + "setuptools_scm>=6.2", + "grpcio", + "grpcio-tools>=1.47.0", + "mypy-protobuf==3.1", + "protobuf>=4.24.0,<5.0.0", + "sphinx!=4.0.0", +] build-backend = "setuptools.build_meta" [tool.setuptools_scm] From 2ba93f650e975f3b02c60dda7c8f5f2407852d74 Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Fri, 23 Aug 2024 12:32:47 -0400 Subject: [PATCH 034/185] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ede28c4c95..10c20050d3 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ [![GitHub Release](https://img.shields.io/github/v/release/feast-dev/feast.svg?style=flat&sort=semver&color=blue)](https://github.com/feast-dev/feast/releases) ## Join us on Slack! -👋👋👋 [Come say hi on Slack!](https://join.slack.com/t/feastopensource/signup) +👋👋👋 [Come say hi on Slack!](https://communityinviter.com/apps/feastopensource/feast-the-open-source-feature-store) ## Overview @@ -230,4 +230,4 @@ Thanks goes to these incredible people:
- \ No newline at end of file + From dda0088f25eab5828613bd6d080aeddf681641f0 Mon Sep 17 00:00:00 2001 From: brijesh-vora-sp <137945907+brijesh-vora-sp@users.noreply.github.com> Date: Sat, 24 Aug 2024 03:48:08 -0500 Subject: [PATCH 035/185] fix: Typos related to k8s (#4442) fix typos Signed-off-by: Brijesh Vora --- sdk/python/feast/infra/materialization/kubernetes/Dockerfile | 2 +- sdk/python/feast/repo_config.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/python/feast/infra/materialization/kubernetes/Dockerfile b/sdk/python/feast/infra/materialization/kubernetes/Dockerfile index 510bb72285..38d4f5f188 100644 --- a/sdk/python/feast/infra/materialization/kubernetes/Dockerfile +++ b/sdk/python/feast/infra/materialization/kubernetes/Dockerfile @@ -5,7 +5,7 @@ RUN apt-get update && \ WORKDIR /app -COPY sdk/python/feast/infra/materialization/kuberentes/main.py /app +COPY sdk/python/feast/infra/materialization/kubernetes/main.py /app # Copy necessary parts of the Feast codebase COPY sdk/python sdk/python diff --git a/sdk/python/feast/repo_config.py b/sdk/python/feast/repo_config.py index 069b579999..a270e6795c 100644 --- a/sdk/python/feast/repo_config.py +++ b/sdk/python/feast/repo_config.py @@ -46,7 +46,7 @@ "local": "feast.infra.materialization.local_engine.LocalMaterializationEngine", "snowflake.engine": "feast.infra.materialization.snowflake_engine.SnowflakeMaterializationEngine", "lambda": "feast.infra.materialization.aws_lambda.lambda_engine.LambdaMaterializationEngine", - "k8s": "feast.infra.materialization.kubernetes.kubernetes_materialization_engine.KubernetesMaterializationEngine", + "k8s": "feast.infra.materialization.kubernetes.k8s_materialization_engine.KubernetesMaterializationEngine", "spark.engine": "feast.infra.materialization.contrib.spark.spark_materialization_engine.SparkMaterializationEngine", } From 896360af19a37c9a2a4634ec88021c4f69bdb141 Mon Sep 17 00:00:00 2001 From: lokeshrangineni <19699092+lokeshrangineni@users.noreply.github.com> Date: Sat, 24 Aug 2024 05:16:46 -0400 Subject: [PATCH 036/185] feat: Refactoring code to get oidc end points from discovery URL. (#4429) * refactoring the permissions side server side code to get the OIDC end points from the discovery URL. Also removing the auth_server_url config from oidc auth config. Signed-off-by: Lokesh Rangineni <19699092+lokeshrangineni@users.noreply.github.com> * refactoring the permissions side server side code to get the OIDC end points from the discovery URL. Also removing the auth_server_url config from oidc auth config. Signed-off-by: Lokesh Rangineni <19699092+lokeshrangineni@users.noreply.github.com> * refactoring the permissions side server side code to get the OIDC end points from the discovery URL. Also removing the auth_server_url config from oidc auth config. Signed-off-by: Lokesh Rangineni <19699092+lokeshrangineni@users.noreply.github.com> * refactoring the permissions side server side code to get the OIDC end points from the discovery URL. Also removing the auth_server_url config from oidc auth config. Signed-off-by: Lokesh Rangineni <19699092+lokeshrangineni@users.noreply.github.com> * Fixing the issue with pre-commit hook template. Accidentally this was reverted in previous rebase and reverting it now. Signed-off-by: Lokesh Rangineni <19699092+lokeshrangineni@users.noreply.github.com> --------- Signed-off-by: Lokesh Rangineni <19699092+lokeshrangineni@users.noreply.github.com> --- .../components/authz_manager.md | 3 +- .../permissions/auth/oidc_token_parser.py | 15 ++++--- sdk/python/feast/permissions/auth_model.py | 1 - .../oidc_authentication_client_manager.py | 21 ++-------- sdk/python/feast/permissions/oidc_service.py | 40 +++++++++++++++++++ sdk/python/tests/conftest.py | 1 - .../feature_repos/repo_configuration.py | 1 - .../universal/data_sources/file.py | 1 - .../infra/scaffolding/test_repo_config.py | 4 -- .../tests/unit/permissions/auth/conftest.py | 3 +- .../permissions/auth/server/mock_utils.py | 9 +++++ .../permissions/auth/test_token_parser.py | 9 ++++- 12 files changed, 73 insertions(+), 35 deletions(-) create mode 100644 sdk/python/feast/permissions/oidc_service.py diff --git a/docs/getting-started/components/authz_manager.md b/docs/getting-started/components/authz_manager.md index 09ca4d1366..876dd84f2e 100644 --- a/docs/getting-started/components/authz_manager.md +++ b/docs/getting-started/components/authz_manager.md @@ -68,8 +68,7 @@ auth: type: oidc client_id: _CLIENT_ID__ client_secret: _CLIENT_SECRET__ - realm: _REALM__ - auth_server_url: _OIDC_SERVER_URL_ + realm: _REALM__ auth_discovery_url: _OIDC_SERVER_URL_/realms/master/.well-known/openid-configuration ... ``` diff --git a/sdk/python/feast/permissions/auth/oidc_token_parser.py b/sdk/python/feast/permissions/auth/oidc_token_parser.py index 921a585bc2..fce9fdcbb2 100644 --- a/sdk/python/feast/permissions/auth/oidc_token_parser.py +++ b/sdk/python/feast/permissions/auth/oidc_token_parser.py @@ -11,6 +11,7 @@ from feast.permissions.auth.token_parser import TokenParser from feast.permissions.auth_model import OidcAuthConfig +from feast.permissions.oidc_service import OIDCDiscoveryService from feast.permissions.user import User logger = logging.getLogger(__name__) @@ -27,6 +28,9 @@ class OidcTokenParser(TokenParser): def __init__(self, auth_config: OidcAuthConfig): self._auth_config = auth_config + self.oidc_discovery_service = OIDCDiscoveryService( + self._auth_config.auth_discovery_url + ) async def _validate_token(self, access_token: str): """ @@ -38,9 +42,9 @@ async def _validate_token(self, access_token: str): request.headers = {"Authorization": f"Bearer {access_token}"} oauth_2_scheme = OAuth2AuthorizationCodeBearer( - tokenUrl=f"{self._auth_config.auth_server_url}/realms/{self._auth_config.realm}/protocol/openid-connect/token", - authorizationUrl=f"{self._auth_config.auth_server_url}/realms/{self._auth_config.realm}/protocol/openid-connect/auth", - refreshUrl=f"{self._auth_config.auth_server_url}/realms/{self._auth_config.realm}/protocol/openid-connect/token", + tokenUrl=self.oidc_discovery_service.get_token_url(), + authorizationUrl=self.oidc_discovery_service.get_authorization_url(), + refreshUrl=self.oidc_discovery_service.get_refresh_url(), ) await oauth_2_scheme(request=request) @@ -62,9 +66,10 @@ async def user_details_from_access_token(self, access_token: str) -> User: except Exception as e: raise AuthenticationError(f"Invalid token: {e}") - url = f"{self._auth_config.auth_server_url}/realms/{self._auth_config.realm}/protocol/openid-connect/certs" optional_custom_headers = {"User-agent": "custom-user-agent"} - jwks_client = PyJWKClient(url, headers=optional_custom_headers) + jwks_client = PyJWKClient( + self.oidc_discovery_service.get_jwks_url(), headers=optional_custom_headers + ) try: signing_key = jwks_client.get_signing_key_from_jwt(access_token) diff --git a/sdk/python/feast/permissions/auth_model.py b/sdk/python/feast/permissions/auth_model.py index afb0a22bc9..28eeb951a7 100644 --- a/sdk/python/feast/permissions/auth_model.py +++ b/sdk/python/feast/permissions/auth_model.py @@ -8,7 +8,6 @@ class AuthConfig(FeastConfigBaseModel): class OidcAuthConfig(AuthConfig): - auth_server_url: Optional[str] = None auth_discovery_url: str client_id: str client_secret: Optional[str] = None diff --git a/sdk/python/feast/permissions/client/oidc_authentication_client_manager.py b/sdk/python/feast/permissions/client/oidc_authentication_client_manager.py index 544764aae0..6744a1d2ad 100644 --- a/sdk/python/feast/permissions/client/oidc_authentication_client_manager.py +++ b/sdk/python/feast/permissions/client/oidc_authentication_client_manager.py @@ -4,6 +4,7 @@ from feast.permissions.auth_model import OidcAuthConfig from feast.permissions.client.auth_client_manager import AuthenticationClientManager +from feast.permissions.oidc_service import OIDCDiscoveryService logger = logging.getLogger(__name__) @@ -12,25 +13,11 @@ class OidcAuthClientManager(AuthenticationClientManager): def __init__(self, auth_config: OidcAuthConfig): self.auth_config = auth_config - def _get_token_endpoint(self): - response = requests.get(self.auth_config.auth_discovery_url) - if response.status_code == 200: - oidc_config = response.json() - if not oidc_config["token_endpoint"]: - raise RuntimeError( - " OIDC token_endpoint is not available from discovery url response." - ) - return oidc_config["token_endpoint"].replace( - "master", self.auth_config.realm - ) - else: - raise RuntimeError( - f"Error fetching OIDC token endpoint configuration: {response.status_code} - {response.text}" - ) - def get_token(self): # Fetch the token endpoint from the discovery URL - token_endpoint = self._get_token_endpoint() + token_endpoint = OIDCDiscoveryService( + self.auth_config.auth_discovery_url + ).get_token_url() token_request_body = { "grant_type": "password", diff --git a/sdk/python/feast/permissions/oidc_service.py b/sdk/python/feast/permissions/oidc_service.py new file mode 100644 index 0000000000..73d0ec8f1b --- /dev/null +++ b/sdk/python/feast/permissions/oidc_service.py @@ -0,0 +1,40 @@ +import requests + + +class OIDCDiscoveryService: + def __init__(self, discovery_url: str): + self.discovery_url = discovery_url + self._discovery_data = None # Initialize it lazily. + + @property + def discovery_data(self): + """Lazily fetches and caches the OIDC discovery data.""" + if self._discovery_data is None: + self._discovery_data = self._fetch_discovery_data() + return self._discovery_data + + def _fetch_discovery_data(self) -> dict: + try: + response = requests.get(self.discovery_url) + response.raise_for_status() + return response.json() + except requests.RequestException as e: + raise RuntimeError( + f"Error fetching OIDC discovery response, discovery url - {self.discovery_url}, exception - {e} " + ) + + def get_authorization_url(self) -> str: + """Returns the authorization endpoint URL.""" + return self.discovery_data.get("authorization_endpoint") + + def get_token_url(self) -> str: + """Returns the token endpoint URL.""" + return self.discovery_data.get("token_endpoint") + + def get_jwks_url(self) -> str: + """Returns the jwks endpoint URL.""" + return self.discovery_data.get("jwks_uri") + + def get_refresh_url(self) -> str: + """Returns the refresh token URL (usually same as token URL).""" + return self.get_token_url() diff --git a/sdk/python/tests/conftest.py b/sdk/python/tests/conftest.py index 74aa68e984..d40f699b6b 100644 --- a/sdk/python/tests/conftest.py +++ b/sdk/python/tests/conftest.py @@ -463,7 +463,6 @@ def is_integration_test(all_markers_from_module): username: reader_writer password: password realm: master - auth_server_url: KEYCLOAK_URL_PLACE_HOLDER auth_discovery_url: KEYCLOAK_URL_PLACE_HOLDER/realms/master/.well-known/openid-configuration """), ], diff --git a/sdk/python/tests/integration/feature_repos/repo_configuration.py b/sdk/python/tests/integration/feature_repos/repo_configuration.py index 235c909d5f..0bf737f616 100644 --- a/sdk/python/tests/integration/feature_repos/repo_configuration.py +++ b/sdk/python/tests/integration/feature_repos/repo_configuration.py @@ -464,7 +464,6 @@ def setup(self): password="password", realm="master", type="oidc", - auth_server_url=keycloak_url, auth_discovery_url=f"{keycloak_url}/realms/master/.well-known" f"/openid-configuration", ) diff --git a/sdk/python/tests/integration/feature_repos/universal/data_sources/file.py b/sdk/python/tests/integration/feature_repos/universal/data_sources/file.py index b600699f81..adbb248a20 100644 --- a/sdk/python/tests/integration/feature_repos/universal/data_sources/file.py +++ b/sdk/python/tests/integration/feature_repos/universal/data_sources/file.py @@ -449,7 +449,6 @@ def __init__(self, project_name: str, *args, **kwargs): username: reader_writer password: password realm: master - auth_server_url: {keycloak_url} auth_discovery_url: {keycloak_url}/realms/master/.well-known/openid-configuration """ self.auth_config = auth_config_template.format(keycloak_url=self.keycloak_url) diff --git a/sdk/python/tests/unit/infra/scaffolding/test_repo_config.py b/sdk/python/tests/unit/infra/scaffolding/test_repo_config.py index 0725d6d261..5331d350e2 100644 --- a/sdk/python/tests/unit/infra/scaffolding/test_repo_config.py +++ b/sdk/python/tests/unit/infra/scaffolding/test_repo_config.py @@ -214,7 +214,6 @@ def test_auth_config(): username: test_user_name password: test_password realm: master - auth_server_url: http://localhost:8712 auth_discovery_url: http://localhost:8080/realms/master/.well-known/openid-configuration registry: "registry.db" provider: local @@ -237,7 +236,6 @@ def test_auth_config(): username: test_user_name password: test_password realm: master - auth_server_url: http://localhost:8712 auth_discovery_url: http://localhost:8080/realms/master/.well-known/openid-configuration registry: "registry.db" provider: local @@ -260,7 +258,6 @@ def test_auth_config(): username: test_user_name password: test_password realm: master - auth_server_url: http://localhost:8080 auth_discovery_url: http://localhost:8080/realms/master/.well-known/openid-configuration registry: "registry.db" provider: local @@ -278,7 +275,6 @@ def test_auth_config(): assert oidc_repo_config.auth_config.username == "test_user_name" assert oidc_repo_config.auth_config.password == "test_password" assert oidc_repo_config.auth_config.realm == "master" - assert oidc_repo_config.auth_config.auth_server_url == "http://localhost:8080" assert ( oidc_repo_config.auth_config.auth_discovery_url == "http://localhost:8080/realms/master/.well-known/openid-configuration" diff --git a/sdk/python/tests/unit/permissions/auth/conftest.py b/sdk/python/tests/unit/permissions/auth/conftest.py index dc71aba23b..0d6acd7fb2 100644 --- a/sdk/python/tests/unit/permissions/auth/conftest.py +++ b/sdk/python/tests/unit/permissions/auth/conftest.py @@ -73,8 +73,7 @@ def clusterrolebindings(sa_name, namespace) -> dict: @pytest.fixture def oidc_config() -> OidcAuthConfig: return OidcAuthConfig( - auth_server_url="", - auth_discovery_url="", + auth_discovery_url="https://localhost:8080/realms/master/.well-known/openid-configuration", client_id=_CLIENT_ID, client_secret="", username="", diff --git a/sdk/python/tests/unit/permissions/auth/server/mock_utils.py b/sdk/python/tests/unit/permissions/auth/server/mock_utils.py index 8f598774ee..12f7785b05 100644 --- a/sdk/python/tests/unit/permissions/auth/server/mock_utils.py +++ b/sdk/python/tests/unit/permissions/auth/server/mock_utils.py @@ -42,6 +42,15 @@ async def mock_oath2(self, request): lambda url, data, headers: token_response, ) + monkeypatch.setattr( + "feast.permissions.oidc_service.OIDCDiscoveryService._fetch_discovery_data", + lambda self, *args, **kwargs: { + "authorization_endpoint": "https://localhost:8080/realms/master/protocol/openid-connect/auth", + "token_endpoint": "https://localhost:8080/realms/master/protocol/openid-connect/token", + "jwks_uri": "https://localhost:8080/realms/master/protocol/openid-connect/certs", + }, + ) + def mock_kubernetes(request, monkeypatch): sa_name = request.getfixturevalue("sa_name") diff --git a/sdk/python/tests/unit/permissions/auth/test_token_parser.py b/sdk/python/tests/unit/permissions/auth/test_token_parser.py index 6ae9094f81..cb153a17c9 100644 --- a/sdk/python/tests/unit/permissions/auth/test_token_parser.py +++ b/sdk/python/tests/unit/permissions/auth/test_token_parser.py @@ -21,13 +21,20 @@ ) @patch("feast.permissions.auth.oidc_token_parser.PyJWKClient.get_signing_key_from_jwt") @patch("feast.permissions.auth.oidc_token_parser.jwt.decode") +@patch("feast.permissions.oidc_service.OIDCDiscoveryService._fetch_discovery_data") def test_oidc_token_validation_success( - mock_jwt, mock_signing_key, mock_oauth2, oidc_config + mock_discovery_data, mock_jwt, mock_signing_key, mock_oauth2, oidc_config ): signing_key = MagicMock() signing_key.key = "a-key" mock_signing_key.return_value = signing_key + mock_discovery_data.return_value = { + "authorization_endpoint": "https://localhost:8080/realms/master/protocol/openid-connect/auth", + "token_endpoint": "https://localhost:8080/realms/master/protocol/openid-connect/token", + "jwks_uri": "https://localhost:8080/realms/master/protocol/openid-connect/certs", + } + user_data = { "preferred_username": "my-name", "resource_access": {_CLIENT_ID: {"roles": ["reader", "writer"]}}, From 19cf2222214d5d5b7db766e833720d7706caeb30 Mon Sep 17 00:00:00 2001 From: Tornike Gurgenidze Date: Sun, 25 Aug 2024 10:23:47 +0400 Subject: [PATCH 037/185] chore: Mark tests using keycloak with xdist_group (#4436) * mark keycloak tests with xdist_group Signed-off-by: tokoko * apply changes to test-python-integration Signed-off-by: tokoko --------- Signed-off-by: tokoko Co-authored-by: tokoko --- Makefile | 4 ++-- sdk/python/tests/conftest.py | 6 +++++- .../feature_repos/universal/data_source_creator.py | 3 +++ .../feature_repos/universal/data_sources/file.py | 3 +++ 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 7851c1f1c5..6ebab4e3be 100644 --- a/Makefile +++ b/Makefile @@ -86,14 +86,14 @@ test-python-unit: python -m pytest -n 8 --color=yes sdk/python/tests test-python-integration: - python -m pytest -n 4 --integration --color=yes --durations=10 --timeout=1200 --timeout_method=thread \ + python -m pytest -n 8 --integration --color=yes --durations=10 --timeout=1200 --timeout_method=thread --dist loadgroup \ -k "(not snowflake or not test_historical_features_main)" \ sdk/python/tests test-python-integration-local: FEAST_IS_LOCAL_TEST=True \ FEAST_LOCAL_ONLINE_CONTAINER=True \ - python -m pytest -n 4 --color=yes --integration --durations=10 --timeout=1200 --timeout_method=thread --dist loadgroup \ + python -m pytest -n 8 --color=yes --integration --durations=10 --timeout=1200 --timeout_method=thread --dist loadgroup \ -k "not test_lambda_materialization and not test_snowflake_materialization" \ sdk/python/tests diff --git a/sdk/python/tests/conftest.py b/sdk/python/tests/conftest.py index d40f699b6b..b5b3e2d9e5 100644 --- a/sdk/python/tests/conftest.py +++ b/sdk/python/tests/conftest.py @@ -279,7 +279,11 @@ def pytest_generate_tests(metafunc: pytest.Metafunc): c = IntegrationTestRepoConfig(**config) if c not in _config_cache: - _config_cache[c] = c + marks = [ + pytest.mark.xdist_group(name=m) + for m in c.offline_store_creator.xdist_groups() + ] + _config_cache[c] = pytest.param(c, marks=marks) configs.append(_config_cache[c]) else: diff --git a/sdk/python/tests/integration/feature_repos/universal/data_source_creator.py b/sdk/python/tests/integration/feature_repos/universal/data_source_creator.py index f1cab21429..aa46160358 100644 --- a/sdk/python/tests/integration/feature_repos/universal/data_source_creator.py +++ b/sdk/python/tests/integration/feature_repos/universal/data_source_creator.py @@ -60,3 +60,6 @@ def create_logged_features_destination(self) -> LoggingDestination: @abstractmethod def teardown(self): raise NotImplementedError + + def xdist_groups() -> list[str]: + return [] diff --git a/sdk/python/tests/integration/feature_repos/universal/data_sources/file.py b/sdk/python/tests/integration/feature_repos/universal/data_sources/file.py index adbb248a20..10d348c056 100644 --- a/sdk/python/tests/integration/feature_repos/universal/data_sources/file.py +++ b/sdk/python/tests/integration/feature_repos/universal/data_sources/file.py @@ -455,6 +455,9 @@ def __init__(self, project_name: str, *args, **kwargs): self.server_port: int = 0 self.proc = None + def xdist_groups() -> list[str]: + return ["keycloak"] + def setup(self, registry: RegistryConfig): parent_offline_config = super().create_offline_store_config() config = RepoConfig( From 20290ce28c513f705db1dbb6b0f719ba1846217f Mon Sep 17 00:00:00 2001 From: "Yang, Bo" Date: Sun, 25 Aug 2024 06:58:44 -0700 Subject: [PATCH 038/185] fix: Locate feature_store.yaml from __file__ (#4443) fix: locate feature_store.yaml from __file__ Signed-off-by: Yang, Bo --- .../feast/templates/postgres/feature_repo/test_workflow.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sdk/python/feast/templates/postgres/feature_repo/test_workflow.py b/sdk/python/feast/templates/postgres/feature_repo/test_workflow.py index f657aba15f..30927d3c7a 100644 --- a/sdk/python/feast/templates/postgres/feature_repo/test_workflow.py +++ b/sdk/python/feast/templates/postgres/feature_repo/test_workflow.py @@ -1,3 +1,4 @@ +import os.path import subprocess from datetime import datetime @@ -8,7 +9,7 @@ def run_demo(): - store = FeatureStore(repo_path=".") + store = FeatureStore(repo_path=os.path.dirname(__file__)) print("\n--- Run feast apply to setup feature store on Postgres ---") subprocess.run(["feast", "apply"]) From 34238d2a0bfe9dbad753fec9613c83d848b1a520 Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Sun, 25 Aug 2024 10:19:54 -0400 Subject: [PATCH 039/185] feat: Update roadmap.md (#4445) --- docs/roadmap.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/roadmap.md b/docs/roadmap.md index e1ba6f3333..ff6549a3cb 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -42,6 +42,7 @@ The list below contains the functionality that contributors are planning to deve * [x] On-demand Transformations (Beta release. See [RFC](https://docs.google.com/document/d/1lgfIw0Drc65LpaxbUu49RCeJgMew547meSJttnUqz7c/edit#)) * [x] Streaming Transformations (Alpha release. See [RFC](https://docs.google.com/document/d/1UzEyETHUaGpn0ap4G82DHluiCj7zEbrQLkJJkKSv4e8/edit)) * [ ] Batch transformation (In progress. See [RFC](https://docs.google.com/document/d/1964OkzuBljifDvkV-0fakp2uaijnVzdwWNGdz7Vz50A/edit)) + * [ ] Persistent On-demand Transformations (Beta release. See [GitHub Issue](https://github.com/feast-dev/feast/issues/4376)) * **Streaming** * [x] [Custom streaming ingestion job support](https://docs.feast.dev/how-to-guides/customizing-feast/creating-a-custom-provider) * [x] [Push based streaming data ingestion to online store](https://docs.feast.dev/reference/data-sources/push) @@ -63,3 +64,6 @@ The list below contains the functionality that contributors are planning to deve * [x] Amundsen integration (see [Feast extractor](https://github.com/amundsen-io/amundsen/blob/main/databuilder/databuilder/extractor/feast_extractor.py)) * [x] DataHub integration (see [DataHub Feast docs](https://datahubproject.io/docs/generated/ingestion/sources/feast/)) * [x] Feast Web UI (Beta release. See [docs](https://docs.feast.dev/reference/alpha-web-ui)) + * [ ] Feast Lineage Explorer +* **Natural Language Processing** + * [x] Vector Search (Alpha release. See [RFC](https://docs.google.com/document/d/18IWzLEA9i2lDWnbfbwXnMCg3StlqaLVI-uRpQjr_Vos/edit#heading=h.9gaqqtox9jg6)) From 55a61e8de4b6240670588c7e212fce2fd473f2ce Mon Sep 17 00:00:00 2001 From: Tornike Gurgenidze Date: Mon, 26 Aug 2024 14:30:39 +0400 Subject: [PATCH 040/185] chore: Remove Rockset from feast (#4434) --- docs/SUMMARY.md | 1 - docs/reference/online-stores/README.md | 4 - docs/reference/online-stores/rockset.md | 84 --- ...feast.infra.feature_servers.aws_lambda.rst | 29 - ...ast.infra.feature_servers.gcp_cloudrun.rst | 29 - ....infra.materialization.contrib.bytewax.rst | 29 - .../feast.infra.materialization.lambda.rst | 29 - ...ne_stores.contrib.rockset_online_store.rst | 21 - .../feast.infra.online_stores.contrib.rst | 1 - .../feast.infra.registry.contrib.postgres.rst | 21 - .../source/feast.infra.registry_stores.rst | 21 - .../docs/source/feast.permissions.auth.rst | 61 ++ .../docs/source/feast.permissions.client.rst | 69 +++ sdk/python/docs/source/feast.permissions.rst | 103 ++++ .../docs/source/feast.permissions.server.rst | 69 +++ .../docs/source/feast.protos.feast.core.rst | 32 ++ sdk/python/docs/source/feast.rst | 16 +- sdk/python/feast/cli.py | 1 - .../contrib/rockset_online_store/__init__.py | 0 .../contrib/rockset_online_store/rockset.py | 520 ------------------ sdk/python/feast/repo_config.py | 1 - sdk/python/feast/templates/rockset/README.md | 21 - .../feast/templates/rockset/__init__.py | 0 .../feast/templates/rockset/bootstrap.py | 30 - .../rockset/feature_repo/feature_store.yaml | 8 - .../requirements/py3.10-ci-requirements.txt | 162 +----- .../requirements/py3.10-requirements.txt | 89 +-- .../requirements/py3.11-ci-requirements.txt | 157 +----- .../requirements/py3.11-requirements.txt | 89 +-- .../requirements/py3.9-ci-requirements.txt | 161 +----- .../requirements/py3.9-requirements.txt | 89 +-- .../feature_repos/repo_configuration.py | 10 - setup.py | 6 - 33 files changed, 532 insertions(+), 1431 deletions(-) delete mode 100644 docs/reference/online-stores/rockset.md delete mode 100644 sdk/python/docs/source/feast.infra.feature_servers.aws_lambda.rst delete mode 100644 sdk/python/docs/source/feast.infra.feature_servers.gcp_cloudrun.rst delete mode 100644 sdk/python/docs/source/feast.infra.materialization.contrib.bytewax.rst delete mode 100644 sdk/python/docs/source/feast.infra.materialization.lambda.rst delete mode 100644 sdk/python/docs/source/feast.infra.online_stores.contrib.rockset_online_store.rst delete mode 100644 sdk/python/docs/source/feast.infra.registry.contrib.postgres.rst delete mode 100644 sdk/python/docs/source/feast.infra.registry_stores.rst create mode 100644 sdk/python/docs/source/feast.permissions.auth.rst create mode 100644 sdk/python/docs/source/feast.permissions.client.rst create mode 100644 sdk/python/docs/source/feast.permissions.rst create mode 100644 sdk/python/docs/source/feast.permissions.server.rst delete mode 100644 sdk/python/feast/infra/online_stores/contrib/rockset_online_store/__init__.py delete mode 100644 sdk/python/feast/infra/online_stores/contrib/rockset_online_store/rockset.py delete mode 100644 sdk/python/feast/templates/rockset/README.md delete mode 100644 sdk/python/feast/templates/rockset/__init__.py delete mode 100644 sdk/python/feast/templates/rockset/bootstrap.py delete mode 100644 sdk/python/feast/templates/rockset/feature_repo/feature_store.yaml diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index c8e313850f..1c4cece799 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -108,7 +108,6 @@ * [PostgreSQL (contrib)](reference/online-stores/postgres.md) * [Cassandra + Astra DB (contrib)](reference/online-stores/cassandra.md) * [MySQL (contrib)](reference/online-stores/mysql.md) - * [Rockset (contrib)](reference/online-stores/rockset.md) * [Hazelcast (contrib)](reference/online-stores/hazelcast.md) * [ScyllaDB (contrib)](reference/online-stores/scylladb.md) * [SingleStore (contrib)](reference/online-stores/singlestore.md) diff --git a/docs/reference/online-stores/README.md b/docs/reference/online-stores/README.md index bf5419b249..cdb9c37c1d 100644 --- a/docs/reference/online-stores/README.md +++ b/docs/reference/online-stores/README.md @@ -50,10 +50,6 @@ Please see [Online Store](../../getting-started/components/online-store.md) for [mysql.md](mysql.md) {% endcontent-ref %} -{% content-ref url="rockset.md" %} -[rockset.md](rockset.md) -{% endcontent-ref %} - {% content-ref url="hazelcast.md" %} [hazelcast.md](hazelcast.md) {% endcontent-ref %} diff --git a/docs/reference/online-stores/rockset.md b/docs/reference/online-stores/rockset.md deleted file mode 100644 index 082bddf37b..0000000000 --- a/docs/reference/online-stores/rockset.md +++ /dev/null @@ -1,84 +0,0 @@ -# Rockset (contrib) - -## Description - -In Alpha Development. - -The [Rockset](https://rockset.com/demo-signup/) online store provides support for materializing feature values within a Rockset collection in order to serve features in real-time. - -* Each document is uniquely identified by its '_id' value. Repeated inserts into the same document '_id' will result in an upsert. - -Rockset indexes all columns allowing for quick per feature look up and also allows for a dynamic typed schema that can change based on any new requirements. API Keys can be found in the Rockset console. -You can also find host urls on the same tab by clicking "View Region Endpoint Urls". - -Data Model Used Per Doc - -``` -{ - "_id": (STRING) Unique Identifier for the feature document. - : (STRING) Feature Values Mapped by Feature Name. Feature - values stored as a serialized hex string. - .... - "event_ts": (STRING) ISO Stringified Timestamp. - "created_ts": (STRING) ISO Stringified Timestamp. -} -``` - - -## Example - -```yaml -project: my_feature_app -registry: data/registry.db -provider: local -online_store: - ## Basic Configs ## - - # If apikey or host is left blank the driver will try to pull - # these values from environment variables ROCKSET_APIKEY and - # ROCKSET_APISERVER respectively. - type: rockset - api_key: - host: - - ## Advanced Configs ## - - # Batch size of records that will be turned per page when - # paginating a batched read. - # - # read_pagination_batch_size: 100 - - # The amount of time, in seconds, we will wait for the - # collection to become visible to the API. - # - # collection_created_timeout_secs: 60 - - # The amount of time, in seconds, we will wait for the - # collection to enter READY state. - # - # collection_ready_timeout_secs: 1800 - - # Whether to wait for all writes to be flushed from log - # and queryable before returning write as completed. If - # False, documents that are written may not be seen - # immediately in subsequent reads. - # - # fence_all_writes: True - - # The amount of time we will wait, in seconds, for the - # write fence to be passed - # - # fence_timeout_secs: 600 - - # Initial backoff, in seconds, we will wait between - # requests when polling for a response. - # - # initial_request_backoff_secs: 2 - - # Initial backoff, in seconds, we will wait between - # requests when polling for a response. - # max_request_backoff_secs: 30 - - # The max amount of times we will retry a failed request. - # max_request_attempts: 10000 -``` diff --git a/sdk/python/docs/source/feast.infra.feature_servers.aws_lambda.rst b/sdk/python/docs/source/feast.infra.feature_servers.aws_lambda.rst deleted file mode 100644 index de90bfc000..0000000000 --- a/sdk/python/docs/source/feast.infra.feature_servers.aws_lambda.rst +++ /dev/null @@ -1,29 +0,0 @@ -feast.infra.feature\_servers.aws\_lambda package -================================================ - -Submodules ----------- - -feast.infra.feature\_servers.aws\_lambda.app module ---------------------------------------------------- - -.. automodule:: feast.infra.feature_servers.aws_lambda.app - :members: - :undoc-members: - :show-inheritance: - -feast.infra.feature\_servers.aws\_lambda.config module ------------------------------------------------------- - -.. automodule:: feast.infra.feature_servers.aws_lambda.config - :members: - :undoc-members: - :show-inheritance: - -Module contents ---------------- - -.. automodule:: feast.infra.feature_servers.aws_lambda - :members: - :undoc-members: - :show-inheritance: diff --git a/sdk/python/docs/source/feast.infra.feature_servers.gcp_cloudrun.rst b/sdk/python/docs/source/feast.infra.feature_servers.gcp_cloudrun.rst deleted file mode 100644 index f7fdaf5b36..0000000000 --- a/sdk/python/docs/source/feast.infra.feature_servers.gcp_cloudrun.rst +++ /dev/null @@ -1,29 +0,0 @@ -feast.infra.feature\_servers.gcp\_cloudrun package -================================================== - -Submodules ----------- - -feast.infra.feature\_servers.gcp\_cloudrun.app module ------------------------------------------------------ - -.. automodule:: feast.infra.feature_servers.gcp_cloudrun.app - :members: - :undoc-members: - :show-inheritance: - -feast.infra.feature\_servers.gcp\_cloudrun.config module --------------------------------------------------------- - -.. automodule:: feast.infra.feature_servers.gcp_cloudrun.config - :members: - :undoc-members: - :show-inheritance: - -Module contents ---------------- - -.. automodule:: feast.infra.feature_servers.gcp_cloudrun - :members: - :undoc-members: - :show-inheritance: diff --git a/sdk/python/docs/source/feast.infra.materialization.contrib.bytewax.rst b/sdk/python/docs/source/feast.infra.materialization.contrib.bytewax.rst deleted file mode 100644 index 86fbaa6151..0000000000 --- a/sdk/python/docs/source/feast.infra.materialization.contrib.bytewax.rst +++ /dev/null @@ -1,29 +0,0 @@ -feast.infra.materialization.contrib.bytewax package -================================================================= - -Submodules ----------- - -feast.infra.materialization.contrib.bytewax.bytewax\_materialization\_engine ----------------------------------------------------------------------- - -.. automodule:: feast.infra.materialization.contrib.bytewax.bytewax_materialization_engine - :members: - :undoc-members: - :show-inheritance: - -feast.infra.materialization.contrib.bytewax.bytewax\_materialization\_job ----------------------------------------------------------------------- - -.. automodule:: feast.infra.materialization.contrib.bytewax.bytewax_materialization_job - :members: - :undoc-members: - :show-inheritance: - -Module contents ---------------- - -.. automodule:: feast.infra.materialization.contrib.bytewax - :members: - :undoc-members: - :show-inheritance: diff --git a/sdk/python/docs/source/feast.infra.materialization.lambda.rst b/sdk/python/docs/source/feast.infra.materialization.lambda.rst deleted file mode 100644 index 7ca1d44314..0000000000 --- a/sdk/python/docs/source/feast.infra.materialization.lambda.rst +++ /dev/null @@ -1,29 +0,0 @@ -feast.infra.materialization.lambda package -========================================== - -Submodules ----------- - -feast.infra.materialization.lambda.app module ---------------------------------------------- - -.. automodule:: feast.infra.materialization.lambda.app - :members: - :undoc-members: - :show-inheritance: - -feast.infra.materialization.lambda.lambda\_engine module --------------------------------------------------------- - -.. automodule:: feast.infra.materialization.lambda.lambda_engine - :members: - :undoc-members: - :show-inheritance: - -Module contents ---------------- - -.. automodule:: feast.infra.materialization.lambda - :members: - :undoc-members: - :show-inheritance: diff --git a/sdk/python/docs/source/feast.infra.online_stores.contrib.rockset_online_store.rst b/sdk/python/docs/source/feast.infra.online_stores.contrib.rockset_online_store.rst deleted file mode 100644 index b3de7479a0..0000000000 --- a/sdk/python/docs/source/feast.infra.online_stores.contrib.rockset_online_store.rst +++ /dev/null @@ -1,21 +0,0 @@ -feast.infra.online\_stores.contrib.rockset\_online\_store package -================================================================= - -Submodules ----------- - -feast.infra.online\_stores.contrib.rockset\_online\_store.rockset module ------------------------------------------------------------------------- - -.. automodule:: feast.infra.online_stores.contrib.rockset_online_store.rockset - :members: - :undoc-members: - :show-inheritance: - -Module contents ---------------- - -.. automodule:: feast.infra.online_stores.contrib.rockset_online_store - :members: - :undoc-members: - :show-inheritance: diff --git a/sdk/python/docs/source/feast.infra.online_stores.contrib.rst b/sdk/python/docs/source/feast.infra.online_stores.contrib.rst index 9d301fcd0d..8c9dd7e549 100644 --- a/sdk/python/docs/source/feast.infra.online_stores.contrib.rst +++ b/sdk/python/docs/source/feast.infra.online_stores.contrib.rst @@ -12,7 +12,6 @@ Subpackages feast.infra.online_stores.contrib.hbase_online_store feast.infra.online_stores.contrib.ikv_online_store feast.infra.online_stores.contrib.mysql_online_store - feast.infra.online_stores.contrib.rockset_online_store Submodules ---------- diff --git a/sdk/python/docs/source/feast.infra.registry.contrib.postgres.rst b/sdk/python/docs/source/feast.infra.registry.contrib.postgres.rst deleted file mode 100644 index 3f31990805..0000000000 --- a/sdk/python/docs/source/feast.infra.registry.contrib.postgres.rst +++ /dev/null @@ -1,21 +0,0 @@ -feast.infra.registry.contrib.postgres package -============================================= - -Submodules ----------- - -feast.infra.registry.contrib.postgres.postgres\_registry\_store module ----------------------------------------------------------------------- - -.. automodule:: feast.infra.registry.contrib.postgres.postgres_registry_store - :members: - :undoc-members: - :show-inheritance: - -Module contents ---------------- - -.. automodule:: feast.infra.registry.contrib.postgres - :members: - :undoc-members: - :show-inheritance: diff --git a/sdk/python/docs/source/feast.infra.registry_stores.rst b/sdk/python/docs/source/feast.infra.registry_stores.rst deleted file mode 100644 index cff02fa338..0000000000 --- a/sdk/python/docs/source/feast.infra.registry_stores.rst +++ /dev/null @@ -1,21 +0,0 @@ -feast.infra.registry\_stores package -==================================== - -Submodules ----------- - -feast.infra.registry\_stores.sql module ---------------------------------------- - -.. automodule:: feast.infra.registry_stores.sql - :members: - :undoc-members: - :show-inheritance: - -Module contents ---------------- - -.. automodule:: feast.infra.registry_stores - :members: - :undoc-members: - :show-inheritance: diff --git a/sdk/python/docs/source/feast.permissions.auth.rst b/sdk/python/docs/source/feast.permissions.auth.rst new file mode 100644 index 0000000000..3826bfc217 --- /dev/null +++ b/sdk/python/docs/source/feast.permissions.auth.rst @@ -0,0 +1,61 @@ +feast.permissions.auth package +============================== + +Submodules +---------- + +feast.permissions.auth.auth\_manager module +------------------------------------------- + +.. automodule:: feast.permissions.auth.auth_manager + :members: + :undoc-members: + :show-inheritance: + +feast.permissions.auth.auth\_type module +---------------------------------------- + +.. automodule:: feast.permissions.auth.auth_type + :members: + :undoc-members: + :show-inheritance: + +feast.permissions.auth.kubernetes\_token\_parser module +------------------------------------------------------- + +.. automodule:: feast.permissions.auth.kubernetes_token_parser + :members: + :undoc-members: + :show-inheritance: + +feast.permissions.auth.oidc\_token\_parser module +------------------------------------------------- + +.. automodule:: feast.permissions.auth.oidc_token_parser + :members: + :undoc-members: + :show-inheritance: + +feast.permissions.auth.token\_extractor module +---------------------------------------------- + +.. automodule:: feast.permissions.auth.token_extractor + :members: + :undoc-members: + :show-inheritance: + +feast.permissions.auth.token\_parser module +------------------------------------------- + +.. automodule:: feast.permissions.auth.token_parser + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.permissions.auth + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.permissions.client.rst b/sdk/python/docs/source/feast.permissions.client.rst new file mode 100644 index 0000000000..f346801210 --- /dev/null +++ b/sdk/python/docs/source/feast.permissions.client.rst @@ -0,0 +1,69 @@ +feast.permissions.client package +================================ + +Submodules +---------- + +feast.permissions.client.arrow\_flight\_auth\_interceptor module +---------------------------------------------------------------- + +.. automodule:: feast.permissions.client.arrow_flight_auth_interceptor + :members: + :undoc-members: + :show-inheritance: + +feast.permissions.client.auth\_client\_manager module +----------------------------------------------------- + +.. automodule:: feast.permissions.client.auth_client_manager + :members: + :undoc-members: + :show-inheritance: + +feast.permissions.client.auth\_client\_manager\_factory module +-------------------------------------------------------------- + +.. automodule:: feast.permissions.client.auth_client_manager_factory + :members: + :undoc-members: + :show-inheritance: + +feast.permissions.client.grpc\_client\_auth\_interceptor module +--------------------------------------------------------------- + +.. automodule:: feast.permissions.client.grpc_client_auth_interceptor + :members: + :undoc-members: + :show-inheritance: + +feast.permissions.client.http\_auth\_requests\_wrapper module +------------------------------------------------------------- + +.. automodule:: feast.permissions.client.http_auth_requests_wrapper + :members: + :undoc-members: + :show-inheritance: + +feast.permissions.client.kubernetes\_auth\_client\_manager module +----------------------------------------------------------------- + +.. automodule:: feast.permissions.client.kubernetes_auth_client_manager + :members: + :undoc-members: + :show-inheritance: + +feast.permissions.client.oidc\_authentication\_client\_manager module +--------------------------------------------------------------------- + +.. automodule:: feast.permissions.client.oidc_authentication_client_manager + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.permissions.client + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.permissions.rst b/sdk/python/docs/source/feast.permissions.rst new file mode 100644 index 0000000000..8c33ab6273 --- /dev/null +++ b/sdk/python/docs/source/feast.permissions.rst @@ -0,0 +1,103 @@ +feast.permissions package +========================= + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + feast.permissions.auth + feast.permissions.client + feast.permissions.server + +Submodules +---------- + +feast.permissions.action module +------------------------------- + +.. automodule:: feast.permissions.action + :members: + :undoc-members: + :show-inheritance: + +feast.permissions.auth\_model module +------------------------------------ + +.. automodule:: feast.permissions.auth_model + :members: + :undoc-members: + :show-inheritance: + +feast.permissions.decision module +--------------------------------- + +.. automodule:: feast.permissions.decision + :members: + :undoc-members: + :show-inheritance: + +feast.permissions.decorator module +---------------------------------- + +.. automodule:: feast.permissions.decorator + :members: + :undoc-members: + :show-inheritance: + +feast.permissions.enforcer module +--------------------------------- + +.. automodule:: feast.permissions.enforcer + :members: + :undoc-members: + :show-inheritance: + +feast.permissions.matcher module +-------------------------------- + +.. automodule:: feast.permissions.matcher + :members: + :undoc-members: + :show-inheritance: + +feast.permissions.permission module +----------------------------------- + +.. automodule:: feast.permissions.permission + :members: + :undoc-members: + :show-inheritance: + +feast.permissions.policy module +------------------------------- + +.. automodule:: feast.permissions.policy + :members: + :undoc-members: + :show-inheritance: + +feast.permissions.security\_manager module +------------------------------------------ + +.. automodule:: feast.permissions.security_manager + :members: + :undoc-members: + :show-inheritance: + +feast.permissions.user module +----------------------------- + +.. automodule:: feast.permissions.user + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.permissions + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.permissions.server.rst b/sdk/python/docs/source/feast.permissions.server.rst new file mode 100644 index 0000000000..33a9d8df64 --- /dev/null +++ b/sdk/python/docs/source/feast.permissions.server.rst @@ -0,0 +1,69 @@ +feast.permissions.server package +================================ + +Submodules +---------- + +feast.permissions.server.arrow module +------------------------------------- + +.. automodule:: feast.permissions.server.arrow + :members: + :undoc-members: + :show-inheritance: + +feast.permissions.server.arrow\_flight\_token\_extractor module +--------------------------------------------------------------- + +.. automodule:: feast.permissions.server.arrow_flight_token_extractor + :members: + :undoc-members: + :show-inheritance: + +feast.permissions.server.grpc module +------------------------------------ + +.. automodule:: feast.permissions.server.grpc + :members: + :undoc-members: + :show-inheritance: + +feast.permissions.server.grpc\_token\_extractor module +------------------------------------------------------ + +.. automodule:: feast.permissions.server.grpc_token_extractor + :members: + :undoc-members: + :show-inheritance: + +feast.permissions.server.rest module +------------------------------------ + +.. automodule:: feast.permissions.server.rest + :members: + :undoc-members: + :show-inheritance: + +feast.permissions.server.rest\_token\_extractor module +------------------------------------------------------ + +.. automodule:: feast.permissions.server.rest_token_extractor + :members: + :undoc-members: + :show-inheritance: + +feast.permissions.server.utils module +------------------------------------- + +.. automodule:: feast.permissions.server.utils + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.permissions.server + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.protos.feast.core.rst b/sdk/python/docs/source/feast.protos.feast.core.rst index a8691c20fe..9d079953c1 100644 --- a/sdk/python/docs/source/feast.protos.feast.core.rst +++ b/sdk/python/docs/source/feast.protos.feast.core.rst @@ -212,6 +212,38 @@ feast.protos.feast.core.OnDemandFeatureView\_pb2\_grpc module :undoc-members: :show-inheritance: +feast.protos.feast.core.Permission\_pb2 module +---------------------------------------------- + +.. automodule:: feast.protos.feast.core.Permission_pb2 + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.Permission\_pb2\_grpc module +---------------------------------------------------- + +.. automodule:: feast.protos.feast.core.Permission_pb2_grpc + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.Policy\_pb2 module +------------------------------------------ + +.. automodule:: feast.protos.feast.core.Policy_pb2 + :members: + :undoc-members: + :show-inheritance: + +feast.protos.feast.core.Policy\_pb2\_grpc module +------------------------------------------------ + +.. automodule:: feast.protos.feast.core.Policy_pb2_grpc + :members: + :undoc-members: + :show-inheritance: + feast.protos.feast.core.Registry\_pb2 module -------------------------------------------- diff --git a/sdk/python/docs/source/feast.rst b/sdk/python/docs/source/feast.rst index 83137574dd..b8c04ebde6 100644 --- a/sdk/python/docs/source/feast.rst +++ b/sdk/python/docs/source/feast.rst @@ -52,6 +52,14 @@ feast.cli module :undoc-members: :show-inheritance: +feast.cli\_utils module +----------------------- + +.. automodule:: feast.cli_utils + :members: + :undoc-members: + :show-inheritance: + feast.constants module ---------------------- @@ -252,14 +260,6 @@ feast.proto\_json module :undoc-members: :show-inheritance: -feast.prova module ------------------- - -.. automodule:: feast.prova - :members: - :undoc-members: - :show-inheritance: - feast.registry\_server module ----------------------------- diff --git a/sdk/python/feast/cli.py b/sdk/python/feast/cli.py index 737704dd36..0a12d1dcbc 100644 --- a/sdk/python/feast/cli.py +++ b/sdk/python/feast/cli.py @@ -614,7 +614,6 @@ def materialize_incremental_command(ctx: click.Context, end_ts: str, views: List "postgres", "hbase", "cassandra", - "rockset", "hazelcast", "ikv", ], diff --git a/sdk/python/feast/infra/online_stores/contrib/rockset_online_store/__init__.py b/sdk/python/feast/infra/online_stores/contrib/rockset_online_store/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/sdk/python/feast/infra/online_stores/contrib/rockset_online_store/rockset.py b/sdk/python/feast/infra/online_stores/contrib/rockset_online_store/rockset.py deleted file mode 100644 index 31de7f9e9b..0000000000 --- a/sdk/python/feast/infra/online_stores/contrib/rockset_online_store/rockset.py +++ /dev/null @@ -1,520 +0,0 @@ -# Copyright 2022 The Feast 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 -# -# https://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 json -import logging -import os -import random -import time -from datetime import datetime -from typing import Any, Callable, Dict, List, Literal, Optional, Sequence, Tuple, cast - -import requests -from rockset.exceptions import BadRequestException, RocksetException -from rockset.models import QueryRequestSql -from rockset.query_paginator import QueryPaginator -from rockset.rockset_client import RocksetClient - -from feast.entity import Entity -from feast.feature_view import FeatureView -from feast.infra.online_stores.helpers import compute_entity_id -from feast.infra.online_stores.online_store import OnlineStore -from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto -from feast.protos.feast.types.Value_pb2 import Value as ValueProto -from feast.repo_config import FeastConfigBaseModel, RepoConfig - -logger = logging.getLogger(__name__) - - -class RocksetOnlineStoreConfig(FeastConfigBaseModel): - """Online store config for Rockset store""" - - type: Literal["rockset"] = "rockset" - """Online store type selector""" - - api_key: Optional[str] = None - """Api Key to be used for Rockset Account. If not set the env var ROCKSET_APIKEY will be used.""" - - host: Optional[str] = None - """The Host Url for Rockset requests. If not set the env var ROCKSET_APISERVER will be used.""" - - read_pagination_batch_size: int = 100 - """Batch size of records that will be turned per page when paginating a batched read""" - - collection_created_timeout_secs: int = 60 - """The amount of time, in seconds, we will wait for the collection to become visible to the API""" - - collection_ready_timeout_secs: int = 30 * 60 - """The amount of time, in seconds, we will wait for the collection to enter READY state""" - - fence_all_writes: bool = True - """Whether to wait for all writes to be flushed from log and queryable. If False, documents that are written may not be seen immediately in subsequent reads""" - - fence_timeout_secs: int = 10 * 60 - """The amount of time we will wait, in seconds, for the write fence to be passed""" - - initial_request_backoff_secs: int = 2 - """Initial backoff, in seconds, we will wait between requests when polling for a response""" - - max_request_backoff_secs: int = 30 - """Initial backoff, in seconds, we will wait between requests when polling for a response""" - - max_request_attempts: int = 10 * 1000 - """The max amount of times we will retry a failed request""" - - -class RocksetOnlineStore(OnlineStore): - """ - Rockset implementation of the online store interface. - - Attributes: - _rockset_client: Rockset openapi client. - """ - - _rockset_client = None - - def online_write_batch( - self, - config: RepoConfig, - table: FeatureView, - data: List[ - Tuple[EntityKeyProto, Dict[str, ValueProto], datetime, Optional[datetime]] - ], - progress: Optional[Callable[[int], Any]], - ) -> None: - """ - Write a batch of feature rows to online Rockset store. - - Args: - config: The RepoConfig for the current FeatureStore. - table: Feast FeatureView. - data: a list of quadruplets containing Feature data. Each quadruplet contains an Entity Key, - a dict containing feature values, an event timestamp for the row, and - the created timestamp for the row if it exists. - progress: Optional function to be called once every mini-batch of rows is written to - the online store. Can be used to display progress. - """ - - online_config = config.online_store - assert isinstance(online_config, RocksetOnlineStoreConfig) - - rs = self.get_rockset_client(online_config) - collection_name = self.get_collection_name(config, table) - - # We need to deduplicate on entity_id and we will save the latest timestamp version. - dedup_dict = {} - for feature_vals in data: - entity_key, features, timestamp, created_ts = feature_vals - serialized_key = compute_entity_id( - entity_key=entity_key, - entity_key_serialization_version=config.entity_key_serialization_version, - ) - - if serialized_key not in dedup_dict: - dedup_dict[serialized_key] = feature_vals - continue - - # If the entity already existings in the dictionary ignore the entry if it has a lower timestamp. - if timestamp <= dedup_dict[serialized_key][2]: - continue - - dedup_dict[serialized_key] = feature_vals - - request_batch = [] - for serialized_key, feature_vals in dedup_dict.items(): - document = {} - entity_key, features, timestamp, created_ts = feature_vals - document["_id"] = serialized_key - - # Rockset python client currently does not handle datetime correctly and will convert - # to string instead of native Rockset DATETIME. This will be fixed, but until then we - # use isoformat. - document["event_ts"] = timestamp.isoformat() - document["created_ts"] = ( - "" if created_ts is None else created_ts.isoformat() - ) - for k, v in features.items(): - # Rockset client currently does not support bytes type. - document[k] = v.SerializeToString().hex() - - # TODO: Implement async batching with retries. - request_batch.append(document) - - if progress: - progress(1) - - resp = rs.Documents.add_documents( - collection=collection_name, data=request_batch - ) - if online_config.fence_all_writes: - self.wait_for_fence(rs, collection_name, resp["last_offset"], online_config) - - return None - - def online_read( - self, - config: RepoConfig, - table: FeatureView, - entity_keys: List[EntityKeyProto], - requested_features: Optional[List[str]] = None, - ) -> List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]]: - """ - Retrieve feature values from the online Rockset store. - - Args: - config: The RepoConfig for the current FeatureStore. - table: Feast FeatureView. - entity_keys: a list of entity keys that should be read from the FeatureStore. - """ - online_config = config.online_store - assert isinstance(online_config, RocksetOnlineStoreConfig) - - rs = self.get_rockset_client(online_config) - collection_name = self.get_collection_name(config, table) - - feature_list = "" - if requested_features is not None: - feature_list = ",".join(requested_features) - - entity_serialized_key_list = [ - compute_entity_id( - k, - entity_key_serialization_version=config.entity_key_serialization_version, - ) - for k in entity_keys - ] - - entity_query_str = ",".join( - "'{id}'".format(id=s) for s in entity_serialized_key_list - ) - - query_str = f""" - SELECT - "_id", - "event_ts", - {feature_list} - FROM - {collection_name} - WHERE - "_id" IN ({entity_query_str}) - """ - - feature_set = set() - if requested_features: - feature_set.update(requested_features) - - result_map = {} - for page in QueryPaginator( - rs, - rs.Queries.query( - sql=QueryRequestSql( - query=query_str, - paginate=True, - initial_paginate_response_doc_count=online_config.read_pagination_batch_size, - ) - ), - ): - for doc in page: - result = {} - for k, v in doc.items(): - if k not in feature_set: - # We want to skip deserializing values that are not feature values like bookeeping values. - continue - - val = ValueProto() - - # TODO: Remove bytes <-> string parsing once client supports bytes. - val.ParseFromString(bytes.fromhex(v)) - result[k] = val - result_map[doc["_id"]] = ( - datetime.fromisoformat(doc["event_ts"]), - result, - ) - - results_list: List[ - Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]] - ] = [] - for key in entity_serialized_key_list: - if key not in result_map: - # If not found, we add a gap to let the client know. - results_list.append((None, None)) - continue - - results_list.append(result_map[key]) - - return results_list - - def update( - self, - config: RepoConfig, - tables_to_delete: Sequence[FeatureView], - tables_to_keep: Sequence[FeatureView], - entities_to_delete: Sequence[Entity], - entities_to_keep: Sequence[Entity], - partial: bool, - ): - """ - Update tables from the Rockset Online Store. - - Args: - config: The RepoConfig for the current FeatureStore. - tables_to_delete: Tables to delete from the Rockset Online Store. - tables_to_keep: Tables to keep in the Rockset Online Store. - """ - online_config = config.online_store - assert isinstance(online_config, RocksetOnlineStoreConfig) - rs = self.get_rockset_client(online_config) - - created_collections = [] - for table_instance in tables_to_keep: - try: - collection_name = self.get_collection_name(config, table_instance) - rs.Collections.create_file_upload_collection(name=collection_name) - created_collections.append(collection_name) - except BadRequestException as e: - if self.parse_request_error_type(e) == "AlreadyExists": - # Table already exists nothing to do. We should still make sure it is ready though. - created_collections.append(collection_name) - continue - raise - - for table_to_delete in tables_to_delete: - self.delete_collection( - rs, collection_name=self.get_collection_name(config, table_to_delete) - ) - - # Now wait for all collections to be READY. - self.wait_for_ready_collections( - rs, created_collections, online_config=online_config - ) - - def teardown( - self, - config: RepoConfig, - tables: Sequence[FeatureView], - entities: Sequence[Entity], - ): - """ - Delete all collections from the Rockset Online Store. - - Args: - config: The RepoConfig for the current FeatureStore. - tables: Tables to delete from the feature repo. - """ - online_config = config.online_store - assert isinstance(online_config, RocksetOnlineStoreConfig) - rs = self.get_rockset_client(online_config) - for table in tables: - self.delete_collection( - rs, collection_name=self.get_collection_name(config, table) - ) - - def get_rockset_client( - self, onlineConfig: RocksetOnlineStoreConfig - ) -> RocksetClient: - """ - Fetches the RocksetClient to be used for all requests for this online store based on the api - configuration in the provided config. If no configuration provided local ENV vars will be used. - - Args: - onlineConfig: The RocksetOnlineStoreConfig associated with this online store. - """ - if self._rockset_client is not None: - return self._rockset_client - - _api_key = ( - os.getenv("ROCKSET_APIKEY") - if isinstance(onlineConfig.api_key, type(None)) - else onlineConfig.api_key - ) - _host = ( - os.getenv("ROCKSET_APISERVER") - if isinstance(onlineConfig.host, type(None)) - else onlineConfig.host - ) - self._rockset_client = RocksetClient(host=_host, api_key=_api_key) - return self._rockset_client - - @staticmethod - def delete_collection(rs: RocksetClient, collection_name: str): - """ - Deletes the collection whose name was provided - - Args: - rs: The RocksetClient to be used for the deletion. - collection_name: The name of the collection to be deleted. - """ - - try: - rs.Collections.delete(collection=collection_name) - except RocksetException as e: - if RocksetOnlineStore.parse_request_error_type(e) == "NotFound": - logger.warning( - f"Trying to delete collection that does not exist {collection_name}" - ) - return - raise - - @staticmethod - def get_collection_name(config: RepoConfig, feature_view: FeatureView) -> str: - """ - Returns the collection name based on the provided config and FeatureView. - - Args: - config: RepoConfig for the online store. - feature_view: FeatureView that is backed by the returned collection name. - - Returns: - The collection name as a string. - """ - project_val = config.project if config.project else "feast" - table_name = feature_view.name if feature_view.name else "feature_store" - return f"{project_val}_{table_name}" - - @staticmethod - def parse_request_error_type(e: RocksetException) -> str: - """ - Parse a throw RocksetException. Will return a string representing the type of error that was thrown. - - Args: - e: The RockException that is being parsed. - - Returns: - Error type parsed as a string. - """ - - body_dict = json.loads(e.body) - return body_dict["type"] - - @staticmethod - def wait_for_fence( - rs: RocksetClient, - collection_name: str, - last_offset: str, - online_config: RocksetOnlineStoreConfig, - ): - """ - Waits until 'last_offset' is flushed and values are ready to be read. If wait lasts longer than the timeout specified in config - a timeout exception will be throw. - - Args: - rs: Rockset client that will be used to make all requests. - collection_name: Collection associated with the offsets we are waiting for. - last_offset: The actual offsets we are waiting to be flushed. - online_config: The config that will be used to determine timeouts and backout configurations. - """ - - resource_path = ( - f"/v1/orgs/self/ws/commons/collections/{collection_name}/offsets/commit" - ) - request = {"name": [last_offset]} - - headers = {} - headers["Content-Type"] = "application/json" - headers["Authorization"] = f"ApiKey {rs.api_client.configuration.api_key}" - - t_start = time.time() - for num_attempts in range(online_config.max_request_attempts): - delay = time.time() - t_start - resp = requests.post( - url=f"{rs.api_client.configuration.host}{resource_path}", - json=request, - headers=headers, - ) - - if resp.status_code == 200 and resp.json()["data"]["passed"] is True: - break - - if delay > online_config.fence_timeout_secs: - raise TimeoutError( - f"Write to collection {collection_name} at offset {last_offset} was not available for read after {delay} secs" - ) - - if resp.status_code == 429: - RocksetOnlineStore.backoff_sleep(num_attempts, online_config) - continue - elif resp.status_code != 200: - raise Exception(f"[{resp.status_code}]: {resp.reason}") - - RocksetOnlineStore.backoff_sleep(num_attempts, online_config) - - @staticmethod - def wait_for_ready_collections( - rs: RocksetClient, - collection_names: List[str], - online_config: RocksetOnlineStoreConfig, - ): - """ - Waits until all collections provided have entered READY state and can accept new documents. If wait - lasts longer than timeout a TimeoutError exception will be thrown. - - Args: - rs: Rockset client that will be used to make all requests. - collection_names: All collections that we will wait for. - timeout: The max amount of time we will wait for the collections to become READY. - """ - - t_start = time.time() - for cname in collection_names: - # We will wait until the provided timeout for all collections to become READY. - for num_attempts in range(online_config.max_request_attempts): - resp = None - delay = time.time() - t_start - try: - resp = rs.Collections.get(collection=cname) - except RocksetException as e: - error_type = RocksetOnlineStore.parse_request_error_type(e) - if error_type == "NotFound": - if delay > online_config.collection_created_timeout_secs: - raise TimeoutError( - f"Collection {cname} failed to become visible after {delay} seconds" - ) - elif error_type == "RateLimitExceeded": - RocksetOnlineStore.backoff_sleep(num_attempts, online_config) - continue - else: - raise - - if ( - resp is not None - and cast(Dict[str, dict], resp)["data"]["status"] == "READY" - ): - break - - if delay > online_config.collection_ready_timeout_secs: - raise TimeoutError( - f"Collection {cname} failed to become ready after {delay} seconds" - ) - - RocksetOnlineStore.backoff_sleep(num_attempts, online_config) - - @staticmethod - def backoff_sleep(attempts: int, online_config: RocksetOnlineStoreConfig): - """ - Sleep for the needed amount of time based on the number of request attempts. - - Args: - backoff: The amount of time we will sleep for - max_backoff: The max amount of time we should ever backoff for. - rate_limited: Whether this method is being called as part of a rate limited request. - """ - - default_backoff = online_config.initial_request_backoff_secs - - # Full jitter, exponential backoff. - backoff = random.uniform( - default_backoff, - min(default_backoff << attempts, online_config.max_request_backoff_secs), - ) - time.sleep(backoff) diff --git a/sdk/python/feast/repo_config.py b/sdk/python/feast/repo_config.py index a270e6795c..199ef31412 100644 --- a/sdk/python/feast/repo_config.py +++ b/sdk/python/feast/repo_config.py @@ -61,7 +61,6 @@ "hbase": "feast.infra.online_stores.contrib.hbase_online_store.hbase.HbaseOnlineStore", "cassandra": "feast.infra.online_stores.contrib.cassandra_online_store.cassandra_online_store.CassandraOnlineStore", "mysql": "feast.infra.online_stores.contrib.mysql_online_store.mysql.MySQLOnlineStore", - "rockset": "feast.infra.online_stores.contrib.rockset_online_store.rockset.RocksetOnlineStore", "hazelcast": "feast.infra.online_stores.contrib.hazelcast_online_store.hazelcast_online_store.HazelcastOnlineStore", "ikv": "feast.infra.online_stores.contrib.ikv_online_store.ikv.IKVOnlineStore", "elasticsearch": "feast.infra.online_stores.contrib.elasticsearch.ElasticSearchOnlineStore", diff --git a/sdk/python/feast/templates/rockset/README.md b/sdk/python/feast/templates/rockset/README.md deleted file mode 100644 index d4f1ef6faf..0000000000 --- a/sdk/python/feast/templates/rockset/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# Feast Quickstart -A quick view of what's in this repository: - -* `data/` contains raw demo parquet data -* `feature_repo/driver_repo.py` contains demo feature definitions -* `feature_repo/feature_store.yaml` contains a demo setup configuring where data sources are -* `test_workflow.py` showcases how to run all key Feast commands, including defining, retrieving, and pushing features. - -You can run the overall workflow with `python test_workflow.py`. - -## To move from this into a more production ready workflow: -> See more details in [Running Feast in production](https://docs.feast.dev/how-to-guides/running-feast-in-production) - -1. `feature_store.yaml` points to a local file as a registry. You'll want to setup a remote file (e.g. in S3/GCS) or a - SQL registry. See [registry docs](https://docs.feast.dev/getting-started/concepts/registry) for more details. -2. Setup CI/CD + dev vs staging vs prod environments to automatically update the registry as you change Feast feature definitions. See [docs](https://docs.feast.dev/how-to-guides/running-feast-in-production#1.-automatically-deploying-changes-to-your-feature-definitions). -3. (optional) Regularly scheduled materialization to power low latency feature retrieval (e.g. via Airflow). See [Batch data ingestion](https://docs.feast.dev/getting-started/concepts/data-ingestion#batch-data-ingestion) - for more details. -4. (optional) Deploy feature server instances with `feast serve` to expose endpoints to retrieve online features. - - See [Python feature server](https://docs.feast.dev/reference/feature-servers/python-feature-server) for details. - - Use cases can also directly call the Feast client to fetch features as per [Feature retrieval](https://docs.feast.dev/getting-started/concepts/feature-retrieval) diff --git a/sdk/python/feast/templates/rockset/__init__.py b/sdk/python/feast/templates/rockset/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/sdk/python/feast/templates/rockset/bootstrap.py b/sdk/python/feast/templates/rockset/bootstrap.py deleted file mode 100644 index a3dc17f18e..0000000000 --- a/sdk/python/feast/templates/rockset/bootstrap.py +++ /dev/null @@ -1,30 +0,0 @@ -import click - -from feast.file_utils import replace_str_in_file - - -def bootstrap(): - # Bootstrap() will automatically be called from the init_repo() during `feast init` - import pathlib - - repo_path = pathlib.Path(__file__).parent.absolute() / "feature_repo" - config_file = repo_path / "feature_store.yaml" - data_path = repo_path / "data" - data_path.mkdir(exist_ok=True) - - rockset_apikey = click.prompt( - "Rockset Api Key (If blank will be read from ROCKSET_APIKEY in ENV):", - default="", - ) - - rockset_host = click.prompt( - "Rockset Host (If blank will be read from ROCKSET_APISERVER in ENV):", - default="", - ) - - replace_str_in_file(config_file, "ROCKSET_APIKEY", rockset_apikey) - replace_str_in_file(config_file, "ROCKSET_APISERVER", rockset_host) - - -if __name__ == "__main__": - bootstrap() diff --git a/sdk/python/feast/templates/rockset/feature_repo/feature_store.yaml b/sdk/python/feast/templates/rockset/feature_repo/feature_store.yaml deleted file mode 100644 index 57cf8e73bb..0000000000 --- a/sdk/python/feast/templates/rockset/feature_repo/feature_store.yaml +++ /dev/null @@ -1,8 +0,0 @@ -project: my_project -registry: registry.db -provider: local -online_store: - type: rockset - api_key: ROCKSET_APIKEY - host: ROCKSET_APISERVER # (api.usw2a1.rockset.com, api.euc1a1.rockset.com, api.use1a1.rockset.com) -entity_key_serialization_version: 2 diff --git a/sdk/python/requirements/py3.10-ci-requirements.txt b/sdk/python/requirements/py3.10-ci-requirements.txt index 89459d1a69..8128eb094d 100644 --- a/sdk/python/requirements/py3.10-ci-requirements.txt +++ b/sdk/python/requirements/py3.10-ci-requirements.txt @@ -1,7 +1,6 @@ # This file was autogenerated by uv via the following command: # uv pip compile --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.10-ci-requirements.txt aiobotocore==2.13.1 - # via feast (setup.py) aiohttp==3.9.5 # via aiobotocore aioitertools==0.11.0 @@ -20,8 +19,6 @@ anyio==4.4.0 # jupyter-server # starlette # watchfiles -appnope==0.1.4 - # via ipykernel argon2-cffi==23.1.0 # via jupyter-server argon2-cffi-bindings==21.2.0 @@ -31,17 +28,16 @@ arrow==1.3.0 asn1crypto==1.5.1 # via snowflake-connector-python assertpy==1.1 - # via feast (setup.py) asttokens==2.4.1 # via stack-data async-lru==2.0.4 # via jupyterlab +async-property==0.2.2 + # via python-keycloak async-timeout==4.0.3 # via # aiohttp # redis -async-property==0.2.2 - # via python-keycloak atpublic==4.1.0 # via ibis-framework attrs==23.2.0 @@ -54,9 +50,7 @@ azure-core==1.30.2 # azure-identity # azure-storage-blob azure-identity==1.17.1 - # via feast (setup.py) azure-storage-blob==12.20.0 - # via feast (setup.py) babel==2.15.0 # via # jupyterlab-server @@ -66,13 +60,10 @@ beautifulsoup4==4.12.3 bidict==0.23.1 # via ibis-framework bigtree==0.19.2 - # via feast (setup.py) bleach==6.1.0 # via nbconvert boto3==1.34.131 - # via - # feast (setup.py) - # moto + # via moto botocore==1.34.131 # via # aiobotocore @@ -81,13 +72,11 @@ botocore==1.34.131 # s3transfer build==1.2.1 # via - # feast (setup.py) # pip-tools # singlestoredb cachetools==5.3.3 # via google-auth cassandra-driver==3.29.1 - # via feast (setup.py) certifi==2024.7.4 # via # elastic-transport @@ -110,7 +99,6 @@ charset-normalizer==3.3.2 # snowflake-connector-python click==8.1.7 # via - # feast (setup.py) # dask # geomet # great-expectations @@ -120,9 +108,7 @@ click==8.1.7 cloudpickle==3.0.0 # via dask colorama==0.4.6 - # via - # feast (setup.py) - # great-expectations + # via great-expectations comm==0.2.2 # via # ipykernel @@ -131,7 +117,6 @@ coverage[toml]==7.5.4 # via pytest-cov cryptography==42.0.8 # via - # feast (setup.py) # azure-identity # azure-storage-blob # great-expectations @@ -144,9 +129,7 @@ cryptography==42.0.8 # types-pyopenssl # types-redis dask[dataframe]==2024.6.2 - # via - # feast (setup.py) - # dask-expr + # via dask-expr dask-expr==1.1.6 # via dask db-dtypes==1.2.0 @@ -158,11 +141,9 @@ decorator==5.1.1 defusedxml==0.7.1 # via nbconvert deltalake==0.18.1 - # via feast (setup.py) deprecation==2.1.0 # via python-keycloak dill==0.3.8 - # via feast (setup.py) distlib==0.3.8 # via virtualenv dnspython==2.6.1 @@ -176,7 +157,6 @@ duckdb==0.10.3 elastic-transport==8.13.1 # via elasticsearch elasticsearch==8.14.0 - # via feast (setup.py) email-validator==2.2.0 # via fastapi entrypoints==0.4 @@ -191,7 +171,6 @@ execnet==2.1.1 executing==2.0.1 # via stack-data fastapi==0.111.0 - # via feast (setup.py) fastapi-cli==0.0.4 # via fastapi fastjsonschema==2.20.0 @@ -207,16 +186,11 @@ frozenlist==1.4.1 # aiohttp # aiosignal fsspec==2023.12.2 - # via - # feast (setup.py) - # dask -geojson==2.5.0 - # via rockset + # via dask geomet==0.2.1.post1 # via cassandra-driver google-api-core[grpc]==2.19.1 # via - # feast (setup.py) # google-cloud-bigquery # google-cloud-bigquery-storage # google-cloud-bigtable @@ -231,11 +205,8 @@ google-auth==2.30.0 # google-cloud-storage # kubernetes google-cloud-bigquery[pandas]==3.13.0 - # via feast (setup.py) google-cloud-bigquery-storage==2.25.0 - # via feast (setup.py) google-cloud-bigtable==2.24.0 - # via feast (setup.py) google-cloud-core==2.4.1 # via # google-cloud-bigquery @@ -243,9 +214,7 @@ google-cloud-core==2.4.1 # google-cloud-datastore # google-cloud-storage google-cloud-datastore==2.19.0 - # via feast (setup.py) google-cloud-storage==2.17.0 - # via feast (setup.py) google-crc32c==1.5.0 # via # google-cloud-storage @@ -256,17 +225,16 @@ google-resumable-media==2.7.1 # google-cloud-storage googleapis-common-protos[grpc]==1.63.2 # via - # feast (setup.py) # google-api-core # grpc-google-iam-v1 # grpcio-status great-expectations==0.18.16 - # via feast (setup.py) +greenlet==3.0.3 + # via sqlalchemy grpc-google-iam-v1==0.13.1 # via google-cloud-bigtable grpcio==1.64.1 # via - # feast (setup.py) # google-api-core # google-cloud-bigquery # googleapis-common-protos @@ -277,43 +245,31 @@ grpcio==1.64.1 # grpcio-testing # grpcio-tools grpcio-health-checking==1.62.2 - # via feast (setup.py) grpcio-reflection==1.62.2 - # via feast (setup.py) grpcio-status==1.62.2 # via google-api-core grpcio-testing==1.62.2 - # via feast (setup.py) grpcio-tools==1.62.2 - # via feast (setup.py) gunicorn==22.0.0 - # via feast (setup.py) h11==0.14.0 # via # httpcore # uvicorn happybase==1.2.0 - # via feast (setup.py) hazelcast-python-client==5.4.0 - # via feast (setup.py) hiredis==2.3.2 - # via feast (setup.py) httpcore==1.0.5 # via httpx httptools==0.6.1 # via uvicorn httpx==0.27.0 # via - # feast (setup.py) # fastapi # jupyterlab # python-keycloak ibis-framework[duckdb]==9.1.0 - # via - # feast (setup.py) - # ibis-substrait + # via ibis-substrait ibis-substrait==4.0.0 - # via feast (setup.py) identify==2.5.36 # via pre-commit idna==3.7 @@ -348,7 +304,6 @@ jedi==0.19.1 # via ipython jinja2==3.1.4 # via - # feast (setup.py) # altair # fastapi # great-expectations @@ -372,7 +327,6 @@ jsonpointer==3.0.0 # jsonschema jsonschema[format-nongpl]==4.22.0 # via - # feast (setup.py) # altair # great-expectations # jupyter-events @@ -420,7 +374,6 @@ jupyterlab-widgets==3.0.11 jwcrypto==1.5.6 # via python-keycloak kubernetes==20.13.0 - # via feast (setup.py) locket==1.0.0 # via partd makefun==1.15.2 @@ -441,17 +394,13 @@ matplotlib-inline==0.1.7 mdurl==0.1.2 # via markdown-it-py minio==7.1.0 - # via feast (setup.py) mistune==3.0.2 # via # great-expectations # nbconvert mmh3==4.1.0 - # via feast (setup.py) mock==2.0.0 - # via feast (setup.py) moto==4.2.14 - # via feast (setup.py) msal==1.29.0 # via # azure-identity @@ -463,13 +412,10 @@ multidict==6.0.5 # aiohttp # yarl mypy==1.10.1 - # via - # feast (setup.py) - # sqlalchemy + # via sqlalchemy mypy-extensions==1.0.0 # via mypy mypy-protobuf==3.3.0 - # via feast (setup.py) nbclient==0.10.0 # via nbconvert nbconvert==7.16.4 @@ -492,7 +438,6 @@ notebook-shim==0.2.4 # notebook numpy==1.26.4 # via - # feast (setup.py) # altair # dask # db-dtypes @@ -528,7 +473,6 @@ packaging==24.1 # sphinx pandas==2.2.2 # via - # feast (setup.py) # altair # dask # dask-expr @@ -554,7 +498,6 @@ pexpect==4.9.0 pip==24.1.1 # via pip-tools pip-tools==7.4.1 - # via feast (setup.py) platformdirs==3.11.0 # via # jupyter-core @@ -567,11 +510,8 @@ ply==3.11 portalocker==2.10.0 # via msal-extensions pre-commit==3.3.1 - # via feast (setup.py) prometheus-client==0.20.0 - # via - # feast (setup.py) - # jupyter-server + # via jupyter-server prompt-toolkit==3.0.47 # via ipython proto-plus==1.24.0 @@ -583,7 +523,6 @@ proto-plus==1.24.0 # google-cloud-datastore protobuf==4.25.3 # via - # feast (setup.py) # google-api-core # google-cloud-bigquery # google-cloud-bigquery-storage @@ -600,11 +539,8 @@ protobuf==4.25.3 # proto-plus # substrait psutil==5.9.0 - # via - # feast (setup.py) - # ipykernel + # via ipykernel psycopg[binary, pool]==3.1.19 - # via feast (setup.py) psycopg-binary==3.1.19 # via psycopg psycopg-pool==3.2.2 @@ -616,14 +552,12 @@ ptyprocess==0.7.0 pure-eval==0.2.2 # via stack-data py==1.11.0 - # via feast (setup.py) py-cpuinfo==9.0.0 # via pytest-benchmark py4j==0.10.9.7 # via pyspark pyarrow==15.0.2 # via - # feast (setup.py) # dask-expr # db-dtypes # deltalake @@ -641,19 +575,16 @@ pyasn1==0.6.0 pyasn1-modules==0.4.0 # via google-auth pybindgen==0.22.1 - # via feast (setup.py) pycparser==2.22 # via cffi pydantic==2.7.4 # via - # feast (setup.py) # fastapi # great-expectations pydantic-core==2.18.4 # via pydantic pygments==2.18.0 # via - # feast (setup.py) # ipython # nbconvert # rich @@ -664,11 +595,8 @@ pyjwt[crypto]==2.8.0 # singlestoredb # snowflake-connector-python pymssql==2.3.0 - # via feast (setup.py) pymysql==1.1.1 - # via feast (setup.py) pyodbc==5.1.0 - # via feast (setup.py) pyopenssl==24.1.0 # via snowflake-connector-python pyparsing==3.1.2 @@ -678,10 +606,8 @@ pyproject-hooks==1.1.0 # build # pip-tools pyspark==3.5.1 - # via feast (setup.py) pytest==7.4.4 # via - # feast (setup.py) # pytest-benchmark # pytest-cov # pytest-env @@ -691,21 +617,13 @@ pytest==7.4.4 # pytest-timeout # pytest-xdist pytest-benchmark==3.4.1 - # via feast (setup.py) pytest-cov==5.0.0 - # via feast (setup.py) pytest-env==1.1.3 - # via feast (setup.py) pytest-lazy-fixture==0.6.3 - # via feast (setup.py) pytest-mock==1.10.4 - # via feast (setup.py) pytest-ordering==0.6 - # via feast (setup.py) pytest-timeout==1.4.2 - # via feast (setup.py) pytest-xdist==3.6.1 - # via feast (setup.py) python-dateutil==2.9.0.post0 # via # arrow @@ -717,14 +635,12 @@ python-dateutil==2.9.0.post0 # kubernetes # moto # pandas - # rockset # trino python-dotenv==1.0.1 # via uvicorn python-json-logger==2.0.7 # via jupyter-events python-keycloak==4.2.2 - # via feast (setup.py) python-multipart==0.0.9 # via fastapi pytz==2024.1 @@ -736,7 +652,6 @@ pytz==2024.1 # trino pyyaml==6.0.1 # via - # feast (setup.py) # dask # ibis-substrait # jupyter-events @@ -750,19 +665,15 @@ pyzmq==26.0.3 # jupyter-client # jupyter-server redis==4.6.0 - # via feast (setup.py) referencing==0.35.1 # via # jsonschema # jsonschema-specifications # jupyter-events regex==2024.5.15 - # via - # feast (setup.py) - # parsimonious + # via parsimonious requests==2.32.3 # via - # feast (setup.py) # azure-core # docker # google-api-core @@ -799,8 +710,6 @@ rich==13.7.1 # via # ibis-framework # typer -rockset==2.1.2 - # via feast (setup.py) rpds-py==0.18.1 # via # jsonschema @@ -810,7 +719,6 @@ rsa==4.9 ruamel-yaml==0.17.17 # via great-expectations ruff==0.4.10 - # via feast (setup.py) s3transfer==0.10.2 # via boto3 scipy==1.14.0 @@ -827,7 +735,6 @@ setuptools==70.1.1 shellingham==1.5.4 # via typer singlestoredb==1.4.0 - # via feast (setup.py) six==1.16.0 # via # asttokens @@ -848,13 +755,11 @@ sniffio==1.3.1 snowballstemmer==2.2.0 # via sphinx snowflake-connector-python[pandas]==3.11.0 - # via feast (setup.py) sortedcontainers==2.4.0 # via snowflake-connector-python soupsieve==2.5 # via beautifulsoup4 sphinx==6.2.1 - # via feast (setup.py) sphinxcontrib-applehelp==1.0.8 # via sphinx sphinxcontrib-devhelp==1.0.6 @@ -868,11 +773,9 @@ sphinxcontrib-qthelp==1.0.7 sphinxcontrib-serializinghtml==1.1.10 # via sphinx sqlalchemy[mypy]==2.0.31 - # via feast (setup.py) sqlglot==25.1.0 # via ibis-framework sqlite-vec==0.1.1 - # via feast (setup.py) sqlparams==6.0.1 # via singlestoredb stack-data==0.6.3 @@ -882,21 +785,17 @@ starlette==0.37.2 substrait==0.19.0 # via ibis-substrait tabulate==0.9.0 - # via feast (setup.py) tenacity==8.4.2 - # via feast (setup.py) terminado==0.18.1 # via # jupyter-server # jupyter-server-terminals testcontainers==4.4.0 - # via feast (setup.py) thriftpy2==0.5.1 # via happybase tinycss2==1.3.0 # via nbconvert toml==0.10.2 - # via feast (setup.py) tomli==2.0.1 # via # build @@ -924,9 +823,7 @@ tornado==6.4.1 # notebook # terminado tqdm==4.66.4 - # via - # feast (setup.py) - # great-expectations + # via great-expectations traitlets==5.14.3 # via # comm @@ -943,39 +840,25 @@ traitlets==5.14.3 # nbconvert # nbformat trino==0.328.0 - # via feast (setup.py) typeguard==4.3.0 - # via feast (setup.py) typer==0.12.3 # via fastapi-cli types-cffi==1.16.0.20240331 # via types-pyopenssl types-protobuf==3.19.22 - # via - # feast (setup.py) - # mypy-protobuf + # via mypy-protobuf types-pymysql==1.1.0.20240524 - # via feast (setup.py) types-pyopenssl==24.1.0.20240425 # via types-redis types-python-dateutil==2.9.0.20240316 - # via - # feast (setup.py) - # arrow + # via arrow types-pytz==2024.1.0.20240417 - # via feast (setup.py) types-pyyaml==6.0.12.20240311 - # via feast (setup.py) types-redis==4.6.0.20240425 - # via feast (setup.py) types-requests==2.30.0.0 - # via feast (setup.py) types-setuptools==70.1.0.20240627 - # via - # feast (setup.py) - # types-cffi + # via types-cffi types-tabulate==0.9.0.20240106 - # via feast (setup.py) types-urllib3==1.26.25.14 # via types-requests typing-extensions==4.12.2 @@ -1013,7 +896,6 @@ uri-template==1.3.0 # via jsonschema urllib3==1.26.19 # via - # feast (setup.py) # botocore # docker # elastic-transport @@ -1022,18 +904,13 @@ urllib3==1.26.19 # minio # requests # responses - # rockset # testcontainers uvicorn[standard]==0.30.1 - # via - # feast (setup.py) - # fastapi + # via fastapi uvloop==0.19.0 # via uvicorn virtualenv==20.23.0 - # via - # feast (setup.py) - # pre-commit + # via pre-commit watchfiles==0.22.0 # via uvicorn wcwidth==0.2.13 @@ -1068,4 +945,3 @@ yarl==1.9.4 # via aiohttp zipp==3.19.1 # via importlib-metadata -bigtree==0.19.2 diff --git a/sdk/python/requirements/py3.10-requirements.txt b/sdk/python/requirements/py3.10-requirements.txt index f1ec2b16ab..eed2baaefe 100644 --- a/sdk/python/requirements/py3.10-requirements.txt +++ b/sdk/python/requirements/py3.10-requirements.txt @@ -11,31 +11,30 @@ attrs==23.2.0 # via # jsonschema # referencing +bigtree==0.19.2 +cachetools==5.5.0 + # via google-auth certifi==2024.7.4 # via # httpcore # httpx + # kubernetes # requests charset-normalizer==3.3.2 # via requests click==8.1.7 # via - # feast (setup.py) # dask # typer # uvicorn cloudpickle==3.0.0 # via dask colorama==0.4.6 - # via feast (setup.py) dask[dataframe]==2024.5.0 - # via - # feast (setup.py) - # dask-expr + # via dask-expr dask-expr==1.1.0 # via dask dill==0.3.8 - # via feast (setup.py) dnspython==2.6.1 # via email-validator email-validator==2.1.1 @@ -43,15 +42,16 @@ email-validator==2.1.1 exceptiongroup==1.2.1 # via anyio fastapi==0.111.0 - # via - # feast (setup.py) - # fastapi-cli + # via fastapi-cli fastapi-cli==0.0.2 # via fastapi fsspec==2024.3.1 # via dask +google-auth==2.34.0 + # via kubernetes +greenlet==3.0.3 + # via sqlalchemy gunicorn==22.0.0 - # via feast (setup.py) h11==0.14.0 # via # httpcore @@ -71,13 +71,11 @@ idna==3.7 importlib-metadata==7.1.0 # via dask jinja2==3.1.4 - # via - # feast (setup.py) - # fastapi + # via fastapi jsonschema==4.22.0 - # via feast (setup.py) jsonschema-specifications==2023.12.1 # via jsonschema +kubernetes==20.13.0 locket==1.0.0 # via partd markdown-it-py==3.0.0 @@ -87,19 +85,18 @@ markupsafe==2.1.5 mdurl==0.1.2 # via markdown-it-py mmh3==4.1.0 - # via feast (setup.py) mypy==1.10.0 # via sqlalchemy mypy-extensions==1.0.0 # via mypy mypy-protobuf==3.6.0 - # via feast (setup.py) numpy==1.26.4 # via - # feast (setup.py) # dask # pandas # pyarrow +oauthlib==3.2.2 + # via requests-oauthlib orjson==3.10.3 # via fastapi packaging==24.0 @@ -108,35 +105,33 @@ packaging==24.0 # gunicorn pandas==2.2.2 # via - # feast (setup.py) # dask # dask-expr partd==1.4.2 # via dask prometheus-client==0.20.0 - # via feast (setup.py) protobuf==4.25.3 - # via - # feast (setup.py) - # mypy-protobuf + # via mypy-protobuf psutil==6.0.0 - # via feast (setup.py) pyarrow==16.0.0 + # via dask-expr +pyasn1==0.6.0 # via - # feast (setup.py) - # dask-expr + # pyasn1-modules + # rsa +pyasn1-modules==0.4.0 + # via google-auth pydantic==2.7.1 - # via - # feast (setup.py) - # fastapi + # via fastapi pydantic-core==2.18.2 # via pydantic pygments==2.18.0 - # via - # feast (setup.py) - # rich + # via rich +pyjwt==2.9.0 python-dateutil==2.9.0.post0 - # via pandas + # via + # kubernetes + # pandas python-dotenv==1.0.1 # via uvicorn python-multipart==0.0.9 @@ -145,39 +140,45 @@ pytz==2024.1 # via pandas pyyaml==6.0.1 # via - # feast (setup.py) # dask + # kubernetes # uvicorn referencing==0.35.1 # via # jsonschema # jsonschema-specifications requests==2.31.0 - # via feast (setup.py) + # via + # kubernetes + # requests-oauthlib +requests-oauthlib==2.0.0 + # via kubernetes rich==13.7.1 # via typer rpds-py==0.18.1 # via # jsonschema # referencing +rsa==4.9 + # via google-auth +setuptools==73.0.1 + # via kubernetes shellingham==1.5.4 # via typer six==1.16.0 - # via python-dateutil + # via + # kubernetes + # python-dateutil sniffio==1.3.1 # via # anyio # httpx sqlalchemy[mypy]==2.0.30 - # via feast (setup.py) starlette==0.37.2 # via fastapi tabulate==0.9.0 - # via feast (setup.py) tenacity==8.3.0 - # via feast (setup.py) toml==0.10.2 - # via feast (setup.py) tomli==2.0.1 # via mypy toolz==0.12.1 @@ -185,9 +186,7 @@ toolz==0.12.1 # dask # partd tqdm==4.66.4 - # via feast (setup.py) typeguard==4.2.1 - # via feast (setup.py) typer==0.12.3 # via fastapi-cli types-protobuf==5.26.0.20240422 @@ -208,18 +207,20 @@ tzdata==2024.1 ujson==5.9.0 # via fastapi urllib3==2.2.1 - # via requests + # via + # kubernetes + # requests uvicorn[standard]==0.29.0 # via - # feast (setup.py) # fastapi # fastapi-cli uvloop==0.19.0 # via uvicorn watchfiles==0.21.0 # via uvicorn +websocket-client==1.8.0 + # via kubernetes websockets==12.0 # via uvicorn zipp==3.19.1 # via importlib-metadata -bigtree==0.19.2 diff --git a/sdk/python/requirements/py3.11-ci-requirements.txt b/sdk/python/requirements/py3.11-ci-requirements.txt index fd0b5a6d26..6458540f27 100644 --- a/sdk/python/requirements/py3.11-ci-requirements.txt +++ b/sdk/python/requirements/py3.11-ci-requirements.txt @@ -1,7 +1,6 @@ # This file was autogenerated by uv via the following command: # uv pip compile --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.11-ci-requirements.txt aiobotocore==2.13.1 - # via feast (setup.py) aiohttp==3.9.5 # via aiobotocore aioitertools==0.11.0 @@ -20,8 +19,6 @@ anyio==4.4.0 # jupyter-server # starlette # watchfiles -appnope==0.1.4 - # via ipykernel argon2-cffi==23.1.0 # via jupyter-server argon2-cffi-bindings==21.2.0 @@ -31,7 +28,6 @@ arrow==1.3.0 asn1crypto==1.5.1 # via snowflake-connector-python assertpy==1.1 - # via feast (setup.py) asttokens==2.4.1 # via stack-data async-lru==2.0.4 @@ -50,9 +46,7 @@ azure-core==1.30.2 # azure-identity # azure-storage-blob azure-identity==1.17.1 - # via feast (setup.py) azure-storage-blob==12.20.0 - # via feast (setup.py) babel==2.15.0 # via # jupyterlab-server @@ -62,13 +56,10 @@ beautifulsoup4==4.12.3 bidict==0.23.1 # via ibis-framework bigtree==0.19.2 - # via feast (setup.py) bleach==6.1.0 # via nbconvert boto3==1.34.131 - # via - # feast (setup.py) - # moto + # via moto botocore==1.34.131 # via # aiobotocore @@ -77,13 +68,11 @@ botocore==1.34.131 # s3transfer build==1.2.1 # via - # feast (setup.py) # pip-tools # singlestoredb cachetools==5.3.3 # via google-auth cassandra-driver==3.29.1 - # via feast (setup.py) certifi==2024.7.4 # via # elastic-transport @@ -106,7 +95,6 @@ charset-normalizer==3.3.2 # snowflake-connector-python click==8.1.7 # via - # feast (setup.py) # dask # geomet # great-expectations @@ -116,9 +104,7 @@ click==8.1.7 cloudpickle==3.0.0 # via dask colorama==0.4.6 - # via - # feast (setup.py) - # great-expectations + # via great-expectations comm==0.2.2 # via # ipykernel @@ -127,7 +113,6 @@ coverage[toml]==7.5.4 # via pytest-cov cryptography==42.0.8 # via - # feast (setup.py) # azure-identity # azure-storage-blob # great-expectations @@ -140,9 +125,7 @@ cryptography==42.0.8 # types-pyopenssl # types-redis dask[dataframe]==2024.6.2 - # via - # feast (setup.py) - # dask-expr + # via dask-expr dask-expr==1.1.6 # via dask db-dtypes==1.2.0 @@ -154,11 +137,9 @@ decorator==5.1.1 defusedxml==0.7.1 # via nbconvert deltalake==0.18.1 - # via feast (setup.py) deprecation==2.1.0 # via python-keycloak dill==0.3.8 - # via feast (setup.py) distlib==0.3.8 # via virtualenv dnspython==2.6.1 @@ -172,7 +153,6 @@ duckdb==0.10.3 elastic-transport==8.13.1 # via elasticsearch elasticsearch==8.14.0 - # via feast (setup.py) email-validator==2.2.0 # via fastapi entrypoints==0.4 @@ -182,7 +162,6 @@ execnet==2.1.1 executing==2.0.1 # via stack-data fastapi==0.111.0 - # via feast (setup.py) fastapi-cli==0.0.4 # via fastapi fastjsonschema==2.20.0 @@ -198,16 +177,11 @@ frozenlist==1.4.1 # aiohttp # aiosignal fsspec==2023.12.2 - # via - # feast (setup.py) - # dask -geojson==2.5.0 - # via rockset + # via dask geomet==0.2.1.post1 # via cassandra-driver google-api-core[grpc]==2.19.1 # via - # feast (setup.py) # google-cloud-bigquery # google-cloud-bigquery-storage # google-cloud-bigtable @@ -222,11 +196,8 @@ google-auth==2.30.0 # google-cloud-storage # kubernetes google-cloud-bigquery[pandas]==3.13.0 - # via feast (setup.py) google-cloud-bigquery-storage==2.25.0 - # via feast (setup.py) google-cloud-bigtable==2.24.0 - # via feast (setup.py) google-cloud-core==2.4.1 # via # google-cloud-bigquery @@ -234,9 +205,7 @@ google-cloud-core==2.4.1 # google-cloud-datastore # google-cloud-storage google-cloud-datastore==2.19.0 - # via feast (setup.py) google-cloud-storage==2.17.0 - # via feast (setup.py) google-crc32c==1.5.0 # via # google-cloud-storage @@ -247,17 +216,16 @@ google-resumable-media==2.7.1 # google-cloud-storage googleapis-common-protos[grpc]==1.63.2 # via - # feast (setup.py) # google-api-core # grpc-google-iam-v1 # grpcio-status great-expectations==0.18.16 - # via feast (setup.py) +greenlet==3.0.3 + # via sqlalchemy grpc-google-iam-v1==0.13.1 # via google-cloud-bigtable grpcio==1.64.1 # via - # feast (setup.py) # google-api-core # google-cloud-bigquery # googleapis-common-protos @@ -268,43 +236,31 @@ grpcio==1.64.1 # grpcio-testing # grpcio-tools grpcio-health-checking==1.62.2 - # via feast (setup.py) grpcio-reflection==1.62.2 - # via feast (setup.py) grpcio-status==1.62.2 # via google-api-core grpcio-testing==1.62.2 - # via feast (setup.py) grpcio-tools==1.62.2 - # via feast (setup.py) gunicorn==22.0.0 - # via feast (setup.py) h11==0.14.0 # via # httpcore # uvicorn happybase==1.2.0 - # via feast (setup.py) hazelcast-python-client==5.4.0 - # via feast (setup.py) hiredis==2.3.2 - # via feast (setup.py) httpcore==1.0.5 # via httpx httptools==0.6.1 # via uvicorn httpx==0.27.0 # via - # feast (setup.py) # fastapi # jupyterlab # python-keycloak ibis-framework[duckdb]==9.1.0 - # via - # feast (setup.py) - # ibis-substrait + # via ibis-substrait ibis-substrait==4.0.0 - # via feast (setup.py) identify==2.5.36 # via pre-commit idna==3.7 @@ -339,7 +295,6 @@ jedi==0.19.1 # via ipython jinja2==3.1.4 # via - # feast (setup.py) # altair # fastapi # great-expectations @@ -363,7 +318,6 @@ jsonpointer==3.0.0 # jsonschema jsonschema[format-nongpl]==4.22.0 # via - # feast (setup.py) # altair # great-expectations # jupyter-events @@ -411,7 +365,6 @@ jupyterlab-widgets==3.0.11 jwcrypto==1.5.6 # via python-keycloak kubernetes==20.13.0 - # via feast (setup.py) locket==1.0.0 # via partd makefun==1.15.2 @@ -432,17 +385,13 @@ matplotlib-inline==0.1.7 mdurl==0.1.2 # via markdown-it-py minio==7.1.0 - # via feast (setup.py) mistune==3.0.2 # via # great-expectations # nbconvert mmh3==4.1.0 - # via feast (setup.py) mock==2.0.0 - # via feast (setup.py) moto==4.2.14 - # via feast (setup.py) msal==1.29.0 # via # azure-identity @@ -454,13 +403,10 @@ multidict==6.0.5 # aiohttp # yarl mypy==1.10.1 - # via - # feast (setup.py) - # sqlalchemy + # via sqlalchemy mypy-extensions==1.0.0 # via mypy mypy-protobuf==3.3.0 - # via feast (setup.py) nbclient==0.10.0 # via nbconvert nbconvert==7.16.4 @@ -483,7 +429,6 @@ notebook-shim==0.2.4 # notebook numpy==1.26.4 # via - # feast (setup.py) # altair # dask # db-dtypes @@ -519,7 +464,6 @@ packaging==24.1 # sphinx pandas==2.2.2 # via - # feast (setup.py) # altair # dask # dask-expr @@ -545,7 +489,6 @@ pexpect==4.9.0 pip==24.1.1 # via pip-tools pip-tools==7.4.1 - # via feast (setup.py) platformdirs==3.11.0 # via # jupyter-core @@ -558,11 +501,8 @@ ply==3.11 portalocker==2.10.0 # via msal-extensions pre-commit==3.3.1 - # via feast (setup.py) prometheus-client==0.20.0 - # via - # feast (setup.py) - # jupyter-server + # via jupyter-server prompt-toolkit==3.0.47 # via ipython proto-plus==1.24.0 @@ -574,7 +514,6 @@ proto-plus==1.24.0 # google-cloud-datastore protobuf==4.25.3 # via - # feast (setup.py) # google-api-core # google-cloud-bigquery # google-cloud-bigquery-storage @@ -591,11 +530,8 @@ protobuf==4.25.3 # proto-plus # substrait psutil==5.9.0 - # via - # feast (setup.py) - # ipykernel + # via ipykernel psycopg[binary, pool]==3.1.19 - # via feast (setup.py) psycopg-binary==3.1.19 # via psycopg psycopg-pool==3.2.2 @@ -607,14 +543,12 @@ ptyprocess==0.7.0 pure-eval==0.2.2 # via stack-data py==1.11.0 - # via feast (setup.py) py-cpuinfo==9.0.0 # via pytest-benchmark py4j==0.10.9.7 # via pyspark pyarrow==15.0.2 # via - # feast (setup.py) # dask-expr # db-dtypes # deltalake @@ -632,19 +566,16 @@ pyasn1==0.6.0 pyasn1-modules==0.4.0 # via google-auth pybindgen==0.22.1 - # via feast (setup.py) pycparser==2.22 # via cffi pydantic==2.7.4 # via - # feast (setup.py) # fastapi # great-expectations pydantic-core==2.18.4 # via pydantic pygments==2.18.0 # via - # feast (setup.py) # ipython # nbconvert # rich @@ -655,11 +586,8 @@ pyjwt[crypto]==2.8.0 # singlestoredb # snowflake-connector-python pymssql==2.3.0 - # via feast (setup.py) pymysql==1.1.1 - # via feast (setup.py) pyodbc==5.1.0 - # via feast (setup.py) pyopenssl==24.1.0 # via snowflake-connector-python pyparsing==3.1.2 @@ -669,10 +597,8 @@ pyproject-hooks==1.1.0 # build # pip-tools pyspark==3.5.1 - # via feast (setup.py) pytest==7.4.4 # via - # feast (setup.py) # pytest-benchmark # pytest-cov # pytest-env @@ -682,21 +608,13 @@ pytest==7.4.4 # pytest-timeout # pytest-xdist pytest-benchmark==3.4.1 - # via feast (setup.py) pytest-cov==5.0.0 - # via feast (setup.py) pytest-env==1.1.3 - # via feast (setup.py) pytest-lazy-fixture==0.6.3 - # via feast (setup.py) pytest-mock==1.10.4 - # via feast (setup.py) pytest-ordering==0.6 - # via feast (setup.py) pytest-timeout==1.4.2 - # via feast (setup.py) pytest-xdist==3.6.1 - # via feast (setup.py) python-dateutil==2.9.0.post0 # via # arrow @@ -708,14 +626,12 @@ python-dateutil==2.9.0.post0 # kubernetes # moto # pandas - # rockset # trino python-dotenv==1.0.1 # via uvicorn python-json-logger==2.0.7 # via jupyter-events python-keycloak==4.2.2 - # via feast (setup.py) python-multipart==0.0.9 # via fastapi pytz==2024.1 @@ -727,7 +643,6 @@ pytz==2024.1 # trino pyyaml==6.0.1 # via - # feast (setup.py) # dask # ibis-substrait # jupyter-events @@ -741,19 +656,15 @@ pyzmq==26.0.3 # jupyter-client # jupyter-server redis==4.6.0 - # via feast (setup.py) referencing==0.35.1 # via # jsonschema # jsonschema-specifications # jupyter-events regex==2024.5.15 - # via - # feast (setup.py) - # parsimonious + # via parsimonious requests==2.32.3 # via - # feast (setup.py) # azure-core # docker # google-api-core @@ -790,8 +701,6 @@ rich==13.7.1 # via # ibis-framework # typer -rockset==2.1.2 - # via feast (setup.py) rpds-py==0.18.1 # via # jsonschema @@ -801,7 +710,6 @@ rsa==4.9 ruamel-yaml==0.17.17 # via great-expectations ruff==0.4.10 - # via feast (setup.py) s3transfer==0.10.2 # via boto3 scipy==1.14.0 @@ -818,7 +726,6 @@ setuptools==70.1.1 shellingham==1.5.4 # via typer singlestoredb==1.4.0 - # via feast (setup.py) six==1.16.0 # via # asttokens @@ -839,13 +746,11 @@ sniffio==1.3.1 snowballstemmer==2.2.0 # via sphinx snowflake-connector-python[pandas]==3.11.0 - # via feast (setup.py) sortedcontainers==2.4.0 # via snowflake-connector-python soupsieve==2.5 # via beautifulsoup4 sphinx==6.2.1 - # via feast (setup.py) sphinxcontrib-applehelp==1.0.8 # via sphinx sphinxcontrib-devhelp==1.0.6 @@ -859,11 +764,9 @@ sphinxcontrib-qthelp==1.0.7 sphinxcontrib-serializinghtml==1.1.10 # via sphinx sqlalchemy[mypy]==2.0.31 - # via feast (setup.py) sqlglot==25.1.0 # via ibis-framework sqlite-vec==0.1.1 - # via feast (setup.py) sqlparams==6.0.1 # via singlestoredb stack-data==0.6.3 @@ -873,21 +776,17 @@ starlette==0.37.2 substrait==0.19.0 # via ibis-substrait tabulate==0.9.0 - # via feast (setup.py) tenacity==8.4.2 - # via feast (setup.py) terminado==0.18.1 # via # jupyter-server # jupyter-server-terminals testcontainers==4.4.0 - # via feast (setup.py) thriftpy2==0.5.1 # via happybase tinycss2==1.3.0 # via nbconvert toml==0.10.2 - # via feast (setup.py) tomlkit==0.12.5 # via snowflake-connector-python toolz==0.12.1 @@ -905,9 +804,7 @@ tornado==6.4.1 # notebook # terminado tqdm==4.66.4 - # via - # feast (setup.py) - # great-expectations + # via great-expectations traitlets==5.14.3 # via # comm @@ -924,39 +821,25 @@ traitlets==5.14.3 # nbconvert # nbformat trino==0.328.0 - # via feast (setup.py) typeguard==4.3.0 - # via feast (setup.py) typer==0.12.3 # via fastapi-cli types-cffi==1.16.0.20240331 # via types-pyopenssl types-protobuf==3.19.22 - # via - # feast (setup.py) - # mypy-protobuf + # via mypy-protobuf types-pymysql==1.1.0.20240524 - # via feast (setup.py) types-pyopenssl==24.1.0.20240425 # via types-redis types-python-dateutil==2.9.0.20240316 - # via - # feast (setup.py) - # arrow + # via arrow types-pytz==2024.1.0.20240417 - # via feast (setup.py) types-pyyaml==6.0.12.20240311 - # via feast (setup.py) types-redis==4.6.0.20240425 - # via feast (setup.py) types-requests==2.30.0.0 - # via feast (setup.py) types-setuptools==70.1.0.20240627 - # via - # feast (setup.py) - # types-cffi + # via types-cffi types-tabulate==0.9.0.20240106 - # via feast (setup.py) types-urllib3==1.26.25.14 # via types-requests typing-extensions==4.12.2 @@ -991,7 +874,6 @@ uri-template==1.3.0 # via jsonschema urllib3==1.26.19 # via - # feast (setup.py) # botocore # docker # elastic-transport @@ -1000,18 +882,13 @@ urllib3==1.26.19 # minio # requests # responses - # rockset # testcontainers uvicorn[standard]==0.30.1 - # via - # feast (setup.py) - # fastapi + # via fastapi uvloop==0.19.0 # via uvicorn virtualenv==20.23.0 - # via - # feast (setup.py) - # pre-commit + # via pre-commit watchfiles==0.22.0 # via uvicorn wcwidth==0.2.13 diff --git a/sdk/python/requirements/py3.11-requirements.txt b/sdk/python/requirements/py3.11-requirements.txt index e51452a594..9f6dff962b 100644 --- a/sdk/python/requirements/py3.11-requirements.txt +++ b/sdk/python/requirements/py3.11-requirements.txt @@ -11,45 +11,45 @@ attrs==23.2.0 # via # jsonschema # referencing +bigtree==0.19.2 +cachetools==5.5.0 + # via google-auth certifi==2024.7.4 # via # httpcore # httpx + # kubernetes # requests charset-normalizer==3.3.2 # via requests click==8.1.7 # via - # feast (setup.py) # dask # typer # uvicorn cloudpickle==3.0.0 # via dask colorama==0.4.6 - # via feast (setup.py) dask[dataframe]==2024.5.0 - # via - # feast (setup.py) - # dask-expr + # via dask-expr dask-expr==1.1.0 # via dask dill==0.3.8 - # via feast (setup.py) dnspython==2.6.1 # via email-validator email-validator==2.1.1 # via fastapi fastapi==0.111.0 - # via - # feast (setup.py) - # fastapi-cli + # via fastapi-cli fastapi-cli==0.0.2 # via fastapi fsspec==2024.3.1 # via dask +google-auth==2.34.0 + # via kubernetes +greenlet==3.0.3 + # via sqlalchemy gunicorn==22.0.0 - # via feast (setup.py) h11==0.14.0 # via # httpcore @@ -69,13 +69,11 @@ idna==3.7 importlib-metadata==7.1.0 # via dask jinja2==3.1.4 - # via - # feast (setup.py) - # fastapi + # via fastapi jsonschema==4.22.0 - # via feast (setup.py) jsonschema-specifications==2023.12.1 # via jsonschema +kubernetes==20.13.0 locket==1.0.0 # via partd markdown-it-py==3.0.0 @@ -85,19 +83,18 @@ markupsafe==2.1.5 mdurl==0.1.2 # via markdown-it-py mmh3==4.1.0 - # via feast (setup.py) mypy==1.10.0 # via sqlalchemy mypy-extensions==1.0.0 # via mypy mypy-protobuf==3.6.0 - # via feast (setup.py) numpy==1.26.4 # via - # feast (setup.py) # dask # pandas # pyarrow +oauthlib==3.2.2 + # via requests-oauthlib orjson==3.10.3 # via fastapi packaging==24.0 @@ -106,35 +103,33 @@ packaging==24.0 # gunicorn pandas==2.2.2 # via - # feast (setup.py) # dask # dask-expr partd==1.4.2 # via dask prometheus-client==0.20.0 - # via feast (setup.py) protobuf==4.25.3 - # via - # feast (setup.py) - # mypy-protobuf + # via mypy-protobuf psutil==6.0.0 - # via feast (setup.py) pyarrow==16.0.0 + # via dask-expr +pyasn1==0.6.0 # via - # feast (setup.py) - # dask-expr + # pyasn1-modules + # rsa +pyasn1-modules==0.4.0 + # via google-auth pydantic==2.7.1 - # via - # feast (setup.py) - # fastapi + # via fastapi pydantic-core==2.18.2 # via pydantic pygments==2.18.0 - # via - # feast (setup.py) - # rich + # via rich +pyjwt==2.9.0 python-dateutil==2.9.0.post0 - # via pandas + # via + # kubernetes + # pandas python-dotenv==1.0.1 # via uvicorn python-multipart==0.0.9 @@ -143,47 +138,51 @@ pytz==2024.1 # via pandas pyyaml==6.0.1 # via - # feast (setup.py) # dask + # kubernetes # uvicorn referencing==0.35.1 # via # jsonschema # jsonschema-specifications requests==2.31.0 - # via feast (setup.py) + # via + # kubernetes + # requests-oauthlib +requests-oauthlib==2.0.0 + # via kubernetes rich==13.7.1 # via typer rpds-py==0.18.1 # via # jsonschema # referencing +rsa==4.9 + # via google-auth +setuptools==73.0.1 + # via kubernetes shellingham==1.5.4 # via typer six==1.16.0 - # via python-dateutil + # via + # kubernetes + # python-dateutil sniffio==1.3.1 # via # anyio # httpx sqlalchemy[mypy]==2.0.30 - # via feast (setup.py) starlette==0.37.2 # via fastapi tabulate==0.9.0 - # via feast (setup.py) tenacity==8.3.0 - # via feast (setup.py) toml==0.10.2 - # via feast (setup.py) toolz==0.12.1 # via # dask # partd tqdm==4.66.4 - # via feast (setup.py) typeguard==4.2.1 - # via feast (setup.py) typer==0.12.3 # via fastapi-cli types-protobuf==5.26.0.20240422 @@ -202,18 +201,20 @@ tzdata==2024.1 ujson==5.9.0 # via fastapi urllib3==2.2.1 - # via requests + # via + # kubernetes + # requests uvicorn[standard]==0.29.0 # via - # feast (setup.py) # fastapi # fastapi-cli uvloop==0.19.0 # via uvicorn watchfiles==0.21.0 # via uvicorn +websocket-client==1.8.0 + # via kubernetes websockets==12.0 # via uvicorn zipp==3.19.1 # via importlib-metadata -bigtree==0.19.2 diff --git a/sdk/python/requirements/py3.9-ci-requirements.txt b/sdk/python/requirements/py3.9-ci-requirements.txt index be30f032a9..58ec69fe2d 100644 --- a/sdk/python/requirements/py3.9-ci-requirements.txt +++ b/sdk/python/requirements/py3.9-ci-requirements.txt @@ -1,7 +1,6 @@ # This file was autogenerated by uv via the following command: # uv pip compile --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.9-ci-requirements.txt aiobotocore==2.13.1 - # via feast (setup.py) aiohttp==3.9.5 # via aiobotocore aioitertools==0.11.0 @@ -20,8 +19,6 @@ anyio==4.4.0 # jupyter-server # starlette # watchfiles -appnope==0.1.4 - # via ipykernel argon2-cffi==23.1.0 # via jupyter-server argon2-cffi-bindings==21.2.0 @@ -31,17 +28,16 @@ arrow==1.3.0 asn1crypto==1.5.1 # via snowflake-connector-python assertpy==1.1 - # via feast (setup.py) asttokens==2.4.1 # via stack-data async-lru==2.0.4 # via jupyterlab +async-property==0.2.2 + # via python-keycloak async-timeout==4.0.3 # via # aiohttp # redis -async-property==0.2.2 - # via python-keycloak atpublic==4.1.0 # via ibis-framework attrs==23.2.0 @@ -54,9 +50,7 @@ azure-core==1.30.2 # azure-identity # azure-storage-blob azure-identity==1.17.1 - # via feast (setup.py) azure-storage-blob==12.20.0 - # via feast (setup.py) babel==2.15.0 # via # jupyterlab-server @@ -66,13 +60,10 @@ beautifulsoup4==4.12.3 bidict==0.23.1 # via ibis-framework bigtree==0.19.2 - # via feast (setup.py) bleach==6.1.0 # via nbconvert boto3==1.34.131 - # via - # feast (setup.py) - # moto + # via moto botocore==1.34.131 # via # aiobotocore @@ -81,13 +72,11 @@ botocore==1.34.131 # s3transfer build==1.2.1 # via - # feast (setup.py) # pip-tools # singlestoredb cachetools==5.3.3 # via google-auth cassandra-driver==3.29.1 - # via feast (setup.py) certifi==2024.7.4 # via # elastic-transport @@ -110,7 +99,6 @@ charset-normalizer==3.3.2 # snowflake-connector-python click==8.1.7 # via - # feast (setup.py) # dask # geomet # great-expectations @@ -120,9 +108,7 @@ click==8.1.7 cloudpickle==3.0.0 # via dask colorama==0.4.6 - # via - # feast (setup.py) - # great-expectations + # via great-expectations comm==0.2.2 # via # ipykernel @@ -131,7 +117,6 @@ coverage[toml]==7.5.4 # via pytest-cov cryptography==42.0.8 # via - # feast (setup.py) # azure-identity # azure-storage-blob # great-expectations @@ -144,9 +129,7 @@ cryptography==42.0.8 # types-pyopenssl # types-redis dask[dataframe]==2024.6.2 - # via - # feast (setup.py) - # dask-expr + # via dask-expr dask-expr==1.1.6 # via dask db-dtypes==1.2.0 @@ -158,11 +141,9 @@ decorator==5.1.1 defusedxml==0.7.1 # via nbconvert deltalake==0.18.1 - # via feast (setup.py) deprecation==2.1.0 # via python-keycloak dill==0.3.8 - # via feast (setup.py) distlib==0.3.8 # via virtualenv dnspython==2.6.1 @@ -176,7 +157,6 @@ duckdb==0.10.3 elastic-transport==8.13.1 # via elasticsearch elasticsearch==8.14.0 - # via feast (setup.py) email-validator==2.2.0 # via fastapi entrypoints==0.4 @@ -191,7 +171,6 @@ execnet==2.1.1 executing==2.0.1 # via stack-data fastapi==0.111.0 - # via feast (setup.py) fastapi-cli==0.0.4 # via fastapi fastjsonschema==2.20.0 @@ -207,16 +186,11 @@ frozenlist==1.4.1 # aiohttp # aiosignal fsspec==2023.12.2 - # via - # feast (setup.py) - # dask -geojson==2.5.0 - # via rockset + # via dask geomet==0.2.1.post1 # via cassandra-driver google-api-core[grpc]==2.19.1 # via - # feast (setup.py) # google-cloud-bigquery # google-cloud-bigquery-storage # google-cloud-bigtable @@ -231,11 +205,8 @@ google-auth==2.30.0 # google-cloud-storage # kubernetes google-cloud-bigquery[pandas]==3.13.0 - # via feast (setup.py) google-cloud-bigquery-storage==2.25.0 - # via feast (setup.py) google-cloud-bigtable==2.24.0 - # via feast (setup.py) google-cloud-core==2.4.1 # via # google-cloud-bigquery @@ -243,9 +214,7 @@ google-cloud-core==2.4.1 # google-cloud-datastore # google-cloud-storage google-cloud-datastore==2.19.0 - # via feast (setup.py) google-cloud-storage==2.17.0 - # via feast (setup.py) google-crc32c==1.5.0 # via # google-cloud-storage @@ -256,17 +225,16 @@ google-resumable-media==2.7.1 # google-cloud-storage googleapis-common-protos[grpc]==1.63.2 # via - # feast (setup.py) # google-api-core # grpc-google-iam-v1 # grpcio-status great-expectations==0.18.16 - # via feast (setup.py) +greenlet==3.0.3 + # via sqlalchemy grpc-google-iam-v1==0.13.1 # via google-cloud-bigtable grpcio==1.64.1 # via - # feast (setup.py) # google-api-core # google-cloud-bigquery # googleapis-common-protos @@ -277,43 +245,31 @@ grpcio==1.64.1 # grpcio-testing # grpcio-tools grpcio-health-checking==1.62.2 - # via feast (setup.py) grpcio-reflection==1.62.2 - # via feast (setup.py) grpcio-status==1.62.2 # via google-api-core grpcio-testing==1.62.2 - # via feast (setup.py) grpcio-tools==1.62.2 - # via feast (setup.py) gunicorn==22.0.0 - # via feast (setup.py) h11==0.14.0 # via # httpcore # uvicorn happybase==1.2.0 - # via feast (setup.py) hazelcast-python-client==5.4.0 - # via feast (setup.py) hiredis==2.3.2 - # via feast (setup.py) httpcore==1.0.5 # via httpx httptools==0.6.1 # via uvicorn httpx==0.27.0 # via - # feast (setup.py) # fastapi # jupyterlab # python-keycloak ibis-framework[duckdb]==9.0.0 - # via - # feast (setup.py) - # ibis-substrait + # via ibis-substrait ibis-substrait==4.0.0 - # via feast (setup.py) identify==2.5.36 # via pre-commit idna==3.7 @@ -357,7 +313,6 @@ jedi==0.19.1 # via ipython jinja2==3.1.4 # via - # feast (setup.py) # altair # fastapi # great-expectations @@ -381,7 +336,6 @@ jsonpointer==3.0.0 # jsonschema jsonschema[format-nongpl]==4.22.0 # via - # feast (setup.py) # altair # great-expectations # jupyter-events @@ -429,7 +383,6 @@ jupyterlab-widgets==3.0.11 jwcrypto==1.5.6 # via python-keycloak kubernetes==20.13.0 - # via feast (setup.py) locket==1.0.0 # via partd makefun==1.15.2 @@ -450,17 +403,13 @@ matplotlib-inline==0.1.7 mdurl==0.1.2 # via markdown-it-py minio==7.1.0 - # via feast (setup.py) mistune==3.0.2 # via # great-expectations # nbconvert mmh3==4.1.0 - # via feast (setup.py) mock==2.0.0 - # via feast (setup.py) moto==4.2.14 - # via feast (setup.py) msal==1.29.0 # via # azure-identity @@ -472,13 +421,10 @@ multidict==6.0.5 # aiohttp # yarl mypy==1.10.1 - # via - # feast (setup.py) - # sqlalchemy + # via sqlalchemy mypy-extensions==1.0.0 # via mypy mypy-protobuf==3.3.0 - # via feast (setup.py) nbclient==0.10.0 # via nbconvert nbconvert==7.16.4 @@ -501,7 +447,6 @@ notebook-shim==0.2.4 # notebook numpy==1.26.4 # via - # feast (setup.py) # altair # dask # db-dtypes @@ -537,7 +482,6 @@ packaging==24.1 # sphinx pandas==2.2.2 # via - # feast (setup.py) # altair # dask # dask-expr @@ -563,7 +507,6 @@ pexpect==4.9.0 pip==24.1.1 # via pip-tools pip-tools==7.4.1 - # via feast (setup.py) platformdirs==3.11.0 # via # jupyter-core @@ -576,11 +519,8 @@ ply==3.11 portalocker==2.10.0 # via msal-extensions pre-commit==3.3.1 - # via feast (setup.py) prometheus-client==0.20.0 - # via - # feast (setup.py) - # jupyter-server + # via jupyter-server prompt-toolkit==3.0.47 # via ipython proto-plus==1.24.0 @@ -592,7 +532,6 @@ proto-plus==1.24.0 # google-cloud-datastore protobuf==4.25.3 # via - # feast (setup.py) # google-api-core # google-cloud-bigquery # google-cloud-bigquery-storage @@ -609,11 +548,8 @@ protobuf==4.25.3 # proto-plus # substrait psutil==5.9.0 - # via - # feast (setup.py) - # ipykernel + # via ipykernel psycopg[binary, pool]==3.1.18 - # via feast (setup.py) psycopg-binary==3.1.18 # via psycopg psycopg-pool==3.2.2 @@ -625,14 +561,12 @@ ptyprocess==0.7.0 pure-eval==0.2.2 # via stack-data py==1.11.0 - # via feast (setup.py) py-cpuinfo==9.0.0 # via pytest-benchmark py4j==0.10.9.7 # via pyspark pyarrow==15.0.2 # via - # feast (setup.py) # dask-expr # db-dtypes # deltalake @@ -650,19 +584,16 @@ pyasn1==0.6.0 pyasn1-modules==0.4.0 # via google-auth pybindgen==0.22.1 - # via feast (setup.py) pycparser==2.22 # via cffi pydantic==2.7.4 # via - # feast (setup.py) # fastapi # great-expectations pydantic-core==2.18.4 # via pydantic pygments==2.18.0 # via - # feast (setup.py) # ipython # nbconvert # rich @@ -673,11 +604,8 @@ pyjwt[crypto]==2.8.0 # singlestoredb # snowflake-connector-python pymssql==2.3.0 - # via feast (setup.py) pymysql==1.1.1 - # via feast (setup.py) pyodbc==5.1.0 - # via feast (setup.py) pyopenssl==24.1.0 # via snowflake-connector-python pyparsing==3.1.2 @@ -687,10 +615,8 @@ pyproject-hooks==1.1.0 # build # pip-tools pyspark==3.5.1 - # via feast (setup.py) pytest==7.4.4 # via - # feast (setup.py) # pytest-benchmark # pytest-cov # pytest-env @@ -700,21 +626,13 @@ pytest==7.4.4 # pytest-timeout # pytest-xdist pytest-benchmark==3.4.1 - # via feast (setup.py) pytest-cov==5.0.0 - # via feast (setup.py) pytest-env==1.1.3 - # via feast (setup.py) pytest-lazy-fixture==0.6.3 - # via feast (setup.py) pytest-mock==1.10.4 - # via feast (setup.py) pytest-ordering==0.6 - # via feast (setup.py) pytest-timeout==1.4.2 - # via feast (setup.py) pytest-xdist==3.6.1 - # via feast (setup.py) python-dateutil==2.9.0.post0 # via # arrow @@ -726,14 +644,12 @@ python-dateutil==2.9.0.post0 # kubernetes # moto # pandas - # rockset # trino python-dotenv==1.0.1 # via uvicorn python-json-logger==2.0.7 # via jupyter-events python-keycloak==4.2.2 - # via feast (setup.py) python-multipart==0.0.9 # via fastapi pytz==2024.1 @@ -745,7 +661,6 @@ pytz==2024.1 # trino pyyaml==6.0.1 # via - # feast (setup.py) # dask # ibis-substrait # jupyter-events @@ -759,19 +674,15 @@ pyzmq==26.0.3 # jupyter-client # jupyter-server redis==4.6.0 - # via feast (setup.py) referencing==0.35.1 # via # jsonschema # jsonschema-specifications # jupyter-events regex==2024.5.15 - # via - # feast (setup.py) - # parsimonious + # via parsimonious requests==2.32.3 # via - # feast (setup.py) # azure-core # docker # google-api-core @@ -808,8 +719,6 @@ rich==13.7.1 # via # ibis-framework # typer -rockset==2.1.2 - # via feast (setup.py) rpds-py==0.18.1 # via # jsonschema @@ -821,7 +730,6 @@ ruamel-yaml==0.17.17 ruamel-yaml-clib==0.2.8 # via ruamel-yaml ruff==0.4.10 - # via feast (setup.py) s3transfer==0.10.2 # via boto3 scipy==1.13.1 @@ -838,7 +746,6 @@ setuptools==70.1.1 shellingham==1.5.4 # via typer singlestoredb==1.4.0 - # via feast (setup.py) six==1.16.0 # via # asttokens @@ -859,13 +766,11 @@ sniffio==1.3.1 snowballstemmer==2.2.0 # via sphinx snowflake-connector-python[pandas]==3.11.0 - # via feast (setup.py) sortedcontainers==2.4.0 # via snowflake-connector-python soupsieve==2.5 # via beautifulsoup4 sphinx==6.2.1 - # via feast (setup.py) sphinxcontrib-applehelp==1.0.8 # via sphinx sphinxcontrib-devhelp==1.0.6 @@ -879,11 +784,9 @@ sphinxcontrib-qthelp==1.0.7 sphinxcontrib-serializinghtml==1.1.10 # via sphinx sqlalchemy[mypy]==2.0.31 - # via feast (setup.py) sqlglot==23.12.2 # via ibis-framework sqlite-vec==0.1.1 - # via feast (setup.py) sqlparams==6.0.1 # via singlestoredb stack-data==0.6.3 @@ -893,21 +796,17 @@ starlette==0.37.2 substrait==0.19.0 # via ibis-substrait tabulate==0.9.0 - # via feast (setup.py) tenacity==8.4.2 - # via feast (setup.py) terminado==0.18.1 # via # jupyter-server # jupyter-server-terminals testcontainers==4.4.0 - # via feast (setup.py) thriftpy2==0.5.1 # via happybase tinycss2==1.3.0 # via nbconvert toml==0.10.2 - # via feast (setup.py) tomli==2.0.1 # via # build @@ -935,9 +834,7 @@ tornado==6.4.1 # notebook # terminado tqdm==4.66.4 - # via - # feast (setup.py) - # great-expectations + # via great-expectations traitlets==5.14.3 # via # comm @@ -954,39 +851,25 @@ traitlets==5.14.3 # nbconvert # nbformat trino==0.328.0 - # via feast (setup.py) typeguard==4.3.0 - # via feast (setup.py) typer==0.12.3 # via fastapi-cli types-cffi==1.16.0.20240331 # via types-pyopenssl types-protobuf==3.19.22 - # via - # feast (setup.py) - # mypy-protobuf + # via mypy-protobuf types-pymysql==1.1.0.20240524 - # via feast (setup.py) types-pyopenssl==24.1.0.20240425 # via types-redis types-python-dateutil==2.9.0.20240316 - # via - # feast (setup.py) - # arrow + # via arrow types-pytz==2024.1.0.20240417 - # via feast (setup.py) types-pyyaml==6.0.12.20240311 - # via feast (setup.py) types-redis==4.6.0.20240425 - # via feast (setup.py) types-requests==2.30.0.0 - # via feast (setup.py) types-setuptools==70.1.0.20240627 - # via - # feast (setup.py) - # types-cffi + # via types-cffi types-tabulate==0.9.0.20240106 - # via feast (setup.py) types-urllib3==1.26.25.14 # via types-requests typing-extensions==4.12.2 @@ -1026,7 +909,6 @@ uri-template==1.3.0 # via jsonschema urllib3==1.26.19 # via - # feast (setup.py) # botocore # docker # elastic-transport @@ -1035,19 +917,14 @@ urllib3==1.26.19 # minio # requests # responses - # rockset # snowflake-connector-python # testcontainers uvicorn[standard]==0.30.1 - # via - # feast (setup.py) - # fastapi + # via fastapi uvloop==0.19.0 # via uvicorn virtualenv==20.23.0 - # via - # feast (setup.py) - # pre-commit + # via pre-commit watchfiles==0.22.0 # via uvicorn wcwidth==0.2.13 diff --git a/sdk/python/requirements/py3.9-requirements.txt b/sdk/python/requirements/py3.9-requirements.txt index 0b3c8a33c9..960eaa6554 100644 --- a/sdk/python/requirements/py3.9-requirements.txt +++ b/sdk/python/requirements/py3.9-requirements.txt @@ -11,31 +11,30 @@ attrs==23.2.0 # via # jsonschema # referencing +bigtree==0.19.2 +cachetools==5.5.0 + # via google-auth certifi==2024.7.4 # via # httpcore # httpx + # kubernetes # requests charset-normalizer==3.3.2 # via requests click==8.1.7 # via - # feast (setup.py) # dask # typer # uvicorn cloudpickle==3.0.0 # via dask colorama==0.4.6 - # via feast (setup.py) dask[dataframe]==2024.5.0 - # via - # feast (setup.py) - # dask-expr + # via dask-expr dask-expr==1.1.0 # via dask dill==0.3.8 - # via feast (setup.py) dnspython==2.6.1 # via email-validator email-validator==2.1.1 @@ -43,15 +42,16 @@ email-validator==2.1.1 exceptiongroup==1.2.2 # via anyio fastapi==0.111.0 - # via - # feast (setup.py) - # fastapi-cli + # via fastapi-cli fastapi-cli==0.0.2 # via fastapi fsspec==2024.3.1 # via dask +google-auth==2.34.0 + # via kubernetes +greenlet==3.0.3 + # via sqlalchemy gunicorn==22.0.0 - # via feast (setup.py) h11==0.14.0 # via # httpcore @@ -73,13 +73,11 @@ importlib-metadata==8.2.0 # dask # typeguard jinja2==3.1.4 - # via - # feast (setup.py) - # fastapi + # via fastapi jsonschema==4.22.0 - # via feast (setup.py) jsonschema-specifications==2023.12.1 # via jsonschema +kubernetes==20.13.0 locket==1.0.0 # via partd markdown-it-py==3.0.0 @@ -89,19 +87,18 @@ markupsafe==2.1.5 mdurl==0.1.2 # via markdown-it-py mmh3==4.1.0 - # via feast (setup.py) mypy==1.10.0 # via sqlalchemy mypy-extensions==1.0.0 # via mypy mypy-protobuf==3.6.0 - # via feast (setup.py) numpy==1.26.4 # via - # feast (setup.py) # dask # pandas # pyarrow +oauthlib==3.2.2 + # via requests-oauthlib orjson==3.10.3 # via fastapi packaging==24.0 @@ -110,35 +107,33 @@ packaging==24.0 # gunicorn pandas==2.2.2 # via - # feast (setup.py) # dask # dask-expr partd==1.4.2 # via dask prometheus-client==0.20.0 - # via feast (setup.py) protobuf==4.25.3 - # via - # feast (setup.py) - # mypy-protobuf + # via mypy-protobuf psutil==6.0.0 - # via feast (setup.py) pyarrow==16.0.0 + # via dask-expr +pyasn1==0.6.0 # via - # feast (setup.py) - # dask-expr + # pyasn1-modules + # rsa +pyasn1-modules==0.4.0 + # via google-auth pydantic==2.7.1 - # via - # feast (setup.py) - # fastapi + # via fastapi pydantic-core==2.18.2 # via pydantic pygments==2.18.0 - # via - # feast (setup.py) - # rich + # via rich +pyjwt==2.9.0 python-dateutil==2.9.0.post0 - # via pandas + # via + # kubernetes + # pandas python-dotenv==1.0.1 # via uvicorn python-multipart==0.0.9 @@ -147,39 +142,45 @@ pytz==2024.1 # via pandas pyyaml==6.0.1 # via - # feast (setup.py) # dask + # kubernetes # uvicorn referencing==0.35.1 # via # jsonschema # jsonschema-specifications requests==2.31.0 - # via feast (setup.py) + # via + # kubernetes + # requests-oauthlib +requests-oauthlib==2.0.0 + # via kubernetes rich==13.7.1 # via typer rpds-py==0.18.1 # via # jsonschema # referencing +rsa==4.9 + # via google-auth +setuptools==73.0.1 + # via kubernetes shellingham==1.5.4 # via typer six==1.16.0 - # via python-dateutil + # via + # kubernetes + # python-dateutil sniffio==1.3.1 # via # anyio # httpx sqlalchemy[mypy]==2.0.30 - # via feast (setup.py) starlette==0.37.2 # via fastapi tabulate==0.9.0 - # via feast (setup.py) tenacity==8.3.0 - # via feast (setup.py) toml==0.10.2 - # via feast (setup.py) tomli==2.0.1 # via mypy toolz==0.12.1 @@ -187,9 +188,7 @@ toolz==0.12.1 # dask # partd tqdm==4.66.4 - # via feast (setup.py) typeguard==4.2.1 - # via feast (setup.py) typer==0.12.3 # via fastapi-cli types-protobuf==5.26.0.20240422 @@ -211,18 +210,20 @@ tzdata==2024.1 ujson==5.9.0 # via fastapi urllib3==2.2.1 - # via requests + # via + # kubernetes + # requests uvicorn[standard]==0.29.0 # via - # feast (setup.py) # fastapi # fastapi-cli uvloop==0.19.0 # via uvicorn watchfiles==0.21.0 # via uvicorn +websocket-client==1.8.0 + # via kubernetes websockets==12.0 # via uvicorn zipp==3.19.2 # via importlib-metadata -bigtree==0.19.2 diff --git a/sdk/python/tests/integration/feature_repos/repo_configuration.py b/sdk/python/tests/integration/feature_repos/repo_configuration.py index 0bf737f616..660a937f5a 100644 --- a/sdk/python/tests/integration/feature_repos/repo_configuration.py +++ b/sdk/python/tests/integration/feature_repos/repo_configuration.py @@ -110,12 +110,6 @@ "instance": os.getenv("BIGTABLE_INSTANCE_ID", "feast-integration-tests"), } -ROCKSET_CONFIG = { - "type": "rockset", - "api_key": os.getenv("ROCKSET_APIKEY", ""), - "host": os.getenv("ROCKSET_APISERVER", "api.rs2.usw2.rockset.com"), -} - IKV_CONFIG = { "type": "ikv", "account_id": os.getenv("IKV_ACCOUNT_ID", ""), @@ -166,10 +160,6 @@ AVAILABLE_ONLINE_STORES["datastore"] = ("datastore", None) AVAILABLE_ONLINE_STORES["snowflake"] = (SNOWFLAKE_CONFIG, None) AVAILABLE_ONLINE_STORES["bigtable"] = (BIGTABLE_CONFIG, None) - # Uncomment to test using private Rockset account. Currently not enabled as - # there is no dedicated Rockset instance for CI testing and there is no - # containerized version of Rockset. - # AVAILABLE_ONLINE_STORES["rockset"] = (ROCKSET_CONFIG, None) # Uncomment to test using private IKV account. Currently not enabled as # there is no dedicated IKV instance for CI testing and there is no diff --git a/setup.py b/setup.py index d53aee1002..a9f9cafacc 100644 --- a/setup.py +++ b/setup.py @@ -125,10 +125,6 @@ "pymssql", ] -ROCKSET_REQUIRED = [ - "rockset>=1.0.3", -] - IKV_REQUIRED = [ "ikvpy>=0.0.36", ] @@ -214,7 +210,6 @@ + HBASE_REQUIRED + CASSANDRA_REQUIRED + AZURE_REQUIRED - + ROCKSET_REQUIRED + HAZELCAST_REQUIRED + IBIS_REQUIRED + GRPCIO_REQUIRED @@ -386,7 +381,6 @@ def run(self): "cassandra": CASSANDRA_REQUIRED, "hazelcast": HAZELCAST_REQUIRED, "grpcio": GRPCIO_REQUIRED, - "rockset": ROCKSET_REQUIRED, "ibis": IBIS_REQUIRED, "duckdb": DUCKDB_REQUIRED, "ikv": IKV_REQUIRED, From 6b2f026747b8adebe659aed3d4d2f95d551d5d1e Mon Sep 17 00:00:00 2001 From: "Yang, Bo" Date: Mon, 26 Aug 2024 21:07:27 -0700 Subject: [PATCH 041/185] fix: Add --chdir to test_workflow.py (#4453) Signed-off-by: Yang, Bo --- .../feast/templates/postgres/feature_repo/test_workflow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/python/feast/templates/postgres/feature_repo/test_workflow.py b/sdk/python/feast/templates/postgres/feature_repo/test_workflow.py index 30927d3c7a..843ade164c 100644 --- a/sdk/python/feast/templates/postgres/feature_repo/test_workflow.py +++ b/sdk/python/feast/templates/postgres/feature_repo/test_workflow.py @@ -11,7 +11,7 @@ def run_demo(): store = FeatureStore(repo_path=os.path.dirname(__file__)) print("\n--- Run feast apply to setup feature store on Postgres ---") - subprocess.run(["feast", "apply"]) + subprocess.run(["feast", "--chdir", os.path.dirname(__file__), "apply"]) print("\n--- Historical features for training ---") fetch_historical_features_entity_df(store, for_batch_scoring=False) @@ -55,7 +55,7 @@ def run_demo(): fetch_online_features(store, source="push") print("\n--- Run feast teardown ---") - subprocess.run(["feast", "teardown"]) + subprocess.run(["feast", "--chdir", os.path.dirname(__file__), "teardown"]) def fetch_historical_features_entity_df(store: FeatureStore, for_batch_scoring: bool): From 635a01b4c77db781d67f9f5ebb1067806b1e2a13 Mon Sep 17 00:00:00 2001 From: Daniele Martinoli <86618610+dmartinol@users.noreply.github.com> Date: Tue, 27 Aug 2024 12:51:21 +0200 Subject: [PATCH 042/185] fix: Validating permission to update an existing request on both the new and the old instance (#4449) * Validating permission to update an existing request on both the new and the old instance Signed-off-by: Daniele Martinoli <86618610+dmartinol@users.noreply.github.com> * Reviewed update permission logic as per comment, added UT Signed-off-by: Daniele Martinoli --------- Signed-off-by: Daniele Martinoli <86618610+dmartinol@users.noreply.github.com> Signed-off-by: Daniele Martinoli --- sdk/python/feast/permissions/enforcer.py | 2 +- .../feast/permissions/security_manager.py | 48 ++++++- sdk/python/feast/registry_server.py | 123 ++++++++++-------- sdk/python/tests/unit/permissions/conftest.py | 24 +++- .../unit/permissions/test_security_manager.py | 92 ++++++++++++- 5 files changed, 226 insertions(+), 63 deletions(-) diff --git a/sdk/python/feast/permissions/enforcer.py b/sdk/python/feast/permissions/enforcer.py index af41d12a2c..ae45b8a78b 100644 --- a/sdk/python/feast/permissions/enforcer.py +++ b/sdk/python/feast/permissions/enforcer.py @@ -22,7 +22,7 @@ def enforce_policy( Define the logic to apply the configured permissions when a given action is requested on a protected resource. - If no permissions are defined, the result is to allow the execution. + If no permissions are defined, the result is to deny the execution. Args: permissions: The configured set of `Permission`. diff --git a/sdk/python/feast/permissions/security_manager.py b/sdk/python/feast/permissions/security_manager.py index 178db6e6e9..2322602388 100644 --- a/sdk/python/feast/permissions/security_manager.py +++ b/sdk/python/feast/permissions/security_manager.py @@ -1,7 +1,8 @@ import logging from contextvars import ContextVar -from typing import List, Optional, Union +from typing import Callable, List, Optional, Union +from feast.errors import FeastObjectNotFoundException from feast.feast_object import FeastObject from feast.infra.registry.base_registry import BaseRegistry from feast.permissions.action import AuthzedAction @@ -59,9 +60,9 @@ def assert_permissions( filter_only: bool = False, ) -> list[FeastObject]: """ - Verify if the current user is authorized ro execute the requested actions on the given resources. + Verify if the current user is authorized to execute the requested actions on the given resources. - If no permissions are defined, the result is to allow the execution. + If no permissions are defined, the result is to deny the execution. Args: resources: The resources for which we need to enforce authorized permission. @@ -73,7 +74,7 @@ def assert_permissions( list[FeastObject]: A filtered list of the permitted resources, possibly empty. Raises: - PermissionError: If the current user is not authorized to eecute all the requested actions on the given resources. + PermissionError: If the current user is not authorized to execute all the requested actions on the given resources. """ return enforce_policy( permissions=self.permissions, @@ -84,6 +85,45 @@ def assert_permissions( ) +def assert_permissions_to_update( + resource: FeastObject, + getter: Callable[[str, str, bool], FeastObject], + project: str, + allow_cache: bool = True, +) -> FeastObject: + """ + Verify if the current user is authorized to create or update the given resource. + If the resource already exists, the user must be granted permission to execute DESCRIBE and UPDATE actions. + If the resource does not exist, the user must be granted permission to execute the CREATE action. + + If no permissions are defined, the result is to deny the execution. + + Args: + resource: The resources for which we need to enforce authorized permission. + getter: The getter function used to retrieve the existing resource instance by name. + The signature must be `get_permission(self, name: str, project: str, allow_cache: bool)` + project: The project nane used in the getter function. + allow_cache: Whether to use cached data. Defaults to `True`. + Returns: + FeastObject: The original `resource`, if permitted. + + Raises: + PermissionError: If the current user is not authorized to execute all the requested actions on the given resource or on the existing one. + """ + actions = [AuthzedAction.DESCRIBE, AuthzedAction.UPDATE] + try: + existing_resource = getter( + name=resource.name, + project=project, + allow_cache=allow_cache, + ) # type: ignore[call-arg] + assert_permissions(resource=existing_resource, actions=actions) + except FeastObjectNotFoundException: + actions = [AuthzedAction.CREATE] + resource_to_update = assert_permissions(resource=resource, actions=actions) + return resource_to_update + + def assert_permissions( resource: FeastObject, actions: Union[AuthzedAction, List[AuthzedAction]], diff --git a/sdk/python/feast/registry_server.py b/sdk/python/feast/registry_server.py index 6b37aba08d..7b779e9f9e 100644 --- a/sdk/python/feast/registry_server.py +++ b/sdk/python/feast/registry_server.py @@ -16,9 +16,13 @@ from feast.infra.infra_object import Infra from feast.infra.registry.base_registry import BaseRegistry from feast.on_demand_feature_view import OnDemandFeatureView -from feast.permissions.action import CRUD, AuthzedAction +from feast.permissions.action import AuthzedAction from feast.permissions.permission import Permission -from feast.permissions.security_manager import assert_permissions, permitted_resources +from feast.permissions.security_manager import ( + assert_permissions, + assert_permissions_to_update, + permitted_resources, +) from feast.permissions.server.grpc import grpc_interceptors from feast.permissions.server.utils import ( ServerType, @@ -37,14 +41,16 @@ def __init__(self, registry: BaseRegistry) -> None: self.proxied_registry = registry def ApplyEntity(self, request: RegistryServer_pb2.ApplyEntityRequest, context): - self.proxied_registry.apply_entity( - entity=cast( - Entity, - assert_permissions( - resource=Entity.from_proto(request.entity), - actions=CRUD, - ), + entity = cast( + Entity, + assert_permissions_to_update( + resource=Entity.from_proto(request.entity), + getter=self.proxied_registry.get_entity, + project=request.project, ), + ) + self.proxied_registry.apply_entity( + entity=entity, project=request.project, commit=request.commit, ) @@ -95,19 +101,19 @@ def DeleteEntity(self, request: RegistryServer_pb2.DeleteEntityRequest, context) def ApplyDataSource( self, request: RegistryServer_pb2.ApplyDataSourceRequest, context ): - ( - self.proxied_registry.apply_data_source( - data_source=cast( - DataSource, - assert_permissions( - resource=DataSource.from_proto(request.data_source), - actions=CRUD, - ), - ), + data_source = cast( + DataSource, + assert_permissions_to_update( + resource=DataSource.from_proto(request.data_source), + getter=self.proxied_registry.get_data_source, project=request.project, - commit=request.commit, ), ) + self.proxied_registry.apply_data_source( + data_source=data_source, + project=request.project, + commit=request.commit, + ) return Empty() @@ -182,12 +188,16 @@ def ApplyFeatureView( elif feature_view_type == "stream_feature_view": feature_view = StreamFeatureView.from_proto(request.stream_feature_view) + assert_permissions_to_update( + resource=feature_view, + # Will replace with the new get_any_feature_view method later + getter=self.proxied_registry.get_feature_view, + project=request.project, + ) + ( self.proxied_registry.apply_feature_view( - feature_view=cast( - FeatureView, - assert_permissions(resource=feature_view, actions=CRUD), - ), + feature_view=feature_view, project=request.project, commit=request.commit, ), @@ -305,14 +315,16 @@ def ListOnDemandFeatureViews( def ApplyFeatureService( self, request: RegistryServer_pb2.ApplyFeatureServiceRequest, context ): - self.proxied_registry.apply_feature_service( - feature_service=cast( - FeatureService, - assert_permissions( - resource=FeatureService.from_proto(request.feature_service), - actions=CRUD, - ), + feature_service = cast( + FeatureService, + assert_permissions_to_update( + resource=FeatureService.from_proto(request.feature_service), + getter=self.proxied_registry.get_feature_service, + project=request.project, ), + ) + self.proxied_registry.apply_feature_service( + feature_service=feature_service, project=request.project, commit=request.commit, ) @@ -371,19 +383,19 @@ def DeleteFeatureService( def ApplySavedDataset( self, request: RegistryServer_pb2.ApplySavedDatasetRequest, context ): - ( - self.proxied_registry.apply_saved_dataset( - saved_dataset=cast( - SavedDataset, - assert_permissions( - resource=SavedDataset.from_proto(request.saved_dataset), - actions=CRUD, - ), - ), + saved_dataset = cast( + SavedDataset, + assert_permissions_to_update( + resource=SavedDataset.from_proto(request.saved_dataset), + getter=self.proxied_registry.get_saved_dataset, project=request.project, - commit=request.commit, ), ) + self.proxied_registry.apply_saved_dataset( + saved_dataset=saved_dataset, + project=request.project, + commit=request.commit, + ) return Empty() @@ -437,14 +449,16 @@ def DeleteSavedDataset( def ApplyValidationReference( self, request: RegistryServer_pb2.ApplyValidationReferenceRequest, context ): - self.proxied_registry.apply_validation_reference( - validation_reference=cast( - ValidationReference, - assert_permissions( - ValidationReference.from_proto(request.validation_reference), - actions=CRUD, - ), + validation_reference = cast( + ValidationReference, + assert_permissions_to_update( + resource=ValidationReference.from_proto(request.validation_reference), + getter=self.proxied_registry.get_validation_reference, + project=request.project, ), + ) + self.proxied_registry.apply_validation_reference( + validation_reference=validation_reference, project=request.project, commit=request.commit, ) @@ -547,13 +561,16 @@ def GetInfra(self, request: RegistryServer_pb2.GetInfraRequest, context): def ApplyPermission( self, request: RegistryServer_pb2.ApplyPermissionRequest, context ): - self.proxied_registry.apply_permission( - permission=cast( - Permission, - assert_permissions( - Permission.from_proto(request.permission), actions=CRUD - ), + permission = cast( + Permission, + assert_permissions_to_update( + resource=Permission.from_proto(request.permission), + getter=self.proxied_registry.get_permission, + project=request.project, ), + ) + self.proxied_registry.apply_permission( + permission=permission, project=request.project, commit=request.commit, ) diff --git a/sdk/python/tests/unit/permissions/conftest.py b/sdk/python/tests/unit/permissions/conftest.py index 7cd944fb47..6adbc6ec54 100644 --- a/sdk/python/tests/unit/permissions/conftest.py +++ b/sdk/python/tests/unit/permissions/conftest.py @@ -3,6 +3,7 @@ import pytest from feast import FeatureView +from feast.entity import Entity from feast.infra.registry.base_registry import BaseRegistry from feast.permissions.decorator import require_permissions from feast.permissions.permission import AuthzedAction, Permission @@ -48,7 +49,10 @@ def users() -> list[User]: users.append(User("r", ["reader"])) users.append(User("w", ["writer"])) users.append(User("rw", ["reader", "writer"])) - users.append(User("admin", ["reader", "writer", "admin"])) + users.append(User("special", ["reader", "writer", "special-reader"])) + users.append(User("updater", ["updater"])) + users.append(User("creator", ["creator"])) + users.append(User("admin", ["updater", "creator"])) return dict([(u.username, u) for u in users]) @@ -76,10 +80,26 @@ def security_manager() -> SecurityManager: name="special", types=FeatureView, name_pattern="special.*", - policy=RoleBasedPolicy(roles=["admin", "special-reader"]), + policy=RoleBasedPolicy(roles=["special-reader"]), actions=[AuthzedAction.DESCRIBE, AuthzedAction.UPDATE], ) ) + permissions.append( + Permission( + name="entity_updater", + types=Entity, + policy=RoleBasedPolicy(roles=["updater"]), + actions=[AuthzedAction.DESCRIBE, AuthzedAction.UPDATE], + ) + ) + permissions.append( + Permission( + name="entity_creator", + types=Entity, + policy=RoleBasedPolicy(roles=["creator"]), + actions=[AuthzedAction.CREATE], + ) + ) registry = Mock(spec=BaseRegistry) registry.list_permissions = Mock(return_value=permissions) diff --git a/sdk/python/tests/unit/permissions/test_security_manager.py b/sdk/python/tests/unit/permissions/test_security_manager.py index 192542da78..228dddb01f 100644 --- a/sdk/python/tests/unit/permissions/test_security_manager.py +++ b/sdk/python/tests/unit/permissions/test_security_manager.py @@ -1,8 +1,14 @@ import assertpy import pytest +from feast.entity import Entity +from feast.errors import FeastObjectNotFoundException from feast.permissions.action import READ, AuthzedAction -from feast.permissions.security_manager import assert_permissions, permitted_resources +from feast.permissions.security_manager import ( + assert_permissions, + assert_permissions_to_update, + permitted_resources, +) @pytest.mark.parametrize( @@ -24,7 +30,7 @@ True, ), ( - "admin", + "special", [AuthzedAction.DESCRIBE, AuthzedAction.UPDATE], False, [False, True], @@ -32,7 +38,7 @@ True, ), ( - "admin", + "special", READ + [AuthzedAction.UPDATE], False, [False, False], @@ -81,3 +87,83 @@ def test_access_SecuredFeatureView( else: result = assert_permissions(resource=r, actions=requested_actions) assertpy.assert_that(result).is_none() + + +@pytest.mark.parametrize( + "username, allowed", + [ + (None, False), + ("r", False), + ("w", False), + ("rw", False), + ("special", False), + ("updater", False), + ("creator", True), + ("admin", True), + ], +) +def test_create_entity( + security_manager, + users, + username, + allowed, +): + sm = security_manager + entity = Entity( + name="", + ) + + user = users.get(username) + sm.set_current_user(user) + + def getter(name: str, project: str, allow_cache: bool): + raise FeastObjectNotFoundException() + + if allowed: + result = assert_permissions_to_update( + resource=entity, getter=getter, project="" + ) + assertpy.assert_that(result).is_equal_to(entity) + else: + with pytest.raises(PermissionError): + assert_permissions_to_update(resource=entity, getter=getter, project="") + + +@pytest.mark.parametrize( + "username, allowed", + [ + (None, False), + ("r", False), + ("w", False), + ("rw", False), + ("special", False), + ("updater", True), + ("creator", False), + ("admin", True), + ], +) +def test_update_entity( + security_manager, + users, + username, + allowed, +): + sm = security_manager + entity = Entity( + name="", + ) + + user = users.get(username) + sm.set_current_user(user) + + def getter(name: str, project: str, allow_cache: bool): + return entity + + if allowed: + result = assert_permissions_to_update( + resource=entity, getter=getter, project="" + ) + assertpy.assert_that(result).is_equal_to(entity) + else: + with pytest.raises(PermissionError): + assert_permissions_to_update(resource=entity, getter=getter, project="") From 5e753e4f72898f3723b5f69e11c2c968744e0815 Mon Sep 17 00:00:00 2001 From: Daniele Martinoli <86618610+dmartinol@users.noreply.github.com> Date: Thu, 29 Aug 2024 09:39:17 +0200 Subject: [PATCH 043/185] refactor: Introduced base class FeastError for all Feast exceptions (#4465) introduced base class FeastError for all Feast exceptions, with initial methods to map the grpc and HTTP status code Signed-off-by: Daniele Martinoli --- sdk/python/feast/cli_utils.py | 2 +- sdk/python/feast/errors.py | 125 +++++++++++------- sdk/python/feast/permissions/enforcer.py | 9 +- .../feast/permissions/security_manager.py | 8 +- .../tests/unit/permissions/test_decorator.py | 6 +- .../unit/permissions/test_security_manager.py | 10 +- 6 files changed, 96 insertions(+), 64 deletions(-) diff --git a/sdk/python/feast/cli_utils.py b/sdk/python/feast/cli_utils.py index edfdab93e3..264a633c31 100644 --- a/sdk/python/feast/cli_utils.py +++ b/sdk/python/feast/cli_utils.py @@ -279,7 +279,7 @@ def handler_list_all_permissions_roles_verbose( for o in objects: permitted_actions = ALL_ACTIONS.copy() for action in ALL_ACTIONS: - # Following code is derived from enforcer.enforce_policy but has a different return type and does not raise PermissionError + # Following code is derived from enforcer.enforce_policy but has a different return type and does not raise FeastPermissionError matching_permissions = [ p for p in permissions diff --git a/sdk/python/feast/errors.py b/sdk/python/feast/errors.py index ffafe31125..2eed986d7f 100644 --- a/sdk/python/feast/errors.py +++ b/sdk/python/feast/errors.py @@ -1,34 +1,52 @@ from typing import Any, List, Set from colorama import Fore, Style +from fastapi import status as HttpStatusCode +from grpc import StatusCode as GrpcStatusCode from feast.field import Field -class DataSourceNotFoundException(Exception): +class FeastError(Exception): + pass + + def rpc_status_code(self) -> GrpcStatusCode: + return GrpcStatusCode.INTERNAL + + def http_status_code(self) -> int: + return HttpStatusCode.HTTP_500_INTERNAL_SERVER_ERROR + + +class DataSourceNotFoundException(FeastError): def __init__(self, path): super().__init__( f"Unable to find table at '{path}'. Please check that table exists." ) -class DataSourceNoNameException(Exception): +class DataSourceNoNameException(FeastError): def __init__(self): super().__init__( "Unable to infer a name for this data source. Either table or name must be specified." ) -class DataSourceRepeatNamesException(Exception): +class DataSourceRepeatNamesException(FeastError): def __init__(self, ds_name: str): super().__init__( f"Multiple data sources share the same case-insensitive name {ds_name}." ) -class FeastObjectNotFoundException(Exception): +class FeastObjectNotFoundException(FeastError): pass + def rpc_status_code(self) -> GrpcStatusCode: + return GrpcStatusCode.NOT_FOUND + + def http_status_code(self) -> int: + return HttpStatusCode.HTTP_404_NOT_FOUND + class EntityNotFoundException(FeastObjectNotFoundException): def __init__(self, name, project=None): @@ -110,49 +128,49 @@ def __init__(self, name: str, project: str): ) -class FeastProviderLoginError(Exception): +class FeastProviderLoginError(FeastError): """Error class that indicates a user has not authenticated with their provider.""" -class FeastProviderNotImplementedError(Exception): +class FeastProviderNotImplementedError(FeastError): def __init__(self, provider_name): super().__init__(f"Provider '{provider_name}' is not implemented") -class FeastRegistryNotSetError(Exception): +class FeastRegistryNotSetError(FeastError): def __init__(self): super().__init__("Registry is not set, but is required") -class FeastFeatureServerTypeInvalidError(Exception): +class FeastFeatureServerTypeInvalidError(FeastError): def __init__(self, feature_server_type: str): super().__init__( f"Feature server type was set to {feature_server_type}, but this type is invalid" ) -class FeastRegistryTypeInvalidError(Exception): +class FeastRegistryTypeInvalidError(FeastError): def __init__(self, registry_type: str): super().__init__( f"Feature server type was set to {registry_type}, but this type is invalid" ) -class FeastModuleImportError(Exception): +class FeastModuleImportError(FeastError): def __init__(self, module_name: str, class_name: str): super().__init__( f"Could not import module '{module_name}' while attempting to load class '{class_name}'" ) -class FeastClassImportError(Exception): +class FeastClassImportError(FeastError): def __init__(self, module_name: str, class_name: str): super().__init__( f"Could not import class '{class_name}' from module '{module_name}'" ) -class FeastExtrasDependencyImportError(Exception): +class FeastExtrasDependencyImportError(FeastError): def __init__(self, extras_type: str, nested_error: str): message = ( nested_error @@ -162,14 +180,14 @@ def __init__(self, extras_type: str, nested_error: str): super().__init__(message) -class FeastOfflineStoreUnsupportedDataSource(Exception): +class FeastOfflineStoreUnsupportedDataSource(FeastError): def __init__(self, offline_store_name: str, data_source_name: str): super().__init__( f"Offline Store '{offline_store_name}' does not support data source '{data_source_name}'" ) -class FeatureNameCollisionError(Exception): +class FeatureNameCollisionError(FeastError): def __init__(self, feature_refs_collisions: List[str], full_feature_names: bool): if full_feature_names: collisions = [ref.replace(":", "__") for ref in feature_refs_collisions] @@ -191,7 +209,7 @@ def __init__(self, feature_refs_collisions: List[str], full_feature_names: bool) ) -class SpecifiedFeaturesNotPresentError(Exception): +class SpecifiedFeaturesNotPresentError(FeastError): def __init__( self, specified_features: List[Field], @@ -204,47 +222,47 @@ def __init__( ) -class SavedDatasetLocationAlreadyExists(Exception): +class SavedDatasetLocationAlreadyExists(FeastError): def __init__(self, location: str): super().__init__(f"Saved dataset location {location} already exists.") -class FeastOfflineStoreInvalidName(Exception): +class FeastOfflineStoreInvalidName(FeastError): def __init__(self, offline_store_class_name: str): super().__init__( f"Offline Store Class '{offline_store_class_name}' should end with the string `OfflineStore`.'" ) -class FeastOnlineStoreInvalidName(Exception): +class FeastOnlineStoreInvalidName(FeastError): def __init__(self, online_store_class_name: str): super().__init__( f"Online Store Class '{online_store_class_name}' should end with the string `OnlineStore`.'" ) -class FeastInvalidAuthConfigClass(Exception): +class FeastInvalidAuthConfigClass(FeastError): def __init__(self, auth_config_class_name: str): super().__init__( f"Auth Config Class '{auth_config_class_name}' should end with the string `AuthConfig`.'" ) -class FeastInvalidBaseClass(Exception): +class FeastInvalidBaseClass(FeastError): def __init__(self, class_name: str, class_type: str): super().__init__( f"Class '{class_name}' should have `{class_type}` as a base class." ) -class FeastOnlineStoreUnsupportedDataSource(Exception): +class FeastOnlineStoreUnsupportedDataSource(FeastError): def __init__(self, online_store_name: str, data_source_name: str): super().__init__( f"Online Store '{online_store_name}' does not support data source '{data_source_name}'" ) -class FeastEntityDFMissingColumnsError(Exception): +class FeastEntityDFMissingColumnsError(FeastError): def __init__(self, expected, missing): super().__init__( f"The entity dataframe you have provided must contain columns {expected}, " @@ -252,7 +270,7 @@ def __init__(self, expected, missing): ) -class FeastJoinKeysDuringMaterialization(Exception): +class FeastJoinKeysDuringMaterialization(FeastError): def __init__( self, source: str, join_key_columns: Set[str], source_columns: Set[str] ): @@ -262,7 +280,7 @@ def __init__( ) -class DockerDaemonNotRunning(Exception): +class DockerDaemonNotRunning(FeastError): def __init__(self): super().__init__( "The Docker Python sdk cannot connect to the Docker daemon. Please make sure you have" @@ -270,7 +288,7 @@ def __init__(self): ) -class RegistryInferenceFailure(Exception): +class RegistryInferenceFailure(FeastError): def __init__(self, repo_obj_type: str, specific_issue: str): super().__init__( f"Inference to fill in missing information for {repo_obj_type} failed. {specific_issue}. " @@ -278,58 +296,58 @@ def __init__(self, repo_obj_type: str, specific_issue: str): ) -class BigQueryJobStillRunning(Exception): +class BigQueryJobStillRunning(FeastError): def __init__(self, job_id): super().__init__(f"The BigQuery job with ID '{job_id}' is still running.") -class BigQueryJobCancelled(Exception): +class BigQueryJobCancelled(FeastError): def __init__(self, job_id): super().__init__(f"The BigQuery job with ID '{job_id}' was cancelled") -class RedshiftCredentialsError(Exception): +class RedshiftCredentialsError(FeastError): def __init__(self): super().__init__("Redshift API failed due to incorrect credentials") -class RedshiftQueryError(Exception): +class RedshiftQueryError(FeastError): def __init__(self, details): super().__init__(f"Redshift SQL Query failed to finish. Details: {details}") -class RedshiftTableNameTooLong(Exception): +class RedshiftTableNameTooLong(FeastError): def __init__(self, table_name: str): super().__init__( f"Redshift table names have a maximum length of 127 characters, but the table name {table_name} has length {len(table_name)} characters." ) -class SnowflakeCredentialsError(Exception): +class SnowflakeCredentialsError(FeastError): def __init__(self): super().__init__("Snowflake Connector failed due to incorrect credentials") -class SnowflakeQueryError(Exception): +class SnowflakeQueryError(FeastError): def __init__(self, details): super().__init__(f"Snowflake SQL Query failed to finish. Details: {details}") -class EntityTimestampInferenceException(Exception): +class EntityTimestampInferenceException(FeastError): def __init__(self, expected_column_name: str): super().__init__( f"Please provide an entity_df with a column named {expected_column_name} representing the time of events." ) -class FeatureViewMissingDuringFeatureServiceInference(Exception): +class FeatureViewMissingDuringFeatureServiceInference(FeastError): def __init__(self, feature_view_name: str, feature_service_name: str): super().__init__( f"Missing {feature_view_name} feature view during inference for {feature_service_name} feature service." ) -class InvalidEntityType(Exception): +class InvalidEntityType(FeastError): def __init__(self, entity_type: type): super().__init__( f"The entity dataframe you have provided must be a Pandas DataFrame or a SQL query, " @@ -337,7 +355,7 @@ def __init__(self, entity_type: type): ) -class ConflictingFeatureViewNames(Exception): +class ConflictingFeatureViewNames(FeastError): # TODO: print file location of conflicting feature views def __init__(self, feature_view_name: str): super().__init__( @@ -345,60 +363,60 @@ def __init__(self, feature_view_name: str): ) -class FeastInvalidInfraObjectType(Exception): +class FeastInvalidInfraObjectType(FeastError): def __init__(self): super().__init__("Could not identify the type of the InfraObject.") -class SnowflakeIncompleteConfig(Exception): +class SnowflakeIncompleteConfig(FeastError): def __init__(self, e: KeyError): super().__init__(f"{e} not defined in a config file or feature_store.yaml file") -class SnowflakeQueryUnknownError(Exception): +class SnowflakeQueryUnknownError(FeastError): def __init__(self, query: str): super().__init__(f"Snowflake query failed: {query}") -class InvalidFeaturesParameterType(Exception): +class InvalidFeaturesParameterType(FeastError): def __init__(self, features: Any): super().__init__( f"Invalid `features` parameter type {type(features)}. Expected one of List[str] and FeatureService." ) -class EntitySQLEmptyResults(Exception): +class EntitySQLEmptyResults(FeastError): def __init__(self, entity_sql: str): super().__init__( f"No entity values found from the specified SQL query to generate the entity dataframe: {entity_sql}." ) -class EntityDFNotDateTime(Exception): +class EntityDFNotDateTime(FeastError): def __init__(self): super().__init__( "The entity dataframe specified does not have the timestamp field as a datetime." ) -class PushSourceNotFoundException(Exception): +class PushSourceNotFoundException(FeastError): def __init__(self, push_source_name: str): super().__init__(f"Unable to find push source '{push_source_name}'.") -class ReadOnlyRegistryException(Exception): +class ReadOnlyRegistryException(FeastError): def __init__(self): super().__init__("Registry implementation is read-only.") -class DataFrameSerializationError(Exception): +class DataFrameSerializationError(FeastError): def __init__(self, input_dict: dict): super().__init__( f"Failed to serialize the provided dictionary into a pandas DataFrame: {input_dict.keys()}" ) -class PermissionNotFoundException(Exception): +class PermissionNotFoundException(FeastError): def __init__(self, name, project): super().__init__(f"Permission {name} does not exist in project {project}") @@ -411,11 +429,22 @@ def __init__(self, name, project=None): super().__init__(f"Permission {name} does not exist") -class ZeroRowsQueryResult(Exception): +class ZeroRowsQueryResult(FeastError): def __init__(self, query: str): super().__init__(f"This query returned zero rows:\n{query}") -class ZeroColumnQueryResult(Exception): +class ZeroColumnQueryResult(FeastError): def __init__(self, query: str): super().__init__(f"This query returned zero columns:\n{query}") + + +class FeastPermissionError(FeastError, PermissionError): + def __init__(self, details: str): + super().__init__(f"Permission error:\n{details}") + + def rpc_status_code(self) -> GrpcStatusCode: + return GrpcStatusCode.PERMISSION_DENIED + + def http_status_code(self) -> int: + return HttpStatusCode.HTTP_403_FORBIDDEN diff --git a/sdk/python/feast/permissions/enforcer.py b/sdk/python/feast/permissions/enforcer.py index ae45b8a78b..d94a81ba04 100644 --- a/sdk/python/feast/permissions/enforcer.py +++ b/sdk/python/feast/permissions/enforcer.py @@ -1,5 +1,6 @@ import logging +from feast.errors import FeastPermissionError from feast.feast_object import FeastObject from feast.permissions.decision import DecisionEvaluator from feast.permissions.permission import ( @@ -29,14 +30,14 @@ def enforce_policy( user: The current user. resources: The resources for which we need to enforce authorized permission. actions: The requested actions to be authorized. - filter_only: If `True`, it removes unauthorized resources from the returned value, otherwise it raises a `PermissionError` the + filter_only: If `True`, it removes unauthorized resources from the returned value, otherwise it raises a `FeastPermissionError` the first unauthorized resource. Defaults to `False`. Returns: list[FeastObject]: A filtered list of the permitted resources. Raises: - PermissionError: If the current user is not authorized to eecute the requested actions on the given resources (and `filter_only` is `False`). + FeastPermissionError: If the current user is not authorized to eecute the requested actions on the given resources (and `filter_only` is `False`). """ if not permissions: return resources @@ -66,12 +67,12 @@ def enforce_policy( if evaluator.is_decided(): grant, explanations = evaluator.grant() if not grant and not filter_only: - raise PermissionError(",".join(explanations)) + raise FeastPermissionError(",".join(explanations)) if grant: _permitted_resources.append(resource) break else: message = f"No permissions defined to manage {actions} on {type(resource)}/{resource.name}." logger.exception(f"**PERMISSION NOT GRANTED**: {message}") - raise PermissionError(message) + raise FeastPermissionError(message) return _permitted_resources diff --git a/sdk/python/feast/permissions/security_manager.py b/sdk/python/feast/permissions/security_manager.py index 2322602388..29c0e06753 100644 --- a/sdk/python/feast/permissions/security_manager.py +++ b/sdk/python/feast/permissions/security_manager.py @@ -67,14 +67,14 @@ def assert_permissions( Args: resources: The resources for which we need to enforce authorized permission. actions: The requested actions to be authorized. - filter_only: If `True`, it removes unauthorized resources from the returned value, otherwise it raises a `PermissionError` the + filter_only: If `True`, it removes unauthorized resources from the returned value, otherwise it raises a `FeastPermissionError` the first unauthorized resource. Defaults to `False`. Returns: list[FeastObject]: A filtered list of the permitted resources, possibly empty. Raises: - PermissionError: If the current user is not authorized to execute all the requested actions on the given resources. + FeastPermissionError: If the current user is not authorized to execute all the requested actions on the given resources. """ return enforce_policy( permissions=self.permissions, @@ -108,7 +108,7 @@ def assert_permissions_to_update( FeastObject: The original `resource`, if permitted. Raises: - PermissionError: If the current user is not authorized to execute all the requested actions on the given resource or on the existing one. + FeastPermissionError: If the current user is not authorized to execute all the requested actions on the given resource or on the existing one. """ actions = [AuthzedAction.DESCRIBE, AuthzedAction.UPDATE] try: @@ -140,7 +140,7 @@ def assert_permissions( FeastObject: The original `resource`, if permitted. Raises: - PermissionError: If the current user is not authorized to execute the requested actions on the given resources. + FeastPermissionError: If the current user is not authorized to execute the requested actions on the given resources. """ sm = get_security_manager() if sm is None: diff --git a/sdk/python/tests/unit/permissions/test_decorator.py b/sdk/python/tests/unit/permissions/test_decorator.py index 8f6c2c420b..92db72c93d 100644 --- a/sdk/python/tests/unit/permissions/test_decorator.py +++ b/sdk/python/tests/unit/permissions/test_decorator.py @@ -1,6 +1,8 @@ import assertpy import pytest +from feast.errors import FeastPermissionError + @pytest.mark.parametrize( "username, can_read, can_write", @@ -22,11 +24,11 @@ def test_access_SecuredFeatureView( if can_read: fv.read_protected() else: - with pytest.raises(PermissionError): + with pytest.raises(FeastPermissionError): fv.read_protected() if can_write: fv.write_protected() else: - with pytest.raises(PermissionError): + with pytest.raises(FeastPermissionError): fv.write_protected() assertpy.assert_that(fv.unprotected()).is_true() diff --git a/sdk/python/tests/unit/permissions/test_security_manager.py b/sdk/python/tests/unit/permissions/test_security_manager.py index 228dddb01f..d403c8123b 100644 --- a/sdk/python/tests/unit/permissions/test_security_manager.py +++ b/sdk/python/tests/unit/permissions/test_security_manager.py @@ -2,7 +2,7 @@ import pytest from feast.entity import Entity -from feast.errors import FeastObjectNotFoundException +from feast.errors import FeastObjectNotFoundException, FeastPermissionError from feast.permissions.action import READ, AuthzedAction from feast.permissions.security_manager import ( assert_permissions, @@ -66,7 +66,7 @@ def test_access_SecuredFeatureView( result = [] if raise_error_in_permit: - with pytest.raises(PermissionError): + with pytest.raises(FeastPermissionError): result = permitted_resources(resources=resources, actions=requested_actions) else: result = permitted_resources(resources=resources, actions=requested_actions) @@ -82,7 +82,7 @@ def test_access_SecuredFeatureView( result = assert_permissions(resource=r, actions=requested_actions) assertpy.assert_that(result).is_equal_to(r) elif raise_error_in_assert[i]: - with pytest.raises(PermissionError): + with pytest.raises(FeastPermissionError): assert_permissions(resource=r, actions=requested_actions) else: result = assert_permissions(resource=r, actions=requested_actions) @@ -125,7 +125,7 @@ def getter(name: str, project: str, allow_cache: bool): ) assertpy.assert_that(result).is_equal_to(entity) else: - with pytest.raises(PermissionError): + with pytest.raises(FeastPermissionError): assert_permissions_to_update(resource=entity, getter=getter, project="") @@ -165,5 +165,5 @@ def getter(name: str, project: str, allow_cache: bool): ) assertpy.assert_that(result).is_equal_to(entity) else: - with pytest.raises(PermissionError): + with pytest.raises(FeastPermissionError): assert_permissions_to_update(resource=entity, getter=getter, project="") From 729c874e8c30719f23ad287d3cb84f1d654274ec Mon Sep 17 00:00:00 2001 From: Theodor Mihalache <84387487+tmihalac@users.noreply.github.com> Date: Thu, 29 Aug 2024 12:10:43 -0400 Subject: [PATCH 044/185] feat: Intra server to server communication (#4433) Intra server communication Signed-off-by: Theodor Mihalache --- .../templates/deployment.yaml | 2 + .../auth/kubernetes_token_parser.py | 11 +- .../permissions/auth/oidc_token_parser.py | 26 +- .../client/kubernetes_auth_client_manager.py | 11 + .../oidc_authentication_client_manager.py | 11 + .../feast/permissions/security_manager.py | 25 +- .../permissions/auth/test_token_parser.py | 147 +++++++++++- .../tests/unit/permissions/test_decorator.py | 2 +- .../unit/permissions/test_security_manager.py | 227 +++++++++++++++--- 9 files changed, 417 insertions(+), 45 deletions(-) diff --git a/infra/charts/feast-feature-server/templates/deployment.yaml b/infra/charts/feast-feature-server/templates/deployment.yaml index a550433db5..8dddeed6fd 100644 --- a/infra/charts/feast-feature-server/templates/deployment.yaml +++ b/infra/charts/feast-feature-server/templates/deployment.yaml @@ -36,6 +36,8 @@ spec: env: - name: FEATURE_STORE_YAML_BASE64 value: {{ .Values.feature_store_yaml_base64 }} + - name: INTRA_COMMUNICATION_BASE64 + value: {{ "intra-server-communication" | b64enc }} command: {{- if eq .Values.feast_mode "offline" }} - "feast" diff --git a/sdk/python/feast/permissions/auth/kubernetes_token_parser.py b/sdk/python/feast/permissions/auth/kubernetes_token_parser.py index c16e5232fb..7724163e5f 100644 --- a/sdk/python/feast/permissions/auth/kubernetes_token_parser.py +++ b/sdk/python/feast/permissions/auth/kubernetes_token_parser.py @@ -1,4 +1,5 @@ import logging +import os import jwt from kubernetes import client, config @@ -41,10 +42,14 @@ async def user_details_from_access_token(self, access_token: str) -> User: current_user = f"{sa_namespace}:{sa_name}" logging.info(f"Received request from {sa_name} in {sa_namespace}") - roles = self.get_roles(sa_namespace, sa_name) - logging.info(f"SA roles are: {roles}") + intra_communication_base64 = os.getenv("INTRA_COMMUNICATION_BASE64") + if sa_name is not None and sa_name == intra_communication_base64: + return User(username=sa_name, roles=[]) + else: + roles = self.get_roles(sa_namespace, sa_name) + logging.info(f"SA roles are: {roles}") - return User(username=current_user, roles=roles) + return User(username=current_user, roles=roles) def get_roles(self, namespace: str, service_account_name: str) -> list[str]: """ diff --git a/sdk/python/feast/permissions/auth/oidc_token_parser.py b/sdk/python/feast/permissions/auth/oidc_token_parser.py index fce9fdcbb2..28273e8c10 100644 --- a/sdk/python/feast/permissions/auth/oidc_token_parser.py +++ b/sdk/python/feast/permissions/auth/oidc_token_parser.py @@ -1,4 +1,6 @@ import logging +import os +from typing import Optional from unittest.mock import Mock import jwt @@ -34,7 +36,7 @@ def __init__(self, auth_config: OidcAuthConfig): async def _validate_token(self, access_token: str): """ - Validate the token extracted from the headrer of the user request against the OAuth2 server. + Validate the token extracted from the header of the user request against the OAuth2 server. """ # FastAPI's OAuth2AuthorizationCodeBearer requires a Request type but actually uses only the headers field # https://github.com/tiangolo/fastapi/blob/eca465f4c96acc5f6a22e92fd2211675ca8a20c8/fastapi/security/oauth2.py#L380 @@ -60,6 +62,11 @@ async def user_details_from_access_token(self, access_token: str) -> User: AuthenticationError if any error happens. """ + # check if intra server communication + user = self._get_intra_comm_user(access_token) + if user: + return user + try: await self._validate_token(access_token) logger.info("Validated token") @@ -108,3 +115,20 @@ async def user_details_from_access_token(self, access_token: str) -> User: except jwt.exceptions.InvalidTokenError: logger.exception("Exception while parsing the token:") raise AuthenticationError("Invalid token.") + + def _get_intra_comm_user(self, access_token: str) -> Optional[User]: + intra_communication_base64 = os.getenv("INTRA_COMMUNICATION_BASE64") + + if intra_communication_base64: + decoded_token = jwt.decode( + access_token, options={"verify_signature": False} + ) + if "preferred_username" in decoded_token: + preferred_username: str = decoded_token["preferred_username"] + if ( + preferred_username is not None + and preferred_username == intra_communication_base64 + ): + return User(username=preferred_username, roles=[]) + + return None diff --git a/sdk/python/feast/permissions/client/kubernetes_auth_client_manager.py b/sdk/python/feast/permissions/client/kubernetes_auth_client_manager.py index 1ca3c5a2ae..9957ff93a7 100644 --- a/sdk/python/feast/permissions/client/kubernetes_auth_client_manager.py +++ b/sdk/python/feast/permissions/client/kubernetes_auth_client_manager.py @@ -1,6 +1,8 @@ import logging import os +import jwt + from feast.permissions.auth_model import KubernetesAuthConfig from feast.permissions.client.auth_client_manager import AuthenticationClientManager @@ -13,6 +15,15 @@ def __init__(self, auth_config: KubernetesAuthConfig): self.token_file_path = "/var/run/secrets/kubernetes.io/serviceaccount/token" def get_token(self): + intra_communication_base64 = os.getenv("INTRA_COMMUNICATION_BASE64") + # If intra server communication call + if intra_communication_base64: + payload = { + "sub": f":::{intra_communication_base64}", # Subject claim + } + + return jwt.encode(payload, "") + try: token = self._read_token_from_file() return token diff --git a/sdk/python/feast/permissions/client/oidc_authentication_client_manager.py b/sdk/python/feast/permissions/client/oidc_authentication_client_manager.py index 6744a1d2ad..0f99cea86f 100644 --- a/sdk/python/feast/permissions/client/oidc_authentication_client_manager.py +++ b/sdk/python/feast/permissions/client/oidc_authentication_client_manager.py @@ -1,5 +1,7 @@ import logging +import os +import jwt import requests from feast.permissions.auth_model import OidcAuthConfig @@ -14,6 +16,15 @@ def __init__(self, auth_config: OidcAuthConfig): self.auth_config = auth_config def get_token(self): + intra_communication_base64 = os.getenv("INTRA_COMMUNICATION_BASE64") + # If intra server communication call + if intra_communication_base64: + payload = { + "preferred_username": f"{intra_communication_base64}", # Subject claim + } + + return jwt.encode(payload, "") + # Fetch the token endpoint from the discovery URL token_endpoint = OIDCDiscoveryService( self.auth_config.auth_discovery_url diff --git a/sdk/python/feast/permissions/security_manager.py b/sdk/python/feast/permissions/security_manager.py index 29c0e06753..c00a3d8853 100644 --- a/sdk/python/feast/permissions/security_manager.py +++ b/sdk/python/feast/permissions/security_manager.py @@ -1,4 +1,5 @@ import logging +import os from contextvars import ContextVar from typing import Callable, List, Optional, Union @@ -110,6 +111,10 @@ def assert_permissions_to_update( Raises: FeastPermissionError: If the current user is not authorized to execute all the requested actions on the given resource or on the existing one. """ + sm = get_security_manager() + if not is_auth_necessary(sm): + return resource + actions = [AuthzedAction.DESCRIBE, AuthzedAction.UPDATE] try: existing_resource = getter( @@ -142,10 +147,11 @@ def assert_permissions( Raises: FeastPermissionError: If the current user is not authorized to execute the requested actions on the given resources. """ + sm = get_security_manager() - if sm is None: + if not is_auth_necessary(sm): return resource - return sm.assert_permissions( + return sm.assert_permissions( # type: ignore[union-attr] resources=[resource], actions=actions, filter_only=False )[0] @@ -165,10 +171,11 @@ def permitted_resources( Returns: list[FeastObject]]: A filtered list of the permitted resources, possibly empty. """ + sm = get_security_manager() - if sm is None: + if not is_auth_necessary(sm): return resources - return sm.assert_permissions(resources=resources, actions=actions, filter_only=True) + return sm.assert_permissions(resources=resources, actions=actions, filter_only=True) # type: ignore[union-attr] """ @@ -201,3 +208,13 @@ def no_security_manager(): global _sm _sm = None + + +def is_auth_necessary(sm: Optional[SecurityManager]) -> bool: + intra_communication_base64 = os.getenv("INTRA_COMMUNICATION_BASE64") + + return ( + sm is not None + and sm.current_user is not None + and sm.current_user.username != intra_communication_base64 + ) diff --git a/sdk/python/tests/unit/permissions/auth/test_token_parser.py b/sdk/python/tests/unit/permissions/auth/test_token_parser.py index cb153a17c9..bac2103b4f 100644 --- a/sdk/python/tests/unit/permissions/auth/test_token_parser.py +++ b/sdk/python/tests/unit/permissions/auth/test_token_parser.py @@ -1,6 +1,6 @@ -# test_token_validator.py - import asyncio +import os +from unittest import mock from unittest.mock import MagicMock, patch import assertpy @@ -70,6 +70,75 @@ def test_oidc_token_validation_failure(mock_oauth2, oidc_config): ) +@mock.patch.dict(os.environ, {"INTRA_COMMUNICATION_BASE64": "test1234"}) +@pytest.mark.parametrize( + "intra_communication_val, is_intra_server", + [ + ("test1234", True), + ("my-name", False), + ], +) +def test_oidc_inter_server_comm( + intra_communication_val, is_intra_server, oidc_config, monkeypatch +): + async def mock_oath2(self, request): + return "OK" + + monkeypatch.setattr( + "feast.permissions.auth.oidc_token_parser.OAuth2AuthorizationCodeBearer.__call__", + mock_oath2, + ) + signing_key = MagicMock() + signing_key.key = "a-key" + monkeypatch.setattr( + "feast.permissions.auth.oidc_token_parser.PyJWKClient.get_signing_key_from_jwt", + lambda self, access_token: signing_key, + ) + + user_data = { + "preferred_username": f"{intra_communication_val}", + } + + if not is_intra_server: + user_data["resource_access"] = {_CLIENT_ID: {"roles": ["reader", "writer"]}} + + monkeypatch.setattr( + "feast.permissions.oidc_service.OIDCDiscoveryService._fetch_discovery_data", + lambda self, *args, **kwargs: { + "authorization_endpoint": "https://localhost:8080/realms/master/protocol/openid-connect/auth", + "token_endpoint": "https://localhost:8080/realms/master/protocol/openid-connect/token", + "jwks_uri": "https://localhost:8080/realms/master/protocol/openid-connect/certs", + }, + ) + + monkeypatch.setattr( + "feast.permissions.auth.oidc_token_parser.jwt.decode", + lambda self, *args, **kwargs: user_data, + ) + + access_token = "aaa-bbb-ccc" + token_parser = OidcTokenParser(auth_config=oidc_config) + user = asyncio.run( + token_parser.user_details_from_access_token(access_token=access_token) + ) + + if is_intra_server: + assertpy.assert_that(user).is_not_none() + assertpy.assert_that(user.username).is_equal_to(intra_communication_val) + assertpy.assert_that(user.roles).is_equal_to([]) + else: + assertpy.assert_that(user).is_not_none() + assertpy.assert_that(user).is_type_of(User) + if isinstance(user, User): + assertpy.assert_that(user.username).is_equal_to("my-name") + assertpy.assert_that(user.roles.sort()).is_equal_to( + ["reader", "writer"].sort() + ) + assertpy.assert_that(user.has_matching_role(["reader"])).is_true() + assertpy.assert_that(user.has_matching_role(["writer"])).is_true() + assertpy.assert_that(user.has_matching_role(["updater"])).is_false() + + # TODO RBAC: Move role bindings to a reusable fixture @patch("feast.permissions.auth.kubernetes_token_parser.config.load_incluster_config") @patch("feast.permissions.auth.kubernetes_token_parser.jwt.decode") @@ -127,3 +196,77 @@ def test_k8s_token_validation_failure(mock_jwt, mock_config): asyncio.run( token_parser.user_details_from_access_token(access_token=access_token) ) + + +@mock.patch.dict(os.environ, {"INTRA_COMMUNICATION_BASE64": "test1234"}) +@pytest.mark.parametrize( + "intra_communication_val, is_intra_server", + [ + ("test1234", True), + ("my-name", False), + ], +) +def test_k8s_inter_server_comm( + intra_communication_val, + is_intra_server, + oidc_config, + request, + rolebindings, + clusterrolebindings, + monkeypatch, +): + if is_intra_server: + subject = f":::{intra_communication_val}" + else: + sa_name = request.getfixturevalue("sa_name") + namespace = request.getfixturevalue("namespace") + subject = f"system:serviceaccount:{namespace}:{sa_name}" + rolebindings = request.getfixturevalue("rolebindings") + clusterrolebindings = request.getfixturevalue("clusterrolebindings") + + monkeypatch.setattr( + "feast.permissions.auth.kubernetes_token_parser.client.RbacAuthorizationV1Api.list_namespaced_role_binding", + lambda *args, **kwargs: rolebindings["items"], + ) + monkeypatch.setattr( + "feast.permissions.auth.kubernetes_token_parser.client.RbacAuthorizationV1Api.list_cluster_role_binding", + lambda *args, **kwargs: clusterrolebindings["items"], + ) + monkeypatch.setattr( + "feast.permissions.client.kubernetes_auth_client_manager.KubernetesAuthClientManager.get_token", + lambda self: "my-token", + ) + + monkeypatch.setattr( + "feast.permissions.auth.kubernetes_token_parser.config.load_incluster_config", + lambda: None, + ) + + monkeypatch.setattr( + "feast.permissions.auth.kubernetes_token_parser.jwt.decode", + lambda *args, **kwargs: {"sub": subject}, + ) + + roles = rolebindings["roles"] + croles = clusterrolebindings["roles"] + + access_token = "aaa-bbb-ccc" + token_parser = KubernetesTokenParser() + user = asyncio.run( + token_parser.user_details_from_access_token(access_token=access_token) + ) + + if is_intra_server: + assertpy.assert_that(user).is_not_none() + assertpy.assert_that(user.username).is_equal_to(intra_communication_val) + assertpy.assert_that(user.roles).is_equal_to([]) + else: + assertpy.assert_that(user).is_type_of(User) + if isinstance(user, User): + assertpy.assert_that(user.username).is_equal_to(f"{namespace}:{sa_name}") + assertpy.assert_that(user.roles.sort()).is_equal_to((roles + croles).sort()) + for r in roles: + assertpy.assert_that(user.has_matching_role([r])).is_true() + for cr in croles: + assertpy.assert_that(user.has_matching_role([cr])).is_true() + assertpy.assert_that(user.has_matching_role(["foo"])).is_false() diff --git a/sdk/python/tests/unit/permissions/test_decorator.py b/sdk/python/tests/unit/permissions/test_decorator.py index 92db72c93d..f434301a2c 100644 --- a/sdk/python/tests/unit/permissions/test_decorator.py +++ b/sdk/python/tests/unit/permissions/test_decorator.py @@ -7,7 +7,7 @@ @pytest.mark.parametrize( "username, can_read, can_write", [ - (None, False, False), + (None, True, True), ("r", True, False), ("w", False, True), ("rw", True, True), diff --git a/sdk/python/tests/unit/permissions/test_security_manager.py b/sdk/python/tests/unit/permissions/test_security_manager.py index d403c8123b..11b8dfb88e 100644 --- a/sdk/python/tests/unit/permissions/test_security_manager.py +++ b/sdk/python/tests/unit/permissions/test_security_manager.py @@ -9,18 +9,107 @@ assert_permissions_to_update, permitted_resources, ) +from feast.permissions.user import User @pytest.mark.parametrize( - "username, requested_actions, allowed, allowed_single, raise_error_in_assert, raise_error_in_permit", + "username, requested_actions, allowed, allowed_single, raise_error_in_assert, raise_error_in_permit, intra_communication_flag", [ - (None, [], False, [False, False], [True, True], False), - ("r", [AuthzedAction.DESCRIBE], True, [True, True], [False, False], False), - ("r", [AuthzedAction.UPDATE], False, [False, False], [True, True], False), - ("w", [AuthzedAction.DESCRIBE], False, [False, False], [True, True], False), - ("w", [AuthzedAction.UPDATE], False, [True, True], [False, False], False), - ("rw", [AuthzedAction.DESCRIBE], False, [True, True], [False, False], False), - ("rw", [AuthzedAction.UPDATE], False, [True, True], [False, False], False), + (None, [], True, [True, True], [False, False], False, False), + (None, [], True, [True, True], [False, False], False, True), + ( + "r", + [AuthzedAction.DESCRIBE], + True, + [True, True], + [False, False], + False, + False, + ), + ( + "r", + [AuthzedAction.DESCRIBE], + True, + [True, True], + [False, False], + False, + True, + ), + ("server_intra_com_val", [], True, [True, True], [False, False], False, True), + ( + "r", + [AuthzedAction.UPDATE], + False, + [False, False], + [True, True], + False, + False, + ), + ("r", [AuthzedAction.UPDATE], True, [True, True], [False, False], False, True), + ( + "w", + [AuthzedAction.DESCRIBE], + False, + [False, False], + [True, True], + False, + False, + ), + ( + "w", + [AuthzedAction.DESCRIBE], + True, + [True, True], + [True, True], + False, + True, + ), + ( + "w", + [AuthzedAction.UPDATE], + False, + [True, True], + [False, False], + False, + False, + ), + ("w", [AuthzedAction.UPDATE], False, [True, True], [False, False], False, True), + ( + "rw", + [AuthzedAction.DESCRIBE], + False, + [True, True], + [False, False], + False, + False, + ), + ( + "rw", + [AuthzedAction.DESCRIBE], + False, + [True, True], + [False, False], + False, + True, + ), + ( + "rw", + [AuthzedAction.UPDATE], + False, + [True, True], + [False, False], + False, + False, + ), + ( + "rw", + [AuthzedAction.UPDATE], + False, + [True, True], + [False, False], + False, + True, + ), ( "rw", [AuthzedAction.DESCRIBE, AuthzedAction.UPDATE], @@ -28,6 +117,16 @@ [False, False], [True, True], True, + False, + ), + ( + "rw", + [AuthzedAction.DESCRIBE, AuthzedAction.UPDATE], + True, + [True, True], + [False, False], + False, + True, ), ( "special", @@ -36,6 +135,16 @@ [False, True], [True, False], True, + False, + ), + ( + "admin", + [AuthzedAction.DESCRIBE, AuthzedAction.UPDATE], + True, + [True, True], + [False, False], + False, + True, ), ( "special", @@ -44,6 +153,16 @@ [False, False], [True, True], True, + False, + ), + ( + "admin", + READ + [AuthzedAction.UPDATE], + True, + [True, True], + [False, False], + False, + True, ), ], ) @@ -57,13 +176,21 @@ def test_access_SecuredFeatureView( allowed_single, raise_error_in_assert, raise_error_in_permit, + intra_communication_flag, + monkeypatch, ): sm = security_manager - resources = feature_views - user = users.get(username) sm.set_current_user(user) + if intra_communication_flag: + monkeypatch.setenv("INTRA_COMMUNICATION_BASE64", "server_intra_com_val") + sm.set_current_user(User("server_intra_com_val", [])) + else: + monkeypatch.delenv("INTRA_COMMUNICATION_BASE64", False) + + resources = feature_views + result = [] if raise_error_in_permit: with pytest.raises(FeastPermissionError): @@ -90,16 +217,24 @@ def test_access_SecuredFeatureView( @pytest.mark.parametrize( - "username, allowed", + "username, allowed, intra_communication_flag", [ - (None, False), - ("r", False), - ("w", False), - ("rw", False), - ("special", False), - ("updater", False), - ("creator", True), - ("admin", True), + (None, True, False), + (None, True, True), + ("r", False, False), + ("r", True, True), + ("w", False, False), + ("w", True, True), + ("rw", False, False), + ("rw", True, True), + ("special", False, False), + ("special", True, True), + ("updater", False, False), + ("updater", True, True), + ("creator", True, False), + ("creator", True, True), + ("admin", True, False), + ("admin", True, True), ], ) def test_create_entity( @@ -107,15 +242,23 @@ def test_create_entity( users, username, allowed, + intra_communication_flag, + monkeypatch, ): sm = security_manager + user = users.get(username) + sm.set_current_user(user) + + if intra_communication_flag: + monkeypatch.setenv("INTRA_COMMUNICATION_BASE64", "server_intra_com_val") + sm.set_current_user(User("server_intra_com_val", [])) + else: + monkeypatch.delenv("INTRA_COMMUNICATION_BASE64", False) + entity = Entity( name="", ) - user = users.get(username) - sm.set_current_user(user) - def getter(name: str, project: str, allow_cache: bool): raise FeastObjectNotFoundException() @@ -130,16 +273,24 @@ def getter(name: str, project: str, allow_cache: bool): @pytest.mark.parametrize( - "username, allowed", + "username, allowed, intra_communication_flag", [ - (None, False), - ("r", False), - ("w", False), - ("rw", False), - ("special", False), - ("updater", True), - ("creator", False), - ("admin", True), + (None, True, False), + (None, True, True), + ("r", False, False), + ("r", True, True), + ("w", False, False), + ("w", True, True), + ("rw", False, False), + ("rw", True, True), + ("special", False, False), + ("special", True, True), + ("updater", True, False), + ("updater", True, True), + ("creator", False, False), + ("creator", True, True), + ("admin", True, False), + ("admin", True, True), ], ) def test_update_entity( @@ -147,15 +298,23 @@ def test_update_entity( users, username, allowed, + intra_communication_flag, + monkeypatch, ): sm = security_manager + user = users.get(username) + sm.set_current_user(user) + + if intra_communication_flag: + monkeypatch.setenv("INTRA_COMMUNICATION_BASE64", "server_intra_com_val") + sm.set_current_user(User("server_intra_com_val", [])) + else: + monkeypatch.delenv("INTRA_COMMUNICATION_BASE64", False) + entity = Entity( name="", ) - user = users.get(username) - sm.set_current_user(user) - def getter(name: str, project: str, allow_cache: bool): return entity From 4186f0346ea0140b6c4cc1ae626582e6a70ec8f6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 30 Aug 2024 00:53:27 +0400 Subject: [PATCH 045/185] chore: Bump notebook from 7.2.1 to 7.2.2 in /sdk/python/requirements (#4467) Bumps [notebook](https://github.com/jupyter/notebook) from 7.2.1 to 7.2.2. - [Release notes](https://github.com/jupyter/notebook/releases) - [Changelog](https://github.com/jupyter/notebook/blob/@jupyter-notebook/tree@7.2.2/CHANGELOG.md) - [Commits](https://github.com/jupyter/notebook/compare/@jupyter-notebook/tree@7.2.1...@jupyter-notebook/tree@7.2.2) --- updated-dependencies: - dependency-name: notebook dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdk/python/requirements/py3.10-ci-requirements.txt | 2 +- sdk/python/requirements/py3.11-ci-requirements.txt | 2 +- sdk/python/requirements/py3.9-ci-requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdk/python/requirements/py3.10-ci-requirements.txt b/sdk/python/requirements/py3.10-ci-requirements.txt index 8128eb094d..a0ed668216 100644 --- a/sdk/python/requirements/py3.10-ci-requirements.txt +++ b/sdk/python/requirements/py3.10-ci-requirements.txt @@ -430,7 +430,7 @@ nest-asyncio==1.6.0 # via ipykernel nodeenv==1.9.1 # via pre-commit -notebook==7.2.1 +notebook==7.2.2 # via great-expectations notebook-shim==0.2.4 # via diff --git a/sdk/python/requirements/py3.11-ci-requirements.txt b/sdk/python/requirements/py3.11-ci-requirements.txt index 6458540f27..b0021b8980 100644 --- a/sdk/python/requirements/py3.11-ci-requirements.txt +++ b/sdk/python/requirements/py3.11-ci-requirements.txt @@ -421,7 +421,7 @@ nest-asyncio==1.6.0 # via ipykernel nodeenv==1.9.1 # via pre-commit -notebook==7.2.1 +notebook==7.2.2 # via great-expectations notebook-shim==0.2.4 # via diff --git a/sdk/python/requirements/py3.9-ci-requirements.txt b/sdk/python/requirements/py3.9-ci-requirements.txt index 58ec69fe2d..8ca628844c 100644 --- a/sdk/python/requirements/py3.9-ci-requirements.txt +++ b/sdk/python/requirements/py3.9-ci-requirements.txt @@ -439,7 +439,7 @@ nest-asyncio==1.6.0 # via ipykernel nodeenv==1.9.1 # via pre-commit -notebook==7.2.1 +notebook==7.2.2 # via great-expectations notebook-shim==0.2.4 # via From a68cf3707495c375fc3fa9a2001ac02e0293772a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Aug 2024 22:41:06 -0400 Subject: [PATCH 046/185] chore: Bump jupyterlab from 4.2.3 to 4.2.5 in /sdk/python/requirements (#4468) Bumps [jupyterlab](https://github.com/jupyterlab/jupyterlab) from 4.2.3 to 4.2.5. - [Release notes](https://github.com/jupyterlab/jupyterlab/releases) - [Changelog](https://github.com/jupyterlab/jupyterlab/blob/@jupyterlab/lsp@4.2.5/CHANGELOG.md) - [Commits](https://github.com/jupyterlab/jupyterlab/compare/@jupyterlab/lsp@4.2.3...@jupyterlab/lsp@4.2.5) --- updated-dependencies: - dependency-name: jupyterlab dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdk/python/requirements/py3.10-ci-requirements.txt | 2 +- sdk/python/requirements/py3.11-ci-requirements.txt | 2 +- sdk/python/requirements/py3.9-ci-requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdk/python/requirements/py3.10-ci-requirements.txt b/sdk/python/requirements/py3.10-ci-requirements.txt index a0ed668216..6970dd2aed 100644 --- a/sdk/python/requirements/py3.10-ci-requirements.txt +++ b/sdk/python/requirements/py3.10-ci-requirements.txt @@ -361,7 +361,7 @@ jupyter-server==2.14.1 # notebook-shim jupyter-server-terminals==0.5.3 # via jupyter-server -jupyterlab==4.2.3 +jupyterlab==4.2.5 # via notebook jupyterlab-pygments==0.3.0 # via nbconvert diff --git a/sdk/python/requirements/py3.11-ci-requirements.txt b/sdk/python/requirements/py3.11-ci-requirements.txt index b0021b8980..2d7a5b252e 100644 --- a/sdk/python/requirements/py3.11-ci-requirements.txt +++ b/sdk/python/requirements/py3.11-ci-requirements.txt @@ -352,7 +352,7 @@ jupyter-server==2.14.1 # notebook-shim jupyter-server-terminals==0.5.3 # via jupyter-server -jupyterlab==4.2.3 +jupyterlab==4.2.5 # via notebook jupyterlab-pygments==0.3.0 # via nbconvert diff --git a/sdk/python/requirements/py3.9-ci-requirements.txt b/sdk/python/requirements/py3.9-ci-requirements.txt index 8ca628844c..62f9280fe5 100644 --- a/sdk/python/requirements/py3.9-ci-requirements.txt +++ b/sdk/python/requirements/py3.9-ci-requirements.txt @@ -370,7 +370,7 @@ jupyter-server==2.14.1 # notebook-shim jupyter-server-terminals==0.5.3 # via jupyter-server -jupyterlab==4.2.3 +jupyterlab==4.2.5 # via notebook jupyterlab-pygments==0.3.0 # via nbconvert From c365b4e71a16fb69883608c5f781c6d55502bb8e Mon Sep 17 00:00:00 2001 From: Ben Stuart Date: Fri, 30 Aug 2024 05:45:20 +0100 Subject: [PATCH 047/185] fix: Check for snowflake functions when setting up materialization engine (#4456) * Check for snowflake functions over stage Signed-off-by: Ben Stuart * Refactor stage_list to function_list Signed-off-by: Ben Stuart --------- Signed-off-by: Ben Stuart --- .../feast/infra/materialization/snowflake_engine.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sdk/python/feast/infra/materialization/snowflake_engine.py b/sdk/python/feast/infra/materialization/snowflake_engine.py index 9f9f41c83d..e8b0857e5d 100644 --- a/sdk/python/feast/infra/materialization/snowflake_engine.py +++ b/sdk/python/feast/infra/materialization/snowflake_engine.py @@ -128,16 +128,16 @@ def update( stage_context = f'"{self.repo_config.batch_engine.database}"."{self.repo_config.batch_engine.schema_}"' stage_path = f'{stage_context}."feast_{project}"' with GetSnowflakeConnection(self.repo_config.batch_engine) as conn: - query = f"SHOW STAGES IN {stage_context}" + query = f"SHOW USER FUNCTIONS LIKE 'FEAST_{project.upper()}%' IN SCHEMA {stage_context}" cursor = execute_snowflake_statement(conn, query) - stage_list = pd.DataFrame( + function_list = pd.DataFrame( cursor.fetchall(), columns=[column.name for column in cursor.description], ) - # if the stage already exists, + # if the SHOW FUNCTIONS query returns results, # assumes that the materialization functions have been deployed - if f"feast_{project}" in stage_list["name"].tolist(): + if len(function_list.index) > 0: click.echo( f"Materialization functions for {Style.BRIGHT + Fore.GREEN}{project}{Style.RESET_ALL} already detected." ) @@ -149,7 +149,7 @@ def update( ) click.echo() - query = f"CREATE STAGE {stage_path}" + query = f"CREATE STAGE IF NOT EXISTS {stage_path}" execute_snowflake_statement(conn, query) copy_path, zip_path = package_snowpark_zip(project) From 484240c4e783d68bc521b62b723c2dcbd00fab5e Mon Sep 17 00:00:00 2001 From: Daniele Martinoli <86618610+dmartinol@users.noreply.github.com> Date: Fri, 30 Aug 2024 08:57:56 +0200 Subject: [PATCH 048/185] fix: Initial commit targetting grpc registry server (#4458) * initial commit targetting grpc registry server Signed-off-by: Daniele Martinoli * refactor: Introduced base class FeastError for all Feast exceptions (#4465) introduced base class FeastError for all Feast exceptions, with initial methods to map the grpc and HTTP status code Signed-off-by: Daniele Martinoli * initial commit targetting grpc registry server Signed-off-by: Daniele Martinoli * fixed merge error Signed-off-by: Daniele Martinoli * initial commit targetting grpc registry server Signed-off-by: Daniele Martinoli * fixed merge error Signed-off-by: Daniele Martinoli * integrated comment Signed-off-by: Daniele Martinoli * moved imports as per comment Signed-off-by: Daniele Martinoli --------- Signed-off-by: Daniele Martinoli --- sdk/python/feast/errors.py | 56 +++++++++++++++++-- sdk/python/feast/grpc_error_interceptor.py | 48 ++++++++++++++++ .../client/grpc_client_auth_interceptor.py | 20 ++++--- sdk/python/feast/permissions/server/grpc.py | 22 -------- sdk/python/feast/registry_server.py | 26 ++++++++- .../auth/server/test_auth_registry_server.py | 35 ++++++++++++ sdk/python/tests/unit/test_errors.py | 26 +++++++++ 7 files changed, 197 insertions(+), 36 deletions(-) create mode 100644 sdk/python/feast/grpc_error_interceptor.py create mode 100644 sdk/python/tests/unit/test_errors.py diff --git a/sdk/python/feast/errors.py b/sdk/python/feast/errors.py index 2eed986d7f..d39009ae7a 100644 --- a/sdk/python/feast/errors.py +++ b/sdk/python/feast/errors.py @@ -1,4 +1,7 @@ -from typing import Any, List, Set +import importlib +import json +import logging +from typing import Any, List, Optional, Set from colorama import Fore, Style from fastapi import status as HttpStatusCode @@ -6,16 +9,61 @@ from feast.field import Field +logger = logging.getLogger(__name__) + class FeastError(Exception): pass - def rpc_status_code(self) -> GrpcStatusCode: + def grpc_status_code(self) -> GrpcStatusCode: return GrpcStatusCode.INTERNAL def http_status_code(self) -> int: return HttpStatusCode.HTTP_500_INTERNAL_SERVER_ERROR + def __str__(self) -> str: + if hasattr(self, "__overridden_message__"): + return str(getattr(self, "__overridden_message__")) + return super().__str__() + + def __repr__(self) -> str: + if hasattr(self, "__overridden_message__"): + return f"{type(self).__name__}('{getattr(self,'__overridden_message__')}')" + return super().__repr__() + + def to_error_detail(self) -> str: + """ + Returns a JSON representation of the error for serialization purposes. + + Returns: + str: a string representation of a JSON document including `module`, `class` and `message` fields. + """ + + m = { + "module": f"{type(self).__module__}", + "class": f"{type(self).__name__}", + "message": f"{str(self)}", + } + return json.dumps(m) + + @staticmethod + def from_error_detail(detail: str) -> Optional["FeastError"]: + try: + m = json.loads(detail) + if all(f in m for f in ["module", "class", "message"]): + module_name = m["module"] + class_name = m["class"] + message = m["message"] + module = importlib.import_module(module_name) + class_reference = getattr(module, class_name) + + instance = class_reference(message) + setattr(instance, "__overridden_message__", message) + return instance + except Exception as e: + logger.warning(f"Invalid error detail: {detail}: {e}") + return None + class DataSourceNotFoundException(FeastError): def __init__(self, path): @@ -41,7 +89,7 @@ def __init__(self, ds_name: str): class FeastObjectNotFoundException(FeastError): pass - def rpc_status_code(self) -> GrpcStatusCode: + def grpc_status_code(self) -> GrpcStatusCode: return GrpcStatusCode.NOT_FOUND def http_status_code(self) -> int: @@ -443,7 +491,7 @@ class FeastPermissionError(FeastError, PermissionError): def __init__(self, details: str): super().__init__(f"Permission error:\n{details}") - def rpc_status_code(self) -> GrpcStatusCode: + def grpc_status_code(self) -> GrpcStatusCode: return GrpcStatusCode.PERMISSION_DENIED def http_status_code(self) -> int: diff --git a/sdk/python/feast/grpc_error_interceptor.py b/sdk/python/feast/grpc_error_interceptor.py new file mode 100644 index 0000000000..c638d461ed --- /dev/null +++ b/sdk/python/feast/grpc_error_interceptor.py @@ -0,0 +1,48 @@ +import grpc + +from feast.errors import FeastError + + +def exception_wrapper(behavior, request, context): + try: + return behavior(request, context) + except grpc.RpcError as e: + context.abort(e.code(), e.details()) + except FeastError as e: + context.abort( + e.grpc_status_code(), + e.to_error_detail(), + ) + + +class ErrorInterceptor(grpc.ServerInterceptor): + def intercept_service(self, continuation, handler_call_details): + handler = continuation(handler_call_details) + if handler is None: + return None + + if handler.unary_unary: + return grpc.unary_unary_rpc_method_handler( + lambda req, ctx: exception_wrapper(handler.unary_unary, req, ctx), + request_deserializer=handler.request_deserializer, + response_serializer=handler.response_serializer, + ) + elif handler.unary_stream: + return grpc.unary_stream_rpc_method_handler( + lambda req, ctx: exception_wrapper(handler.unary_stream, req, ctx), + request_deserializer=handler.request_deserializer, + response_serializer=handler.response_serializer, + ) + elif handler.stream_unary: + return grpc.stream_unary_rpc_method_handler( + lambda req, ctx: exception_wrapper(handler.stream_unary, req, ctx), + request_deserializer=handler.request_deserializer, + response_serializer=handler.response_serializer, + ) + elif handler.stream_stream: + return grpc.stream_stream_rpc_method_handler( + lambda req, ctx: exception_wrapper(handler.stream_stream, req, ctx), + request_deserializer=handler.request_deserializer, + response_serializer=handler.response_serializer, + ) + return handler diff --git a/sdk/python/feast/permissions/client/grpc_client_auth_interceptor.py b/sdk/python/feast/permissions/client/grpc_client_auth_interceptor.py index 98cc445c7b..5155b80cb5 100644 --- a/sdk/python/feast/permissions/client/grpc_client_auth_interceptor.py +++ b/sdk/python/feast/permissions/client/grpc_client_auth_interceptor.py @@ -2,6 +2,7 @@ import grpc +from feast.errors import FeastError from feast.permissions.auth_model import AuthConfig from feast.permissions.client.auth_client_manager_factory import get_auth_token @@ -20,26 +21,31 @@ def __init__(self, auth_type: AuthConfig): def intercept_unary_unary( self, continuation, client_call_details, request_iterator ): - client_call_details = self._append_auth_header_metadata(client_call_details) - return continuation(client_call_details, request_iterator) + return self._handle_call(continuation, client_call_details, request_iterator) def intercept_unary_stream( self, continuation, client_call_details, request_iterator ): - client_call_details = self._append_auth_header_metadata(client_call_details) - return continuation(client_call_details, request_iterator) + return self._handle_call(continuation, client_call_details, request_iterator) def intercept_stream_unary( self, continuation, client_call_details, request_iterator ): - client_call_details = self._append_auth_header_metadata(client_call_details) - return continuation(client_call_details, request_iterator) + return self._handle_call(continuation, client_call_details, request_iterator) def intercept_stream_stream( self, continuation, client_call_details, request_iterator ): + return self._handle_call(continuation, client_call_details, request_iterator) + + def _handle_call(self, continuation, client_call_details, request_iterator): client_call_details = self._append_auth_header_metadata(client_call_details) - return continuation(client_call_details, request_iterator) + result = continuation(client_call_details, request_iterator) + if result.exception() is not None: + mapped_error = FeastError.from_error_detail(result.exception().details()) + if mapped_error is not None: + raise mapped_error + return result def _append_auth_header_metadata(self, client_call_details): logger.debug( diff --git a/sdk/python/feast/permissions/server/grpc.py b/sdk/python/feast/permissions/server/grpc.py index 3c94240869..96f2690b88 100644 --- a/sdk/python/feast/permissions/server/grpc.py +++ b/sdk/python/feast/permissions/server/grpc.py @@ -1,6 +1,5 @@ import asyncio import logging -from typing import Optional import grpc @@ -8,32 +7,11 @@ get_auth_manager, ) from feast.permissions.security_manager import get_security_manager -from feast.permissions.server.utils import ( - AuthManagerType, -) logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) -def grpc_interceptors( - auth_type: AuthManagerType, -) -> Optional[list[grpc.ServerInterceptor]]: - """ - A list of the authorization interceptors. - - Args: - auth_type: The type of authorization manager, from the feature store configuration. - - Returns: - list[grpc.ServerInterceptor]: Optional list of interceptors. If the authorization type is set to `NONE`, it returns `None`. - """ - if auth_type == AuthManagerType.NONE: - return None - - return [AuthInterceptor()] - - class AuthInterceptor(grpc.ServerInterceptor): def intercept_service(self, continuation, handler_call_details): sm = get_security_manager() diff --git a/sdk/python/feast/registry_server.py b/sdk/python/feast/registry_server.py index 7b779e9f9e..40475aa580 100644 --- a/sdk/python/feast/registry_server.py +++ b/sdk/python/feast/registry_server.py @@ -1,6 +1,6 @@ from concurrent import futures from datetime import datetime, timezone -from typing import Union, cast +from typing import Optional, Union, cast import grpc from google.protobuf.empty_pb2 import Empty @@ -13,6 +13,7 @@ from feast.errors import FeatureViewNotFoundException from feast.feast_object import FeastObject from feast.feature_view import FeatureView +from feast.grpc_error_interceptor import ErrorInterceptor from feast.infra.infra_object import Infra from feast.infra.registry.base_registry import BaseRegistry from feast.on_demand_feature_view import OnDemandFeatureView @@ -23,8 +24,9 @@ assert_permissions_to_update, permitted_resources, ) -from feast.permissions.server.grpc import grpc_interceptors +from feast.permissions.server.grpc import AuthInterceptor from feast.permissions.server.utils import ( + AuthManagerType, ServerType, init_auth_manager, init_security_manager, @@ -645,7 +647,7 @@ def start_server(store: FeatureStore, port: int, wait_for_termination: bool = Tr server = grpc.server( futures.ThreadPoolExecutor(max_workers=10), - interceptors=grpc_interceptors(auth_manager_type), + interceptors=_grpc_interceptors(auth_manager_type), ) RegistryServer_pb2_grpc.add_RegistryServerServicer_to_server( RegistryServer(store.registry), server @@ -668,3 +670,21 @@ def start_server(store: FeatureStore, port: int, wait_for_termination: bool = Tr server.wait_for_termination() else: return server + + +def _grpc_interceptors( + auth_type: AuthManagerType, +) -> Optional[list[grpc.ServerInterceptor]]: + """ + A list of the interceptors for the registry server. + + Args: + auth_type: The type of authorization manager, from the feature store configuration. + + Returns: + list[grpc.ServerInterceptor]: Optional list of interceptors. If the authorization type is set to `NONE`, it returns `None`. + """ + if auth_type == AuthManagerType.NONE: + return [ErrorInterceptor()] + + return [AuthInterceptor(), ErrorInterceptor()] diff --git a/sdk/python/tests/unit/permissions/auth/server/test_auth_registry_server.py b/sdk/python/tests/unit/permissions/auth/server/test_auth_registry_server.py index bc16bdac3b..9e9bc1473e 100644 --- a/sdk/python/tests/unit/permissions/auth/server/test_auth_registry_server.py +++ b/sdk/python/tests/unit/permissions/auth/server/test_auth_registry_server.py @@ -8,6 +8,11 @@ from feast import ( FeatureStore, ) +from feast.errors import ( + EntityNotFoundException, + FeastPermissionError, + FeatureViewNotFoundException, +) from feast.permissions.permission import Permission from feast.registry_server import start_server from feast.wait import wait_retry_backoff # noqa: E402 @@ -70,7 +75,9 @@ def test_registry_apis( print(f"Running for\n:{auth_config}") remote_feature_store = get_remote_registry_store(server_port, feature_store) permissions = _test_list_permissions(remote_feature_store, applied_permissions) + _test_get_entity(remote_feature_store, applied_permissions) _test_list_entities(remote_feature_store, applied_permissions) + _test_get_fv(remote_feature_store, applied_permissions) _test_list_fvs(remote_feature_store, applied_permissions) if _permissions_exist_in_permission_list( @@ -118,6 +125,20 @@ def _test_get_historical_features(client_fs: FeatureStore): assertpy.assert_that(training_df).is_not_none() +def _test_get_entity(client_fs: FeatureStore, permissions: list[Permission]): + if not _is_auth_enabled(client_fs) or _is_permission_enabled( + client_fs, permissions, read_entities_perm + ): + entity = client_fs.get_entity("driver") + assertpy.assert_that(entity).is_not_none() + assertpy.assert_that(entity.name).is_equal_to("driver") + else: + with pytest.raises(FeastPermissionError): + client_fs.get_entity("driver") + with pytest.raises(EntityNotFoundException): + client_fs.get_entity("invalid-name") + + def _test_list_entities(client_fs: FeatureStore, permissions: list[Permission]): entities = client_fs.list_entities() @@ -188,6 +209,20 @@ def _is_auth_enabled(client_fs: FeatureStore) -> bool: return client_fs.config.auth_config.type != "no_auth" +def _test_get_fv(client_fs: FeatureStore, permissions: list[Permission]): + if not _is_auth_enabled(client_fs) or _is_permission_enabled( + client_fs, permissions, read_fv_perm + ): + fv = client_fs.get_feature_view("driver_hourly_stats") + assertpy.assert_that(fv).is_not_none() + assertpy.assert_that(fv.name).is_equal_to("driver_hourly_stats") + else: + with pytest.raises(FeastPermissionError): + client_fs.get_feature_view("driver_hourly_stats") + with pytest.raises(FeatureViewNotFoundException): + client_fs.get_feature_view("invalid-name") + + def _test_list_fvs(client_fs: FeatureStore, permissions: list[Permission]): if _is_auth_enabled(client_fs) and _permissions_exist_in_permission_list( [invalid_list_entities_perm], permissions diff --git a/sdk/python/tests/unit/test_errors.py b/sdk/python/tests/unit/test_errors.py new file mode 100644 index 0000000000..b3f33690da --- /dev/null +++ b/sdk/python/tests/unit/test_errors.py @@ -0,0 +1,26 @@ +import re + +import assertpy + +import feast.errors as errors + + +def test_error_error_detail(): + e = errors.FeatureViewNotFoundException("abc") + + d = e.to_error_detail() + + assertpy.assert_that(d).is_not_none() + assertpy.assert_that(d).contains('"module": "feast.errors"') + assertpy.assert_that(d).contains('"class": "FeatureViewNotFoundException"') + assertpy.assert_that(re.search(r"abc", d)).is_true() + + converted_e = errors.FeastError.from_error_detail(d) + assertpy.assert_that(converted_e).is_not_none() + assertpy.assert_that(str(converted_e)).is_equal_to(str(e)) + assertpy.assert_that(repr(converted_e)).is_equal_to(repr(e)) + + +def test_invalid_error_error_detail(): + e = errors.FeastError.from_error_detail("invalid") + assertpy.assert_that(e).is_none() From 8fef1944d9b6acb6b47a80d0fab27a724cf1bc5c Mon Sep 17 00:00:00 2001 From: cburroughs Date: Tue, 3 Sep 2024 03:01:41 -0400 Subject: [PATCH 049/185] build: Explicit protobuf build version; consistent build/setup deps (#4472) build: explicit protobuf build version; consistent build/setup deps Right now if one downloads `feast-0.40.1-py2.py3-none-any.whl` from PyPi it contains: ``` $ grep 'Protobuf Python Version' feast/protos/feast/registry/RegistryServer_pb2.py ``` Which is outside ``` $ grep 'protobuf<' feast-0.40.1.dist-info/METADATA Requires-Dist: protobuf<5.0.0,>=4.24.0 ``` Leading to runtime errors (#4437). This was mitigated by #4438. This change tightens this up further by: * Deleting the Makefile command that was trying to do this unsuccessfully. * Aligns the setup/build requirements * Sets the version of protobuf to match the *minimum* of the range. There is no guarantee that protos generated by `4.X` will work with `4.(X-1)`. Signed-off-by: Chris Burroughs --- .github/workflows/build_wheels.yml | 1 - Makefile | 3 --- environment-setup.md | 3 +-- pyproject.toml | 11 ++++++----- setup.py | 7 ++++--- 5 files changed, 11 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index f04015a989..df8534d078 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -97,7 +97,6 @@ jobs: # There's a `git restore` in here because `make install-go-ci-dependencies` is actually messing up go.mod & go.sum. run: | pip install -U pip setuptools wheel twine - make install-protoc-dependencies make build-ui git status git restore go.mod go.sum diff --git a/Makefile b/Makefile index 6ebab4e3be..78a0b6d328 100644 --- a/Makefile +++ b/Makefile @@ -395,9 +395,6 @@ test-trino-plugin-locally: kill-trino-locally: cd ${ROOT_DIR}; docker stop trino -install-protoc-dependencies: - pip install --ignore-installed protobuf==4.24.0 "grpcio-tools>=1.56.2,<2" mypy-protobuf==3.1.0 - # Docker build-docker: build-feature-server-python-aws-docker build-feature-transformation-server-docker build-feature-server-java-docker diff --git a/environment-setup.md b/environment-setup.md index 5dde9dfd94..581dc35f77 100644 --- a/environment-setup.md +++ b/environment-setup.md @@ -13,11 +13,10 @@ pip install cryptography -U conda install protobuf conda install pymssql pip install -e ".[dev]" -make install-protoc-dependencies PYTHON=3.9 make install-python-ci-dependencies PYTHON=3.9 ``` 4. start the docker daemon 5. run unit tests: ```bash make test-python-unit -``` \ No newline at end of file +``` diff --git a/pyproject.toml b/pyproject.toml index af44861502..15921e633c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,14 @@ [build-system] requires = [ + "grpcio-tools>=1.56.2,<2", + "grpcio>=1.56.2,<2", + "mypy-protobuf==3.1", + "protobuf==4.24.0", + "pybindgen==0.22.0", "setuptools>=60", - "wheel", "setuptools_scm>=6.2", - "grpcio", - "grpcio-tools>=1.47.0", - "mypy-protobuf==3.1", - "protobuf>=4.24.0,<5.0.0", "sphinx!=4.0.0", + "wheel", ] build-backend = "setuptools.build_meta" diff --git a/setup.py b/setup.py index a9f9cafacc..6da5e8226a 100644 --- a/setup.py +++ b/setup.py @@ -403,11 +403,12 @@ def run(self): entry_points={"console_scripts": ["feast=feast.cli:cli"]}, use_scm_version=use_scm_version, setup_requires=[ - "setuptools_scm", - "grpcio>=1.56.2,<2", "grpcio-tools>=1.56.2,<2", - "mypy-protobuf>=3.1", + "grpcio>=1.56.2,<2", + "mypy-protobuf==3.1", + "protobuf==4.24.0", "pybindgen==0.22.0", + "setuptools_scm>=6.2", ], cmdclass={ "build_python_protos": BuildPythonProtosCommand, From 3f3a4e852c3f508e38560e248c1ba68d64c4e799 Mon Sep 17 00:00:00 2001 From: lokeshrangineni <19699092+lokeshrangineni@users.noreply.github.com> Date: Wed, 4 Sep 2024 12:05:08 -0400 Subject: [PATCH 050/185] =?UTF-8?q?refactor:=20Making=20username=20and=20p?= =?UTF-8?q?assword=20fields=20in=20OidcAuthModel=20as=20mandatory=20onl?= =?UTF-8?q?=E2=80=A6=20(#4460)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * squashed the last 23 commits and make username, password, client_Secret fields are required for oidc client configuration Signed-off-by: Lokesh Rangineni <19699092+lokeshrangineni@users.noreply.github.com> * squashed the last 23 commits and make username, password, client_Secret fields are required for oidc client configuration Signed-off-by: Lokesh Rangineni <19699092+lokeshrangineni@users.noreply.github.com> * Fixing the failing tests. Signed-off-by: Lokesh Rangineni <19699092+lokeshrangineni@users.noreply.github.com> * Fixing the Integration test failures. Signed-off-by: Lokesh Rangineni <19699092+lokeshrangineni@users.noreply.github.com> * Fixing the Integration test failures. Signed-off-by: Lokesh Rangineni <19699092+lokeshrangineni@users.noreply.github.com> * Removing the unnecessary configuration not needed after recent change. Signed-off-by: Lokesh Rangineni <19699092+lokeshrangineni@users.noreply.github.com> * Added client_secret also to calculate the oidc_client type calculation. Signed-off-by: Lokesh Rangineni <19699092+lokeshrangineni@users.noreply.github.com> --------- Signed-off-by: Lokesh Rangineni <19699092+lokeshrangineni@users.noreply.github.com> --- .../components/authz_manager.md | 20 ++++++-- sdk/python/feast/permissions/auth_model.py | 8 ++-- .../client/auth_client_manager_factory.py | 4 +- .../oidc_authentication_client_manager.py | 4 +- sdk/python/feast/permissions/server/utils.py | 5 +- sdk/python/feast/repo_config.py | 26 +++++++---- sdk/python/tests/conftest.py | 1 - .../feature_repos/repo_configuration.py | 11 ++--- .../universal/data_sources/file.py | 4 -- .../infra/scaffolding/test_repo_config.py | 46 ++++++++++++++----- .../tests/unit/permissions/auth/conftest.py | 5 +- .../unit/permissions/test_oidc_auth_client.py | 7 +-- 12 files changed, 90 insertions(+), 51 deletions(-) diff --git a/docs/getting-started/components/authz_manager.md b/docs/getting-started/components/authz_manager.md index 876dd84f2e..0d011fbf2b 100644 --- a/docs/getting-started/components/authz_manager.md +++ b/docs/getting-started/components/authz_manager.md @@ -61,24 +61,36 @@ For example, the access token for a client `app` of a user with `reader` role sh } ``` -An example of OIDC authorization configuration is the following: +An example of feast OIDC authorization configuration on the server side is the following: ```yaml project: my-project auth: type: oidc client_id: _CLIENT_ID__ - client_secret: _CLIENT_SECRET__ - realm: _REALM__ auth_discovery_url: _OIDC_SERVER_URL_/realms/master/.well-known/openid-configuration ... ``` -In case of client configuration, the following settings must be added to specify the current user: +In case of client configuration, the following settings username, password and client_secret must be added to specify the current user: ```yaml auth: + type: oidc ... username: _USERNAME_ password: _PASSWORD_ + client_secret: _CLIENT_SECRET__ +``` + +Below is an example of feast full OIDC client auth configuration: +```yaml +project: my-project +auth: + type: oidc + client_id: test_client_id + client_secret: test_client_secret + username: test_user_name + password: test_password + auth_discovery_url: http://localhost:8080/realms/master/.well-known/openid-configuration ``` ### Kubernetes RBAC Authorization diff --git a/sdk/python/feast/permissions/auth_model.py b/sdk/python/feast/permissions/auth_model.py index 28eeb951a7..a3a3b32a4b 100644 --- a/sdk/python/feast/permissions/auth_model.py +++ b/sdk/python/feast/permissions/auth_model.py @@ -1,4 +1,4 @@ -from typing import Literal, Optional +from typing import Literal from feast.repo_config import FeastConfigBaseModel @@ -10,10 +10,12 @@ class AuthConfig(FeastConfigBaseModel): class OidcAuthConfig(AuthConfig): auth_discovery_url: str client_id: str - client_secret: Optional[str] = None + + +class OidcClientAuthConfig(OidcAuthConfig): username: str password: str - realm: str = "master" + client_secret: str class NoAuthConfig(AuthConfig): diff --git a/sdk/python/feast/permissions/client/auth_client_manager_factory.py b/sdk/python/feast/permissions/client/auth_client_manager_factory.py index 4e49802047..3dff5fb45d 100644 --- a/sdk/python/feast/permissions/client/auth_client_manager_factory.py +++ b/sdk/python/feast/permissions/client/auth_client_manager_factory.py @@ -2,7 +2,7 @@ from feast.permissions.auth_model import ( AuthConfig, KubernetesAuthConfig, - OidcAuthConfig, + OidcClientAuthConfig, ) from feast.permissions.client.auth_client_manager import AuthenticationClientManager from feast.permissions.client.kubernetes_auth_client_manager import ( @@ -15,7 +15,7 @@ def get_auth_client_manager(auth_config: AuthConfig) -> AuthenticationClientManager: if auth_config.type == AuthType.OIDC.value: - assert isinstance(auth_config, OidcAuthConfig) + assert isinstance(auth_config, OidcClientAuthConfig) return OidcAuthClientManager(auth_config) elif auth_config.type == AuthType.KUBERNETES.value: assert isinstance(auth_config, KubernetesAuthConfig) diff --git a/sdk/python/feast/permissions/client/oidc_authentication_client_manager.py b/sdk/python/feast/permissions/client/oidc_authentication_client_manager.py index 0f99cea86f..3ba1c1b6a7 100644 --- a/sdk/python/feast/permissions/client/oidc_authentication_client_manager.py +++ b/sdk/python/feast/permissions/client/oidc_authentication_client_manager.py @@ -4,7 +4,7 @@ import jwt import requests -from feast.permissions.auth_model import OidcAuthConfig +from feast.permissions.auth_model import OidcClientAuthConfig from feast.permissions.client.auth_client_manager import AuthenticationClientManager from feast.permissions.oidc_service import OIDCDiscoveryService @@ -12,7 +12,7 @@ class OidcAuthClientManager(AuthenticationClientManager): - def __init__(self, auth_config: OidcAuthConfig): + def __init__(self, auth_config: OidcClientAuthConfig): self.auth_config = auth_config def get_token(self): diff --git a/sdk/python/feast/permissions/server/utils.py b/sdk/python/feast/permissions/server/utils.py index 34a2c0024a..ac70f187ce 100644 --- a/sdk/python/feast/permissions/server/utils.py +++ b/sdk/python/feast/permissions/server/utils.py @@ -15,7 +15,10 @@ from feast.permissions.auth.oidc_token_parser import OidcTokenParser from feast.permissions.auth.token_extractor import TokenExtractor from feast.permissions.auth.token_parser import TokenParser -from feast.permissions.auth_model import AuthConfig, OidcAuthConfig +from feast.permissions.auth_model import ( + AuthConfig, + OidcAuthConfig, +) from feast.permissions.security_manager import ( SecurityManager, no_security_manager, diff --git a/sdk/python/feast/repo_config.py b/sdk/python/feast/repo_config.py index 199ef31412..52372f2987 100644 --- a/sdk/python/feast/repo_config.py +++ b/sdk/python/feast/repo_config.py @@ -87,10 +87,13 @@ "local": "feast.infra.feature_servers.local_process.config.LocalFeatureServerConfig", } +ALLOWED_AUTH_TYPES = ["no_auth", "kubernetes", "oidc"] + AUTH_CONFIGS_CLASS_FOR_TYPE = { "no_auth": "feast.permissions.auth_model.NoAuthConfig", "kubernetes": "feast.permissions.auth_model.KubernetesAuthConfig", "oidc": "feast.permissions.auth_model.OidcAuthConfig", + "oidc_client": "feast.permissions.auth_model.OidcClientAuthConfig", } @@ -291,11 +294,17 @@ def offline_store(self): def auth_config(self): if not self._auth: if isinstance(self.auth, Dict): - self._auth = get_auth_config_from_type(self.auth.get("type"))( - **self.auth + is_oidc_client = ( + self.auth.get("type") == AuthType.OIDC.value + and "username" in self.auth + and "password" in self.auth + and "client_secret" in self.auth ) + self._auth = get_auth_config_from_type( + "oidc_client" if is_oidc_client else self.auth.get("type") + )(**self.auth) elif isinstance(self.auth, str): - self._auth = get_auth_config_from_type(self.auth.get("type"))() + self._auth = get_auth_config_from_type(self.auth)() elif self.auth: self._auth = self.auth @@ -336,22 +345,21 @@ def _validate_auth_config(cls, values: Any) -> Any: from feast.permissions.auth_model import AuthConfig if "auth" in values: - allowed_auth_types = AUTH_CONFIGS_CLASS_FOR_TYPE.keys() if isinstance(values["auth"], Dict): if values["auth"].get("type") is None: raise ValueError( - f"auth configuration is missing authentication type. Possible values={allowed_auth_types}" + f"auth configuration is missing authentication type. Possible values={ALLOWED_AUTH_TYPES}" ) - elif values["auth"]["type"] not in allowed_auth_types: + elif values["auth"]["type"] not in ALLOWED_AUTH_TYPES: raise ValueError( f'auth configuration has invalid authentication type={values["auth"]["type"]}. Possible ' - f'values={allowed_auth_types}' + f'values={ALLOWED_AUTH_TYPES}' ) elif isinstance(values["auth"], AuthConfig): - if values["auth"].type not in allowed_auth_types: + if values["auth"].type not in ALLOWED_AUTH_TYPES: raise ValueError( f'auth configuration has invalid authentication type={values["auth"].type}. Possible ' - f'values={allowed_auth_types}' + f'values={ALLOWED_AUTH_TYPES}' ) return values diff --git a/sdk/python/tests/conftest.py b/sdk/python/tests/conftest.py index b5b3e2d9e5..5e70da074c 100644 --- a/sdk/python/tests/conftest.py +++ b/sdk/python/tests/conftest.py @@ -466,7 +466,6 @@ def is_integration_test(all_markers_from_module): client_secret: feast-integration-client-secret username: reader_writer password: password - realm: master auth_discovery_url: KEYCLOAK_URL_PLACE_HOLDER/realms/master/.well-known/openid-configuration """), ], diff --git a/sdk/python/tests/integration/feature_repos/repo_configuration.py b/sdk/python/tests/integration/feature_repos/repo_configuration.py index 660a937f5a..73f99fb7c2 100644 --- a/sdk/python/tests/integration/feature_repos/repo_configuration.py +++ b/sdk/python/tests/integration/feature_repos/repo_configuration.py @@ -28,7 +28,7 @@ ) from feast.infra.feature_servers.local_process.config import LocalFeatureServerConfig from feast.permissions.action import AuthzedAction -from feast.permissions.auth_model import OidcAuthConfig +from feast.permissions.auth_model import OidcClientAuthConfig from feast.permissions.permission import Permission from feast.permissions.policy import RoleBasedPolicy from feast.repo_config import RegistryConfig, RepoConfig @@ -447,15 +447,14 @@ class OfflineServerPermissionsEnvironment(Environment): def setup(self): self.data_source_creator.setup(self.registry) keycloak_url = self.data_source_creator.get_keycloak_url() - auth_config = OidcAuthConfig( + auth_config = OidcClientAuthConfig( client_id="feast-integration-client", - client_secret="feast-integration-client-secret", - username="reader_writer", - password="password", - realm="master", type="oidc", auth_discovery_url=f"{keycloak_url}/realms/master/.well-known" f"/openid-configuration", + client_secret="feast-integration-client-secret", + username="reader_writer", + password="password", ) self.config = RepoConfig( registry=self.registry, diff --git a/sdk/python/tests/integration/feature_repos/universal/data_sources/file.py b/sdk/python/tests/integration/feature_repos/universal/data_sources/file.py index 10d348c056..d8b75aca24 100644 --- a/sdk/python/tests/integration/feature_repos/universal/data_sources/file.py +++ b/sdk/python/tests/integration/feature_repos/universal/data_sources/file.py @@ -445,10 +445,6 @@ def __init__(self, project_name: str, *args, **kwargs): auth: type: oidc client_id: feast-integration-client - client_secret: feast-integration-client-secret - username: reader_writer - password: password - realm: master auth_discovery_url: {keycloak_url}/realms/master/.well-known/openid-configuration """ self.auth_config = auth_config_template.format(keycloak_url=self.keycloak_url) diff --git a/sdk/python/tests/unit/infra/scaffolding/test_repo_config.py b/sdk/python/tests/unit/infra/scaffolding/test_repo_config.py index 5331d350e2..9dcf7e4caf 100644 --- a/sdk/python/tests/unit/infra/scaffolding/test_repo_config.py +++ b/sdk/python/tests/unit/infra/scaffolding/test_repo_config.py @@ -9,6 +9,7 @@ KubernetesAuthConfig, NoAuthConfig, OidcAuthConfig, + OidcClientAuthConfig, ) from feast.repo_config import FeastConfigError, load_repo_config @@ -213,7 +214,6 @@ def test_auth_config(): client_secret: test_client_secret username: test_user_name password: test_password - realm: master auth_discovery_url: http://localhost:8080/realms/master/.well-known/openid-configuration registry: "registry.db" provider: local @@ -235,7 +235,6 @@ def test_auth_config(): client_secret: test_client_secret username: test_user_name password: test_password - realm: master auth_discovery_url: http://localhost:8080/realms/master/.well-known/openid-configuration registry: "registry.db" provider: local @@ -247,7 +246,32 @@ def test_auth_config(): expect_error="invalid authentication type=not_valid_auth_type", ) - oidc_repo_config = _test_config( + oidc_server_repo_config = _test_config( + dedent( + """ + project: foo + auth: + type: oidc + client_id: test_client_id + auth_discovery_url: http://localhost:8080/realms/master/.well-known/openid-configuration + registry: "registry.db" + provider: local + online_store: + path: foo + entity_key_serialization_version: 2 + """ + ), + expect_error=None, + ) + assert oidc_server_repo_config.auth["type"] == AuthType.OIDC.value + assert isinstance(oidc_server_repo_config.auth_config, OidcAuthConfig) + assert oidc_server_repo_config.auth_config.client_id == "test_client_id" + assert ( + oidc_server_repo_config.auth_config.auth_discovery_url + == "http://localhost:8080/realms/master/.well-known/openid-configuration" + ) + + oidc_client_repo_config = _test_config( dedent( """ project: foo @@ -257,7 +281,6 @@ def test_auth_config(): client_secret: test_client_secret username: test_user_name password: test_password - realm: master auth_discovery_url: http://localhost:8080/realms/master/.well-known/openid-configuration registry: "registry.db" provider: local @@ -268,15 +291,14 @@ def test_auth_config(): ), expect_error=None, ) - assert oidc_repo_config.auth["type"] == AuthType.OIDC.value - assert isinstance(oidc_repo_config.auth_config, OidcAuthConfig) - assert oidc_repo_config.auth_config.client_id == "test_client_id" - assert oidc_repo_config.auth_config.client_secret == "test_client_secret" - assert oidc_repo_config.auth_config.username == "test_user_name" - assert oidc_repo_config.auth_config.password == "test_password" - assert oidc_repo_config.auth_config.realm == "master" + assert oidc_client_repo_config.auth["type"] == AuthType.OIDC.value + assert isinstance(oidc_client_repo_config.auth_config, OidcClientAuthConfig) + assert oidc_client_repo_config.auth_config.client_id == "test_client_id" + assert oidc_client_repo_config.auth_config.client_secret == "test_client_secret" + assert oidc_client_repo_config.auth_config.username == "test_user_name" + assert oidc_client_repo_config.auth_config.password == "test_password" assert ( - oidc_repo_config.auth_config.auth_discovery_url + oidc_client_repo_config.auth_config.auth_discovery_url == "http://localhost:8080/realms/master/.well-known/openid-configuration" ) diff --git a/sdk/python/tests/unit/permissions/auth/conftest.py b/sdk/python/tests/unit/permissions/auth/conftest.py index 0d6acd7fb2..ea6e2e4311 100644 --- a/sdk/python/tests/unit/permissions/auth/conftest.py +++ b/sdk/python/tests/unit/permissions/auth/conftest.py @@ -75,10 +75,7 @@ def oidc_config() -> OidcAuthConfig: return OidcAuthConfig( auth_discovery_url="https://localhost:8080/realms/master/.well-known/openid-configuration", client_id=_CLIENT_ID, - client_secret="", - username="", - password="", - realm="", + type="oidc", ) diff --git a/sdk/python/tests/unit/permissions/test_oidc_auth_client.py b/sdk/python/tests/unit/permissions/test_oidc_auth_client.py index 22ed5b6f87..68aec70fc7 100644 --- a/sdk/python/tests/unit/permissions/test_oidc_auth_client.py +++ b/sdk/python/tests/unit/permissions/test_oidc_auth_client.py @@ -5,7 +5,7 @@ from feast.permissions.auth_model import ( KubernetesAuthConfig, NoAuthConfig, - OidcAuthConfig, + OidcClientAuthConfig, ) from feast.permissions.client.http_auth_requests_wrapper import ( AuthenticatedRequestsSession, @@ -21,13 +21,14 @@ MOCKED_TOKEN_VALUE: str = "dummy_token" -def _get_dummy_oidc_auth_type() -> OidcAuthConfig: - oidc_config = OidcAuthConfig( +def _get_dummy_oidc_auth_type() -> OidcClientAuthConfig: + oidc_config = OidcClientAuthConfig( auth_discovery_url="http://localhost:8080/realms/master/.well-known/openid-configuration", type="oidc", username="admin_test", password="password_test", client_id="dummy_client_id", + client_secret="client_secret", ) return oidc_config From 2bd03fa4da5e76f6b29b0b54b455d5552d256838 Mon Sep 17 00:00:00 2001 From: Daniele Martinoli <86618610+dmartinol@users.noreply.github.com> Date: Thu, 5 Sep 2024 10:34:28 +0200 Subject: [PATCH 051/185] fix: Added Permission API docs (#4485) * Updated API docs with Permission types and functions Signed-off-by: Daniele Martinoli * Updated API docs with Permission types and functions Signed-off-by: Daniele Martinoli --------- Signed-off-by: Daniele Martinoli --- sdk/python/docs/index.rst | 58 +++++++++++++++++++- sdk/python/docs/source/feast.permissions.rst | 8 +++ sdk/python/docs/source/index.rst | 58 +++++++++++++++++++- 3 files changed, 122 insertions(+), 2 deletions(-) diff --git a/sdk/python/docs/index.rst b/sdk/python/docs/index.rst index 1ef6bd16c8..86354f80c7 100644 --- a/sdk/python/docs/index.rst +++ b/sdk/python/docs/index.rst @@ -453,4 +453,60 @@ Snowflake Engine :members: .. autoclass:: feast.infra.materialization.contrib.spark.spark_materialization_engine.SparkMaterializationJob - :members: \ No newline at end of file + :members: + +Permission +============================ + +.. autoclass:: feast.permissions.permission.Permission + :members: + +.. autoclass:: feast.permissions.action.AuthzedAction + :members: + +.. autoclass:: feast.permissions.policy.Policy + :members: + +.. autofunction:: feast.permissions.enforcer.enforce_policy + +Auth Config +--------------------------- + +.. autoclass:: feast.permissions.auth_model.AuthConfig + :members: + +.. autoclass:: feast.permissions.auth_model.KubernetesAuthConfig + :members: + +.. autoclass:: feast.permissions.auth_model.OidcAuthConfig + :members: + +Auth Manager +--------------------------- + +.. autoclass:: feast.permissions.auth.AuthManager + :members: + +.. autoclass:: feast.permissions.auth.token_parser.TokenParser + :members: + +.. autoclass:: feast.permissions.auth.token_extractor.TokenExtractor + :members: + +.. autoclass:: feast.permissions.auth.kubernetes_token_parser.KubernetesTokenParser + :members: + +.. autoclass:: feast.permissions.auth.oidc_token_parser.OidcTokenParser + :members: + +Auth Client Manager +--------------------------- + +.. autoclass:: feast.permissions.client.auth_client_manager.AuthenticationClientManager + :members: + +.. autoclass:: feast.permissions.client.kubernetes_auth_client_manager.KubernetesAuthClientManager + :members: + +.. autoclass:: feast.permissions.client.oidc_authentication_client_manager.OidcAuthClientManager + :members: diff --git a/sdk/python/docs/source/feast.permissions.rst b/sdk/python/docs/source/feast.permissions.rst index 8c33ab6273..d8731111e1 100644 --- a/sdk/python/docs/source/feast.permissions.rst +++ b/sdk/python/docs/source/feast.permissions.rst @@ -62,6 +62,14 @@ feast.permissions.matcher module :undoc-members: :show-inheritance: +feast.permissions.oidc\_service module +-------------------------------------- + +.. automodule:: feast.permissions.oidc_service + :members: + :undoc-members: + :show-inheritance: + feast.permissions.permission module ----------------------------------- diff --git a/sdk/python/docs/source/index.rst b/sdk/python/docs/source/index.rst index 1ef6bd16c8..86354f80c7 100644 --- a/sdk/python/docs/source/index.rst +++ b/sdk/python/docs/source/index.rst @@ -453,4 +453,60 @@ Snowflake Engine :members: .. autoclass:: feast.infra.materialization.contrib.spark.spark_materialization_engine.SparkMaterializationJob - :members: \ No newline at end of file + :members: + +Permission +============================ + +.. autoclass:: feast.permissions.permission.Permission + :members: + +.. autoclass:: feast.permissions.action.AuthzedAction + :members: + +.. autoclass:: feast.permissions.policy.Policy + :members: + +.. autofunction:: feast.permissions.enforcer.enforce_policy + +Auth Config +--------------------------- + +.. autoclass:: feast.permissions.auth_model.AuthConfig + :members: + +.. autoclass:: feast.permissions.auth_model.KubernetesAuthConfig + :members: + +.. autoclass:: feast.permissions.auth_model.OidcAuthConfig + :members: + +Auth Manager +--------------------------- + +.. autoclass:: feast.permissions.auth.AuthManager + :members: + +.. autoclass:: feast.permissions.auth.token_parser.TokenParser + :members: + +.. autoclass:: feast.permissions.auth.token_extractor.TokenExtractor + :members: + +.. autoclass:: feast.permissions.auth.kubernetes_token_parser.KubernetesTokenParser + :members: + +.. autoclass:: feast.permissions.auth.oidc_token_parser.OidcTokenParser + :members: + +Auth Client Manager +--------------------------- + +.. autoclass:: feast.permissions.client.auth_client_manager.AuthenticationClientManager + :members: + +.. autoclass:: feast.permissions.client.kubernetes_auth_client_manager.KubernetesAuthClientManager + :members: + +.. autoclass:: feast.permissions.client.oidc_authentication_client_manager.OidcAuthClientManager + :members: From 21187199173f4c4f5417205d99535af6be492a9a Mon Sep 17 00:00:00 2001 From: Theodor Mihalache <84387487+tmihalac@users.noreply.github.com> Date: Thu, 5 Sep 2024 14:39:47 -0400 Subject: [PATCH 052/185] fix: Added Online Store REST client errors handler (#4488) * Added Online Store rest client errors handler Signed-off-by: Theodor Mihalache * Added Online Store rest client errors handler - Small refactor to from_error_detail and FeastErrors - Fixed tests Signed-off-by: Theodor Mihalache * Added Online Store rest client errors handler - Fixed linter Signed-off-by: Theodor Mihalache --------- Signed-off-by: Theodor Mihalache --- sdk/python/feast/errors.py | 5 +- sdk/python/feast/feature_server.py | 278 ++++++++---------- .../feast/infra/online_stores/remote.py | 18 +- sdk/python/feast/rest_error_handler.py | 57 ++++ .../test_python_feature_server.py | 34 ++- .../tests/unit/test_rest_error_decorator.py | 78 +++++ 6 files changed, 299 insertions(+), 171 deletions(-) create mode 100644 sdk/python/feast/rest_error_handler.py create mode 100644 sdk/python/tests/unit/test_rest_error_decorator.py diff --git a/sdk/python/feast/errors.py b/sdk/python/feast/errors.py index d39009ae7a..fd5955fd98 100644 --- a/sdk/python/feast/errors.py +++ b/sdk/python/feast/errors.py @@ -57,7 +57,7 @@ def from_error_detail(detail: str) -> Optional["FeastError"]: module = importlib.import_module(module_name) class_reference = getattr(module, class_name) - instance = class_reference(message) + instance = class_reference.__new__(class_reference) setattr(instance, "__overridden_message__", message) return instance except Exception as e: @@ -451,6 +451,9 @@ class PushSourceNotFoundException(FeastError): def __init__(self, push_source_name: str): super().__init__(f"Unable to find push source '{push_source_name}'.") + def http_status_code(self) -> int: + return HttpStatusCode.HTTP_422_UNPROCESSABLE_ENTITY + class ReadOnlyRegistryException(FeastError): def __init__(self): diff --git a/sdk/python/feast/feature_server.py b/sdk/python/feast/feature_server.py index 7f24580b7a..4f8de1eef5 100644 --- a/sdk/python/feast/feature_server.py +++ b/sdk/python/feast/feature_server.py @@ -9,8 +9,9 @@ import pandas as pd import psutil from dateutil import parser -from fastapi import Depends, FastAPI, HTTPException, Request, Response, status +from fastapi import Depends, FastAPI, Request, Response, status from fastapi.logger import logger +from fastapi.responses import JSONResponse from google.protobuf.json_format import MessageToDict from prometheus_client import Gauge, start_http_server from pydantic import BaseModel @@ -19,7 +20,10 @@ from feast import proto_json, utils from feast.constants import DEFAULT_FEATURE_SERVER_REGISTRY_TTL from feast.data_source import PushMode -from feast.errors import FeatureViewNotFoundException, PushSourceNotFoundException +from feast.errors import ( + FeastError, + FeatureViewNotFoundException, +) from feast.permissions.action import WRITE, AuthzedAction from feast.permissions.security_manager import assert_permissions from feast.permissions.server.rest import inject_user_details @@ -101,147 +105,119 @@ async def lifespan(app: FastAPI): async def get_body(request: Request): return await request.body() - # TODO RBAC: complete the dependencies for the other endpoints @app.post( "/get-online-features", dependencies=[Depends(inject_user_details)], ) def get_online_features(body=Depends(get_body)): - try: - body = json.loads(body) - full_feature_names = body.get("full_feature_names", False) - entity_rows = body["entities"] - # Initialize parameters for FeatureStore.get_online_features(...) call - if "feature_service" in body: - feature_service = store.get_feature_service( - body["feature_service"], allow_cache=True + body = json.loads(body) + full_feature_names = body.get("full_feature_names", False) + entity_rows = body["entities"] + # Initialize parameters for FeatureStore.get_online_features(...) call + if "feature_service" in body: + feature_service = store.get_feature_service( + body["feature_service"], allow_cache=True + ) + assert_permissions( + resource=feature_service, actions=[AuthzedAction.READ_ONLINE] + ) + features = feature_service + else: + features = body["features"] + all_feature_views, all_on_demand_feature_views = ( + utils._get_feature_views_to_use( + store.registry, + store.project, + features, + allow_cache=True, + hide_dummy_entity=False, ) + ) + for feature_view in all_feature_views: assert_permissions( - resource=feature_service, actions=[AuthzedAction.READ_ONLINE] + resource=feature_view, actions=[AuthzedAction.READ_ONLINE] ) - features = feature_service - else: - features = body["features"] - all_feature_views, all_on_demand_feature_views = ( - utils._get_feature_views_to_use( - store.registry, - store.project, - features, - allow_cache=True, - hide_dummy_entity=False, - ) + for od_feature_view in all_on_demand_feature_views: + assert_permissions( + resource=od_feature_view, actions=[AuthzedAction.READ_ONLINE] ) - for feature_view in all_feature_views: - assert_permissions( - resource=feature_view, actions=[AuthzedAction.READ_ONLINE] - ) - for od_feature_view in all_on_demand_feature_views: - assert_permissions( - resource=od_feature_view, actions=[AuthzedAction.READ_ONLINE] - ) - - response_proto = store.get_online_features( - features=features, - entity_rows=entity_rows, - full_feature_names=full_feature_names, - ).proto - - # Convert the Protobuf object to JSON and return it - return MessageToDict( - response_proto, preserving_proto_field_name=True, float_precision=18 - ) - except Exception as e: - # Print the original exception on the server side - logger.exception(traceback.format_exc()) - # Raise HTTPException to return the error message to the client - raise HTTPException(status_code=500, detail=str(e)) + + response_proto = store.get_online_features( + features=features, + entity_rows=entity_rows, + full_feature_names=full_feature_names, + ).proto + + # Convert the Protobuf object to JSON and return it + return MessageToDict( + response_proto, preserving_proto_field_name=True, float_precision=18 + ) @app.post("/push", dependencies=[Depends(inject_user_details)]) def push(body=Depends(get_body)): - try: - request = PushFeaturesRequest(**json.loads(body)) - df = pd.DataFrame(request.df) - actions = [] - if request.to == "offline": - to = PushMode.OFFLINE - actions = [AuthzedAction.WRITE_OFFLINE] - elif request.to == "online": - to = PushMode.ONLINE - actions = [AuthzedAction.WRITE_ONLINE] - elif request.to == "online_and_offline": - to = PushMode.ONLINE_AND_OFFLINE - actions = WRITE - else: - raise ValueError( - f"{request.to} is not a supported push format. Please specify one of these ['online', 'offline', 'online_and_offline']." - ) - - from feast.data_source import PushSource + request = PushFeaturesRequest(**json.loads(body)) + df = pd.DataFrame(request.df) + actions = [] + if request.to == "offline": + to = PushMode.OFFLINE + actions = [AuthzedAction.WRITE_OFFLINE] + elif request.to == "online": + to = PushMode.ONLINE + actions = [AuthzedAction.WRITE_ONLINE] + elif request.to == "online_and_offline": + to = PushMode.ONLINE_AND_OFFLINE + actions = WRITE + else: + raise ValueError( + f"{request.to} is not a supported push format. Please specify one of these ['online', 'offline', 'online_and_offline']." + ) - all_fvs = store.list_feature_views( - allow_cache=request.allow_registry_cache - ) + store.list_stream_feature_views( - allow_cache=request.allow_registry_cache + from feast.data_source import PushSource + + all_fvs = store.list_feature_views( + allow_cache=request.allow_registry_cache + ) + store.list_stream_feature_views(allow_cache=request.allow_registry_cache) + fvs_with_push_sources = { + fv + for fv in all_fvs + if ( + fv.stream_source is not None + and isinstance(fv.stream_source, PushSource) + and fv.stream_source.name == request.push_source_name ) - fvs_with_push_sources = { - fv - for fv in all_fvs - if ( - fv.stream_source is not None - and isinstance(fv.stream_source, PushSource) - and fv.stream_source.name == request.push_source_name - ) - } + } - for feature_view in fvs_with_push_sources: - assert_permissions(resource=feature_view, actions=actions) + for feature_view in fvs_with_push_sources: + assert_permissions(resource=feature_view, actions=actions) - store.push( - push_source_name=request.push_source_name, - df=df, - allow_registry_cache=request.allow_registry_cache, - to=to, - ) - except PushSourceNotFoundException as e: - # Print the original exception on the server side - logger.exception(traceback.format_exc()) - # Raise HTTPException to return the error message to the client - raise HTTPException(status_code=422, detail=str(e)) - except Exception as e: - # Print the original exception on the server side - logger.exception(traceback.format_exc()) - # Raise HTTPException to return the error message to the client - raise HTTPException(status_code=500, detail=str(e)) + store.push( + push_source_name=request.push_source_name, + df=df, + allow_registry_cache=request.allow_registry_cache, + to=to, + ) @app.post("/write-to-online-store", dependencies=[Depends(inject_user_details)]) def write_to_online_store(body=Depends(get_body)): + request = WriteToFeatureStoreRequest(**json.loads(body)) + df = pd.DataFrame(request.df) + feature_view_name = request.feature_view_name + allow_registry_cache = request.allow_registry_cache try: - request = WriteToFeatureStoreRequest(**json.loads(body)) - df = pd.DataFrame(request.df) - feature_view_name = request.feature_view_name - allow_registry_cache = request.allow_registry_cache - try: - feature_view = store.get_stream_feature_view( - feature_view_name, allow_registry_cache=allow_registry_cache - ) - except FeatureViewNotFoundException: - feature_view = store.get_feature_view( - feature_view_name, allow_registry_cache=allow_registry_cache - ) - - assert_permissions( - resource=feature_view, actions=[AuthzedAction.WRITE_ONLINE] + feature_view = store.get_stream_feature_view( + feature_view_name, allow_registry_cache=allow_registry_cache ) - store.write_to_online_store( - feature_view_name=feature_view_name, - df=df, - allow_registry_cache=allow_registry_cache, + except FeatureViewNotFoundException: + feature_view = store.get_feature_view( + feature_view_name, allow_registry_cache=allow_registry_cache ) - except Exception as e: - # Print the original exception on the server side - logger.exception(traceback.format_exc()) - # Raise HTTPException to return the error message to the client - raise HTTPException(status_code=500, detail=str(e)) + + assert_permissions(resource=feature_view, actions=[AuthzedAction.WRITE_ONLINE]) + store.write_to_online_store( + feature_view_name=feature_view_name, + df=df, + allow_registry_cache=allow_registry_cache, + ) @app.get("/health") def health(): @@ -249,39 +225,43 @@ def health(): @app.post("/materialize", dependencies=[Depends(inject_user_details)]) def materialize(body=Depends(get_body)): - try: - request = MaterializeRequest(**json.loads(body)) - for feature_view in request.feature_views: - assert_permissions( - resource=feature_view, actions=[AuthzedAction.WRITE_ONLINE] - ) - store.materialize( - utils.make_tzaware(parser.parse(request.start_ts)), - utils.make_tzaware(parser.parse(request.end_ts)), - request.feature_views, + request = MaterializeRequest(**json.loads(body)) + for feature_view in request.feature_views: + assert_permissions( + resource=feature_view, actions=[AuthzedAction.WRITE_ONLINE] ) - except Exception as e: - # Print the original exception on the server side - logger.exception(traceback.format_exc()) - # Raise HTTPException to return the error message to the client - raise HTTPException(status_code=500, detail=str(e)) + store.materialize( + utils.make_tzaware(parser.parse(request.start_ts)), + utils.make_tzaware(parser.parse(request.end_ts)), + request.feature_views, + ) @app.post("/materialize-incremental", dependencies=[Depends(inject_user_details)]) def materialize_incremental(body=Depends(get_body)): - try: - request = MaterializeIncrementalRequest(**json.loads(body)) - for feature_view in request.feature_views: - assert_permissions( - resource=feature_view, actions=[AuthzedAction.WRITE_ONLINE] - ) - store.materialize_incremental( - utils.make_tzaware(parser.parse(request.end_ts)), request.feature_views + request = MaterializeIncrementalRequest(**json.loads(body)) + for feature_view in request.feature_views: + assert_permissions( + resource=feature_view, actions=[AuthzedAction.WRITE_ONLINE] + ) + store.materialize_incremental( + utils.make_tzaware(parser.parse(request.end_ts)), request.feature_views + ) + + @app.exception_handler(Exception) + async def rest_exception_handler(request: Request, exc: Exception): + # Print the original exception on the server side + logger.exception(traceback.format_exc()) + + if isinstance(exc, FeastError): + return JSONResponse( + status_code=exc.http_status_code(), + content=exc.to_error_detail(), + ) + else: + return JSONResponse( + status_code=500, + content=str(exc), ) - except Exception as e: - # Print the original exception on the server side - logger.exception(traceback.format_exc()) - # Raise HTTPException to return the error message to the client - raise HTTPException(status_code=500, detail=str(e)) return app diff --git a/sdk/python/feast/infra/online_stores/remote.py b/sdk/python/feast/infra/online_stores/remote.py index 93fbcaf771..5f65d8da8b 100644 --- a/sdk/python/feast/infra/online_stores/remote.py +++ b/sdk/python/feast/infra/online_stores/remote.py @@ -16,16 +16,15 @@ from datetime import datetime from typing import Any, Callable, Dict, List, Literal, Optional, Sequence, Tuple +import requests from pydantic import StrictStr from feast import Entity, FeatureView, RepoConfig from feast.infra.online_stores.online_store import OnlineStore -from feast.permissions.client.http_auth_requests_wrapper import ( - get_http_auth_requests_session, -) from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto from feast.protos.feast.types.Value_pb2 import Value as ValueProto from feast.repo_config import FeastConfigBaseModel +from feast.rest_error_handler import rest_error_handling_decorator from feast.type_map import python_values_to_proto_values from feast.value_type import ValueType @@ -72,9 +71,7 @@ def online_read( req_body = self._construct_online_read_api_json_request( entity_keys, table, requested_features ) - response = get_http_auth_requests_session(config.auth_config).post( - f"{config.online_store.path}/get-online-features", data=req_body - ) + response = get_remote_online_features(config=config, req_body=req_body) if response.status_code == 200: logger.debug("Able to retrieve the online features from feature server.") response_json = json.loads(response.text) @@ -167,3 +164,12 @@ def teardown( entities: Sequence[Entity], ): pass + + +@rest_error_handling_decorator +def get_remote_online_features( + session: requests.Session, config: RepoConfig, req_body: str +) -> requests.Response: + return session.post( + f"{config.online_store.path}/get-online-features", data=req_body + ) diff --git a/sdk/python/feast/rest_error_handler.py b/sdk/python/feast/rest_error_handler.py new file mode 100644 index 0000000000..fc802866f9 --- /dev/null +++ b/sdk/python/feast/rest_error_handler.py @@ -0,0 +1,57 @@ +import logging +from functools import wraps + +import requests + +from feast import RepoConfig +from feast.errors import FeastError +from feast.permissions.client.http_auth_requests_wrapper import ( + get_http_auth_requests_session, +) + +logger = logging.getLogger(__name__) + + +def rest_error_handling_decorator(func): + @wraps(func) + def wrapper(config: RepoConfig, *args, **kwargs): + assert isinstance(config, RepoConfig) + + # Get a Session object + with get_http_auth_requests_session(config.auth_config) as session: + # Define a wrapper for session methods + def method_wrapper(method_name): + original_method = getattr(session, method_name) + + @wraps(original_method) + def wrapped_method(*args, **kwargs): + logger.debug( + f"Calling {method_name} with args: {args}, kwargs: {kwargs}" + ) + response = original_method(*args, **kwargs) + logger.debug( + f"{method_name} response status code: {response.status_code}" + ) + + try: + response.raise_for_status() + except requests.RequestException: + logger.debug(f"response.json() = {response.json()}") + mapped_error = FeastError.from_error_detail(response.json()) + logger.debug(f"mapped_error = {str(mapped_error)}") + if mapped_error is not None: + raise mapped_error + return response + + return wrapped_method + + # Enhance session methods + session.get = method_wrapper("get") # type: ignore[method-assign] + session.post = method_wrapper("post") # type: ignore[method-assign] + session.put = method_wrapper("put") # type: ignore[method-assign] + session.delete = method_wrapper("delete") # type: ignore[method-assign] + + # Pass the enhanced session object to the decorated function + return func(session, config, *args, **kwargs) + + return wrapper diff --git a/sdk/python/tests/integration/online_store/test_python_feature_server.py b/sdk/python/tests/integration/online_store/test_python_feature_server.py index 1010e73178..d08e1104eb 100644 --- a/sdk/python/tests/integration/online_store/test_python_feature_server.py +++ b/sdk/python/tests/integration/online_store/test_python_feature_server.py @@ -4,6 +4,7 @@ import pytest from fastapi.testclient import TestClient +from feast.errors import PushSourceNotFoundException from feast.feast_object import FeastObject from feast.feature_server import get_app from feast.utils import _utc_now @@ -90,21 +91,24 @@ def test_push_source_does_not_exist(python_fs_client): initial_temp = _get_temperatures_from_feature_server( python_fs_client, location_ids=[1] )[0] - response = python_fs_client.post( - "/push", - data=json.dumps( - { - "push_source_name": "push_source_does_not_exist", - "df": { - "location_id": [1], - "temperature": [initial_temp * 100], - "event_timestamp": [str(_utc_now())], - "created": [str(_utc_now())], - }, - } - ), - ) - assert response.status_code == 422 + with pytest.raises( + PushSourceNotFoundException, + match="Unable to find push source 'push_source_does_not_exist'", + ): + python_fs_client.post( + "/push", + data=json.dumps( + { + "push_source_name": "push_source_does_not_exist", + "df": { + "location_id": [1], + "temperature": [initial_temp * 100], + "event_timestamp": [str(_utc_now())], + "created": [str(_utc_now())], + }, + } + ), + ) def _get_temperatures_from_feature_server(client, location_ids: List[int]): diff --git a/sdk/python/tests/unit/test_rest_error_decorator.py b/sdk/python/tests/unit/test_rest_error_decorator.py new file mode 100644 index 0000000000..147ae767bd --- /dev/null +++ b/sdk/python/tests/unit/test_rest_error_decorator.py @@ -0,0 +1,78 @@ +from unittest.mock import Mock, patch + +import assertpy +import pytest +import requests + +from feast import RepoConfig +from feast.errors import PermissionNotFoundException +from feast.infra.online_stores.remote import ( + RemoteOnlineStoreConfig, + get_remote_online_features, +) + + +@pytest.fixture +def feast_exception() -> PermissionNotFoundException: + return PermissionNotFoundException("dummy_name", "dummy_project") + + +@pytest.fixture +def none_feast_exception() -> RuntimeError: + return RuntimeError("dummy_name", "dummy_project") + + +@patch("feast.infra.online_stores.remote.requests.sessions.Session.post") +def test_rest_error_handling_with_feast_exception( + mock_post, environment, feast_exception +): + # Create a mock response object + mock_response = Mock() + mock_response.status_code = feast_exception.http_status_code() + mock_response.json.return_value = feast_exception.to_error_detail() + mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError() + + # Configure the mock to return the mock response + mock_post.return_value = mock_response + + store = environment.feature_store + online_config = RemoteOnlineStoreConfig(type="remote", path="dummy") + + with pytest.raises( + PermissionNotFoundException, + match="Permission dummy_name does not exist in project dummy_project", + ): + get_remote_online_features( + config=RepoConfig( + project="test", online_store=online_config, registry=store.registry + ), + req_body="{test:test}", + ) + + +@patch("feast.infra.online_stores.remote.requests.sessions.Session.post") +def test_rest_error_handling_with_none_feast_exception( + mock_post, environment, none_feast_exception +): + # Create a mock response object + mock_response = Mock() + mock_response.status_code = 500 + mock_response.json.return_value = str(none_feast_exception) + mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError() + + # Configure the mock to return the mock response + mock_post.return_value = mock_response + + store = environment.feature_store + online_config = RemoteOnlineStoreConfig(type="remote", path="dummy") + + response = get_remote_online_features( + config=RepoConfig( + project="test", online_store=online_config, registry=store.registry + ), + req_body="{test:test}", + ) + + assertpy.assert_that(response).is_not_none() + assertpy.assert_that(response.status_code).is_equal_to(500) + assertpy.assert_that(response.json()).is_equal_to("('dummy_name', 'dummy_project')") From 7b250e5eff5de56f5c5da103e91051276940298a Mon Sep 17 00:00:00 2001 From: Tommy Hughes IV Date: Thu, 5 Sep 2024 21:12:15 -0500 Subject: [PATCH 053/185] =?UTF-8?q?feat:=20Add=20cli=20list/describe=20for?= =?UTF-8?q?=20SavedDatasets,=20StreamFeatureViews,=20&=20=E2=80=A6=20(#448?= =?UTF-8?q?7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat: Add cli list/describe for SavedDatasets, StreamFeatureViews, & ValidationReferences Signed-off-by: Tommy Hughes --- sdk/python/feast/cli.py | 150 ++++++++++++++++++ .../offline_store/test_validation.py | 17 ++ .../registration/test_universal_cli.py | 16 ++ 3 files changed, 183 insertions(+) diff --git a/sdk/python/feast/cli.py b/sdk/python/feast/cli.py index 0a12d1dcbc..ec90b31151 100644 --- a/sdk/python/feast/cli.py +++ b/sdk/python/feast/cli.py @@ -470,6 +470,156 @@ def on_demand_feature_view_list(ctx: click.Context, tags: list[str]): print(tabulate(table, headers=["NAME"], tablefmt="plain")) +@cli.group(name="saved-datasets") +def saved_datasets_cmd(): + """ + [Experimental] Access saved datasets + """ + pass + + +@saved_datasets_cmd.command("describe") +@click.argument("name", type=click.STRING) +@click.pass_context +def saved_datasets_describe(ctx: click.Context, name: str): + """ + [Experimental] Describe a saved dataset + """ + store = create_feature_store(ctx) + + try: + saved_dataset = store.get_saved_dataset(name) + except FeastObjectNotFoundException as e: + print(e) + exit(1) + + print( + yaml.dump( + yaml.safe_load(str(saved_dataset)), + default_flow_style=False, + sort_keys=False, + ) + ) + + +@saved_datasets_cmd.command(name="list") +@tagsOption +@click.pass_context +def saved_datasets_list(ctx: click.Context, tags: list[str]): + """ + [Experimental] List all saved datasets + """ + store = create_feature_store(ctx) + table = [] + tags_filter = utils.tags_list_to_dict(tags) + for saved_dataset in store.list_saved_datasets(tags=tags_filter): + table.append([saved_dataset.name]) + + from tabulate import tabulate + + print(tabulate(table, headers=["NAME"], tablefmt="plain")) + + +@cli.group(name="stream-feature-views") +def stream_feature_views_cmd(): + """ + [Experimental] Access stream feature views + """ + pass + + +@stream_feature_views_cmd.command("describe") +@click.argument("name", type=click.STRING) +@click.pass_context +def stream_feature_views_describe(ctx: click.Context, name: str): + """ + [Experimental] Describe a stream feature view + """ + store = create_feature_store(ctx) + + try: + stream_feature_view = store.get_stream_feature_view(name) + except FeastObjectNotFoundException as e: + print(e) + exit(1) + + print( + yaml.dump( + yaml.safe_load(str(stream_feature_view)), + default_flow_style=False, + sort_keys=False, + ) + ) + + +@stream_feature_views_cmd.command(name="list") +@tagsOption +@click.pass_context +def stream_feature_views_list(ctx: click.Context, tags: list[str]): + """ + [Experimental] List all stream feature views + """ + store = create_feature_store(ctx) + table = [] + tags_filter = utils.tags_list_to_dict(tags) + for stream_feature_view in store.list_stream_feature_views(tags=tags_filter): + table.append([stream_feature_view.name]) + + from tabulate import tabulate + + print(tabulate(table, headers=["NAME"], tablefmt="plain")) + + +@cli.group(name="validation-references") +def validation_references_cmd(): + """ + [Experimental] Access validation references + """ + pass + + +@validation_references_cmd.command("describe") +@click.argument("name", type=click.STRING) +@click.pass_context +def validation_references_describe(ctx: click.Context, name: str): + """ + [Experimental] Describe a validation reference + """ + store = create_feature_store(ctx) + + try: + validation_reference = store.get_validation_reference(name) + except FeastObjectNotFoundException as e: + print(e) + exit(1) + + print( + yaml.dump( + yaml.safe_load(str(validation_reference)), + default_flow_style=False, + sort_keys=False, + ) + ) + + +@validation_references_cmd.command(name="list") +@tagsOption +@click.pass_context +def validation_references_list(ctx: click.Context, tags: list[str]): + """ + [Experimental] List all validation references + """ + store = create_feature_store(ctx) + table = [] + tags_filter = utils.tags_list_to_dict(tags) + for validation_reference in store.list_validation_references(tags=tags_filter): + table.append([validation_reference.name]) + + from tabulate import tabulate + + print(tabulate(table, headers=["NAME"], tablefmt="plain")) + + @cli.command("plan", cls=NoOptionDefaultFormat) @click.option( "--skip-source-validation", diff --git a/sdk/python/tests/integration/offline_store/test_validation.py b/sdk/python/tests/integration/offline_store/test_validation.py index 6f0496e8c8..52d83ab8d8 100644 --- a/sdk/python/tests/integration/offline_store/test_validation.py +++ b/sdk/python/tests/integration/offline_store/test_validation.py @@ -305,6 +305,23 @@ def test_e2e_validation_via_cli(environment, universal_data_sources): assert p.returncode == 0, p.stderr.decode() assert "Validation successful" in p.stdout.decode(), p.stderr.decode() + p = runner.run( + ["saved-datasets", "describe", saved_dataset.name], cwd=local_repo.repo_path + ) + assert p.returncode == 0, p.stderr.decode() + + p = runner.run( + ["validation-references", "describe", reference.name], + cwd=local_repo.repo_path, + ) + assert p.returncode == 0, p.stderr.decode() + + p = runner.run( + ["feature-services", "describe", feature_service.name], + cwd=local_repo.repo_path, + ) + assert p.returncode == 0, p.stderr.decode() + # make sure second validation will use cached profile shutil.rmtree(saved_dataset.storage.file_options.uri) diff --git a/sdk/python/tests/integration/registration/test_universal_cli.py b/sdk/python/tests/integration/registration/test_universal_cli.py index 9e02ded4e4..5c238da24d 100644 --- a/sdk/python/tests/integration/registration/test_universal_cli.py +++ b/sdk/python/tests/integration/registration/test_universal_cli.py @@ -63,6 +63,12 @@ def test_universal_cli(): assertpy.assert_that(result.returncode).is_equal_to(0) result = runner.run(["permissions", "list"], cwd=repo_path) assertpy.assert_that(result.returncode).is_equal_to(0) + result = runner.run(["validation-references", "list"], cwd=repo_path) + assertpy.assert_that(result.returncode).is_equal_to(0) + result = runner.run(["stream-feature-views", "list"], cwd=repo_path) + assertpy.assert_that(result.returncode).is_equal_to(0) + result = runner.run(["saved-datasets", "list"], cwd=repo_path) + assertpy.assert_that(result.returncode).is_equal_to(0) # entity & feature view describe commands should succeed when objects exist result = runner.run(["entities", "describe", "driver"], cwd=repo_path) @@ -95,6 +101,16 @@ def test_universal_cli(): assertpy.assert_that(result.returncode).is_equal_to(1) result = runner.run(["permissions", "describe", "foo"], cwd=repo_path) assertpy.assert_that(result.returncode).is_equal_to(1) + result = runner.run( + ["validation-references", "describe", "foo"], cwd=repo_path + ) + assertpy.assert_that(result.returncode).is_equal_to(1) + result = runner.run( + ["stream-feature-views", "describe", "foo"], cwd=repo_path + ) + assertpy.assert_that(result.returncode).is_equal_to(1) + result = runner.run(["saved-datasets", "describe", "foo"], cwd=repo_path) + assertpy.assert_that(result.returncode).is_equal_to(1) # Doing another apply should be a no op, and should not cause errors result = runner.run(["apply"], cwd=repo_path) From 1015618f4828ab00f733971791a7b76a6c099189 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Sep 2024 22:12:35 -0400 Subject: [PATCH 054/185] chore: Bump cryptography from 42.0.8 to 43.0.1 in /sdk/python/requirements (#4483) chore: Bump cryptography in /sdk/python/requirements Bumps [cryptography](https://github.com/pyca/cryptography) from 42.0.8 to 43.0.1. - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/42.0.8...43.0.1) --- updated-dependencies: - dependency-name: cryptography dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdk/python/requirements/py3.10-ci-requirements.txt | 2 +- sdk/python/requirements/py3.11-ci-requirements.txt | 2 +- sdk/python/requirements/py3.9-ci-requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdk/python/requirements/py3.10-ci-requirements.txt b/sdk/python/requirements/py3.10-ci-requirements.txt index 6970dd2aed..bfe855a2d8 100644 --- a/sdk/python/requirements/py3.10-ci-requirements.txt +++ b/sdk/python/requirements/py3.10-ci-requirements.txt @@ -115,7 +115,7 @@ comm==0.2.2 # ipywidgets coverage[toml]==7.5.4 # via pytest-cov -cryptography==42.0.8 +cryptography==43.0.1 # via # azure-identity # azure-storage-blob diff --git a/sdk/python/requirements/py3.11-ci-requirements.txt b/sdk/python/requirements/py3.11-ci-requirements.txt index 2d7a5b252e..6a097526d7 100644 --- a/sdk/python/requirements/py3.11-ci-requirements.txt +++ b/sdk/python/requirements/py3.11-ci-requirements.txt @@ -111,7 +111,7 @@ comm==0.2.2 # ipywidgets coverage[toml]==7.5.4 # via pytest-cov -cryptography==42.0.8 +cryptography==43.0.1 # via # azure-identity # azure-storage-blob diff --git a/sdk/python/requirements/py3.9-ci-requirements.txt b/sdk/python/requirements/py3.9-ci-requirements.txt index 62f9280fe5..f32f6790d3 100644 --- a/sdk/python/requirements/py3.9-ci-requirements.txt +++ b/sdk/python/requirements/py3.9-ci-requirements.txt @@ -115,7 +115,7 @@ comm==0.2.2 # ipywidgets coverage[toml]==7.5.4 # via pytest-cov -cryptography==42.0.8 +cryptography==43.0.1 # via # azure-identity # azure-storage-blob From 3c4745c64ddbc589864d81572fe720dd9c0aaddc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Sep 2024 22:15:48 -0400 Subject: [PATCH 055/185] chore: Bump webpack from 5.76.1 to 5.94.0 in /sdk/python/feast/ui (#4469) Bumps [webpack](https://github.com/webpack/webpack) from 5.76.1 to 5.94.0. - [Release notes](https://github.com/webpack/webpack/releases) - [Commits](https://github.com/webpack/webpack/compare/v5.76.1...v5.94.0) --- updated-dependencies: - dependency-name: webpack dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdk/python/feast/ui/yarn.lock | 428 ++++++++++++++++++++++------------ 1 file changed, 273 insertions(+), 155 deletions(-) diff --git a/sdk/python/feast/ui/yarn.lock b/sdk/python/feast/ui/yarn.lock index cd1913bbb1..452b6f9f31 100644 --- a/sdk/python/feast/ui/yarn.lock +++ b/sdk/python/feast/ui/yarn.lock @@ -1743,11 +1743,25 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.9" +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" + integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.24" + "@jridgewell/resolve-uri@3.1.0", "@jridgewell/resolve-uri@^3.0.3": version "3.1.0" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + "@jridgewell/set-array@^1.0.0": version "1.1.1" resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.1.tgz#36a6acc93987adcf0ba50c66908bd0b70de8afea" @@ -1758,6 +1772,11 @@ resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + "@jridgewell/source-map@^0.3.2": version "0.3.2" resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb" @@ -1766,11 +1785,24 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" +"@jridgewell/source-map@^0.3.3": + version "0.3.6" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.6.tgz#9d71ca886e32502eb9362c9a74a46787c36df81a" + integrity sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + "@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10": version "1.4.14" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== +"@jridgewell/sourcemap-codec@^1.4.14": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + "@jridgewell/trace-mapping@^0.3.17": version "0.3.18" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz#25783b2086daf6ff1dcb53c9249ae480e4dd4cd6" @@ -1779,6 +1811,14 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" +"@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + "@jridgewell/trace-mapping@^0.3.9": version "0.3.14" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz#b231a081d8f66796e475ad588a1ef473112701ed" @@ -2400,22 +2440,6 @@ "@types/d3-transition" "*" "@types/d3-zoom" "*" -"@types/eslint-scope@^3.7.3": - version "3.7.3" - resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.3.tgz#125b88504b61e3c8bc6f870882003253005c3224" - integrity sha512-PB3ldyrcnAicT35TWPs5IcwKD8S333HMaa2VVv4+wdvebJkjWuW/xESoB8IwRcog8HYVYamb1g/R31Qv5Bx03g== - dependencies: - "@types/eslint" "*" - "@types/estree" "*" - -"@types/eslint@*": - version "8.4.2" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.4.2.tgz#48f2ac58ab9c631cb68845c3d956b28f79fad575" - integrity sha512-Z1nseZON+GEnFjJc04sv4NSALGjhFwy6K0HXt7qsn5ArfAKtb63dXNJHf+1YW6IpOIYRBGUbu3GwJdj8DGnCjA== - dependencies: - "@types/estree" "*" - "@types/json-schema" "*" - "@types/eslint@^7.28.2": version "7.29.0" resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.29.0.tgz#e56ddc8e542815272720bb0b4ccc2aff9c3e1c78" @@ -2424,7 +2448,7 @@ "@types/estree" "*" "@types/json-schema" "*" -"@types/estree@*", "@types/estree@^0.0.51": +"@types/estree@*": version "0.0.51" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== @@ -2434,6 +2458,11 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== +"@types/estree@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" + integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== + "@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.18": version "4.17.28" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz#c47def9f34ec81dc6328d0b1b5303d1ec98d86b8" @@ -2869,125 +2898,125 @@ "@typescript-eslint/types" "5.23.0" eslint-visitor-keys "^3.0.0" -"@webassemblyjs/ast@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7" - integrity sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw== +"@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.12.1.tgz#bb16a0e8b1914f979f45864c23819cc3e3f0d4bb" + integrity sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg== dependencies: - "@webassemblyjs/helper-numbers" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/helper-numbers" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" -"@webassemblyjs/floating-point-hex-parser@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz#f6c61a705f0fd7a6aecaa4e8198f23d9dc179e4f" - integrity sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ== +"@webassemblyjs/floating-point-hex-parser@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz#dacbcb95aff135c8260f77fa3b4c5fea600a6431" + integrity sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw== -"@webassemblyjs/helper-api-error@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz#1a63192d8788e5c012800ba6a7a46c705288fd16" - integrity sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg== +"@webassemblyjs/helper-api-error@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" + integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== -"@webassemblyjs/helper-buffer@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz#832a900eb444884cde9a7cad467f81500f5e5ab5" - integrity sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA== +"@webassemblyjs/helper-buffer@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz#6df20d272ea5439bf20ab3492b7fb70e9bfcb3f6" + integrity sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw== -"@webassemblyjs/helper-numbers@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz#64d81da219fbbba1e3bd1bfc74f6e8c4e10a62ae" - integrity sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ== +"@webassemblyjs/helper-numbers@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz#cbce5e7e0c1bd32cf4905ae444ef64cea919f1b5" + integrity sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g== dependencies: - "@webassemblyjs/floating-point-hex-parser" "1.11.1" - "@webassemblyjs/helper-api-error" "1.11.1" + "@webassemblyjs/floating-point-hex-parser" "1.11.6" + "@webassemblyjs/helper-api-error" "1.11.6" "@xtuc/long" "4.2.2" -"@webassemblyjs/helper-wasm-bytecode@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz#f328241e41e7b199d0b20c18e88429c4433295e1" - integrity sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q== +"@webassemblyjs/helper-wasm-bytecode@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" + integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== -"@webassemblyjs/helper-wasm-section@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz#21ee065a7b635f319e738f0dd73bfbda281c097a" - integrity sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg== +"@webassemblyjs/helper-wasm-section@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz#3da623233ae1a60409b509a52ade9bc22a37f7bf" + integrity sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g== dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-buffer" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/wasm-gen" "1.11.1" + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/wasm-gen" "1.12.1" -"@webassemblyjs/ieee754@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz#963929e9bbd05709e7e12243a099180812992614" - integrity sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ== +"@webassemblyjs/ieee754@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz#bb665c91d0b14fffceb0e38298c329af043c6e3a" + integrity sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg== dependencies: "@xtuc/ieee754" "^1.2.0" -"@webassemblyjs/leb128@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.1.tgz#ce814b45574e93d76bae1fb2644ab9cdd9527aa5" - integrity sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw== +"@webassemblyjs/leb128@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz#70e60e5e82f9ac81118bc25381a0b283893240d7" + integrity sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ== dependencies: "@xtuc/long" "4.2.2" -"@webassemblyjs/utf8@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.1.tgz#d1f8b764369e7c6e6bae350e854dec9a59f0a3ff" - integrity sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ== - -"@webassemblyjs/wasm-edit@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz#ad206ebf4bf95a058ce9880a8c092c5dec8193d6" - integrity sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-buffer" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/helper-wasm-section" "1.11.1" - "@webassemblyjs/wasm-gen" "1.11.1" - "@webassemblyjs/wasm-opt" "1.11.1" - "@webassemblyjs/wasm-parser" "1.11.1" - "@webassemblyjs/wast-printer" "1.11.1" - -"@webassemblyjs/wasm-gen@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz#86c5ea304849759b7d88c47a32f4f039ae3c8f76" - integrity sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/ieee754" "1.11.1" - "@webassemblyjs/leb128" "1.11.1" - "@webassemblyjs/utf8" "1.11.1" - -"@webassemblyjs/wasm-opt@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz#657b4c2202f4cf3b345f8a4c6461c8c2418985f2" - integrity sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-buffer" "1.11.1" - "@webassemblyjs/wasm-gen" "1.11.1" - "@webassemblyjs/wasm-parser" "1.11.1" - -"@webassemblyjs/wasm-parser@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz#86ca734534f417e9bd3c67c7a1c75d8be41fb199" - integrity sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-api-error" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/ieee754" "1.11.1" - "@webassemblyjs/leb128" "1.11.1" - "@webassemblyjs/utf8" "1.11.1" - -"@webassemblyjs/wast-printer@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz#d0c73beda8eec5426f10ae8ef55cee5e7084c2f0" - integrity sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg== - dependencies: - "@webassemblyjs/ast" "1.11.1" +"@webassemblyjs/utf8@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" + integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== + +"@webassemblyjs/wasm-edit@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz#9f9f3ff52a14c980939be0ef9d5df9ebc678ae3b" + integrity sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/helper-wasm-section" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-opt" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" + "@webassemblyjs/wast-printer" "1.12.1" + +"@webassemblyjs/wasm-gen@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz#a6520601da1b5700448273666a71ad0a45d78547" + integrity sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wasm-opt@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz#9e6e81475dfcfb62dab574ac2dda38226c232bc5" + integrity sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" + +"@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz#c47acb90e6f083391e3fa61d113650eea1e95937" + integrity sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-api-error" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wast-printer@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz#bcecf661d7d1abdaf989d8341a4833e33e2b31ac" + integrity sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA== + dependencies: + "@webassemblyjs/ast" "1.12.1" "@xtuc/long" "4.2.2" "@xtuc/ieee754@^1.2.0": @@ -3021,10 +3050,10 @@ acorn-globals@^6.0.0: acorn "^7.1.1" acorn-walk "^7.1.1" -acorn-import-assertions@^1.7.6: - version "1.8.0" - resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz#ba2b5939ce62c238db6d93d81c9b111b29b855e9" - integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw== +acorn-import-attributes@^1.9.5: + version "1.9.5" + resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef" + integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ== acorn-jsx@^5.3.2: version "5.3.2" @@ -3055,6 +3084,11 @@ acorn@^8.2.4, acorn@^8.5.0, acorn@^8.7.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30" integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== +acorn@^8.8.2: + version "8.12.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" + integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== + address@^1.0.1, address@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/address/-/address-1.2.0.tgz#d352a62c92fee90f89a693eccd2a8b2139ab02d9" @@ -3585,7 +3619,7 @@ browser-process-hrtime@^1.0.0: resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== -browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.16.6, browserslist@^4.18.1, browserslist@^4.20.2, browserslist@^4.20.3: +browserslist@^4.0.0, browserslist@^4.16.6, browserslist@^4.18.1, browserslist@^4.20.2, browserslist@^4.20.3: version "4.20.3" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.20.3.tgz#eb7572f49ec430e054f56d52ff0ebe9be915f8bf" integrity sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg== @@ -3596,6 +3630,16 @@ browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.16.6, browserslist@^4 node-releases "^2.0.3" picocolors "^1.0.0" +browserslist@^4.21.10: + version "4.23.3" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.3.tgz#debb029d3c93ebc97ffbc8d9cbb03403e227c800" + integrity sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA== + dependencies: + caniuse-lite "^1.0.30001646" + electron-to-chromium "^1.5.4" + node-releases "^2.0.18" + update-browserslist-db "^1.1.0" + bser@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" @@ -3679,6 +3723,11 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001332, caniuse-lite@^1.0.30001335: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001339.tgz#f9aece4ea8156071613b27791547ba0b33f176cf" integrity sha512-Es8PiVqCe+uXdms0Gu5xP5PF2bxLR7OBp3wUzUnuO7OHzhOfCyg3hdiGWVPVxhiuniOzng+hTc1u3fEQ0TlkSQ== +caniuse-lite@^1.0.30001646: + version "1.0.30001653" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001653.tgz#b8af452f8f33b1c77f122780a4aecebea0caca56" + integrity sha512-XGWQVB8wFQ2+9NZwZ10GxTYC5hk0Fa+q8cSkr0tgvMhYhMHP/QC+WTgrePMDBWiWc/pV+1ik82Al20XOK25Gcw== + case-sensitive-paths-webpack-plugin@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz#db64066c6422eed2e08cc14b986ca43796dbc6d4" @@ -4823,6 +4872,11 @@ electron-to-chromium@^1.4.118: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.137.tgz#186180a45617283f1c012284458510cd99d6787f" integrity sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA== +electron-to-chromium@^1.5.4: + version "1.5.13" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz#1abf0410c5344b2b829b7247e031f02810d442e6" + integrity sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q== + emittery@^0.10.2: version "0.10.2" resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.10.2.tgz#902eec8aedb8c41938c46e9385e9db7e03182933" @@ -4858,10 +4912,10 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= -enhanced-resolve@^5.10.0: - version "5.12.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz#300e1c90228f5b570c4d35babf263f6da7155634" - integrity sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ== +enhanced-resolve@^5.17.1: + version "5.17.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" + integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" @@ -4914,10 +4968,10 @@ es-abstract@^1.17.2, es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19 string.prototype.trimstart "^1.0.5" unbox-primitive "^1.0.2" -es-module-lexer@^0.9.0: - version "0.9.3" - resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19" - integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== +es-module-lexer@^1.2.1: + version "1.5.4" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.4.tgz#a8efec3a3da991e60efa6b633a7cad6ab8d26b78" + integrity sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw== es-shim-unscopables@^1.0.0: version "1.0.0" @@ -4940,6 +4994,11 @@ escalade@^3.1.1: resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== +escalade@^3.1.2: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" @@ -5736,6 +5795,11 @@ graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== +graceful-fs@^4.2.11: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + gzip-size@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462" @@ -7536,6 +7600,11 @@ node-int64@^0.4.0: resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= +node-releases@^2.0.18: + version "2.0.18" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" + integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== + node-releases@^2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.4.tgz#f38252370c43854dc48aa431c766c6c398f40476" @@ -7914,6 +7983,11 @@ picocolors@^1.0.0: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== +picocolors@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" + integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== + picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" @@ -9397,7 +9471,7 @@ schema-utils@^2.6.5: ajv "^6.12.4" ajv-keywords "^3.5.2" -schema-utils@^3.0.0, schema-utils@^3.1.0, schema-utils@^3.1.1: +schema-utils@^3.0.0, schema-utils@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281" integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== @@ -9406,6 +9480,15 @@ schema-utils@^3.0.0, schema-utils@^3.1.0, schema-utils@^3.1.1: ajv "^6.12.5" ajv-keywords "^3.5.2" +schema-utils@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" + integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + schema-utils@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.0.0.tgz#60331e9e3ae78ec5d16353c467c34b3a0a1d3df7" @@ -9483,6 +9566,13 @@ serialize-javascript@^6.0.0: dependencies: randombytes "^2.1.0" +serialize-javascript@^6.0.1: + version "6.0.2" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" + integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== + dependencies: + randombytes "^2.1.0" + serialize-query-params@^1.3.5: version "1.3.6" resolved "https://registry.yarnpkg.com/serialize-query-params/-/serialize-query-params-1.3.6.tgz#5dd5225db85ce747fe6fbc4897628504faafec6d" @@ -10040,7 +10130,7 @@ terminal-link@^2.0.0: ansi-escapes "^4.2.1" supports-hyperlinks "^2.0.0" -terser-webpack-plugin@^5.1.3, terser-webpack-plugin@^5.2.5: +terser-webpack-plugin@^5.2.5: version "5.3.1" resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.1.tgz#0320dcc270ad5372c1e8993fabbd927929773e54" integrity sha512-GvlZdT6wPQKbDNW/GDQzZFg/j4vKU96yl2q6mcUkzKOgW4gwf1Z8cZToUCrz31XHlPWH8MVb1r2tFtdDtTGJ7g== @@ -10051,6 +10141,17 @@ terser-webpack-plugin@^5.1.3, terser-webpack-plugin@^5.2.5: source-map "^0.6.1" terser "^5.7.2" +terser-webpack-plugin@^5.3.10: + version "5.3.10" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz#904f4c9193c6fd2a03f693a2150c62a92f40d199" + integrity sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w== + dependencies: + "@jridgewell/trace-mapping" "^0.3.20" + jest-worker "^27.4.5" + schema-utils "^3.1.1" + serialize-javascript "^6.0.1" + terser "^5.26.0" + terser@^5.0.0, terser@^5.10.0, terser@^5.7.2: version "5.14.2" resolved "https://registry.yarnpkg.com/terser/-/terser-5.14.2.tgz#9ac9f22b06994d736174f4091aa368db896f1c10" @@ -10061,6 +10162,16 @@ terser@^5.0.0, terser@^5.10.0, terser@^5.7.2: commander "^2.20.0" source-map-support "~0.5.20" +terser@^5.26.0: + version "5.31.6" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.31.6.tgz#c63858a0f0703988d0266a82fcbf2d7ba76422b1" + integrity sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.8.2" + commander "^2.20.0" + source-map-support "~0.5.20" + test-exclude@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" @@ -10417,6 +10528,14 @@ upath@^1.2.0: resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== +update-browserslist-db@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz#7ca61c0d8650766090728046e416a8cde682859e" + integrity sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ== + dependencies: + escalade "^3.1.2" + picocolors "^1.0.1" + uri-js@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" @@ -10560,10 +10679,10 @@ walker@^1.0.7: dependencies: makeerror "1.0.12" -watchpack@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" - integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== +watchpack@^2.4.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.2.tgz#2feeaed67412e7c33184e5a79ca738fbd38564da" + integrity sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw== dependencies: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" @@ -10675,33 +10794,32 @@ webpack-sources@^3.2.3: integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== webpack@^5.64.4: - version "5.76.1" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.76.1.tgz#7773de017e988bccb0f13c7d75ec245f377d295c" - integrity sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ== - dependencies: - "@types/eslint-scope" "^3.7.3" - "@types/estree" "^0.0.51" - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/wasm-edit" "1.11.1" - "@webassemblyjs/wasm-parser" "1.11.1" + version "5.94.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.94.0.tgz#77a6089c716e7ab90c1c67574a28da518a20970f" + integrity sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg== + dependencies: + "@types/estree" "^1.0.5" + "@webassemblyjs/ast" "^1.12.1" + "@webassemblyjs/wasm-edit" "^1.12.1" + "@webassemblyjs/wasm-parser" "^1.12.1" acorn "^8.7.1" - acorn-import-assertions "^1.7.6" - browserslist "^4.14.5" + acorn-import-attributes "^1.9.5" + browserslist "^4.21.10" chrome-trace-event "^1.0.2" - enhanced-resolve "^5.10.0" - es-module-lexer "^0.9.0" + enhanced-resolve "^5.17.1" + es-module-lexer "^1.2.1" eslint-scope "5.1.1" events "^3.2.0" glob-to-regexp "^0.4.1" - graceful-fs "^4.2.9" + graceful-fs "^4.2.11" json-parse-even-better-errors "^2.3.1" loader-runner "^4.2.0" mime-types "^2.1.27" neo-async "^2.6.2" - schema-utils "^3.1.0" + schema-utils "^3.2.0" tapable "^2.1.1" - terser-webpack-plugin "^5.1.3" - watchpack "^2.4.0" + terser-webpack-plugin "^5.3.10" + watchpack "^2.4.1" webpack-sources "^3.2.3" websocket-driver@>=0.5.1, websocket-driver@^0.7.4: From c28bee5b37052c6eefacf5417d3232161690f25a Mon Sep 17 00:00:00 2001 From: Tornike Gurgenidze Date: Fri, 6 Sep 2024 06:17:19 +0400 Subject: [PATCH 056/185] chore: Cleanup CODEOWNERS (#4477) Signed-off-by: tokoko --- CODEOWNERS | 36 ++++-------------------------------- 1 file changed, 4 insertions(+), 32 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 18914d9f5d..75ede8b6aa 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -2,13 +2,13 @@ # for more info about CODEOWNERS file # Core Interfaces -/sdk/python/feast/infra/offline_stores/offline_store.py @feast-dev/maintainers @sfc-gh-madkins -/sdk/python/feast/infra/online_stores/online_store.py @feast-dev/maintainers @DvirDukhan -/sdk/python/feast/infra/materialization_engine/batch_materialization_engine.py @feast-dev/maintainers @whoahbot @sfc-gh-madkins +/sdk/python/feast/infra/offline_stores/offline_store.py @feast-dev/maintainers +/sdk/python/feast/infra/online_stores/online_store.py @feast-dev/maintainers +/sdk/python/feast/infra/materialization_engine/batch_materialization_engine.py @feast-dev/maintainers # ==== Offline Stores ==== # Core utils -/sdk/python/feast/infra/offline_stores/offline_utils.py @feast-dev/maintainers @sfc-gh-madkins +/sdk/python/feast/infra/offline_stores/offline_utils.py @feast-dev/maintainers # Offline interfaces /sdk/python/feast/infra/offline_stores/offline_store.py @feast-dev/maintainers @@ -18,38 +18,10 @@ /sdk/python/feast/infra/offline_stores/bigquery_source.py @sudohainguyen /sdk/python/tests/integration/feature_repos/universal/data_sources/bigquery.py @sudohainguyen -# Snowflake -/sdk/python/feast/infra/offline_stores/snowflake* @sfc-gh-madkins - -# Athena (contrib) -/sdk/python/feast/infra/offline_stores/contrib/athena_offline_store/ @toping4445 - -# Azure SQL (contrib) -/sdk/python/feast/infra/offline_stores/contrib/mssql_offline_store/ @kevjumba - -# Spark (contrib) -/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/ @niklasvm @kevjumba - # ==== Online Stores ==== # HBase /sdk/python/feast/infra/online_stores/hbase.py @sudohainguyen /sdk/python/feast/infra/online_stores/contrib/hbase_online_store @sudohainguyen -# Redis -/sdk/python/feast/infra/online_stores/redis.py @DvirDukhan -/java/feast/serving/connectors/redis/ @DvirDukhan - -# Snowflake -/sdk/python/feast/infra/online_stores/snowflake.py @sfc-gh-madkins - -# Cassandra (contrib) -/sdk/python/feast/infra/online_stores/cassandra_online_store/ @hemidactylus - # ==== Batch Materialization Engines ==== - -# Snowflake -/sdk/python/feast/infra/materialization/snowflake* @sfc-gh-madkins - -# AWS Lambda -/sdk/python/feast/infra/materialization/contrib/aws_lambda/ @achals From 4a6b663f80bc91d6de35ed2ec428d34811d17a18 Mon Sep 17 00:00:00 2001 From: Bhargav Dodla <13788369+EXPEbdodla@users.noreply.github.com> Date: Thu, 5 Sep 2024 23:02:01 -0700 Subject: [PATCH 057/185] feat: Added Project object to Feast Objects (#4475) * feat: Added Project object to Feast Objects Signed-off-by: Bhargav Dodla Signed-off-by: Bhargav Dodla * fix: Extend FeastError and fixed integration tests Signed-off-by: Bhargav Dodla * fix: Small optimization to test_modify_feature_views_success test Signed-off-by: Bhargav Dodla * fix: Added Project object to template and quick start Signed-off-by: Bhargav Dodla --------- Signed-off-by: Bhargav Dodla Co-authored-by: Bhargav Dodla --- docs/getting-started/quickstart.md | 5 + protos/feast/core/Permission.proto | 1 + protos/feast/core/Project.proto | 52 ++++ protos/feast/core/Registry.proto | 6 +- protos/feast/registry/RegistryServer.proto | 33 ++ sdk/python/feast/__init__.py | 2 + sdk/python/feast/cli.py | 73 +++++ sdk/python/feast/diff/registry_diff.py | 6 + sdk/python/feast/errors.py | 10 + sdk/python/feast/feast_object.py | 5 + sdk/python/feast/feature_store.py | 58 ++-- sdk/python/feast/feature_view.py | 2 +- .../feast/infra/online_stores/remote.py | 2 +- .../feast/infra/registry/base_registry.py | 69 +++++ .../feast/infra/registry/caching_registry.py | 45 ++- .../infra/registry/proto_registry_utils.py | 28 +- sdk/python/feast/infra/registry/registry.py | 286 ++++++++++++------ sdk/python/feast/infra/registry/remote.py | 82 ++++- sdk/python/feast/infra/registry/snowflake.py | 244 ++++++++++++--- sdk/python/feast/infra/registry/sql.py | 279 +++++++++++++---- .../registry/snowflake_table_creation.sql | 8 + sdk/python/feast/permissions/permission.py | 1 + .../feast/permissions/security_manager.py | 21 +- sdk/python/feast/project.py | 175 +++++++++++ sdk/python/feast/registry_server.py | 53 ++++ sdk/python/feast/repo_config.py | 10 +- sdk/python/feast/repo_contents.py | 3 + sdk/python/feast/repo_operations.py | 97 ++++-- sdk/python/feast/templates/local/bootstrap.py | 2 + .../local/feature_repo/example_repo.py | 4 + sdk/python/tests/conftest.py | 26 +- .../example_feature_repo_with_project_1.py | 151 +++++++++ .../online_store/test_remote_online_store.py | 3 - .../registration/test_universal_cli.py | 145 ++++++++- .../registration/test_universal_registry.py | 257 +++++++++++++++- .../tests/unit/permissions/auth/conftest.py | 2 + .../auth/server/test_auth_registry_server.py | 13 +- .../permissions/auth/server/test_utils.py | 8 + .../tests/unit/test_on_demand_feature_view.py | 8 +- sdk/python/tests/unit/test_project.py | 122 ++++++++ 40 files changed, 2064 insertions(+), 333 deletions(-) create mode 100644 protos/feast/core/Project.proto create mode 100644 sdk/python/feast/project.py create mode 100644 sdk/python/tests/example_repos/example_feature_repo_with_project_1.py create mode 100644 sdk/python/tests/unit/test_project.py diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md index ffc01c9d6e..7169989e7e 100644 --- a/docs/getting-started/quickstart.md +++ b/docs/getting-started/quickstart.md @@ -103,12 +103,17 @@ from feast import ( FeatureView, Field, FileSource, + Project, PushSource, RequestSource, ) from feast.on_demand_feature_view import on_demand_feature_view from feast.types import Float32, Float64, Int64 +# Define a project for the feature repo +project = Project(name="my_project", description="A project for driver statistics") + + # Define an entity for the driver. You can think of an entity as a primary key used to # fetch features. driver = Entity(name="driver", join_keys=["driver_id"]) diff --git a/protos/feast/core/Permission.proto b/protos/feast/core/Permission.proto index 57958d3d81..400f70a11b 100644 --- a/protos/feast/core/Permission.proto +++ b/protos/feast/core/Permission.proto @@ -45,6 +45,7 @@ message PermissionSpec { VALIDATION_REFERENCE = 7; SAVED_DATASET = 8; PERMISSION = 9; + PROJECT = 10; } repeated Type types = 3; diff --git a/protos/feast/core/Project.proto b/protos/feast/core/Project.proto new file mode 100644 index 0000000000..08e8b38f23 --- /dev/null +++ b/protos/feast/core/Project.proto @@ -0,0 +1,52 @@ +// +// * Copyright 2020 The Feast 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 +// * +// * https://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. +// + +syntax = "proto3"; + +package feast.core; +option java_package = "feast.proto.core"; +option java_outer_classname = "ProjectProto"; +option go_package = "github.com/feast-dev/feast/go/protos/feast/core"; + +import "google/protobuf/timestamp.proto"; + +message Project { + // User-specified specifications of this entity. + ProjectSpec spec = 1; + // System-populated metadata for this entity. + ProjectMeta meta = 2; +} + +message ProjectSpec { + // Name of the Project + string name = 1; + + // Description of the Project + string description = 2; + + // User defined metadata + map tags = 3; + + // Owner of the Project + string owner = 4; +} + +message ProjectMeta { + // Time when the Project is created + google.protobuf.Timestamp created_timestamp = 1; + // Time when the Project is last updated with registry changes (Apply stage) + google.protobuf.Timestamp last_updated_timestamp = 2; +} diff --git a/protos/feast/core/Registry.proto b/protos/feast/core/Registry.proto index b4f1ffb0a3..45ecd2c173 100644 --- a/protos/feast/core/Registry.proto +++ b/protos/feast/core/Registry.proto @@ -33,8 +33,9 @@ import "feast/core/SavedDataset.proto"; import "feast/core/ValidationProfile.proto"; import "google/protobuf/timestamp.proto"; import "feast/core/Permission.proto"; +import "feast/core/Project.proto"; -// Next id: 17 +// Next id: 18 message Registry { repeated Entity entities = 1; repeated FeatureTable feature_tables = 2; @@ -47,12 +48,13 @@ message Registry { repeated ValidationReference validation_references = 13; Infra infra = 10; // Tracking metadata of Feast by project - repeated ProjectMetadata project_metadata = 15; + repeated ProjectMetadata project_metadata = 15 [deprecated = true]; string registry_schema_version = 3; // to support migrations; incremented when schema is changed string version_id = 4; // version id, random string generated on each update of the data; now used only for debugging purposes google.protobuf.Timestamp last_updated = 5; repeated Permission permissions = 16; + repeated Project projects = 17; } message ProjectMetadata { diff --git a/protos/feast/registry/RegistryServer.proto b/protos/feast/registry/RegistryServer.proto index 928354077b..3ad64b5b34 100644 --- a/protos/feast/registry/RegistryServer.proto +++ b/protos/feast/registry/RegistryServer.proto @@ -15,6 +15,7 @@ import "feast/core/SavedDataset.proto"; import "feast/core/ValidationProfile.proto"; import "feast/core/InfraObject.proto"; import "feast/core/Permission.proto"; +import "feast/core/Project.proto"; service RegistryServer{ // Entity RPCs @@ -67,6 +68,12 @@ service RegistryServer{ rpc ListPermissions (ListPermissionsRequest) returns (ListPermissionsResponse) {} rpc DeletePermission (DeletePermissionRequest) returns (google.protobuf.Empty) {} + // Project RPCs + rpc ApplyProject (ApplyProjectRequest) returns (google.protobuf.Empty) {} + rpc GetProject (GetProjectRequest) returns (feast.core.Project) {} + rpc ListProjects (ListProjectsRequest) returns (ListProjectsResponse) {} + rpc DeleteProject (DeleteProjectRequest) returns (google.protobuf.Empty) {} + rpc ApplyMaterialization (ApplyMaterializationRequest) returns (google.protobuf.Empty) {} rpc ListProjectMetadata (ListProjectMetadataRequest) returns (ListProjectMetadataResponse) {} rpc UpdateInfra (UpdateInfraRequest) returns (google.protobuf.Empty) {} @@ -356,3 +363,29 @@ message DeletePermissionRequest { string project = 2; bool commit = 3; } + +// Projects + +message ApplyProjectRequest { + feast.core.Project project = 1; + bool commit = 2; +} + +message GetProjectRequest { + string name = 1; + bool allow_cache = 2; +} + +message ListProjectsRequest { + bool allow_cache = 1; + map tags = 2; +} + +message ListProjectsResponse { + repeated feast.core.Project projects = 1; +} + +message DeleteProjectRequest { + string name = 1; + bool commit = 2; +} diff --git a/sdk/python/feast/__init__.py b/sdk/python/feast/__init__.py index 52734bc71e..71122b7047 100644 --- a/sdk/python/feast/__init__.py +++ b/sdk/python/feast/__init__.py @@ -18,6 +18,7 @@ from .feature_view import FeatureView from .field import Field from .on_demand_feature_view import OnDemandFeatureView +from .project import Project from .repo_config import RepoConfig from .stream_feature_view import StreamFeatureView from .value_type import ValueType @@ -49,4 +50,5 @@ "PushSource", "RequestSource", "AthenaSource", + "Project", ] diff --git a/sdk/python/feast/cli.py b/sdk/python/feast/cli.py index ec90b31151..499788101e 100644 --- a/sdk/python/feast/cli.py +++ b/sdk/python/feast/cli.py @@ -254,6 +254,79 @@ def data_source_list(ctx: click.Context, tags: list[str]): print(tabulate(table, headers=["NAME", "CLASS"], tablefmt="plain")) +@cli.group(name="projects") +def projects_cmd(): + """ + Access projects + """ + pass + + +@projects_cmd.command("describe") +@click.argument("name", type=click.STRING) +@click.pass_context +def project_describe(ctx: click.Context, name: str): + """ + Describe a project + """ + store = create_feature_store(ctx) + + try: + project = store.get_project(name) + except FeastObjectNotFoundException as e: + print(e) + exit(1) + + print( + yaml.dump( + yaml.safe_load(str(project)), default_flow_style=False, sort_keys=False + ) + ) + + +@projects_cmd.command("current_project") +@click.pass_context +def project_current(ctx: click.Context): + """ + Returns the current project configured with FeatureStore object + """ + store = create_feature_store(ctx) + + try: + project = store.get_project(name=None) + except FeastObjectNotFoundException as e: + print(e) + exit(1) + + print( + yaml.dump( + yaml.safe_load(str(project)), default_flow_style=False, sort_keys=False + ) + ) + + +@projects_cmd.command(name="list") +@tagsOption +@click.pass_context +def project_list(ctx: click.Context, tags: list[str]): + """ + List all projects + """ + store = create_feature_store(ctx) + table = [] + tags_filter = utils.tags_list_to_dict(tags) + for project in store.list_projects(tags=tags_filter): + table.append([project.name, project.description, project.tags, project.owner]) + + from tabulate import tabulate + + print( + tabulate( + table, headers=["NAME", "DESCRIPTION", "TAGS", "OWNER"], tablefmt="plain" + ) + ) + + @cli.group(name="entities") def entities_cmd(): """ diff --git a/sdk/python/feast/diff/registry_diff.py b/sdk/python/feast/diff/registry_diff.py index 6235025adc..272c4590d8 100644 --- a/sdk/python/feast/diff/registry_diff.py +++ b/sdk/python/feast/diff/registry_diff.py @@ -11,6 +11,7 @@ from feast.infra.registry.base_registry import BaseRegistry from feast.infra.registry.registry import FEAST_OBJECT_TYPES, FeastObjectType from feast.permissions.permission import Permission +from feast.project import Project from feast.protos.feast.core.DataSource_pb2 import DataSource as DataSourceProto from feast.protos.feast.core.Entity_pb2 import Entity as EntityProto from feast.protos.feast.core.FeatureService_pb2 import ( @@ -371,6 +372,11 @@ def apply_diff_to_registry( TransitionType.CREATE, TransitionType.UPDATE, ]: + if feast_object_diff.feast_object_type == FeastObjectType.PROJECT: + registry.apply_project( + cast(Project, feast_object_diff.new_feast_object), + commit=False, + ) if feast_object_diff.feast_object_type == FeastObjectType.DATA_SOURCE: registry.apply_data_source( cast(DataSource, feast_object_diff.new_feast_object), diff --git a/sdk/python/feast/errors.py b/sdk/python/feast/errors.py index fd5955fd98..4dbb220c1e 100644 --- a/sdk/python/feast/errors.py +++ b/sdk/python/feast/errors.py @@ -480,6 +480,16 @@ def __init__(self, name, project=None): super().__init__(f"Permission {name} does not exist") +class ProjectNotFoundException(FeastError): + def __init__(self, project): + super().__init__(f"Project {project} does not exist in registry") + + +class ProjectObjectNotFoundException(FeastObjectNotFoundException): + def __init__(self, name, project=None): + super().__init__(f"Project {name} does not exist") + + class ZeroRowsQueryResult(FeastError): def __init__(self, query: str): super().__init__(f"This query returned zero rows:\n{query}") diff --git a/sdk/python/feast/feast_object.py b/sdk/python/feast/feast_object.py index dfe29b7128..63fa1e913b 100644 --- a/sdk/python/feast/feast_object.py +++ b/sdk/python/feast/feast_object.py @@ -1,5 +1,8 @@ from typing import Union, get_args +from feast.project import Project +from feast.protos.feast.core.Project_pb2 import ProjectSpec + from .batch_feature_view import BatchFeatureView from .data_source import DataSource from .entity import Entity @@ -23,6 +26,7 @@ # Convenience type representing all Feast objects FeastObject = Union[ + Project, FeatureView, OnDemandFeatureView, BatchFeatureView, @@ -36,6 +40,7 @@ ] FeastObjectSpecProto = Union[ + ProjectSpec, FeatureViewSpec, OnDemandFeatureViewSpec, StreamFeatureViewSpec, diff --git a/sdk/python/feast/feature_store.py b/sdk/python/feast/feature_store.py index a03706e56f..27b6eade5b 100644 --- a/sdk/python/feast/feature_store.py +++ b/sdk/python/feast/feature_store.py @@ -60,11 +60,7 @@ ) from feast.feast_object import FeastObject from feast.feature_service import FeatureService -from feast.feature_view import ( - DUMMY_ENTITY, - DUMMY_ENTITY_NAME, - FeatureView, -) +from feast.feature_view import DUMMY_ENTITY, DUMMY_ENTITY_NAME, FeatureView from feast.inference import ( update_data_sources_with_inferred_event_timestamp_col, update_feature_views_with_inferred_features_and_entities, @@ -77,6 +73,7 @@ from feast.on_demand_feature_view import OnDemandFeatureView from feast.online_response import OnlineResponse from feast.permissions.permission import Permission +from feast.project import Project from feast.protos.feast.core.InfraObject_pb2 import Infra as InfraProto from feast.protos.feast.serving.ServingService_pb2 import ( FieldStatus, @@ -162,14 +159,12 @@ def __init__( registry_config, self.config.project, None, self.config.auth_config ) else: - r = Registry( + self._registry = Registry( self.config.project, registry_config, repo_path=self.repo_path, auth_config=self.config.auth_config, ) - r._initialize_registry(self.config.project) - self._registry = r self._provider = get_provider(self.config) @@ -205,16 +200,8 @@ def refresh_registry(self): greater than 0, then once the cache becomes stale (more time than the TTL has passed), a new cache will be downloaded synchronously, which may increase latencies if the triggering method is get_online_features(). """ - registry_config = self.config.registry - registry = Registry( - self.config.project, - registry_config, - repo_path=self.repo_path, - auth_config=self.config.auth_config, - ) - registry.refresh(self.config.project) - self._registry = registry + self._registry.refresh(self.project) def list_entities( self, allow_cache: bool = False, tags: Optional[dict[str, str]] = None @@ -740,6 +727,7 @@ def plan( ... source=driver_hourly_stats, ... ) >>> registry_diff, infra_diff, new_infra = fs.plan(RepoContents( + ... projects=[Project(name="project")], ... data_sources=[driver_hourly_stats], ... feature_views=[driver_hourly_stats_view], ... on_demand_feature_views=list(), @@ -802,6 +790,7 @@ def _apply_diffs( def apply( self, objects: Union[ + Project, DataSource, Entity, FeatureView, @@ -862,6 +851,9 @@ def apply( objects_to_delete = [] # Separate all objects into entities, feature services, and different feature view types. + projects_to_update = [ob for ob in objects if isinstance(ob, Project)] + if len(projects_to_update) > 1: + raise ValueError("Only one project can be applied at a time.") entities_to_update = [ob for ob in objects if isinstance(ob, Entity)] views_to_update = [ ob @@ -924,6 +916,8 @@ def apply( ) # Add all objects to the registry and update the provider's infrastructure. + for project in projects_to_update: + self._registry.apply_project(project, commit=False) for ds in data_sources_to_update: self._registry.apply_data_source(ds, project=self.project, commit=False) for view in itertools.chain(views_to_update, odfvs_to_update, sfvs_to_update): @@ -1990,6 +1984,36 @@ def get_permission(self, name: str) -> Permission: """ return self._registry.get_permission(name, self.project) + def list_projects( + self, allow_cache: bool = False, tags: Optional[dict[str, str]] = None + ) -> List[Project]: + """ + Retrieves the list of projects from the registry. + + Args: + allow_cache: Whether to allow returning projects from a cached registry. + tags: Filter by tags. + + Returns: + A list of projects. + """ + return self._registry.list_projects(allow_cache=allow_cache, tags=tags) + + def get_project(self, name: Optional[str]) -> Project: + """ + Retrieves a project from the registry. + + Args: + name: Name of the project. + + Returns: + The specified project. + + Raises: + ProjectObjectNotFoundException: The project could not be found. + """ + return self._registry.get_project(name or self.project) + def list_saved_datasets( self, allow_cache: bool = False, tags: Optional[dict[str, str]] = None ) -> List[SavedDataset]: diff --git a/sdk/python/feast/feature_view.py b/sdk/python/feast/feature_view.py index 1a85a4b90c..dd01078e20 100644 --- a/sdk/python/feast/feature_view.py +++ b/sdk/python/feast/feature_view.py @@ -423,7 +423,7 @@ def from_proto(cls, feature_view_proto: FeatureViewProto): if len(feature_view.entities) != len(feature_view.entity_columns): warnings.warn( - f"There are some mismatches in your feature view's registered entities. Please check if you have applied your entities correctly." + f"There are some mismatches in your feature view: {feature_view.name} registered entities. Please check if you have applied your entities correctly." f"Entities: {feature_view.entities} vs Entity Columns: {feature_view.entity_columns}" ) diff --git a/sdk/python/feast/infra/online_stores/remote.py b/sdk/python/feast/infra/online_stores/remote.py index 5f65d8da8b..8a7e299516 100644 --- a/sdk/python/feast/infra/online_stores/remote.py +++ b/sdk/python/feast/infra/online_stores/remote.py @@ -109,7 +109,7 @@ def online_read( result_tuples.append((event_ts, feature_values_dict)) return result_tuples else: - error_msg = f"Unable to retrieve the online store data using feature server API. Error_code={response.status_code}, error_message={response.reason}" + error_msg = f"Unable to retrieve the online store data using feature server API. Error_code={response.status_code}, error_message={response.text}" logger.error(error_msg) raise RuntimeError(error_msg) diff --git a/sdk/python/feast/infra/registry/base_registry.py b/sdk/python/feast/infra/registry/base_registry.py index 33adb6b7c9..f5040d9752 100644 --- a/sdk/python/feast/infra/registry/base_registry.py +++ b/sdk/python/feast/infra/registry/base_registry.py @@ -29,6 +29,7 @@ from feast.infra.infra_object import Infra from feast.on_demand_feature_view import OnDemandFeatureView from feast.permissions.permission import Permission +from feast.project import Project from feast.project_metadata import ProjectMetadata from feast.protos.feast.core.Entity_pb2 import Entity as EntityProto from feast.protos.feast.core.FeatureService_pb2 import ( @@ -39,6 +40,7 @@ OnDemandFeatureView as OnDemandFeatureViewProto, ) from feast.protos.feast.core.Permission_pb2 import Permission as PermissionProto +from feast.protos.feast.core.Project_pb2 import Project as ProjectProto from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto from feast.protos.feast.core.SavedDataset_pb2 import SavedDataset as SavedDatasetProto from feast.protos.feast.core.StreamFeatureView_pb2 import ( @@ -663,6 +665,71 @@ def list_permissions( """ raise NotImplementedError + @abstractmethod + def apply_project( + self, + project: Project, + commit: bool = True, + ): + """ + Registers a project with Feast + + Args: + project: A project that will be registered + commit: Whether to immediately commit to the registry + """ + raise NotImplementedError + + @abstractmethod + def delete_project( + self, + name: str, + commit: bool = True, + ): + """ + Deletes a project or raises an ProjectNotFoundException exception if not found. + + Args: + project: Feast project name that needs to be deleted + commit: Whether the change should be persisted immediately + """ + raise NotImplementedError + + @abstractmethod + def get_project( + self, + name: str, + allow_cache: bool = False, + ) -> Project: + """ + Retrieves a project. + + Args: + name: Feast project name + allow_cache: Whether to allow returning this permission from a cached registry + + Returns: + Returns either the specified project, or raises ProjectObjectNotFoundException exception if none is found + """ + raise NotImplementedError + + @abstractmethod + def list_projects( + self, + allow_cache: bool = False, + tags: Optional[dict[str, str]] = None, + ) -> List[Project]: + """ + Retrieve a list of projects from the registry + + Args: + allow_cache: Whether to allow returning permissions from a cached registry + + Returns: + List of project + """ + raise NotImplementedError + @abstractmethod def proto(self) -> RegistryProto: """ @@ -814,4 +881,6 @@ def deserialize_registry_values(serialized_proto, feast_obj_type) -> Any: return FeatureServiceProto.FromString(serialized_proto) if feast_obj_type == Permission: return PermissionProto.FromString(serialized_proto) + if feast_obj_type == Project: + return ProjectProto.FromString(serialized_proto) return None diff --git a/sdk/python/feast/infra/registry/caching_registry.py b/sdk/python/feast/infra/registry/caching_registry.py index 611d67de96..c04a62552b 100644 --- a/sdk/python/feast/infra/registry/caching_registry.py +++ b/sdk/python/feast/infra/registry/caching_registry.py @@ -1,6 +1,7 @@ import atexit import logging import threading +import warnings from abc import abstractmethod from datetime import timedelta from threading import Lock @@ -15,6 +16,7 @@ from feast.infra.registry.base_registry import BaseRegistry from feast.on_demand_feature_view import OnDemandFeatureView from feast.permissions.permission import Permission +from feast.project import Project from feast.project_metadata import ProjectMetadata from feast.saved_dataset import SavedDataset, ValidationReference from feast.stream_feature_view import StreamFeatureView @@ -26,7 +28,6 @@ class CachingRegistry(BaseRegistry): def __init__(self, project: str, cache_ttl_seconds: int, cache_mode: str): self.cached_registry_proto = self.proto() - proto_registry_utils.init_project_metadata(self.cached_registry_proto, project) self.cached_registry_proto_created = _utc_now() self._refresh_lock = Lock() self.cached_registry_proto_ttl = timedelta( @@ -308,6 +309,10 @@ def _list_project_metadata(self, project: str) -> List[ProjectMetadata]: def list_project_metadata( self, project: str, allow_cache: bool = False ) -> List[ProjectMetadata]: + warnings.warn( + "list_project_metadata is deprecated and will be removed in a future version. Use list_projects() and get_project() methods instead.", + DeprecationWarning, + ) if allow_cache: self._refresh_cached_registry_if_necessary() return proto_registry_utils.list_project_metadata( @@ -355,15 +360,35 @@ def list_permissions( ) return self._list_permissions(project, tags) + @abstractmethod + def _get_project(self, name: str) -> Project: + pass + + def get_project( + self, + name: str, + allow_cache: bool = False, + ) -> Project: + if allow_cache: + self._refresh_cached_registry_if_necessary() + return proto_registry_utils.get_project(self.cached_registry_proto, name) + return self._get_project(name) + + @abstractmethod + def _list_projects(self, tags: Optional[dict[str, str]]) -> List[Project]: + pass + + def list_projects( + self, + allow_cache: bool = False, + tags: Optional[dict[str, str]] = None, + ) -> List[Project]: + if allow_cache: + self._refresh_cached_registry_if_necessary() + return proto_registry_utils.list_projects(self.cached_registry_proto, tags) + return self._list_projects(tags) + def refresh(self, project: Optional[str] = None): - if project: - project_metadata = proto_registry_utils.get_project_metadata( - registry_proto=self.cached_registry_proto, project=project - ) - if not project_metadata: - proto_registry_utils.init_project_metadata( - self.cached_registry_proto, project - ) self.cached_registry_proto = self.proto() self.cached_registry_proto_created = _utc_now() @@ -395,7 +420,7 @@ def _start_thread_async_refresh(self, cache_ttl_seconds): self.registry_refresh_thread = threading.Timer( cache_ttl_seconds, self._start_thread_async_refresh, [cache_ttl_seconds] ) - self.registry_refresh_thread.setDaemon(True) + self.registry_refresh_thread.daemon = True self.registry_refresh_thread.start() def _exit_handler(self): diff --git a/sdk/python/feast/infra/registry/proto_registry_utils.py b/sdk/python/feast/infra/registry/proto_registry_utils.py index f67808aab5..b0413fd77e 100644 --- a/sdk/python/feast/infra/registry/proto_registry_utils.py +++ b/sdk/python/feast/infra/registry/proto_registry_utils.py @@ -1,4 +1,3 @@ -import uuid from functools import wraps from typing import List, Optional @@ -11,6 +10,7 @@ FeatureServiceNotFoundException, FeatureViewNotFoundException, PermissionObjectNotFoundException, + ProjectObjectNotFoundException, SavedDatasetNotFound, ValidationReferenceNotFound, ) @@ -18,6 +18,7 @@ from feast.feature_view import FeatureView from feast.on_demand_feature_view import OnDemandFeatureView from feast.permissions.permission import Permission +from feast.project import Project from feast.project_metadata import ProjectMetadata from feast.protos.feast.core.Registry_pb2 import ProjectMetadata as ProjectMetadataProto from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto @@ -69,13 +70,6 @@ def wrapper( return wrapper -def init_project_metadata(cached_registry_proto: RegistryProto, project: str): - new_project_uuid = f"{uuid.uuid4()}" - cached_registry_proto.project_metadata.append( - ProjectMetadata(project_name=project, project_uuid=new_project_uuid).to_proto() - ) - - def get_project_metadata( registry_proto: Optional[RegistryProto], project: str ) -> Optional[ProjectMetadataProto]: @@ -316,3 +310,21 @@ def get_permission( ): return Permission.from_proto(permission_proto) raise PermissionObjectNotFoundException(name=name, project=project) + + +def list_projects( + registry_proto: RegistryProto, + tags: Optional[dict[str, str]], +) -> List[Project]: + projects = [] + for project_proto in registry_proto.projects: + if utils.has_all_tags(project_proto.spec.tags, tags): + projects.append(Project.from_proto(project_proto)) + return projects + + +def get_project(registry_proto: RegistryProto, name: str) -> Project: + for projects_proto in registry_proto.projects: + if projects_proto.spec.name == name: + return Project.from_proto(projects_proto) + raise ProjectObjectNotFoundException(name=name) diff --git a/sdk/python/feast/infra/registry/registry.py b/sdk/python/feast/infra/registry/registry.py index 366f3aacaa..634d6fa7ac 100644 --- a/sdk/python/feast/infra/registry/registry.py +++ b/sdk/python/feast/infra/registry/registry.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import logging -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from enum import Enum from pathlib import Path from threading import Lock @@ -32,6 +32,8 @@ FeatureServiceNotFoundException, FeatureViewNotFoundException, PermissionNotFoundException, + ProjectNotFoundException, + ProjectObjectNotFoundException, ValidationReferenceNotFound, ) from feast.feature_service import FeatureService @@ -44,6 +46,7 @@ from feast.on_demand_feature_view import OnDemandFeatureView from feast.permissions.auth_model import AuthConfig, NoAuthConfig from feast.permissions.permission import Permission +from feast.project import Project from feast.project_metadata import ProjectMetadata from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto from feast.repo_config import RegistryConfig @@ -70,6 +73,7 @@ class FeastObjectType(Enum): + PROJECT = "project" DATA_SOURCE = "data source" ENTITY = "entity" FEATURE_VIEW = "feature view" @@ -83,6 +87,11 @@ def get_objects_from_registry( registry: "BaseRegistry", project: str ) -> Dict["FeastObjectType", List[Any]]: return { + FeastObjectType.PROJECT: [ + project_obj + for project_obj in registry.list_projects() + if project_obj.name == project + ], FeastObjectType.DATA_SOURCE: registry.list_data_sources(project=project), FeastObjectType.ENTITY: registry.list_entities(project=project), FeastObjectType.FEATURE_VIEW: registry.list_feature_views(project=project), @@ -103,6 +112,7 @@ def get_objects_from_repo_contents( repo_contents: RepoContents, ) -> Dict["FeastObjectType", List[Any]]: return { + FeastObjectType.PROJECT: repo_contents.projects, FeastObjectType.DATA_SOURCE: repo_contents.data_sources, FeastObjectType.ENTITY: repo_contents.entities, FeastObjectType.FEATURE_VIEW: repo_contents.feature_views, @@ -157,34 +167,10 @@ def get_user_metadata( # The cached_registry_proto object is used for both reads and writes. In particular, # all write operations refresh the cache and modify it in memory; the write must # then be persisted to the underlying RegistryStore with a call to commit(). - cached_registry_proto: Optional[RegistryProto] = None - cached_registry_proto_created: Optional[datetime] = None + cached_registry_proto: RegistryProto + cached_registry_proto_created: datetime cached_registry_proto_ttl: timedelta - def __new__( - cls, - project: str, - registry_config: Optional[RegistryConfig], - repo_path: Optional[Path], - auth_config: AuthConfig = NoAuthConfig(), - ): - # We override __new__ so that we can inspect registry_config and create a SqlRegistry without callers - # needing to make any changes. - if registry_config and registry_config.registry_type == "sql": - from feast.infra.registry.sql import SqlRegistry - - return SqlRegistry(registry_config, project, repo_path) - elif registry_config and registry_config.registry_type == "snowflake.registry": - from feast.infra.registry.snowflake import SnowflakeRegistry - - return SnowflakeRegistry(registry_config, project, repo_path) - elif registry_config and registry_config.registry_type == "remote": - from feast.infra.registry.remote import RemoteRegistry - - return RemoteRegistry(registry_config, project, repo_path, auth_config) - else: - return super(Registry, cls).__new__(cls) - def __init__( self, project: str, @@ -204,6 +190,17 @@ def __init__( self._refresh_lock = Lock() self._auth_config = auth_config + registry_proto = RegistryProto() + registry_proto.registry_schema_version = REGISTRY_SCHEMA_VERSION + self.cached_registry_proto = registry_proto + self.cached_registry_proto_created = _utc_now() + + self.purge_feast_metadata = ( + registry_config.purge_feast_metadata + if registry_config is not None + else False + ) + if registry_config: registry_store_type = registry_config.registry_store_type registry_path = registry_config.path @@ -214,11 +211,52 @@ def __init__( self._registry_store = cls(registry_config, repo_path) self.cached_registry_proto_ttl = timedelta( - seconds=registry_config.cache_ttl_seconds - if registry_config.cache_ttl_seconds is not None - else 0 + seconds=( + registry_config.cache_ttl_seconds + if registry_config.cache_ttl_seconds is not None + else 0 + ) ) + try: + registry_proto = self._registry_store.get_registry_proto() + self.cached_registry_proto = registry_proto + self.cached_registry_proto_created = _utc_now() + # Sync feast_metadata to projects table + # when purge_feast_metadata is set to True, Delete data from + # feast_metadata table and list_project_metadata will not return any data + self._sync_feast_metadata_to_projects_table() + except FileNotFoundError: + logger.info("Registry file not found. Creating new registry.") + finally: + self.commit() + + def _sync_feast_metadata_to_projects_table(self): + """ + Sync feast_metadata to projects table + """ + feast_metadata_projects = [] + projects_set = [] + # List of project in project_metadata + for project_metadata in self.cached_registry_proto.project_metadata: + project = ProjectMetadata.from_proto(project_metadata) + feast_metadata_projects.append(project.project_name) + if len(feast_metadata_projects) > 0: + # List of project in projects + for project_metadata in self.cached_registry_proto.projects: + project = Project.from_proto(project_metadata) + projects_set.append(project.name) + + # Find object in feast_metadata_projects but not in projects + projects_to_sync = set(feast_metadata_projects) - set(projects_set) + # Sync feast_metadata to projects table + for project_name in projects_to_sync: + project = Project(name=project_name) + self.cached_registry_proto.projects.append(project.to_proto()) + + if self.purge_feast_metadata: + self.cached_registry_proto.project_metadata = [] + def clone(self) -> "Registry": new_registry = Registry("project", None, None, self._auth_config) new_registry.cached_registry_proto_ttl = timedelta(seconds=0) @@ -231,16 +269,6 @@ def clone(self) -> "Registry": new_registry._registry_store = NoopRegistryStore() return new_registry - def _initialize_registry(self, project: str): - """Explicitly initializes the registry with an empty proto if it doesn't exist.""" - try: - self._get_registry_proto(project=project) - except FileNotFoundError: - registry_proto = RegistryProto() - registry_proto.registry_schema_version = REGISTRY_SCHEMA_VERSION - proto_registry_utils.init_project_metadata(registry_proto, project) - self._registry_store.update_registry_proto(registry_proto) - def update_infra(self, infra: Infra, project: str, commit: bool = True): self._prepare_registry_for_changes(project) assert self.cached_registry_proto @@ -320,7 +348,7 @@ def apply_data_source( data_source_proto.data_source_class_type = ( f"{data_source.__class__.__module__}.{data_source.__class__.__name__}" ) - registry.data_sources.append(data_source_proto) + self.cached_registry_proto.data_sources.append(data_source_proto) if commit: self.commit() @@ -363,7 +391,7 @@ def apply_feature_service( feature_service_proto = feature_service.to_proto() feature_service_proto.spec.project = project del registry.feature_services[idx] - registry.feature_services.append(feature_service_proto) + self.cached_registry_proto.feature_services.append(feature_service_proto) if commit: self.commit() @@ -773,15 +801,16 @@ def list_validation_references( ) def delete_validation_reference(self, name: str, project: str, commit: bool = True): - registry_proto = self._prepare_registry_for_changes(project) + self._prepare_registry_for_changes(project) + assert self.cached_registry_proto for idx, existing_validation_reference in enumerate( - registry_proto.validation_references + self.cached_registry_proto.validation_references ): if ( existing_validation_reference.name == name and existing_validation_reference.project == project ): - del registry_proto.validation_references[idx] + del self.cached_registry_proto.validation_references[idx] if commit: self.commit() return @@ -811,37 +840,36 @@ def teardown(self): def proto(self) -> RegistryProto: return self.cached_registry_proto or RegistryProto() - def _prepare_registry_for_changes(self, project: str): + def _prepare_registry_for_changes(self, project_name: str): """Prepares the Registry for changes by refreshing the cache if necessary.""" + + assert self.cached_registry_proto is not None + try: - self._get_registry_proto(project=project, allow_cache=True) - if ( - proto_registry_utils.get_project_metadata( - self.cached_registry_proto, project - ) - is None - ): - # Project metadata not initialized yet. Try pulling without cache - self._get_registry_proto(project=project, allow_cache=False) - except FileNotFoundError: - registry_proto = RegistryProto() - registry_proto.registry_schema_version = REGISTRY_SCHEMA_VERSION + # Check if the project exists in the registry cache + self.get_project(name=project_name, allow_cache=True) + return self.cached_registry_proto + except ProjectObjectNotFoundException: + # If the project does not exist in cache, refresh cache from store + registry_proto = self._registry_store.get_registry_proto() self.cached_registry_proto = registry_proto self.cached_registry_proto_created = _utc_now() - # Initialize project metadata if needed - assert self.cached_registry_proto - if ( - proto_registry_utils.get_project_metadata( - self.cached_registry_proto, project - ) - is None - ): - proto_registry_utils.init_project_metadata( - self.cached_registry_proto, project - ) + try: + # Check if the project exists in the registry cache after refresh from store + self.get_project(name=project_name) + except ProjectObjectNotFoundException: + # If the project still does not exist, create it + project_proto = Project(name=project_name).to_proto() + self.cached_registry_proto.projects.append(project_proto) + if not self.purge_feast_metadata: + project_metadata_proto = ProjectMetadata( + project_name=project_name + ).to_proto() + self.cached_registry_proto.project_metadata.append( + project_metadata_proto + ) self.commit() - return self.cached_registry_proto def _get_registry_proto( @@ -856,10 +884,7 @@ def _get_registry_proto( Returns: Returns a RegistryProto object which represents the state of the registry """ with self._refresh_lock: - expired = ( - self.cached_registry_proto is None - or self.cached_registry_proto_created is None - ) or ( + expired = (self.cached_registry_proto_created is None) or ( self.cached_registry_proto_ttl.total_seconds() > 0 # 0 ttl means infinity and ( @@ -871,33 +896,12 @@ def _get_registry_proto( ) ) - if project: - old_project_metadata = proto_registry_utils.get_project_metadata( - registry_proto=self.cached_registry_proto, project=project - ) - - if allow_cache and not expired and old_project_metadata is not None: - assert isinstance(self.cached_registry_proto, RegistryProto) - return self.cached_registry_proto - elif allow_cache and not expired: - assert isinstance(self.cached_registry_proto, RegistryProto) + if allow_cache and not expired: return self.cached_registry_proto - logger.info("Registry cache expired, so refreshing") registry_proto = self._registry_store.get_registry_proto() self.cached_registry_proto = registry_proto self.cached_registry_proto_created = _utc_now() - - if not project: - return registry_proto - - project_metadata = proto_registry_utils.get_project_metadata( - registry_proto=registry_proto, project=project - ) - if not project_metadata: - proto_registry_utils.init_project_metadata(registry_proto, project) - self.commit() - return registry_proto def _check_conflicting_feature_view_names(self, feature_view: BaseFeatureView): @@ -960,7 +964,7 @@ def apply_permission( permission_proto = permission.to_proto() permission_proto.spec.project = project - registry.permissions.append(permission_proto) + self.cached_registry_proto.permissions.append(permission_proto) if commit: self.commit() @@ -978,3 +982,91 @@ def delete_permission(self, name: str, project: str, commit: bool = True): self.commit() return raise PermissionNotFoundException(name, project) + + def apply_project( + self, + project: Project, + commit: bool = True, + ): + registry = self.cached_registry_proto + + for idx, existing_project_proto in enumerate(registry.projects): + if existing_project_proto.spec.name == project.name: + project.created_timestamp = ( + existing_project_proto.meta.created_timestamp.ToDatetime().replace( + tzinfo=timezone.utc + ) + ) + del registry.projects[idx] + + project_proto = project.to_proto() + self.cached_registry_proto.projects.append(project_proto) + if commit: + self.commit() + + def get_project( + self, + name: str, + allow_cache: bool = False, + ) -> Project: + registry_proto = self._get_registry_proto(project=name, allow_cache=allow_cache) + return proto_registry_utils.get_project(registry_proto, name) + + def list_projects( + self, + allow_cache: bool = False, + tags: Optional[dict[str, str]] = None, + ) -> List[Project]: + registry_proto = self._get_registry_proto(project=None, allow_cache=allow_cache) + return proto_registry_utils.list_projects( + registry_proto=registry_proto, tags=tags + ) + + def delete_project( + self, + name: str, + commit: bool = True, + ): + assert self.cached_registry_proto + + for idx, project_proto in enumerate(self.cached_registry_proto.projects): + if project_proto.spec.name == name: + list_validation_references = self.list_validation_references(name) + for validation_reference in list_validation_references: + self.delete_validation_reference(validation_reference.name, name) + + list_saved_datasets = self.list_saved_datasets(name) + for saved_dataset in list_saved_datasets: + self.delete_saved_dataset(saved_dataset.name, name) + + list_feature_services = self.list_feature_services(name) + for feature_service in list_feature_services: + self.delete_feature_service(feature_service.name, name) + + list_on_demand_feature_views = self.list_on_demand_feature_views(name) + for on_demand_feature_view in list_on_demand_feature_views: + self.delete_feature_view(on_demand_feature_view.name, name) + + list_stream_feature_views = self.list_stream_feature_views(name) + for stream_feature_view in list_stream_feature_views: + self.delete_feature_view(stream_feature_view.name, name) + + list_feature_views = self.list_feature_views(name) + for feature_view in list_feature_views: + self.delete_feature_view(feature_view.name, name) + + list_data_sources = self.list_data_sources(name) + for data_source in list_data_sources: + self.delete_data_source(data_source.name, name) + + list_entities = self.list_entities(name) + for entity in list_entities: + self.delete_entity(entity.name, name) + list_permissions = self.list_permissions(name) + for permission in list_permissions: + self.delete_permission(permission.name, name) + del self.cached_registry_proto.projects[idx] + if commit: + self.commit() + return + raise ProjectNotFoundException(name) diff --git a/sdk/python/feast/infra/registry/remote.py b/sdk/python/feast/infra/registry/remote.py index 618628bc07..ba25ef7dbe 100644 --- a/sdk/python/feast/infra/registry/remote.py +++ b/sdk/python/feast/infra/registry/remote.py @@ -16,14 +16,12 @@ from feast.infra.registry.base_registry import BaseRegistry from feast.on_demand_feature_view import OnDemandFeatureView from feast.permissions.auth.auth_type import AuthType -from feast.permissions.auth_model import ( - AuthConfig, - NoAuthConfig, -) +from feast.permissions.auth_model import AuthConfig, NoAuthConfig from feast.permissions.client.grpc_client_auth_interceptor import ( GrpcClientAuthHeaderInterceptor, ) from feast.permissions.permission import Permission +from feast.project import Project from feast.project_metadata import ProjectMetadata from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto from feast.protos.feast.registry import RegistryServer_pb2, RegistryServer_pb2_grpc @@ -50,11 +48,18 @@ def __init__( auth_config: AuthConfig = NoAuthConfig(), ): self.auth_config = auth_config - channel = grpc.insecure_channel(registry_config.path) + self.channel = grpc.insecure_channel(registry_config.path) if self.auth_config.type != AuthType.NONE.value: auth_header_interceptor = GrpcClientAuthHeaderInterceptor(auth_config) - channel = grpc.intercept_channel(channel, auth_header_interceptor) - self.stub = RegistryServer_pb2_grpc.RegistryServerStub(channel) + self.channel = grpc.intercept_channel(self.channel, auth_header_interceptor) + self.stub = RegistryServer_pb2_grpc.RegistryServerStub(self.channel) + + def close(self): + if self.channel: + self.channel.close() + + def __del__(self): + self.close() def apply_entity(self, entity: Entity, project: str, commit: bool = True): request = RegistryServer_pb2.ApplyEntityRequest( @@ -173,15 +178,17 @@ def apply_feature_view( arg_name = "on_demand_feature_view" request = RegistryServer_pb2.ApplyFeatureViewRequest( - feature_view=feature_view.to_proto() - if arg_name == "feature_view" - else None, - stream_feature_view=feature_view.to_proto() - if arg_name == "stream_feature_view" - else None, - on_demand_feature_view=feature_view.to_proto() - if arg_name == "on_demand_feature_view" - else None, + feature_view=( + feature_view.to_proto() if arg_name == "feature_view" else None + ), + stream_feature_view=( + feature_view.to_proto() if arg_name == "stream_feature_view" else None + ), + on_demand_feature_view=( + feature_view.to_proto() + if arg_name == "on_demand_feature_view" + else None + ), project=project, commit=commit, ) @@ -450,6 +457,49 @@ def list_permissions( Permission.from_proto(permission) for permission in response.permissions ] + def apply_project( + self, + project: Project, + commit: bool = True, + ): + project_proto = project.to_proto() + + request = RegistryServer_pb2.ApplyProjectRequest( + project=project_proto, commit=commit + ) + self.stub.ApplyProject(request) + + def delete_project( + self, + name: str, + commit: bool = True, + ): + request = RegistryServer_pb2.DeleteProjectRequest(name=name, commit=commit) + self.stub.DeleteProject(request) + + def get_project( + self, + name: str, + allow_cache: bool = False, + ) -> Project: + request = RegistryServer_pb2.GetProjectRequest( + name=name, allow_cache=allow_cache + ) + response = self.stub.GetProject(request) + + return Project.from_proto(response) + + def list_projects( + self, + allow_cache: bool = False, + tags: Optional[dict[str, str]] = None, + ) -> List[Project]: + request = RegistryServer_pb2.ListProjectsRequest( + allow_cache=allow_cache, tags=tags + ) + response = self.stub.ListProjects(request) + return [Project.from_proto(project) for project in response.projects] + def proto(self) -> RegistryProto: return self.stub.Proto(Empty()) diff --git a/sdk/python/feast/infra/registry/snowflake.py b/sdk/python/feast/infra/registry/snowflake.py index 801b90afe3..accfa42e12 100644 --- a/sdk/python/feast/infra/registry/snowflake.py +++ b/sdk/python/feast/infra/registry/snowflake.py @@ -5,7 +5,7 @@ from datetime import datetime, timedelta, timezone from enum import Enum from threading import Lock -from typing import Any, Callable, List, Literal, Optional, Set, Union +from typing import Any, Callable, List, Literal, Optional, Union from pydantic import ConfigDict, Field, StrictStr @@ -19,6 +19,8 @@ FeatureServiceNotFoundException, FeatureViewNotFoundException, PermissionNotFoundException, + ProjectNotFoundException, + ProjectObjectNotFoundException, SavedDatasetNotFound, ValidationReferenceNotFound, ) @@ -33,6 +35,7 @@ ) from feast.on_demand_feature_view import OnDemandFeatureView from feast.permissions.permission import Permission +from feast.project import Project from feast.project_metadata import ProjectMetadata from feast.protos.feast.core.DataSource_pb2 import DataSource as DataSourceProto from feast.protos.feast.core.Entity_pb2 import Entity as EntityProto @@ -45,6 +48,7 @@ OnDemandFeatureView as OnDemandFeatureViewProto, ) from feast.protos.feast.core.Permission_pb2 import Permission as PermissionProto +from feast.protos.feast.core.Project_pb2 import Project as ProjectProto from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto from feast.protos.feast.core.SavedDataset_pb2 import SavedDataset as SavedDatasetProto from feast.protos.feast.core.StreamFeatureView_pb2 import ( @@ -138,26 +142,57 @@ def __init__( query = command.replace("REGISTRY_PATH", f"{self.registry_path}") execute_snowflake_statement(conn, query) + self.purge_feast_metadata = registry_config.purge_feast_metadata + self._sync_feast_metadata_to_projects_table() + if not self.purge_feast_metadata: + self._maybe_init_project_metadata(project) + self.cached_registry_proto = self.proto() - proto_registry_utils.init_project_metadata(self.cached_registry_proto, project) self.cached_registry_proto_created = _utc_now() self._refresh_lock = Lock() self.cached_registry_proto_ttl = timedelta( - seconds=registry_config.cache_ttl_seconds - if registry_config.cache_ttl_seconds is not None - else 0 + seconds=( + registry_config.cache_ttl_seconds + if registry_config.cache_ttl_seconds is not None + else 0 + ) ) self.project = project - def refresh(self, project: Optional[str] = None): - if project: - project_metadata = proto_registry_utils.get_project_metadata( - registry_proto=self.cached_registry_proto, project=project + def _sync_feast_metadata_to_projects_table(self): + feast_metadata_projects: set = [] + projects_set: set = [] + + with GetSnowflakeConnection(self.registry_config) as conn: + query = ( + f'SELECT DISTINCT project_id FROM {self.registry_path}."FEAST_METADATA"' ) - if not project_metadata: - proto_registry_utils.init_project_metadata( - self.cached_registry_proto, project - ) + df = execute_snowflake_statement(conn, query).fetch_pandas_all() + + for row in df.iterrows(): + feast_metadata_projects.add(row[1]["PROJECT_ID"]) + + if len(feast_metadata_projects) > 0: + with GetSnowflakeConnection(self.registry_config) as conn: + query = f'SELECT project_id FROM {self.registry_path}."PROJECTS"' + df = execute_snowflake_statement(conn, query).fetch_pandas_all() + + for row in df.iterrows(): + projects_set.add(row[1]["PROJECT_ID"]) + + # Find object in feast_metadata_projects but not in projects + projects_to_sync = set(feast_metadata_projects) - set(projects_set) + for project_name in projects_to_sync: + self.apply_project(Project(name=project_name), commit=True) + + if self.purge_feast_metadata: + with GetSnowflakeConnection(self.registry_config) as conn: + query = f""" + DELETE FROM {self.registry_path}."FEAST_METADATA" + """ + execute_snowflake_statement(conn, query) + + def refresh(self, project: Optional[str] = None): self.cached_registry_proto = self.proto() self.cached_registry_proto_created = _utc_now() @@ -271,6 +306,17 @@ def update_infra(self, infra: Infra, project: str, commit: bool = True): name="infra_obj", ) + def _initialize_project_if_not_exists(self, project_name: str): + try: + self.get_project(project_name, allow_cache=True) + return + except ProjectObjectNotFoundException: + try: + self.get_project(project_name, allow_cache=False) + return + except ProjectObjectNotFoundException: + self.apply_project(Project(name=project_name), commit=True) + def _apply_object( self, table: str, @@ -280,7 +326,11 @@ def _apply_object( proto_field_name: str, name: Optional[str] = None, ): - self._maybe_init_project_metadata(project) + if not self.purge_feast_metadata: + self._maybe_init_project_metadata(project) + # Initialize project is necessary because FeatureStore object can apply objects individually without "feast apply" cli option + if not isinstance(obj, Project): + self._initialize_project_if_not_exists(project_name=project) name = name or (obj.name if hasattr(obj, "name") else None) assert name, f"name needs to be provided for {obj}" @@ -343,7 +393,13 @@ def _apply_object( """ execute_snowflake_statement(conn, query) - self._set_last_updated_metadata(update_datetime, project) + if not isinstance(obj, Project): + self.apply_project( + self.get_project(name=project, allow_cache=False), commit=True + ) + + if not self.purge_feast_metadata: + self._set_last_updated_metadata(update_datetime, project) def apply_permission( self, permission: Permission, project: str, commit: bool = True @@ -620,7 +676,6 @@ def _get_object( proto_field_name: str, not_found_exception: Optional[Callable], ): - self._maybe_init_project_metadata(project) with GetSnowflakeConnection(self.registry_config) as conn: query = f""" SELECT @@ -821,7 +876,6 @@ def _list_objects( proto_field_name: str, tags: Optional[dict[str, str]] = None, ): - self._maybe_init_project_metadata(project) with GetSnowflakeConnection(self.registry_config) as conn: query = f""" SELECT @@ -992,8 +1046,27 @@ def get_user_metadata( def proto(self) -> RegistryProto: r = RegistryProto() last_updated_timestamps = [] - projects = self._get_all_projects() - for project in projects: + + def process_project(project: Project): + nonlocal r, last_updated_timestamps + project_name = project.name + last_updated_timestamp = project.last_updated_timestamp + + try: + cached_project = self.get_project(project_name, True) + except ProjectObjectNotFoundException: + cached_project = None + + allow_cache = False + + if cached_project is not None: + allow_cache = ( + last_updated_timestamp <= cached_project.last_updated_timestamp + ) + + r.projects.extend([project.to_proto()]) + last_updated_timestamps.append(last_updated_timestamp) + for lister, registry_proto_field in [ (self.list_entities, r.entities), (self.list_feature_views, r.feature_views), @@ -1003,53 +1076,31 @@ def proto(self) -> RegistryProto: (self.list_feature_services, r.feature_services), (self.list_saved_datasets, r.saved_datasets), (self.list_validation_references, r.validation_references), - (self.list_project_metadata, r.project_metadata), (self.list_permissions, r.permissions), ]: - objs: List[Any] = lister(project) # type: ignore + objs: List[Any] = lister(project_name, allow_cache) # type: ignore if objs: obj_protos = [obj.to_proto() for obj in objs] for obj_proto in obj_protos: if "spec" in obj_proto.DESCRIPTOR.fields_by_name: - obj_proto.spec.project = project + obj_proto.spec.project = project_name else: - obj_proto.project = project + obj_proto.project = project_name registry_proto_field.extend(obj_protos) # This is suuuper jank. Because of https://github.com/feast-dev/feast/issues/2783, # the registry proto only has a single infra field, which we're currently setting as the "last" project. - r.infra.CopyFrom(self.get_infra(project).to_proto()) - last_updated_timestamps.append(self._get_last_updated_metadata(project)) + r.infra.CopyFrom(self.get_infra(project_name).to_proto()) + + projects_list = self.list_projects(allow_cache=False) + for project in projects_list: + process_project(project) if last_updated_timestamps: r.last_updated.FromDatetime(max(last_updated_timestamps)) return r - def _get_all_projects(self) -> Set[str]: - projects = set() - - base_tables = [ - "DATA_SOURCES", - "ENTITIES", - "FEATURE_VIEWS", - "ON_DEMAND_FEATURE_VIEWS", - "STREAM_FEATURE_VIEWS", - "PERMISSIONS", - ] - - with GetSnowflakeConnection(self.registry_config) as conn: - for table in base_tables: - query = ( - f'SELECT DISTINCT project_id FROM {self.registry_path}."{table}"' - ) - df = execute_snowflake_statement(conn, query).fetch_pandas_all() - - for row in df.iterrows(): - projects.add(row[1]["PROJECT_ID"]) - - return projects - def _get_last_updated_metadata(self, project: str): with GetSnowflakeConnection(self.registry_config) as conn: query = f""" @@ -1153,3 +1204,98 @@ def _set_last_updated_metadata(self, last_updated: datetime, project: str): def commit(self): pass + + def apply_project( + self, + project: Project, + commit: bool = True, + ): + return self._apply_object( + "PROJECTS", project.name, "project_name", project, "project_proto" + ) + + def delete_project( + self, + name: str, + commit: bool = True, + ): + project = self.get_project(name, allow_cache=False) + if project: + with GetSnowflakeConnection(self.registry_config) as conn: + for table in { + "MANAGED_INFRA", + "SAVED_DATASETS", + "VALIDATION_REFERENCES", + "FEATURE_SERVICES", + "FEATURE_VIEWS", + "ON_DEMAND_FEATURE_VIEWS", + "STREAM_FEATURE_VIEWS", + "DATA_SOURCES", + "ENTITIES", + "PERMISSIONS", + "PROJECTS", + }: + query = f""" + DELETE FROM {self.registry_path}."{table}" + WHERE + project_id = '{project}' + """ + execute_snowflake_statement(conn, query) + return + + raise ProjectNotFoundException(name) + + def _get_project( + self, + name: str, + ) -> Project: + return self._get_object( + table="PROJECTS", + name=name, + project=name, + proto_class=ProjectProto, + python_class=Project, + id_field_name="project_name", + proto_field_name="project_proto", + not_found_exception=ProjectObjectNotFoundException, + ) + + def get_project( + self, + name: str, + allow_cache: bool = False, + ) -> Project: + if allow_cache: + self._refresh_cached_registry_if_necessary() + return proto_registry_utils.get_project(self.cached_registry_proto, name) + return self._get_project(name) + + def _list_projects( + self, + tags: Optional[dict[str, str]], + ) -> List[Project]: + with GetSnowflakeConnection(self.registry_config) as conn: + query = f""" + SELECT project_proto FROM {self.registry_path}."PROJECTS" + """ + df = execute_snowflake_statement(conn, query).fetch_pandas_all() + if not df.empty: + objects = [] + for row in df.iterrows(): + obj = Project.from_proto( + ProjectProto.FromString(row[1]["project_proto"]) + ) + if has_all_tags(obj.tags, tags): + objects.append(obj) + return objects + return [] + + def list_projects( + self, + allow_cache: bool = False, + tags: Optional[dict[str, str]] = None, + ) -> List[Project]: + if allow_cache: + self._refresh_cached_registry_if_necessary() + return proto_registry_utils.list_projects(self.cached_registry_proto, tags) + return self._list_projects(tags) diff --git a/sdk/python/feast/infra/registry/sql.py b/sdk/python/feast/infra/registry/sql.py index 90c6e82e7d..2b4a58266c 100644 --- a/sdk/python/feast/infra/registry/sql.py +++ b/sdk/python/feast/infra/registry/sql.py @@ -1,11 +1,12 @@ import logging import uuid +from concurrent.futures import ThreadPoolExecutor from datetime import datetime, timezone from enum import Enum from pathlib import Path -from typing import Any, Callable, Dict, List, Optional, Set, Union +from typing import Any, Callable, Dict, List, Optional, Union -from pydantic import StrictStr +from pydantic import StrictInt, StrictStr from sqlalchemy import ( # type: ignore BigInteger, Column, @@ -31,6 +32,8 @@ FeatureServiceNotFoundException, FeatureViewNotFoundException, PermissionNotFoundException, + ProjectNotFoundException, + ProjectObjectNotFoundException, SavedDatasetNotFound, ValidationReferenceNotFound, ) @@ -40,6 +43,7 @@ from feast.infra.registry.caching_registry import CachingRegistry from feast.on_demand_feature_view import OnDemandFeatureView from feast.permissions.permission import Permission +from feast.project import Project from feast.project_metadata import ProjectMetadata from feast.protos.feast.core.DataSource_pb2 import DataSource as DataSourceProto from feast.protos.feast.core.Entity_pb2 import Entity as EntityProto @@ -52,6 +56,7 @@ OnDemandFeatureView as OnDemandFeatureViewProto, ) from feast.protos.feast.core.Permission_pb2 import Permission as PermissionProto +from feast.protos.feast.core.Project_pb2 import Project as ProjectProto from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto from feast.protos.feast.core.SavedDataset_pb2 import SavedDataset as SavedDatasetProto from feast.protos.feast.core.StreamFeatureView_pb2 import ( @@ -67,11 +72,21 @@ metadata = MetaData() + +projects = Table( + "projects", + metadata, + Column("project_id", String(255), primary_key=True), + Column("project_name", String(255), nullable=False), + Column("last_updated_timestamp", BigInteger, nullable=False), + Column("project_proto", LargeBinary, nullable=False), +) + entities = Table( "entities", metadata, - Column("entity_name", String(50), primary_key=True), - Column("project_id", String(50), primary_key=True), + Column("entity_name", String(255), primary_key=True), + Column("project_id", String(255), primary_key=True), Column("last_updated_timestamp", BigInteger, nullable=False), Column("entity_proto", LargeBinary, nullable=False), ) @@ -80,7 +95,7 @@ "data_sources", metadata, Column("data_source_name", String(255), primary_key=True), - Column("project_id", String(50), primary_key=True), + Column("project_id", String(255), primary_key=True), Column("last_updated_timestamp", BigInteger, nullable=False), Column("data_source_proto", LargeBinary, nullable=False), ) @@ -88,8 +103,8 @@ feature_views = Table( "feature_views", metadata, - Column("feature_view_name", String(50), primary_key=True), - Column("project_id", String(50), primary_key=True), + Column("feature_view_name", String(255), primary_key=True), + Column("project_id", String(255), primary_key=True), Column("last_updated_timestamp", BigInteger, nullable=False), Column("materialized_intervals", LargeBinary, nullable=True), Column("feature_view_proto", LargeBinary, nullable=False), @@ -99,8 +114,8 @@ stream_feature_views = Table( "stream_feature_views", metadata, - Column("feature_view_name", String(50), primary_key=True), - Column("project_id", String(50), primary_key=True), + Column("feature_view_name", String(255), primary_key=True), + Column("project_id", String(255), primary_key=True), Column("last_updated_timestamp", BigInteger, nullable=False), Column("feature_view_proto", LargeBinary, nullable=False), Column("user_metadata", LargeBinary, nullable=True), @@ -109,8 +124,8 @@ on_demand_feature_views = Table( "on_demand_feature_views", metadata, - Column("feature_view_name", String(50), primary_key=True), - Column("project_id", String(50), primary_key=True), + Column("feature_view_name", String(255), primary_key=True), + Column("project_id", String(255), primary_key=True), Column("last_updated_timestamp", BigInteger, nullable=False), Column("feature_view_proto", LargeBinary, nullable=False), Column("user_metadata", LargeBinary, nullable=True), @@ -119,8 +134,8 @@ feature_services = Table( "feature_services", metadata, - Column("feature_service_name", String(50), primary_key=True), - Column("project_id", String(50), primary_key=True), + Column("feature_service_name", String(255), primary_key=True), + Column("project_id", String(255), primary_key=True), Column("last_updated_timestamp", BigInteger, nullable=False), Column("feature_service_proto", LargeBinary, nullable=False), ) @@ -128,8 +143,8 @@ saved_datasets = Table( "saved_datasets", metadata, - Column("saved_dataset_name", String(50), primary_key=True), - Column("project_id", String(50), primary_key=True), + Column("saved_dataset_name", String(255), primary_key=True), + Column("project_id", String(255), primary_key=True), Column("last_updated_timestamp", BigInteger, nullable=False), Column("saved_dataset_proto", LargeBinary, nullable=False), ) @@ -137,8 +152,8 @@ validation_references = Table( "validation_references", metadata, - Column("validation_reference_name", String(50), primary_key=True), - Column("project_id", String(50), primary_key=True), + Column("validation_reference_name", String(255), primary_key=True), + Column("project_id", String(255), primary_key=True), Column("last_updated_timestamp", BigInteger, nullable=False), Column("validation_reference_proto", LargeBinary, nullable=False), ) @@ -146,8 +161,8 @@ managed_infra = Table( "managed_infra", metadata, - Column("infra_name", String(50), primary_key=True), - Column("project_id", String(50), primary_key=True), + Column("infra_name", String(255), primary_key=True), + Column("project_id", String(255), primary_key=True), Column("last_updated_timestamp", BigInteger, nullable=False), Column("infra_proto", LargeBinary, nullable=False), ) @@ -156,7 +171,7 @@ "permissions", metadata, Column("permission_name", String(255), primary_key=True), - Column("project_id", String(50), primary_key=True), + Column("project_id", String(255), primary_key=True), Column("last_updated_timestamp", BigInteger, nullable=False), Column("permission_proto", LargeBinary, nullable=False), ) @@ -170,7 +185,7 @@ class FeastMetadataKeys(Enum): feast_metadata = Table( "feast_metadata", metadata, - Column("project_id", String(50), primary_key=True), + Column("project_id", String(255), primary_key=True), Column("metadata_key", String(50), primary_key=True), Column("metadata_value", String(50), nullable=False), Column("last_updated_timestamp", BigInteger, nullable=False), @@ -190,26 +205,75 @@ class SqlRegistryConfig(RegistryConfig): sqlalchemy_config_kwargs: Dict[str, Any] = {"echo": False} """ Dict[str, Any]: Extra arguments to pass to SQLAlchemy.create_engine. """ + cache_mode: StrictStr = "sync" + """ str: Cache mode type, Possible options are sync and thread(asynchronous caching using threading library)""" + + thread_pool_executor_worker_count: StrictInt = 0 + """ int: Number of worker threads to use for asynchronous caching in SQL Registry. If set to 0, it doesn't use ThreadPoolExecutor. """ + class SqlRegistry(CachingRegistry): def __init__( self, - registry_config: Optional[Union[RegistryConfig, SqlRegistryConfig]], + registry_config, project: str, repo_path: Optional[Path], ): - assert registry_config is not None, "SqlRegistry needs a valid registry_config" + assert registry_config is not None and isinstance( + registry_config, SqlRegistryConfig + ), "SqlRegistry needs a valid registry_config" self.engine: Engine = create_engine( registry_config.path, **registry_config.sqlalchemy_config_kwargs ) + self.thread_pool_executor_worker_count = ( + registry_config.thread_pool_executor_worker_count + ) metadata.create_all(self.engine) + self.purge_feast_metadata = registry_config.purge_feast_metadata + # Sync feast_metadata to projects table + # when purge_feast_metadata is set to True, Delete data from + # feast_metadata table and list_project_metadata will not return any data + self._sync_feast_metadata_to_projects_table() + if not self.purge_feast_metadata: + self._maybe_init_project_metadata(project) super().__init__( project=project, cache_ttl_seconds=registry_config.cache_ttl_seconds, cache_mode=registry_config.cache_mode, ) + def _sync_feast_metadata_to_projects_table(self): + feast_metadata_projects: set = [] + projects_set: set = [] + with self.engine.begin() as conn: + stmt = select(feast_metadata).where( + feast_metadata.c.metadata_key == FeastMetadataKeys.PROJECT_UUID.value + ) + rows = conn.execute(stmt).all() + for row in rows: + feast_metadata_projects.append(row._mapping["project_id"]) + + if len(feast_metadata_projects) > 0: + with self.engine.begin() as conn: + stmt = select(projects) + rows = conn.execute(stmt).all() + for row in rows: + projects_set.append(row._mapping["project_id"]) + + # Find object in feast_metadata_projects but not in projects + projects_to_sync = set(feast_metadata_projects) - set(projects_set) + for project_name in projects_to_sync: + self.apply_project(Project(name=project_name), commit=True) + + if self.purge_feast_metadata: + with self.engine.begin() as conn: + for project_name in feast_metadata_projects: + stmt = delete(feast_metadata).where( + feast_metadata.c.project_id == project_name + ) + conn.execute(stmt) + def teardown(self): for t in { entities, @@ -673,8 +737,27 @@ def get_user_metadata( def proto(self) -> RegistryProto: r = RegistryProto() last_updated_timestamps = [] - projects = self._get_all_projects() - for project in projects: + + def process_project(project: Project): + nonlocal r, last_updated_timestamps + project_name = project.name + last_updated_timestamp = project.last_updated_timestamp + + try: + cached_project = self.get_project(project_name, True) + except ProjectObjectNotFoundException: + cached_project = None + + allow_cache = False + + if cached_project is not None: + allow_cache = ( + last_updated_timestamp <= cached_project.last_updated_timestamp + ) + + r.projects.extend([project.to_proto()]) + last_updated_timestamps.append(last_updated_timestamp) + for lister, registry_proto_field in [ (self.list_entities, r.entities), (self.list_feature_views, r.feature_views), @@ -684,23 +767,31 @@ def proto(self) -> RegistryProto: (self.list_feature_services, r.feature_services), (self.list_saved_datasets, r.saved_datasets), (self.list_validation_references, r.validation_references), - (self.list_project_metadata, r.project_metadata), (self.list_permissions, r.permissions), ]: - objs: List[Any] = lister(project) # type: ignore + objs: List[Any] = lister(project_name, allow_cache) # type: ignore if objs: obj_protos = [obj.to_proto() for obj in objs] for obj_proto in obj_protos: if "spec" in obj_proto.DESCRIPTOR.fields_by_name: - obj_proto.spec.project = project + obj_proto.spec.project = project_name else: - obj_proto.project = project + obj_proto.project = project_name registry_proto_field.extend(obj_protos) # This is suuuper jank. Because of https://github.com/feast-dev/feast/issues/2783, # the registry proto only has a single infra field, which we're currently setting as the "last" project. - r.infra.CopyFrom(self.get_infra(project).to_proto()) - last_updated_timestamps.append(self._get_last_updated_metadata(project)) + r.infra.CopyFrom(self.get_infra(project_name).to_proto()) + + projects_list = self.list_projects(allow_cache=False) + if self.thread_pool_executor_worker_count == 0: + for project in projects_list: + process_project(project) + else: + with ThreadPoolExecutor( + max_workers=self.thread_pool_executor_worker_count + ) as executor: + executor.map(process_project, projects_list) if last_updated_timestamps: r.last_updated.FromDatetime(max(last_updated_timestamps)) @@ -711,6 +802,17 @@ def commit(self): # This method is a no-op since we're always writing values eagerly to the db. pass + def _initialize_project_if_not_exists(self, project_name: str): + try: + self.get_project(project_name, allow_cache=True) + return + except ProjectObjectNotFoundException: + try: + self.get_project(project_name, allow_cache=False) + return + except ProjectObjectNotFoundException: + self.apply_project(Project(name=project_name), commit=True) + def _apply_object( self, table: Table, @@ -720,8 +822,11 @@ def _apply_object( proto_field_name: str, name: Optional[str] = None, ): - self._maybe_init_project_metadata(project) - + if not self.purge_feast_metadata: + self._maybe_init_project_metadata(project) + # Initialize project is necessary because FeatureStore object can apply objects individually without "feast apply" cli option + if not isinstance(obj, Project): + self._initialize_project_if_not_exists(project_name=project) name = name or (obj.name if hasattr(obj, "name") else None) assert name, f"name needs to be provided for {obj}" @@ -742,12 +847,15 @@ def _apply_object( "feature_view_proto", "feature_service_proto", "permission_proto", + "project_proto", ]: deserialized_proto = self.deserialize_registry_values( row._mapping[proto_field_name], type(obj) ) obj.created_timestamp = ( - deserialized_proto.meta.created_timestamp.ToDatetime() + deserialized_proto.meta.created_timestamp.ToDatetime().replace( + tzinfo=timezone.utc + ) ) if isinstance(obj, (FeatureView, StreamFeatureView)): obj.update_materialization_intervals( @@ -789,7 +897,12 @@ def _apply_object( ) conn.execute(insert_stmt) - self._set_last_updated_metadata(update_datetime, project) + if not isinstance(obj, Project): + self.apply_project( + self.get_project(name=project, allow_cache=False), commit=True + ) + if not self.purge_feast_metadata: + self._set_last_updated_metadata(update_datetime, project) def _maybe_init_project_metadata(self, project): # Initialize project metadata if needed @@ -827,7 +940,11 @@ def _delete_object( rows = conn.execute(stmt) if rows.rowcount < 1 and not_found_exception: raise not_found_exception(name, project) - self._set_last_updated_metadata(_utc_now(), project) + self.apply_project( + self.get_project(name=project, allow_cache=False), commit=True + ) + if not self.purge_feast_metadata: + self._set_last_updated_metadata(_utc_now(), project) return rows.rowcount @@ -842,8 +959,6 @@ def _get_object( proto_field_name: str, not_found_exception: Optional[Callable], ): - self._maybe_init_project_metadata(project) - with self.engine.begin() as conn: stmt = select(table).where( getattr(table.c, id_field_name) == name, table.c.project_id == project @@ -866,7 +981,6 @@ def _list_objects( proto_field_name: str, tags: Optional[dict[str, str]] = None, ): - self._maybe_init_project_metadata(project) with self.engine.begin() as conn: stmt = select(table).where(table.c.project_id == project) rows = conn.execute(stmt).all() @@ -929,24 +1043,6 @@ def _get_last_updated_metadata(self, project: str): return datetime.fromtimestamp(update_time, tz=timezone.utc) - def _get_all_projects(self) -> Set[str]: - projects = set() - with self.engine.begin() as conn: - for table in { - entities, - data_sources, - feature_views, - on_demand_feature_views, - stream_feature_views, - permissions, - }: - stmt = select(table) - rows = conn.execute(stmt).all() - for row in rows: - projects.add(row._mapping["project_id"]) - - return projects - def _get_permission(self, name: str, project: str) -> Permission: return self._get_object( table=permissions, @@ -987,3 +1083,72 @@ def delete_permission(self, name: str, project: str, commit: bool = True): rows = conn.execute(stmt) if rows.rowcount < 1: raise PermissionNotFoundException(name, project) + + def _list_projects( + self, + tags: Optional[dict[str, str]], + ) -> List[Project]: + with self.engine.begin() as conn: + stmt = select(projects) + rows = conn.execute(stmt).all() + if rows: + objects = [] + for row in rows: + obj = Project.from_proto( + ProjectProto.FromString(row._mapping["project_proto"]) + ) + if utils.has_all_tags(obj.tags, tags): + objects.append(obj) + return objects + return [] + + def _get_project( + self, + name: str, + ) -> Project: + return self._get_object( + table=projects, + name=name, + project=name, + proto_class=ProjectProto, + python_class=Project, + id_field_name="project_name", + proto_field_name="project_proto", + not_found_exception=ProjectObjectNotFoundException, + ) + + def apply_project( + self, + project: Project, + commit: bool = True, + ): + return self._apply_object( + projects, project.name, "project_name", project, "project_proto" + ) + + def delete_project( + self, + name: str, + commit: bool = True, + ): + project = self.get_project(name, allow_cache=False) + if project: + with self.engine.begin() as conn: + for t in { + managed_infra, + saved_datasets, + validation_references, + feature_services, + feature_views, + on_demand_feature_views, + stream_feature_views, + data_sources, + entities, + permissions, + projects, + }: + stmt = delete(t).where(t.c.project_id == name) + conn.execute(stmt) + return + + raise ProjectNotFoundException(name) diff --git a/sdk/python/feast/infra/utils/snowflake/registry/snowflake_table_creation.sql b/sdk/python/feast/infra/utils/snowflake/registry/snowflake_table_creation.sql index 021d175b4e..fc13332e4b 100644 --- a/sdk/python/feast/infra/utils/snowflake/registry/snowflake_table_creation.sql +++ b/sdk/python/feast/infra/utils/snowflake/registry/snowflake_table_creation.sql @@ -1,3 +1,11 @@ +CREATE TABLE IF NOT EXISTS REGISTRY_PATH."PROJECTS" ( + project_id VARCHAR, + project_name VARCHAR NOT NULL, + last_updated_timestamp TIMESTAMP_LTZ NOT NULL, + project_proto BINARY NOT NULL, + PRIMARY KEY (project_id) +); + CREATE TABLE IF NOT EXISTS REGISTRY_PATH."DATA_SOURCES" ( data_source_name VARCHAR, project_id VARCHAR, diff --git a/sdk/python/feast/permissions/permission.py b/sdk/python/feast/permissions/permission.py index 1117a3ee82..9046abbfa9 100644 --- a/sdk/python/feast/permissions/permission.py +++ b/sdk/python/feast/permissions/permission.py @@ -256,6 +256,7 @@ def get_type_class_from_permission_type(permission_type: str): _PERMISSION_TYPES = { + "PROJECT": "feast.project.Project", "FEATURE_VIEW": "feast.feature_view.FeatureView", "ON_DEMAND_FEATURE_VIEW": "feast.on_demand_feature_view.OnDemandFeatureView", "BATCH_FEATURE_VIEW": "feast.batch_feature_view.BatchFeatureView", diff --git a/sdk/python/feast/permissions/security_manager.py b/sdk/python/feast/permissions/security_manager.py index c00a3d8853..cb8cafd5b9 100644 --- a/sdk/python/feast/permissions/security_manager.py +++ b/sdk/python/feast/permissions/security_manager.py @@ -10,6 +10,7 @@ from feast.permissions.enforcer import enforce_policy from feast.permissions.permission import Permission from feast.permissions.user import User +from feast.project import Project logger = logging.getLogger(__name__) @@ -88,7 +89,9 @@ def assert_permissions( def assert_permissions_to_update( resource: FeastObject, - getter: Callable[[str, str, bool], FeastObject], + getter: Union[ + Callable[[str, str, bool], FeastObject], Callable[[str, bool], FeastObject] + ], project: str, allow_cache: bool = True, ) -> FeastObject: @@ -117,11 +120,17 @@ def assert_permissions_to_update( actions = [AuthzedAction.DESCRIBE, AuthzedAction.UPDATE] try: - existing_resource = getter( - name=resource.name, - project=project, - allow_cache=allow_cache, - ) # type: ignore[call-arg] + if isinstance(resource, Project): + existing_resource = getter( + name=resource.name, + allow_cache=allow_cache, + ) # type: ignore[call-arg] + else: + existing_resource = getter( + name=resource.name, + project=project, + allow_cache=allow_cache, + ) # type: ignore[call-arg] assert_permissions(resource=existing_resource, actions=actions) except FeastObjectNotFoundException: actions = [AuthzedAction.CREATE] diff --git a/sdk/python/feast/project.py b/sdk/python/feast/project.py new file mode 100644 index 0000000000..d9ec45dcc9 --- /dev/null +++ b/sdk/python/feast/project.py @@ -0,0 +1,175 @@ +# Copyright 2019 The Feast 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 +# +# https://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. +from datetime import datetime, timezone +from typing import Dict, Optional + +from google.protobuf.json_format import MessageToJson +from typeguard import typechecked + +from feast.protos.feast.core.Project_pb2 import Project as ProjectProto +from feast.protos.feast.core.Project_pb2 import ProjectMeta as ProjectMetaProto +from feast.protos.feast.core.Project_pb2 import ProjectSpec as ProjectSpecProto +from feast.utils import _utc_now + + +@typechecked +class Project: + """ + Project is a collection of Feast Objects. Projects provide complete isolation of + feature stores at the infrastructure level. + + Attributes: + name: The unique name of the project. + description: A human-readable description. + tags: A dictionary of key-value pairs to store arbitrary metadata. + owner: The owner of the project, typically the email of the primary maintainer. + created_timestamp: The time when the entity was created. + last_updated_timestamp: The time when the entity was last updated. + """ + + name: str + description: str + tags: Dict[str, str] + owner: str + created_timestamp: datetime + last_updated_timestamp: datetime + + def __init__( + self, + *, + name: str, + description: str = "", + tags: Optional[Dict[str, str]] = None, + owner: str = "", + created_timestamp: Optional[datetime] = None, + last_updated_timestamp: Optional[datetime] = None, + ): + """ + Creates Project object. + + Args: + name: The unique name of the project. + description (optional): A human-readable description. + tags (optional): A dictionary of key-value pairs to store arbitrary metadata. + owner (optional): The owner of the project, typically the email of the primary maintainer. + created_timestamp (optional): The time when the project was created. Defaults to + last_updated_timestamp (optional): The time when the project was last updated. + + Raises: + ValueError: Parameters are specified incorrectly. + """ + self.name = name + self.description = description + self.tags = tags if tags is not None else {} + self.owner = owner + updated_time = _utc_now() + self.created_timestamp = created_timestamp or updated_time + self.last_updated_timestamp = last_updated_timestamp or updated_time + + def __hash__(self) -> int: + return hash((self.name)) + + def __eq__(self, other): + if not isinstance(other, Project): + raise TypeError("Comparisons should only involve Project class objects.") + + if ( + self.name != other.name + or self.description != other.description + or self.tags != other.tags + or self.owner != other.owner + or self.created_timestamp != other.created_timestamp + or self.last_updated_timestamp != other.last_updated_timestamp + ): + return False + + return True + + def __str__(self): + return str(MessageToJson(self.to_proto())) + + def __lt__(self, other): + return self.name < other.name + + def is_valid(self): + """ + Validates the state of this project locally. + + Raises: + ValueError: The project does not have a name or does not have a type. + """ + if not self.name: + raise ValueError("The project does not have a name.") + + from feast.repo_operations import is_valid_name + + if not is_valid_name(self.name): + raise ValueError( + f"Project name, {self.name}, should only have " + f"alphanumerical values and underscores but not start with an underscore." + ) + + @classmethod + def from_proto(cls, project_proto: ProjectProto): + """ + Creates a project from a protobuf representation of an project. + + Args: + entity_proto: A protobuf representation of an project. + + Returns: + An Entity object based on the entity protobuf. + """ + project = cls( + name=project_proto.spec.name, + description=project_proto.spec.description, + tags=dict(project_proto.spec.tags), + owner=project_proto.spec.owner, + ) + if project_proto.meta.HasField("created_timestamp"): + project.created_timestamp = ( + project_proto.meta.created_timestamp.ToDatetime().replace( + tzinfo=timezone.utc + ) + ) + if project_proto.meta.HasField("last_updated_timestamp"): + project.last_updated_timestamp = ( + project_proto.meta.last_updated_timestamp.ToDatetime().replace( + tzinfo=timezone.utc + ) + ) + + return project + + def to_proto(self) -> ProjectProto: + """ + Converts an project object to its protobuf representation. + + Returns: + An ProjectProto protobuf. + """ + meta = ProjectMetaProto() + if self.created_timestamp: + meta.created_timestamp.FromDatetime(self.created_timestamp) + if self.last_updated_timestamp: + meta.last_updated_timestamp.FromDatetime(self.last_updated_timestamp) + + spec = ProjectSpecProto( + name=self.name, + description=self.description, + tags=self.tags, + owner=self.owner, + ) + + return ProjectProto(spec=spec, meta=meta) diff --git a/sdk/python/feast/registry_server.py b/sdk/python/feast/registry_server.py index 40475aa580..2661f25882 100644 --- a/sdk/python/feast/registry_server.py +++ b/sdk/python/feast/registry_server.py @@ -32,6 +32,7 @@ init_security_manager, str_to_auth_manager_type, ) +from feast.project import Project from feast.protos.feast.registry import RegistryServer_pb2, RegistryServer_pb2_grpc from feast.saved_dataset import SavedDataset, ValidationReference from feast.stream_feature_view import StreamFeatureView @@ -624,6 +625,58 @@ def DeletePermission( ) return Empty() + def ApplyProject(self, request: RegistryServer_pb2.ApplyProjectRequest, context): + project = cast( + Project, + assert_permissions_to_update( + resource=Project.from_proto(request.project), + getter=self.proxied_registry.get_project, + project=Project.from_proto(request.project).name, + ), + ) + self.proxied_registry.apply_project( + project=project, + commit=request.commit, + ) + return Empty() + + def GetProject(self, request: RegistryServer_pb2.GetProjectRequest, context): + project = self.proxied_registry.get_project( + name=request.name, allow_cache=request.allow_cache + ) + assert_permissions( + resource=project, + actions=[AuthzedAction.DESCRIBE], + ) + return project.to_proto() + + def ListProjects(self, request: RegistryServer_pb2.ListProjectsRequest, context): + return RegistryServer_pb2.ListProjectsResponse( + projects=[ + project.to_proto() + for project in permitted_resources( + resources=cast( + list[FeastObject], + self.proxied_registry.list_projects( + allow_cache=request.allow_cache + ), + ), + actions=AuthzedAction.DESCRIBE, + ) + ] + ) + + def DeleteProject(self, request: RegistryServer_pb2.DeleteProjectRequest, context): + assert_permissions( + resource=self.proxied_registry.get_project( + name=request.name, + ), + actions=[AuthzedAction.DELETE], + ) + + self.proxied_registry.delete_project(name=request.name, commit=request.commit) + return Empty() + def Commit(self, request, context): self.proxied_registry.commit() return Empty() diff --git a/sdk/python/feast/repo_config.py b/sdk/python/feast/repo_config.py index 52372f2987..bf0bde6fcb 100644 --- a/sdk/python/feast/repo_config.py +++ b/sdk/python/feast/repo_config.py @@ -9,6 +9,7 @@ BaseModel, ConfigDict, Field, + StrictBool, StrictInt, StrictStr, ValidationError, @@ -132,11 +133,10 @@ class RegistryConfig(FeastBaseModel): s3_additional_kwargs: Optional[Dict[str, str]] = None """ Dict[str, str]: Extra arguments to pass to boto3 when writing the registry file to S3. """ - sqlalchemy_config_kwargs: Dict[str, Any] = {} - """ Dict[str, Any]: Extra arguments to pass to SQLAlchemy.create_engine. """ - - cache_mode: StrictStr = "sync" - """ str: Cache mode type, Possible options are sync and thread(asynchronous caching using threading library)""" + purge_feast_metadata: StrictBool = False + """ bool: Stops using feast_metadata table and delete data from feast_metadata table. + Once this is set to True, it cannot be reverted back to False. Reverting back to False will + only reset the project but not all the projects""" @field_validator("path") def validate_path(cls, path: str, values: ValidationInfo) -> str: diff --git a/sdk/python/feast/repo_contents.py b/sdk/python/feast/repo_contents.py index 9893d5be4e..d65f6ac7bb 100644 --- a/sdk/python/feast/repo_contents.py +++ b/sdk/python/feast/repo_contents.py @@ -19,6 +19,7 @@ from feast.feature_view import FeatureView from feast.on_demand_feature_view import OnDemandFeatureView from feast.permissions.permission import Permission +from feast.project import Project from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto from feast.stream_feature_view import StreamFeatureView @@ -28,6 +29,7 @@ class RepoContents(NamedTuple): Represents the objects in a Feast feature repo. """ + projects: List[Project] data_sources: List[DataSource] feature_views: List[FeatureView] on_demand_feature_views: List[OnDemandFeatureView] @@ -38,6 +40,7 @@ class RepoContents(NamedTuple): def to_registry_proto(self) -> RegistryProto: registry_proto = RegistryProto() + registry_proto.projects.extend([e.to_proto() for e in self.projects]) registry_proto.data_sources.extend([e.to_proto() for e in self.data_sources]) registry_proto.entities.extend([e.to_proto() for e in self.entities]) registry_proto.feature_views.extend( diff --git a/sdk/python/feast/repo_operations.py b/sdk/python/feast/repo_operations.py index cb27568957..6629768375 100644 --- a/sdk/python/feast/repo_operations.py +++ b/sdk/python/feast/repo_operations.py @@ -1,6 +1,7 @@ import base64 import importlib import json +import logging import os import random import re @@ -24,14 +25,18 @@ from feast.feature_store import FeatureStore from feast.feature_view import DUMMY_ENTITY, FeatureView from feast.file_utils import replace_str_in_file +from feast.infra.registry.base_registry import BaseRegistry from feast.infra.registry.registry import FEAST_OBJECT_TYPES, FeastObjectType, Registry from feast.names import adjectives, animals from feast.on_demand_feature_view import OnDemandFeatureView from feast.permissions.permission import Permission +from feast.project import Project from feast.repo_config import RepoConfig from feast.repo_contents import RepoContents from feast.stream_feature_view import StreamFeatureView +logger = logging.getLogger(__name__) + def py_path_to_module(path: Path) -> str: return ( @@ -115,6 +120,7 @@ def parse_repo(repo_root: Path) -> RepoContents: not result in duplicates, but defining two equal objects will. """ res = RepoContents( + projects=[], data_sources=[], entities=[], feature_views=[], @@ -207,6 +213,8 @@ def parse_repo(repo_root: Path) -> RepoContents: (obj is p) for p in res.permissions ): res.permissions.append(obj) + elif isinstance(obj, Project) and not any((obj is p) for p in res.projects): + res.projects.append(obj) res.entities.append(DUMMY_ENTITY) return res @@ -214,33 +222,57 @@ def parse_repo(repo_root: Path) -> RepoContents: def plan(repo_config: RepoConfig, repo_path: Path, skip_source_validation: bool): os.chdir(repo_path) - project, registry, repo, store = _prepare_registry_and_repo(repo_config, repo_path) - - if not skip_source_validation: - provider = store._get_provider() - data_sources = [t.batch_source for t in repo.feature_views] - # Make sure the data source used by this feature view is supported by Feast - for data_source in data_sources: - provider.validate_data_source(store.config, data_source) + repo = _get_repo_contents(repo_path, repo_config.project) + for project in repo.projects: + repo_config.project = project.name + store, registry = _get_store_and_registry(repo_config) + # TODO: When we support multiple projects in a single repo, we should filter repo contents by project + if not skip_source_validation: + provider = store._get_provider() + data_sources = [t.batch_source for t in repo.feature_views] + # Make sure the data source used by this feature view is supported by Feast + for data_source in data_sources: + provider.validate_data_source(store.config, data_source) + + registry_diff, infra_diff, _ = store.plan(repo) + click.echo(registry_diff.to_string()) + click.echo(infra_diff.to_string()) - registry_diff, infra_diff, _ = store.plan(repo) - click.echo(registry_diff.to_string()) - click.echo(infra_diff.to_string()) +def _get_repo_contents(repo_path, project_name: Optional[str] = None): + sys.dont_write_bytecode = True + repo = parse_repo(repo_path) -def _prepare_registry_and_repo(repo_config, repo_path): - store = FeatureStore(config=repo_config) - project = store.project - if not is_valid_name(project): + if len(repo.projects) < 1: + if project_name: + print( + f"No project found in the repository. Using project name {project_name} defined in feature_store.yaml" + ) + repo.projects.append(Project(name=project_name)) + else: + print( + "No project found in the repository. Either define Project in repository or define a project in feature_store.yaml" + ) + sys.exit(1) + elif len(repo.projects) == 1: + if repo.projects[0].name != project_name: + print( + "Project object name should match with the project name defined in feature_store.yaml" + ) + sys.exit(1) + else: print( - f"{project} is not valid. Project name should only have " - f"alphanumerical values and underscores but not start with an underscore." + "Multiple projects found in the repository. Currently no support for multiple projects" ) sys.exit(1) + + return repo + + +def _get_store_and_registry(repo_config): + store = FeatureStore(config=repo_config) registry = store.registry - sys.dont_write_bytecode = True - repo = parse_repo(repo_path) - return project, registry, repo, store + return store, registry def extract_objects_for_apply_delete(project, registry, repo): @@ -289,8 +321,8 @@ def extract_objects_for_apply_delete(project, registry, repo): def apply_total_with_repo_instance( store: FeatureStore, - project: str, - registry: Registry, + project_name: str, + registry: BaseRegistry, repo: RepoContents, skip_source_validation: bool, ): @@ -307,7 +339,7 @@ def apply_total_with_repo_instance( all_to_delete, views_to_keep, views_to_delete, - ) = extract_objects_for_apply_delete(project, registry, repo) + ) = extract_objects_for_apply_delete(project_name, registry, repo) if store._should_use_plan(): registry_diff, infra_diff, new_infra = store.plan(repo) @@ -357,10 +389,21 @@ def create_feature_store( def apply_total(repo_config: RepoConfig, repo_path: Path, skip_source_validation: bool): os.chdir(repo_path) - project, registry, repo, store = _prepare_registry_and_repo(repo_config, repo_path) - apply_total_with_repo_instance( - store, project, registry, repo, skip_source_validation - ) + repo = _get_repo_contents(repo_path, repo_config.project) + for project in repo.projects: + repo_config.project = project.name + store, registry = _get_store_and_registry(repo_config) + if not is_valid_name(project.name): + print( + f"{project.name} is not valid. Project name should only have " + f"alphanumerical values and underscores but not start with an underscore." + ) + sys.exit(1) + # TODO: When we support multiple projects in a single repo, we should filter repo contents by project. Currently there is no way to associate Feast objects to project. + print(f"Applying changes for project {project.name}") + apply_total_with_repo_instance( + store, project.name, registry, repo, skip_source_validation + ) def teardown(repo_config: RepoConfig, repo_path: Optional[str]): diff --git a/sdk/python/feast/templates/local/bootstrap.py b/sdk/python/feast/templates/local/bootstrap.py index ee2847c19c..e2c1efdbc4 100644 --- a/sdk/python/feast/templates/local/bootstrap.py +++ b/sdk/python/feast/templates/local/bootstrap.py @@ -10,6 +10,7 @@ def bootstrap(): from feast.driver_test_data import create_driver_hourly_stats_df repo_path = pathlib.Path(__file__).parent.absolute() / "feature_repo" + project_name = pathlib.Path(__file__).parent.absolute().name data_path = repo_path / "data" data_path.mkdir(exist_ok=True) @@ -23,6 +24,7 @@ def bootstrap(): driver_df.to_parquet(path=str(driver_stats_path), allow_truncated_timestamps=True) example_py_file = repo_path / "example_repo.py" + replace_str_in_file(example_py_file, "%PROJECT_NAME%", str(project_name)) replace_str_in_file(example_py_file, "%PARQUET_PATH%", str(driver_stats_path)) replace_str_in_file(example_py_file, "%LOGGING_PATH%", str(data_path)) diff --git a/sdk/python/feast/templates/local/feature_repo/example_repo.py b/sdk/python/feast/templates/local/feature_repo/example_repo.py index debe9d45e9..e2fd0a891c 100644 --- a/sdk/python/feast/templates/local/feature_repo/example_repo.py +++ b/sdk/python/feast/templates/local/feature_repo/example_repo.py @@ -10,6 +10,7 @@ FeatureView, Field, FileSource, + Project, PushSource, RequestSource, ) @@ -18,6 +19,9 @@ from feast.on_demand_feature_view import on_demand_feature_view from feast.types import Float32, Float64, Int64 +# Define a project for the feature repo +project = Project(name="%PROJECT_NAME%", description="A project for driver statistics") + # Define an entity for the driver. You can think of an entity as a primary key used to # fetch features. driver = Entity(name="driver", join_keys=["driver_id"]) diff --git a/sdk/python/tests/conftest.py b/sdk/python/tests/conftest.py index 5e70da074c..a9bb9ba9c4 100644 --- a/sdk/python/tests/conftest.py +++ b/sdk/python/tests/conftest.py @@ -35,8 +35,8 @@ create_basic_driver_dataset, # noqa: E402 create_document_dataset, ) -from tests.integration.feature_repos.integration_test_repo_config import ( - IntegrationTestRepoConfig, # noqa: E402 +from tests.integration.feature_repos.integration_test_repo_config import ( # noqa: E402 + IntegrationTestRepoConfig, ) from tests.integration.feature_repos.repo_configuration import ( # noqa: E402 AVAILABLE_OFFLINE_STORES, @@ -48,8 +48,8 @@ construct_universal_feature_views, construct_universal_test_data, ) -from tests.integration.feature_repos.universal.data_sources.file import ( - FileDataSourceCreator, # noqa: E402 +from tests.integration.feature_repos.universal.data_sources.file import ( # noqa: E402 + FileDataSourceCreator, ) from tests.integration.feature_repos.universal.entities import ( # noqa: E402 customer, @@ -451,15 +451,20 @@ def is_integration_test(all_markers_from_module): @pytest.fixture( scope="module", params=[ - dedent(""" + dedent( + """ auth: type: no_auth - """), - dedent(""" + """ + ), + dedent( + """ auth: type: kubernetes - """), - dedent(""" + """ + ), + dedent( + """ auth: type: oidc client_id: feast-integration-client @@ -467,7 +472,8 @@ def is_integration_test(all_markers_from_module): username: reader_writer password: password auth_discovery_url: KEYCLOAK_URL_PLACE_HOLDER/realms/master/.well-known/openid-configuration - """), + """ + ), ], ) def auth_config(request, is_integration_test): diff --git a/sdk/python/tests/example_repos/example_feature_repo_with_project_1.py b/sdk/python/tests/example_repos/example_feature_repo_with_project_1.py new file mode 100644 index 0000000000..ad04d7ae66 --- /dev/null +++ b/sdk/python/tests/example_repos/example_feature_repo_with_project_1.py @@ -0,0 +1,151 @@ +from datetime import timedelta + +import pandas as pd + +from feast import Entity, FeatureService, FeatureView, Field, FileSource, PushSource +from feast.on_demand_feature_view import on_demand_feature_view +from feast.project import Project +from feast.types import Array, Float32, Int64, String +from tests.integration.feature_repos.universal.feature_views import TAGS + +# Note that file source paths are not validated, so there doesn't actually need to be any data +# at the paths for these file sources. Since these paths are effectively fake, this example +# feature repo should not be used for historical retrieval. +project = Project( + name="test_universal_cli_with_project_4567", + description="test_universal_cli_with_project_4567 description", + tags={"application": "integration"}, + owner="test@test.com", +) + +driver_locations_source = FileSource( + path="data/driver_locations.parquet", + timestamp_field="event_timestamp", + created_timestamp_column="created_timestamp", +) + +customer_profile_source = FileSource( + name="customer_profile_source", + path="data/customer_profiles.parquet", + timestamp_field="event_timestamp", +) + +customer_driver_combined_source = FileSource( + path="data/customer_driver_combined.parquet", + timestamp_field="event_timestamp", +) + +driver_locations_push_source = PushSource( + name="driver_locations_push", + batch_source=driver_locations_source, +) + +rag_documents_source = FileSource( + name="rag_documents_source", + path="data/rag_documents.parquet", + timestamp_field="event_timestamp", +) + +driver = Entity( + name="driver", # The name is derived from this argument, not object name. + join_keys=["driver_id"], + description="driver id", + tags=TAGS, +) + +customer = Entity( + name="customer", # The name is derived from this argument, not object name. + join_keys=["customer_id"], + tags=TAGS, +) + +item = Entity( + name="item_id", # The name is derived from this argument, not object name. + join_keys=["item_id"], +) + +driver_locations = FeatureView( + name="driver_locations", + entities=[driver], + ttl=timedelta(days=1), + schema=[ + Field(name="lat", dtype=Float32), + Field(name="lon", dtype=String), + Field(name="driver_id", dtype=Int64), + ], + online=True, + source=driver_locations_source, + tags={}, +) + +pushed_driver_locations = FeatureView( + name="pushed_driver_locations", + entities=[driver], + ttl=timedelta(days=1), + schema=[ + Field(name="driver_lat", dtype=Float32), + Field(name="driver_long", dtype=String), + Field(name="driver_id", dtype=Int64), + ], + online=True, + source=driver_locations_push_source, + tags={}, +) + +customer_profile = FeatureView( + name="customer_profile", + entities=[customer], + ttl=timedelta(days=1), + schema=[ + Field(name="avg_orders_day", dtype=Float32), + Field(name="name", dtype=String), + Field(name="age", dtype=Int64), + Field(name="customer_id", dtype=String), + ], + online=True, + source=customer_profile_source, + tags={}, +) + +customer_driver_combined = FeatureView( + name="customer_driver_combined", + entities=[customer, driver], + ttl=timedelta(days=1), + schema=[ + Field(name="trips", dtype=Int64), + Field(name="driver_id", dtype=Int64), + Field(name="customer_id", dtype=String), + ], + online=True, + source=customer_driver_combined_source, + tags={}, +) + +document_embeddings = FeatureView( + name="document_embeddings", + entities=[item], + schema=[ + Field(name="Embeddings", dtype=Array(Float32)), + Field(name="item_id", dtype=String), + ], + source=rag_documents_source, + ttl=timedelta(hours=24), +) + + +@on_demand_feature_view( + sources=[customer_profile], + schema=[Field(name="on_demand_age", dtype=Int64)], + mode="pandas", +) +def customer_profile_pandas_odfv(inputs: pd.DataFrame) -> pd.DataFrame: + outputs = pd.DataFrame() + outputs["on_demand_age"] = inputs["age"] + 1 + return outputs + + +all_drivers_feature_service = FeatureService( + name="driver_locations_service", + features=[driver_locations], + tags=TAGS, +) diff --git a/sdk/python/tests/integration/online_store/test_remote_online_store.py b/sdk/python/tests/integration/online_store/test_remote_online_store.py index f74fb14a86..d8c92077db 100644 --- a/sdk/python/tests/integration/online_store/test_remote_online_store.py +++ b/sdk/python/tests/integration/online_store/test_remote_online_store.py @@ -187,9 +187,6 @@ def _create_remote_client_feature_store( auth_config=auth_config, ) - result = runner.run(["--chdir", repo_path, "apply"], cwd=temp_dir) - assert result.returncode == 0 - return FeatureStore(repo_path=repo_path) diff --git a/sdk/python/tests/integration/registration/test_universal_cli.py b/sdk/python/tests/integration/registration/test_universal_cli.py index 5c238da24d..735f71407f 100644 --- a/sdk/python/tests/integration/registration/test_universal_cli.py +++ b/sdk/python/tests/integration/registration/test_universal_cli.py @@ -52,7 +52,9 @@ def test_universal_cli(): for key, value in registry_dict.items() } - # entity & feature view list commands should succeed + # project, entity & feature view list commands should succeed + result = runner.run(["projects", "list"], cwd=repo_path) + assertpy.assert_that(result.returncode).is_equal_to(0) result = runner.run(["entities", "list"], cwd=repo_path) assertpy.assert_that(result.returncode).is_equal_to(0) result = runner.run(["feature-views", "list"], cwd=repo_path) @@ -71,6 +73,10 @@ def test_universal_cli(): assertpy.assert_that(result.returncode).is_equal_to(0) # entity & feature view describe commands should succeed when objects exist + result = runner.run(["projects", "describe", project], cwd=repo_path) + assertpy.assert_that(result.returncode).is_equal_to(0) + result = runner.run(["projects", "current_project"], cwd=repo_path) + assertpy.assert_that(result.returncode).is_equal_to(0) result = runner.run(["entities", "describe", "driver"], cwd=repo_path) assertpy.assert_that(result.returncode).is_equal_to(0) result = runner.run( @@ -89,8 +95,132 @@ def test_universal_cli(): ) assertpy.assert_that(result.returncode).is_equal_to(0) assertpy.assert_that(fs.list_data_sources()).is_length(5) + assertpy.assert_that(fs.list_projects()).is_length(1) # entity & feature view describe commands should fail when objects don't exist + result = runner.run(["projects", "describe", "foo"], cwd=repo_path) + assertpy.assert_that(result.returncode).is_equal_to(1) + result = runner.run(["entities", "describe", "foo"], cwd=repo_path) + assertpy.assert_that(result.returncode).is_equal_to(1) + result = runner.run(["feature-views", "describe", "foo"], cwd=repo_path) + assertpy.assert_that(result.returncode).is_equal_to(1) + result = runner.run(["feature-services", "describe", "foo"], cwd=repo_path) + assertpy.assert_that(result.returncode).is_equal_to(1) + result = runner.run(["data-sources", "describe", "foo"], cwd=repo_path) + assertpy.assert_that(result.returncode).is_equal_to(1) + result = runner.run(["permissions", "describe", "foo"], cwd=repo_path) + assertpy.assert_that(result.returncode).is_equal_to(1) + + # Doing another apply should be a no op, and should not cause errors + result = runner.run(["apply"], cwd=repo_path) + assertpy.assert_that(result.returncode).is_equal_to(0) + basic_rw_test( + FeatureStore(repo_path=str(repo_path), config=None), + view_name="driver_locations", + ) + + # Confirm that registry contents have not changed. + registry_dict = fs.registry.to_dict(project=project) + assertpy.assert_that(registry_specs).is_equal_to( + { + key: [fco["spec"] if "spec" in fco else fco for fco in value] + for key, value in registry_dict.items() + } + ) + + result = runner.run(["teardown"], cwd=repo_path) + assertpy.assert_that(result.returncode).is_equal_to(0) + finally: + runner.run(["teardown"], cwd=repo_path) + + +@pytest.mark.integration +def test_universal_cli_with_project(): + project = "test_universal_cli_with_project_4567" + runner = CliRunner() + + with tempfile.TemporaryDirectory() as repo_dir_name: + try: + repo_path = Path(repo_dir_name) + feature_store_yaml = make_feature_store_yaml( + project, + repo_path, + FileDataSourceCreator("project"), + "local", + {"type": "sqlite"}, + ) + + repo_config = repo_path / "feature_store.yaml" + + repo_config.write_text(dedent(feature_store_yaml)) + + repo_example = repo_path / "example.py" + repo_example.write_text( + get_example_repo("example_feature_repo_with_project_1.py") + ) + result = runner.run(["apply"], cwd=repo_path) + assertpy.assert_that(result.returncode).is_equal_to(0) + + # Store registry contents, to be compared later. + fs = FeatureStore(repo_path=str(repo_path)) + registry_dict = fs.registry.to_dict(project=project) + # Save only the specs, not the metadata. + registry_specs = { + key: [fco["spec"] if "spec" in fco else fco for fco in value] + for key, value in registry_dict.items() + } + + # entity & feature view list commands should succeed + result = runner.run(["projects", "list"], cwd=repo_path) + assertpy.assert_that(result.returncode).is_equal_to(0) + result = runner.run(["entities", "list"], cwd=repo_path) + assertpy.assert_that(result.returncode).is_equal_to(0) + result = runner.run(["feature-views", "list"], cwd=repo_path) + assertpy.assert_that(result.returncode).is_equal_to(0) + result = runner.run(["feature-services", "list"], cwd=repo_path) + assertpy.assert_that(result.returncode).is_equal_to(0) + result = runner.run(["data-sources", "list"], cwd=repo_path) + assertpy.assert_that(result.returncode).is_equal_to(0) + result = runner.run(["permissions", "list"], cwd=repo_path) + assertpy.assert_that(result.returncode).is_equal_to(0) + + # entity & feature view describe commands should succeed when objects exist + result = runner.run(["projects", "describe", project], cwd=repo_path) + assertpy.assert_that(result.returncode).is_equal_to(0) + result = runner.run(["projects", "current_project"], cwd=repo_path) + print(result.returncode) + print("result: ", result) + print("result.stdout: ", result.stdout) + assertpy.assert_that(result.returncode).is_equal_to(0) + result = runner.run(["entities", "describe", "driver"], cwd=repo_path) + assertpy.assert_that(result.returncode).is_equal_to(0) + result = runner.run( + ["feature-views", "describe", "driver_locations"], cwd=repo_path + ) + assertpy.assert_that(result.returncode).is_equal_to(0) + result = runner.run( + ["feature-services", "describe", "driver_locations_service"], + cwd=repo_path, + ) + assertpy.assert_that(result.returncode).is_equal_to(0) + assertpy.assert_that(fs.list_feature_views()).is_length(5) + result = runner.run( + ["data-sources", "describe", "customer_profile_source"], + cwd=repo_path, + ) + assertpy.assert_that(result.returncode).is_equal_to(0) + assertpy.assert_that(fs.list_data_sources()).is_length(5) + + projects_list = fs.list_projects() + assertpy.assert_that(projects_list).is_length(1) + assertpy.assert_that(projects_list[0].name).is_equal_to(project) + assertpy.assert_that(projects_list[0].description).is_equal_to( + "test_universal_cli_with_project_4567 description" + ) + + # entity & feature view describe commands should fail when objects don't exist + result = runner.run(["projects", "describe", "foo"], cwd=repo_path) + assertpy.assert_that(result.returncode).is_equal_to(1) result = runner.run(["entities", "describe", "foo"], cwd=repo_path) assertpy.assert_that(result.returncode).is_equal_to(1) result = runner.run(["feature-views", "describe", "foo"], cwd=repo_path) @@ -161,6 +291,12 @@ def test_odfv_apply() -> None: assertpy.assert_that(result.returncode).is_equal_to(0) # entity & feature view list commands should succeed + result = runner.run(["projects", "describe", project], cwd=repo_path) + assertpy.assert_that(result.returncode).is_equal_to(0) + result = runner.run(["projects", "current_project"], cwd=repo_path) + assertpy.assert_that(result.returncode).is_equal_to(0) + result = runner.run(["projects", "list"], cwd=repo_path) + assertpy.assert_that(result.returncode).is_equal_to(0) result = runner.run(["entities", "list"], cwd=repo_path) assertpy.assert_that(result.returncode).is_equal_to(0) result = runner.run(["on-demand-feature-views", "list"], cwd=repo_path) @@ -192,7 +328,14 @@ def test_nullable_online_store(test_nullable_online_store) -> None: repo_example = repo_path / "example.py" repo_example.write_text(get_example_repo("empty_feature_repo.py")) + result = runner.run(["apply"], cwd=repo_path) assertpy.assert_that(result.returncode).is_equal_to(0) + result = runner.run(["projects", "describe", project], cwd=repo_path) + assertpy.assert_that(result.returncode).is_equal_to(0) + result = runner.run(["projects", "current_project"], cwd=repo_path) + assertpy.assert_that(result.returncode).is_equal_to(0) + result = runner.run(["projects", "list"], cwd=repo_path) + assertpy.assert_that(result.returncode).is_equal_to(0) finally: runner.run(["teardown"], cwd=repo_path) diff --git a/sdk/python/tests/integration/registration/test_universal_registry.py b/sdk/python/tests/integration/registration/test_universal_registry.py index c528cee4a8..20f1f5ef0a 100644 --- a/sdk/python/tests/integration/registration/test_universal_registry.py +++ b/sdk/python/tests/integration/registration/test_universal_registry.py @@ -38,11 +38,12 @@ from feast.infra.online_stores.sqlite import SqliteTable from feast.infra.registry.registry import Registry from feast.infra.registry.remote import RemoteRegistry, RemoteRegistryConfig -from feast.infra.registry.sql import SqlRegistry +from feast.infra.registry.sql import SqlRegistry, SqlRegistryConfig from feast.on_demand_feature_view import on_demand_feature_view from feast.permissions.action import AuthzedAction from feast.permissions.permission import Permission from feast.permissions.policy import RoleBasedPolicy +from feast.project import Project from feast.protos.feast.registry import RegistryServer_pb2, RegistryServer_pb2_grpc from feast.registry_server import RegistryServer from feast.repo_config import RegistryConfig @@ -91,7 +92,7 @@ def s3_registry() -> Registry: return Registry("project", registry_config, None) -@pytest.fixture(scope="session") +@pytest.fixture(scope="function") def minio_registry() -> Registry: bucket_name = "test-bucket" @@ -158,7 +159,7 @@ def pg_registry_async(): container.start() - registry_config = _given_registry_config_for_pg_sql(container, 2, "thread") + registry_config = _given_registry_config_for_pg_sql(container, 2, "thread", 3) yield SqlRegistry(registry_config, "project", None) @@ -166,7 +167,11 @@ def pg_registry_async(): def _given_registry_config_for_pg_sql( - container, cache_ttl_seconds=2, cache_mode="sync" + container, + cache_ttl_seconds=2, + cache_mode="sync", + thread_pool_executor_worker_count=0, + purge_feast_metadata=False, ): log_string_to_wait_for = "database system is ready to accept connections" waited = wait_for_logs( @@ -179,7 +184,7 @@ def _given_registry_config_for_pg_sql( container_port = container.get_exposed_port(5432) container_host = container.get_container_host_ip() - return RegistryConfig( + return SqlRegistryConfig( registry_type="sql", cache_ttl_seconds=cache_ttl_seconds, cache_mode=cache_mode, @@ -187,6 +192,8 @@ def _given_registry_config_for_pg_sql( # to understand that we are using psycopg3. path=f"postgresql+psycopg://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{container_host}:{container_port}/{POSTGRES_DB}", sqlalchemy_config_kwargs={"echo": False, "pool_pre_ping": True}, + thread_pool_executor_worker_count=thread_pool_executor_worker_count, + purge_feast_metadata=purge_feast_metadata, ) @@ -207,14 +214,20 @@ def mysql_registry_async(): container = MySqlContainer("mysql:latest") container.start() - registry_config = _given_registry_config_for_mysql(container, 2, "thread") + registry_config = _given_registry_config_for_mysql(container, 2, "thread", 3) yield SqlRegistry(registry_config, "project", None) container.stop() -def _given_registry_config_for_mysql(container, cache_ttl_seconds=2, cache_mode="sync"): +def _given_registry_config_for_mysql( + container, + cache_ttl_seconds=2, + cache_mode="sync", + thread_pool_executor_worker_count=0, + purge_feast_metadata=False, +): import sqlalchemy engine = sqlalchemy.create_engine( @@ -222,18 +235,20 @@ def _given_registry_config_for_mysql(container, cache_ttl_seconds=2, cache_mode= ) engine.connect() - return RegistryConfig( + return SqlRegistryConfig( registry_type="sql", path=container.get_connection_url(), cache_ttl_seconds=cache_ttl_seconds, cache_mode=cache_mode, sqlalchemy_config_kwargs={"echo": False, "pool_pre_ping": True}, + thread_pool_executor_worker_count=thread_pool_executor_worker_count, + purge_feast_metadata=purge_feast_metadata, ) @pytest.fixture(scope="session") def sqlite_registry(): - registry_config = RegistryConfig( + registry_config = SqlRegistryConfig( registry_type="sql", path="sqlite://", ) @@ -250,7 +265,11 @@ def __init__(self, service, servicer): ) def unary_unary( - self, method: str, request_serializer=None, response_deserializer=None + self, + method: str, + request_serializer=None, + response_deserializer=None, + _registered_method=None, ): method_name = method.split("/")[-1] method_descriptor = self.service.methods_by_name[method_name] @@ -347,9 +366,11 @@ def test_apply_entity_success(test_registry): project_uuid = project_metadata[0].project_uuid assert len(project_metadata[0].project_uuid) == 36 assert_project_uuid(project, project_uuid, test_registry) + assert_project(project, test_registry) entities = test_registry.list_entities(project, tags=entity.tags) assert_project_uuid(project, project_uuid, test_registry) + assert_project(project, test_registry) entity = entities[0] assert ( @@ -386,11 +407,12 @@ def test_apply_entity_success(test_registry): updated_entity.created_timestamp is not None and updated_entity.created_timestamp == entity.created_timestamp ) - test_registry.delete_entity("driver_car_id", project) assert_project_uuid(project, project_uuid, test_registry) + assert_project(project, test_registry) entities = test_registry.list_entities(project) assert_project_uuid(project, project_uuid, test_registry) + assert_project(project, test_registry) assert len(entities) == 0 test_registry.teardown() @@ -402,6 +424,14 @@ def assert_project_uuid(project, project_uuid, test_registry): assert project_metadata[0].project_uuid == project_uuid +def assert_project(project_name, test_registry, allow_cache=False): + project_obj = test_registry.list_projects(allow_cache=allow_cache) + assert len(project_obj) == 1 + assert project_obj[0].name == "project" + project_obj = test_registry.get_project(name=project_name, allow_cache=allow_cache) + assert project_obj.name == "project" + + @pytest.mark.integration @pytest.mark.parametrize( "test_registry", @@ -725,9 +755,10 @@ def simple_udf(x: int): project = "project" # Register Feature Views - test_registry.apply_feature_view(odfv1, project) - test_registry.apply_feature_view(fv1, project) - test_registry.apply_feature_view(sfv, project) + test_registry.apply_feature_view(odfv1, project, False) + test_registry.apply_feature_view(fv1, project, False) + test_registry.apply_feature_view(sfv, project, False) + test_registry.commit() # Modify odfv by changing a single feature dtype @on_demand_feature_view( @@ -1283,6 +1314,10 @@ def test_commit(): project_uuid = project_metadata.project_uuid assert len(project_uuid) == 36 validate_project_uuid(project_uuid, test_registry) + assert len(test_registry.cached_registry_proto.projects) == 1 + project_obj = test_registry.cached_registry_proto.projects[0] + assert project == Project.from_proto(project_obj).name + assert_project(project, test_registry, True) # Retrieving the entity should still succeed entities = test_registry.list_entities(project, allow_cache=True, tags=entity.tags) @@ -1295,6 +1330,7 @@ def test_commit(): and entity.tags["team"] == "matchmaking" ) validate_project_uuid(project_uuid, test_registry) + assert_project(project, test_registry, True) entity = test_registry.get_entity("driver_car_id", project, allow_cache=True) assert ( @@ -1304,6 +1340,7 @@ def test_commit(): and entity.tags["team"] == "matchmaking" ) validate_project_uuid(project_uuid, test_registry) + assert_project(project, test_registry, True) # Create new registry that points to the same store registry_with_same_store = Registry("project", registry_config, None) @@ -1312,6 +1349,7 @@ def test_commit(): entities = registry_with_same_store.list_entities(project) assert len(entities) == 0 validate_project_uuid(project_uuid, registry_with_same_store) + assert_project(project, test_registry, True) # commit from the original registry test_registry.commit() @@ -1330,6 +1368,7 @@ def test_commit(): and entity.tags["team"] == "matchmaking" ) validate_project_uuid(project_uuid, registry_with_same_store) + assert_project(project, test_registry) entity = test_registry.get_entity("driver_car_id", project) assert ( @@ -1371,6 +1410,7 @@ def test_apply_permission_success(test_registry): project_uuid = project_metadata[0].project_uuid assert len(project_metadata[0].project_uuid) == 36 assert_project_uuid(project, project_uuid, test_registry) + assert_project(project, test_registry) permissions = test_registry.list_permissions(project) assert_project_uuid(project, project_uuid, test_registry) @@ -1483,5 +1523,194 @@ def test_apply_permission_success(test_registry): permissions = test_registry.list_permissions(project) assert_project_uuid(project, project_uuid, test_registry) assert len(permissions) == 0 + assert_project(project, test_registry) + + test_registry.teardown() + + +@pytest.mark.integration +@pytest.mark.parametrize("test_registry", all_fixtures) +def test_apply_project_success(test_registry): + project = Project( + name="project", + description="Project description", + tags={"team": "project team"}, + owner="owner@mail.com", + ) + + # Register Project + test_registry.apply_project(project) + assert_project(project.name, test_registry, False) + + projects_list = test_registry.list_projects(tags=project.tags) + + assert_project(projects_list[0].name, test_registry) + + project_get = test_registry.get_project("project") + assert ( + project_get.name == project.name + and project_get.description == project.description + and project_get.tags == project.tags + and project_get.owner == project.owner + ) + + # Update project + updated_project = Project( + name=project.name, + description="New Project Description", + tags={"team": "matchmaking", "app": "feast"}, + ) + test_registry.apply_project(updated_project) + + updated_project_get = test_registry.get_project(project.name) + + # The created_timestamp for the entity should be set to the created_timestamp value stored from the previous apply + assert ( + updated_project_get.created_timestamp is not None + and updated_project_get.created_timestamp == project_get.created_timestamp + ) + + assert ( + updated_project_get.created_timestamp + < updated_project_get.last_updated_timestamp + ) + + entity = Entity( + name="driver_car_id", + description="Car driver id", + tags={"team": "matchmaking"}, + ) + + test_registry.apply_entity(entity, project.name) + entities = test_registry.list_entities(project.name) + assert len(entities) == 1 + + test_registry.delete_project(project.name, commit=False) + + test_registry.commit() + + entities = test_registry.list_entities(project.name, False) + assert len(entities) == 0 + projects_list = test_registry.list_projects() + assert len(projects_list) == 0 + + test_registry.refresh(project.name) + + test_registry.teardown() + + +@pytest.fixture +def local_registry_purge_feast_metadata() -> Registry: + fd, registry_path = mkstemp() + registry_config = RegistryConfig( + path=registry_path, cache_ttl_seconds=600, purge_feast_metadata=True + ) + return Registry("project", registry_config, None) + + +@pytest.fixture(scope="function") +def pg_registry_purge_feast_metadata(): + container = ( + DockerContainer("postgres:latest") + .with_exposed_ports(5432) + .with_env("POSTGRES_USER", POSTGRES_USER) + .with_env("POSTGRES_PASSWORD", POSTGRES_PASSWORD) + .with_env("POSTGRES_DB", POSTGRES_DB) + ) + + container.start() + + registry_config = _given_registry_config_for_pg_sql(container, 2, "thread", 3, True) + + yield SqlRegistry(registry_config, "project", None) + + container.stop() + + +@pytest.fixture(scope="function") +def mysql_registry_purge_feast_metadata(): + container = MySqlContainer("mysql:latest") + container.start() + + registry_config = _given_registry_config_for_mysql(container, 2, "thread", 3, True) + + yield SqlRegistry(registry_config, "project", None) + + container.stop() + + +purge_feast_metadata_fixtures = [ + lazy_fixture("local_registry_purge_feast_metadata"), + pytest.param( + lazy_fixture("pg_registry_purge_feast_metadata"), + marks=pytest.mark.xdist_group(name="pg_registry_purge_feast_metadata"), + ), + pytest.param( + lazy_fixture("mysql_registry_purge_feast_metadata"), + marks=pytest.mark.xdist_group(name="mysql_registry_purge_feast_metadata"), + ), +] + + +@pytest.mark.integration +@pytest.mark.parametrize("test_registry", purge_feast_metadata_fixtures) +def test_apply_entity_success_with_purge_feast_metadata(test_registry): + entity = Entity( + name="driver_car_id", + description="Car driver id", + tags={"team": "matchmaking"}, + ) + + project = "project" + + # Register Entity + test_registry.apply_entity(entity, project) + project_metadata = test_registry.list_project_metadata(project=project) + assert len(project_metadata) == 0 + assert_project(project, test_registry) + + entities = test_registry.list_entities(project, tags=entity.tags) + assert_project(project, test_registry) + + entity = entities[0] + assert ( + len(entities) == 1 + and entity.name == "driver_car_id" + and entity.description == "Car driver id" + and "team" in entity.tags + and entity.tags["team"] == "matchmaking" + ) + + entity = test_registry.get_entity("driver_car_id", project) + assert ( + entity.name == "driver_car_id" + and entity.description == "Car driver id" + and "team" in entity.tags + and entity.tags["team"] == "matchmaking" + ) + + # After the first apply, the created_timestamp should be the same as the last_update_timestamp. + assert entity.created_timestamp == entity.last_updated_timestamp + + # Update entity + updated_entity = Entity( + name="driver_car_id", + description="Car driver Id", + tags={"team": "matchmaking"}, + ) + test_registry.apply_entity(updated_entity, project) + + updated_entity = test_registry.get_entity("driver_car_id", project) + + # The created_timestamp for the entity should be set to the created_timestamp value stored from the previous apply + assert ( + updated_entity.created_timestamp is not None + and updated_entity.created_timestamp == entity.created_timestamp + ) + test_registry.delete_entity("driver_car_id", project) + assert_project(project, test_registry) + entities = test_registry.list_entities(project) + assert_project(project, test_registry) + assert len(entities) == 0 test_registry.teardown() diff --git a/sdk/python/tests/unit/permissions/auth/conftest.py b/sdk/python/tests/unit/permissions/auth/conftest.py index ea6e2e4311..5a29f8ec78 100644 --- a/sdk/python/tests/unit/permissions/auth/conftest.py +++ b/sdk/python/tests/unit/permissions/auth/conftest.py @@ -8,6 +8,7 @@ read_fv_perm, read_odfv_perm, read_permissions_perm, + read_projects_perm, read_sfv_perm, ) from tests.unit.permissions.auth.test_token_parser import _CLIENT_ID @@ -90,6 +91,7 @@ def oidc_config() -> OidcAuthConfig: read_fv_perm, read_odfv_perm, read_sfv_perm, + read_projects_perm, ], ], ) diff --git a/sdk/python/tests/unit/permissions/auth/server/test_auth_registry_server.py b/sdk/python/tests/unit/permissions/auth/server/test_auth_registry_server.py index 9e9bc1473e..c72b1aa1e2 100644 --- a/sdk/python/tests/unit/permissions/auth/server/test_auth_registry_server.py +++ b/sdk/python/tests/unit/permissions/auth/server/test_auth_registry_server.py @@ -5,9 +5,7 @@ import pytest import yaml -from feast import ( - FeatureStore, -) +from feast import FeatureStore from feast.errors import ( EntityNotFoundException, FeastPermissionError, @@ -23,6 +21,7 @@ read_fv_perm, read_odfv_perm, read_permissions_perm, + read_projects_perm, read_sfv_perm, ) from tests.utils.auth_permissions_util import get_remote_registry_store @@ -50,7 +49,11 @@ def start_registry_server( assertpy.assert_that(server_port).is_not_equal_to(0) print(f"Starting Registry at {server_port}") - server = start_server(feature_store, server_port, wait_for_termination=False) + server = start_server( + feature_store, + server_port, + wait_for_termination=False, + ) print("Waiting server availability") wait_retry_backoff( lambda: (None, check_port_open("localhost", server_port)), @@ -179,6 +182,7 @@ def _test_list_permissions( read_fv_perm, read_odfv_perm, read_sfv_perm, + read_projects_perm, ], permissions, ): @@ -191,6 +195,7 @@ def _test_list_permissions( read_fv_perm, read_odfv_perm, read_sfv_perm, + read_projects_perm, ] ) ) diff --git a/sdk/python/tests/unit/permissions/auth/server/test_utils.py b/sdk/python/tests/unit/permissions/auth/server/test_utils.py index 5d781919a0..32b4fd8f98 100644 --- a/sdk/python/tests/unit/permissions/auth/server/test_utils.py +++ b/sdk/python/tests/unit/permissions/auth/server/test_utils.py @@ -6,6 +6,7 @@ from feast.permissions.permission import Permission from feast.permissions.policy import RoleBasedPolicy from feast.permissions.server.utils import AuthManagerType, str_to_auth_manager_type +from feast.project import Project read_permissions_perm = Permission( name="read_permissions_perm", @@ -14,6 +15,13 @@ actions=[AuthzedAction.DESCRIBE], ) +read_projects_perm = Permission( + name="read_projects_perm", + types=Project, + policy=RoleBasedPolicy(roles=["reader"]), + actions=[AuthzedAction.DESCRIBE], +) + read_entities_perm = Permission( name="read_entities_perm", types=Entity, diff --git a/sdk/python/tests/unit/test_on_demand_feature_view.py b/sdk/python/tests/unit/test_on_demand_feature_view.py index d9cc5dee50..6073891aba 100644 --- a/sdk/python/tests/unit/test_on_demand_feature_view.py +++ b/sdk/python/tests/unit/test_on_demand_feature_view.py @@ -251,11 +251,9 @@ def test_from_proto_backwards_compatible_udf(): proto.spec.feature_transformation.user_defined_function.body_text ) - # And now we're going to null the feature_transformation proto object before reserializing the entire proto - # proto.spec.user_defined_function.body_text = on_demand_feature_view.transformation.udf_string - proto.spec.feature_transformation.user_defined_function.name = "" - proto.spec.feature_transformation.user_defined_function.body = b"" - proto.spec.feature_transformation.user_defined_function.body_text = "" + # For objects that are already registered, feature_transformation and mode is not set + proto.spec.feature_transformation.Clear() + proto.spec.ClearField("mode") # And now we expect the to get the same object back under feature_transformation reserialized_proto = OnDemandFeatureView.from_proto(proto) diff --git a/sdk/python/tests/unit/test_project.py b/sdk/python/tests/unit/test_project.py new file mode 100644 index 0000000000..f15aef2972 --- /dev/null +++ b/sdk/python/tests/unit/test_project.py @@ -0,0 +1,122 @@ +import unittest +from datetime import datetime, timezone + +from feast.project import Project +from feast.protos.feast.core.Project_pb2 import Project as ProjectProto +from feast.protos.feast.core.Project_pb2 import ProjectMeta as ProjectMetaProto +from feast.protos.feast.core.Project_pb2 import ProjectSpec as ProjectSpecProto + + +class TestProject(unittest.TestCase): + def setUp(self): + self.project_name = "test_project" + self.description = "Test project description" + self.tags = {"env": "test"} + self.owner = "test_owner" + self.created_timestamp = datetime.now(tz=timezone.utc) + self.last_updated_timestamp = datetime.now(tz=timezone.utc) + + def test_initialization(self): + project = Project( + name=self.project_name, + description=self.description, + tags=self.tags, + owner=self.owner, + created_timestamp=self.created_timestamp, + last_updated_timestamp=self.last_updated_timestamp, + ) + self.assertEqual(project.name, self.project_name) + self.assertEqual(project.description, self.description) + self.assertEqual(project.tags, self.tags) + self.assertEqual(project.owner, self.owner) + self.assertEqual(project.created_timestamp, self.created_timestamp) + self.assertEqual(project.last_updated_timestamp, self.last_updated_timestamp) + + def test_equality(self): + project1 = Project(name=self.project_name) + project2 = Project(name=self.project_name) + project3 = Project(name="different_project") + self.assertTrue( + project1.name == project2.name + and project1.description == project2.description + and project1.tags == project2.tags + and project1.owner == project2.owner + ) + self.assertFalse( + project1.name == project3.name + and project1.description == project3.description + and project1.tags == project3.tags + and project1.owner == project3.owner + ) + + def test_is_valid(self): + project = Project(name=self.project_name) + project.is_valid() + with self.assertRaises(ValueError): + invalid_project = Project(name="") + invalid_project.is_valid() + + def test_from_proto(self): + meta = ProjectMetaProto() + meta.created_timestamp.FromDatetime(self.created_timestamp) + meta.last_updated_timestamp.FromDatetime(self.last_updated_timestamp) + project_proto = ProjectProto( + spec=ProjectSpecProto( + name=self.project_name, + description=self.description, + tags=self.tags, + owner=self.owner, + ), + meta=meta, + ) + project = Project.from_proto(project_proto) + self.assertEqual(project.name, self.project_name) + self.assertEqual(project.description, self.description) + self.assertEqual(project.tags, self.tags) + self.assertEqual(project.owner, self.owner) + self.assertEqual(project.created_timestamp, self.created_timestamp) + self.assertEqual(project.last_updated_timestamp, self.last_updated_timestamp) + + def test_to_proto(self): + project = Project( + name=self.project_name, + description=self.description, + tags=self.tags, + owner=self.owner, + created_timestamp=self.created_timestamp, + last_updated_timestamp=self.last_updated_timestamp, + ) + project_proto = project.to_proto() + self.assertEqual(project_proto.spec.name, self.project_name) + self.assertEqual(project_proto.spec.description, self.description) + self.assertEqual(project_proto.spec.tags, self.tags) + self.assertEqual(project_proto.spec.owner, self.owner) + self.assertEqual( + project_proto.meta.created_timestamp.ToDatetime().replace( + tzinfo=timezone.utc + ), + self.created_timestamp, + ) + self.assertEqual( + project_proto.meta.last_updated_timestamp.ToDatetime().replace( + tzinfo=timezone.utc + ), + self.last_updated_timestamp, + ) + + def test_to_proto_and_back(self): + project = Project( + name=self.project_name, + description=self.description, + tags=self.tags, + owner=self.owner, + created_timestamp=self.created_timestamp, + last_updated_timestamp=self.last_updated_timestamp, + ) + project_proto = project.to_proto() + project_from_proto = Project.from_proto(project_proto) + self.assertEqual(project, project_from_proto) + + +if __name__ == "__main__": + unittest.main() From 96344b2b6830dcc280567542d111d1b0f39879e0 Mon Sep 17 00:00:00 2001 From: Hao Xu Date: Fri, 6 Sep 2024 02:41:25 -0700 Subject: [PATCH 058/185] fix: Hao xu request source timestamp_field (#4495) * not sure why fix linting Signed-off-by: cmuhao * not sure why fix linting Signed-off-by: cmuhao --------- Signed-off-by: cmuhao --- README.md | 6 +++++- sdk/python/feast/data_source.py | 11 ++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 10c20050d3..6f17e7fa6c 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,7 @@ The list below contains the functionality that contributors are planning to deve * [x] On-demand Transformations (Beta release. See [RFC](https://docs.google.com/document/d/1lgfIw0Drc65LpaxbUu49RCeJgMew547meSJttnUqz7c/edit#)) * [x] Streaming Transformations (Alpha release. See [RFC](https://docs.google.com/document/d/1UzEyETHUaGpn0ap4G82DHluiCj7zEbrQLkJJkKSv4e8/edit)) * [ ] Batch transformation (In progress. See [RFC](https://docs.google.com/document/d/1964OkzuBljifDvkV-0fakp2uaijnVzdwWNGdz7Vz50A/edit)) + * [ ] Persistent On-demand Transformations (Beta release. See [GitHub Issue](https://github.com/feast-dev/feast/issues/4376)) * **Streaming** * [x] [Custom streaming ingestion job support](https://docs.feast.dev/how-to-guides/customizing-feast/creating-a-custom-provider) * [x] [Push based streaming data ingestion to online store](https://docs.feast.dev/reference/data-sources/push) @@ -208,6 +209,9 @@ The list below contains the functionality that contributors are planning to deve * [x] Amundsen integration (see [Feast extractor](https://github.com/amundsen-io/amundsen/blob/main/databuilder/databuilder/extractor/feast_extractor.py)) * [x] DataHub integration (see [DataHub Feast docs](https://datahubproject.io/docs/generated/ingestion/sources/feast/)) * [x] Feast Web UI (Beta release. See [docs](https://docs.feast.dev/reference/alpha-web-ui)) + * [ ] Feast Lineage Explorer +* **Natural Language Processing** + * [x] Vector Search (Alpha release. See [RFC](https://docs.google.com/document/d/18IWzLEA9i2lDWnbfbwXnMCg3StlqaLVI-uRpQjr_Vos/edit#heading=h.9gaqqtox9jg6)) ## 🎓 Important Resources @@ -230,4 +234,4 @@ Thanks goes to these incredible people: - + \ No newline at end of file diff --git a/sdk/python/feast/data_source.py b/sdk/python/feast/data_source.py index 17fbfd5fcf..f7881c5045 100644 --- a/sdk/python/feast/data_source.py +++ b/sdk/python/feast/data_source.py @@ -524,12 +524,19 @@ def __init__( *, name: str, schema: List[Field], + timestamp_field: Optional[str] = None, description: Optional[str] = "", tags: Optional[Dict[str, str]] = None, owner: Optional[str] = "", ): """Creates a RequestSource object.""" - super().__init__(name=name, description=description, tags=tags, owner=owner) + super().__init__( + name=name, + timestamp_field=timestamp_field, + description=description, + tags=tags, + owner=owner, + ) self.schema = schema def validate(self, config: RepoConfig): @@ -570,6 +577,7 @@ def from_proto(data_source: DataSourceProto): return RequestSource( name=data_source.name, schema=list_schema, + timestamp_field=data_source.timestamp_field, description=data_source.description, tags=dict(data_source.tags), owner=data_source.owner, @@ -593,6 +601,7 @@ def to_proto(self) -> DataSourceProto: tags=self.tags, owner=self.owner, ) + data_source_proto.timestamp_field = self.timestamp_field data_source_proto.request_data_options.schema.extend(schema_pb) return data_source_proto From da246561ea7d222f3eabb22d131acdf0b5efc979 Mon Sep 17 00:00:00 2001 From: Tornike Gurgenidze Date: Sat, 7 Sep 2024 00:07:42 +0400 Subject: [PATCH 059/185] chore: Scope fixtures to session in test_universal_registry (#4497) chore: refactor fixtures in test_universal_registry Signed-off-by: tokoko --- sdk/python/tests/integration/conftest.py | 33 +++ .../registration/test_universal_registry.py | 234 +++++++++--------- 2 files changed, 147 insertions(+), 120 deletions(-) diff --git a/sdk/python/tests/integration/conftest.py b/sdk/python/tests/integration/conftest.py index 5c34a448e2..82f80b8992 100644 --- a/sdk/python/tests/integration/conftest.py +++ b/sdk/python/tests/integration/conftest.py @@ -2,6 +2,9 @@ import pytest from testcontainers.keycloak import KeycloakContainer +from testcontainers.minio import MinioContainer +from testcontainers.mysql import MySqlContainer +from testcontainers.postgres import PostgresContainer from tests.utils.auth_permissions_util import setup_permissions_on_keycloak @@ -14,3 +17,33 @@ def start_keycloak_server(): with KeycloakContainer("quay.io/keycloak/keycloak:24.0.1") as keycloak_container: setup_permissions_on_keycloak(keycloak_container.get_client()) yield keycloak_container.get_url() + + +@pytest.fixture(scope="session") +def mysql_server(): + container = MySqlContainer("mysql:latest") + container.start() + + yield container + + container.stop() + + +@pytest.fixture(scope="session") +def postgres_server(): + container = PostgresContainer() + container.start() + + yield container + + container.stop() + + +@pytest.fixture(scope="session") +def minio_server(): + container = MinioContainer() + container.start() + + yield container + + container.stop() diff --git a/sdk/python/tests/integration/registration/test_universal_registry.py b/sdk/python/tests/integration/registration/test_universal_registry.py index 20f1f5ef0a..aee93b4705 100644 --- a/sdk/python/tests/integration/registration/test_universal_registry.py +++ b/sdk/python/tests/integration/registration/test_universal_registry.py @@ -13,6 +13,8 @@ # limitations under the License. import logging import os +import random +import string import time from datetime import timedelta, timezone from tempfile import mkstemp @@ -22,10 +24,8 @@ import pandas as pd import pytest from pytest_lazyfixture import lazy_fixture -from testcontainers.core.container import DockerContainer -from testcontainers.core.waiting_utils import wait_for_logs -from testcontainers.minio import MinioContainer from testcontainers.mysql import MySqlContainer +from testcontainers.postgres import PostgresContainer from feast import FeatureService, FileSource, RequestSource from feast.data_format import AvroFormat, ParquetFormat @@ -93,16 +93,14 @@ def s3_registry() -> Registry: @pytest.fixture(scope="function") -def minio_registry() -> Registry: - bucket_name = "test-bucket" +def minio_registry(minio_server): + bucket_name = "".join(random.choices(string.ascii_lowercase, k=10)) - container = MinioContainer() - container.start() - client = container.get_client() + client = minio_server.get_client() client.make_bucket(bucket_name) - container_host = container.get_container_host_ip() - exposed_port = container.get_exposed_port(container.port) + container_host = minio_server.get_container_host_ip() + exposed_port = minio_server.get_exposed_port(minio_server.port) registry_config = RegistryConfig( path=f"s3://{bucket_name}/registry.db", cache_ttl_seconds=600 @@ -110,141 +108,121 @@ def minio_registry() -> Registry: mock_environ = { "FEAST_S3_ENDPOINT_URL": f"http://{container_host}:{exposed_port}", - "AWS_ACCESS_KEY_ID": container.access_key, - "AWS_SECRET_ACCESS_KEY": container.secret_key, + "AWS_ACCESS_KEY_ID": minio_server.access_key, + "AWS_SECRET_ACCESS_KEY": minio_server.secret_key, "AWS_SESSION_TOKEN": "", } with mock.patch.dict(os.environ, mock_environ): yield Registry("project", registry_config, None) - container.stop() - - -POSTGRES_USER = "test" -POSTGRES_PASSWORD = "test" -POSTGRES_DB = "test" logger = logging.getLogger(__name__) @pytest.fixture(scope="function") -def pg_registry(): - container = ( - DockerContainer("postgres:latest") - .with_exposed_ports(5432) - .with_env("POSTGRES_USER", POSTGRES_USER) - .with_env("POSTGRES_PASSWORD", POSTGRES_PASSWORD) - .with_env("POSTGRES_DB", POSTGRES_DB) - ) - - container.start() - - registry_config = _given_registry_config_for_pg_sql(container) - - yield SqlRegistry(registry_config, "project", None) +def pg_registry(postgres_server): + db_name = "".join(random.choices(string.ascii_lowercase, k=10)) - container.stop() + _create_pg_database(postgres_server, db_name) + container_port = postgres_server.get_exposed_port(5432) + container_host = postgres_server.get_container_host_ip() -@pytest.fixture(scope="function") -def pg_registry_async(): - container = ( - DockerContainer("postgres:latest") - .with_exposed_ports(5432) - .with_env("POSTGRES_USER", POSTGRES_USER) - .with_env("POSTGRES_PASSWORD", POSTGRES_PASSWORD) - .with_env("POSTGRES_DB", POSTGRES_DB) + registry_config = SqlRegistryConfig( + registry_type="sql", + cache_ttl_seconds=2, + cache_mode="sync", + # The `path` must include `+psycopg` in order for `sqlalchemy.create_engine()` + # to understand that we are using psycopg3. + path=f"postgresql+psycopg://{postgres_server.username}:{postgres_server.password}@{container_host}:{container_port}/{db_name}", + sqlalchemy_config_kwargs={"echo": False, "pool_pre_ping": True}, + thread_pool_executor_worker_count=0, + purge_feast_metadata=False, ) - container.start() - - registry_config = _given_registry_config_for_pg_sql(container, 2, "thread", 3) - yield SqlRegistry(registry_config, "project", None) - container.stop() +@pytest.fixture(scope="function") +def pg_registry_async(postgres_server): + db_name = "".join(random.choices(string.ascii_lowercase, k=10)) + + _create_pg_database(postgres_server, db_name) -def _given_registry_config_for_pg_sql( - container, - cache_ttl_seconds=2, - cache_mode="sync", - thread_pool_executor_worker_count=0, - purge_feast_metadata=False, -): - log_string_to_wait_for = "database system is ready to accept connections" - waited = wait_for_logs( - container=container, - predicate=log_string_to_wait_for, - timeout=30, - interval=10, - ) - logger.info("Waited for %s seconds until postgres container was up", waited) - container_port = container.get_exposed_port(5432) - container_host = container.get_container_host_ip() + container_port = postgres_server.get_exposed_port(5432) + container_host = postgres_server.get_container_host_ip() - return SqlRegistryConfig( + registry_config = SqlRegistryConfig( registry_type="sql", - cache_ttl_seconds=cache_ttl_seconds, - cache_mode=cache_mode, + cache_ttl_seconds=2, + cache_mode="thread", # The `path` must include `+psycopg` in order for `sqlalchemy.create_engine()` # to understand that we are using psycopg3. - path=f"postgresql+psycopg://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{container_host}:{container_port}/{POSTGRES_DB}", + path=f"postgresql+psycopg://{postgres_server.username}:{postgres_server.password}@{container_host}:{container_port}/{db_name}", sqlalchemy_config_kwargs={"echo": False, "pool_pre_ping": True}, - thread_pool_executor_worker_count=thread_pool_executor_worker_count, - purge_feast_metadata=purge_feast_metadata, + thread_pool_executor_worker_count=3, + purge_feast_metadata=False, ) + yield SqlRegistry(registry_config, "project", None) -@pytest.fixture(scope="function") -def mysql_registry(): - container = MySqlContainer("mysql:latest") - container.start() - registry_config = _given_registry_config_for_mysql(container) +def _create_mysql_database(container: MySqlContainer, database: str): + container.exec( + f"mysql -uroot -p{container.root_password} -e 'CREATE DATABASE {database}; GRANT ALL PRIVILEGES ON {database}.* TO {container.username};'" + ) - yield SqlRegistry(registry_config, "project", None) - container.stop() +def _create_pg_database(container: PostgresContainer, database: str): + container.exec(f"psql -U {container.username} -c 'CREATE DATABASE {database}'") @pytest.fixture(scope="function") -def mysql_registry_async(): - container = MySqlContainer("mysql:latest") - container.start() +def mysql_registry(mysql_server): + db_name = "".join(random.choices(string.ascii_lowercase, k=10)) + + _create_mysql_database(mysql_server, db_name) - registry_config = _given_registry_config_for_mysql(container, 2, "thread", 3) + connection_url = ( + "/".join(mysql_server.get_connection_url().split("/")[:-1]) + f"/{db_name}" + ) + + registry_config = SqlRegistryConfig( + registry_type="sql", + path=connection_url, + cache_ttl_seconds=2, + cache_mode="sync", + sqlalchemy_config_kwargs={"echo": False, "pool_pre_ping": True}, + thread_pool_executor_worker_count=0, + purge_feast_metadata=False, + ) yield SqlRegistry(registry_config, "project", None) - container.stop() +@pytest.fixture(scope="function") +def mysql_registry_async(mysql_server): + db_name = "".join(random.choices(string.ascii_lowercase, k=10)) -def _given_registry_config_for_mysql( - container, - cache_ttl_seconds=2, - cache_mode="sync", - thread_pool_executor_worker_count=0, - purge_feast_metadata=False, -): - import sqlalchemy + _create_mysql_database(mysql_server, db_name) - engine = sqlalchemy.create_engine( - container.get_connection_url(), pool_pre_ping=True + connection_url = ( + "/".join(mysql_server.get_connection_url().split("/")[:-1]) + f"/{db_name}" ) - engine.connect() - return SqlRegistryConfig( + registry_config = SqlRegistryConfig( registry_type="sql", - path=container.get_connection_url(), - cache_ttl_seconds=cache_ttl_seconds, - cache_mode=cache_mode, + path=connection_url, + cache_ttl_seconds=2, + cache_mode="thread", sqlalchemy_config_kwargs={"echo": False, "pool_pre_ping": True}, - thread_pool_executor_worker_count=thread_pool_executor_worker_count, - purge_feast_metadata=purge_feast_metadata, + thread_pool_executor_worker_count=3, + purge_feast_metadata=False, ) + yield SqlRegistry(registry_config, "project", None) + @pytest.fixture(scope="session") def sqlite_registry(): @@ -339,11 +317,11 @@ def mock_remote_registry(): async_sql_fixtures = [ pytest.param( lazy_fixture("pg_registry_async"), - marks=pytest.mark.xdist_group(name="pg_registry_async"), + marks=pytest.mark.xdist_group(name="pg_registry"), ), pytest.param( lazy_fixture("mysql_registry_async"), - marks=pytest.mark.xdist_group(name="mysql_registry_async"), + marks=pytest.mark.xdist_group(name="mysql_registry"), ), ] @@ -1609,45 +1587,61 @@ def local_registry_purge_feast_metadata() -> Registry: @pytest.fixture(scope="function") -def pg_registry_purge_feast_metadata(): - container = ( - DockerContainer("postgres:latest") - .with_exposed_ports(5432) - .with_env("POSTGRES_USER", POSTGRES_USER) - .with_env("POSTGRES_PASSWORD", POSTGRES_PASSWORD) - .with_env("POSTGRES_DB", POSTGRES_DB) - ) +def pg_registry_purge_feast_metadata(postgres_server): + db_name = "".join(random.choices(string.ascii_lowercase, k=10)) - container.start() + _create_pg_database(postgres_server, db_name) - registry_config = _given_registry_config_for_pg_sql(container, 2, "thread", 3, True) + container_port = postgres_server.get_exposed_port(5432) + container_host = postgres_server.get_container_host_ip() - yield SqlRegistry(registry_config, "project", None) + registry_config = SqlRegistryConfig( + registry_type="sql", + cache_ttl_seconds=2, + cache_mode="thread", + # The `path` must include `+psycopg` in order for `sqlalchemy.create_engine()` + # to understand that we are using psycopg3. + path=f"postgresql+psycopg://{postgres_server.username}:{postgres_server.password}@{container_host}:{container_port}/{db_name}", + sqlalchemy_config_kwargs={"echo": False, "pool_pre_ping": True}, + thread_pool_executor_worker_count=3, + purge_feast_metadata=True, + ) - container.stop() + yield SqlRegistry(registry_config, "project", None) @pytest.fixture(scope="function") -def mysql_registry_purge_feast_metadata(): - container = MySqlContainer("mysql:latest") - container.start() +def mysql_registry_purge_feast_metadata(mysql_server): + db_name = "".join(random.choices(string.ascii_lowercase, k=10)) - registry_config = _given_registry_config_for_mysql(container, 2, "thread", 3, True) + _create_mysql_database(mysql_server, db_name) - yield SqlRegistry(registry_config, "project", None) + connection_url = ( + "/".join(mysql_server.get_connection_url().split("/")[:-1]) + f"/{db_name}" + ) - container.stop() + registry_config = SqlRegistryConfig( + registry_type="sql", + path=connection_url, + cache_ttl_seconds=2, + cache_mode="thread", + sqlalchemy_config_kwargs={"echo": False, "pool_pre_ping": True}, + thread_pool_executor_worker_count=3, + purge_feast_metadata=True, + ) + + yield SqlRegistry(registry_config, "project", None) purge_feast_metadata_fixtures = [ lazy_fixture("local_registry_purge_feast_metadata"), pytest.param( lazy_fixture("pg_registry_purge_feast_metadata"), - marks=pytest.mark.xdist_group(name="pg_registry_purge_feast_metadata"), + marks=pytest.mark.xdist_group(name="pg_registry"), ), pytest.param( lazy_fixture("mysql_registry_purge_feast_metadata"), - marks=pytest.mark.xdist_group(name="mysql_registry_purge_feast_metadata"), + marks=pytest.mark.xdist_group(name="mysql_registry"), ), ] From ac381b292cfa29804ee5f0822f876d227a0989d9 Mon Sep 17 00:00:00 2001 From: Tornike Gurgenidze Date: Sat, 7 Sep 2024 00:32:19 +0400 Subject: [PATCH 060/185] feat: Add registry methods for dealing with all FV types (#4435) * add new registry method for working with any fv type Signed-off-by: tokoko * fix: different project for each test in test_universal_registry Signed-off-by: tokoko * revert project names to project in test_universal_registry Signed-off-by: tokoko * remove print statements from test_universal_registry Signed-off-by: tokoko --------- Signed-off-by: tokoko --- protos/feast/registry/RegistryServer.proto | 35 ++++++++- sdk/python/feast/cli_utils.py | 5 +- sdk/python/feast/feature_store.py | 62 +++++----------- .../feast/infra/registry/base_registry.py | 38 ++++++++++ .../feast/infra/registry/caching_registry.py | 34 +++++++++ .../infra/registry/proto_registry_utils.py | 39 ++++++++++ sdk/python/feast/infra/registry/registry.py | 21 +++++- sdk/python/feast/infra/registry/remote.py | 49 +++++++++++++ sdk/python/feast/infra/registry/snowflake.py | 72 ++++++++++++++++++- sdk/python/feast/infra/registry/sql.py | 57 ++++++++++++++- sdk/python/feast/registry_server.py | 64 +++++++++++++++++ sdk/python/feast/utils.py | 5 -- .../online_store/test_universal_online.py | 5 +- .../registration/test_universal_registry.py | 22 +++++- 14 files changed, 446 insertions(+), 62 deletions(-) diff --git a/protos/feast/registry/RegistryServer.proto b/protos/feast/registry/RegistryServer.proto index 3ad64b5b34..6685bc0baa 100644 --- a/protos/feast/registry/RegistryServer.proto +++ b/protos/feast/registry/RegistryServer.proto @@ -32,9 +32,13 @@ service RegistryServer{ // FeatureView RPCs rpc ApplyFeatureView (ApplyFeatureViewRequest) returns (google.protobuf.Empty) {} + rpc DeleteFeatureView (DeleteFeatureViewRequest) returns (google.protobuf.Empty) {} + rpc GetAnyFeatureView (GetAnyFeatureViewRequest) returns (GetAnyFeatureViewResponse) {} + rpc ListAllFeatureViews (ListAllFeatureViewsRequest) returns (ListAllFeatureViewsResponse) {} + + // plain FeatureView RPCs rpc GetFeatureView (GetFeatureViewRequest) returns (feast.core.FeatureView) {} rpc ListFeatureViews (ListFeatureViewsRequest) returns (ListFeatureViewsResponse) {} - rpc DeleteFeatureView (DeleteFeatureViewRequest) returns (google.protobuf.Empty) {} // StreamFeatureView RPCs rpc GetStreamFeatureView (GetStreamFeatureViewRequest) returns (feast.core.StreamFeatureView) {} @@ -208,6 +212,35 @@ message DeleteFeatureViewRequest { bool commit = 3; } +message AnyFeatureView { + oneof any_feature_view { + feast.core.FeatureView feature_view = 1; + feast.core.OnDemandFeatureView on_demand_feature_view = 2; + feast.core.StreamFeatureView stream_feature_view = 3; + } +} + +message GetAnyFeatureViewRequest { + string name = 1; + string project = 2; + bool allow_cache = 3; +} + +message GetAnyFeatureViewResponse { + AnyFeatureView any_feature_view = 1; +} + +message ListAllFeatureViewsRequest { + string project = 1; + bool allow_cache = 2; + map tags = 3; +} + +message ListAllFeatureViewsResponse { + repeated AnyFeatureView feature_views = 1; +} + + // StreamFeatureView message GetStreamFeatureViewRequest { diff --git a/sdk/python/feast/cli_utils.py b/sdk/python/feast/cli_utils.py index 264a633c31..4152eb219b 100644 --- a/sdk/python/feast/cli_utils.py +++ b/sdk/python/feast/cli_utils.py @@ -175,7 +175,7 @@ def handle_fv_verbose_permissions_command( tags=tags_filter # type: ignore[assignment] ) for fv in feature_views: - if p.match_resource(fv): + if p.match_resource(fv): # type: ignore[arg-type] feature_views_names.add(fv.name) if len(feature_views_names) > 0: Node( @@ -207,8 +207,7 @@ def handle_not_verbose_permissions_command( def fetch_all_feast_objects(store: FeatureStore) -> list[FeastObject]: objects: list[FeastObject] = [] objects.extend(store.list_entities()) - objects.extend(store.list_all_feature_views()) - objects.extend(store.list_batch_feature_views()) + objects.extend(store.list_all_feature_views()) # type: ignore[arg-type] objects.extend(store.list_feature_services()) objects.extend(store.list_data_sources()) objects.extend(store.list_validation_references()) diff --git a/sdk/python/feast/feature_store.py b/sdk/python/feast/feature_store.py index 27b6eade5b..4f96cfb0fc 100644 --- a/sdk/python/feast/feature_store.py +++ b/sdk/python/feast/feature_store.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. import itertools -import logging import os import warnings from datetime import datetime, timedelta @@ -247,9 +246,26 @@ def list_feature_services( """ return self._registry.list_feature_services(self.project, tags=tags) + def _list_all_feature_views( + self, allow_cache: bool = False, tags: Optional[dict[str, str]] = None + ) -> List[BaseFeatureView]: + feature_views = [] + for fv in self.registry.list_all_feature_views( + self.project, allow_cache=allow_cache, tags=tags + ): + if ( + isinstance(fv, FeatureView) + and fv.entities + and fv.entities[0] == DUMMY_ENTITY_NAME + ): + fv.entities = [] + fv.entity_columns = [] + feature_views.append(fv) + return feature_views + def list_all_feature_views( self, allow_cache: bool = False, tags: Optional[dict[str, str]] = None - ) -> List[Union[FeatureView, StreamFeatureView, OnDemandFeatureView]]: + ) -> List[BaseFeatureView]: """ Retrieves the list of feature views from the registry. @@ -274,10 +290,6 @@ def list_feature_views( Returns: A list of feature views. """ - logging.warning( - "list_feature_views will make breaking changes. Please use list_batch_feature_views instead. " - "list_feature_views will behave like list_all_feature_views in the future." - ) return utils._list_feature_views( self._registry, self.project, allow_cache, tags=tags ) @@ -297,44 +309,6 @@ def list_batch_feature_views( """ return self._list_batch_feature_views(allow_cache=allow_cache, tags=tags) - def _list_all_feature_views( - self, - allow_cache: bool = False, - tags: Optional[dict[str, str]] = None, - ) -> List[Union[FeatureView, StreamFeatureView, OnDemandFeatureView]]: - all_feature_views = ( - utils._list_feature_views( - self._registry, self.project, allow_cache, tags=tags - ) - + self._list_stream_feature_views(allow_cache, tags=tags) - + self.list_on_demand_feature_views(allow_cache, tags=tags) - ) - return all_feature_views - - def _list_feature_views( - self, - allow_cache: bool = False, - hide_dummy_entity: bool = True, - tags: Optional[dict[str, str]] = None, - ) -> List[FeatureView]: - logging.warning( - "_list_feature_views will make breaking changes. Please use _list_batch_feature_views instead. " - "_list_feature_views will behave like _list_all_feature_views in the future." - ) - feature_views = [] - for fv in self._registry.list_feature_views( - self.project, allow_cache=allow_cache, tags=tags - ): - if ( - hide_dummy_entity - and fv.entities - and fv.entities[0] == DUMMY_ENTITY_NAME - ): - fv.entities = [] - fv.entity_columns = [] - feature_views.append(fv) - return feature_views - def _list_batch_feature_views( self, allow_cache: bool = False, diff --git a/sdk/python/feast/infra/registry/base_registry.py b/sdk/python/feast/infra/registry/base_registry.py index f5040d9752..f2374edf1b 100644 --- a/sdk/python/feast/infra/registry/base_registry.py +++ b/sdk/python/feast/infra/registry/base_registry.py @@ -391,6 +391,44 @@ def list_feature_views( """ raise NotImplementedError + @abstractmethod + def get_any_feature_view( + self, name: str, project: str, allow_cache: bool = False + ) -> BaseFeatureView: + """ + Retrieves a feature view of any type. + + Args: + name: Name of feature view + project: Feast project that this feature view belongs to + allow_cache: Allow returning feature view from the cached registry + + Returns: + Returns either the specified feature view, or raises an exception if + none is found + """ + raise NotImplementedError + + @abstractmethod + def list_all_feature_views( + self, + project: str, + allow_cache: bool = False, + tags: Optional[dict[str, str]] = None, + ) -> List[BaseFeatureView]: + """ + Retrieve a list of feature views of all types from the registry + + Args: + allow_cache: Allow returning feature views from the cached registry + project: Filter feature views based on project name + tags: Filter by tags + + Returns: + List of feature views + """ + raise NotImplementedError + @abstractmethod def apply_materialization( self, diff --git a/sdk/python/feast/infra/registry/caching_registry.py b/sdk/python/feast/infra/registry/caching_registry.py index c04a62552b..8f47fab077 100644 --- a/sdk/python/feast/infra/registry/caching_registry.py +++ b/sdk/python/feast/infra/registry/caching_registry.py @@ -7,6 +7,7 @@ from threading import Lock from typing import List, Optional +from feast.base_feature_view import BaseFeatureView from feast.data_source import DataSource from feast.entity import Entity from feast.feature_service import FeatureService @@ -102,6 +103,39 @@ def list_entities( ) return self._list_entities(project, tags) + @abstractmethod + def _get_any_feature_view(self, name: str, project: str) -> BaseFeatureView: + pass + + def get_any_feature_view( + self, name: str, project: str, allow_cache: bool = False + ) -> BaseFeatureView: + if allow_cache: + self._refresh_cached_registry_if_necessary() + return proto_registry_utils.get_any_feature_view( + self.cached_registry_proto, name, project + ) + return self._get_any_feature_view(name, project) + + @abstractmethod + def _list_all_feature_views( + self, project: str, tags: Optional[dict[str, str]] + ) -> List[BaseFeatureView]: + pass + + def list_all_feature_views( + self, + project: str, + allow_cache: bool = False, + tags: Optional[dict[str, str]] = None, + ) -> List[BaseFeatureView]: + if allow_cache: + self._refresh_cached_registry_if_necessary() + return proto_registry_utils.list_all_feature_views( + self.cached_registry_proto, project, tags + ) + return self._list_all_feature_views(project, tags) + @abstractmethod def _get_feature_view(self, name: str, project: str) -> FeatureView: pass diff --git a/sdk/python/feast/infra/registry/proto_registry_utils.py b/sdk/python/feast/infra/registry/proto_registry_utils.py index b0413fd77e..fc5c3f6671 100644 --- a/sdk/python/feast/infra/registry/proto_registry_utils.py +++ b/sdk/python/feast/infra/registry/proto_registry_utils.py @@ -2,6 +2,7 @@ from typing import List, Optional from feast import utils +from feast.base_feature_view import BaseFeatureView from feast.data_source import DataSource from feast.entity import Entity from feast.errors import ( @@ -93,6 +94,33 @@ def get_feature_service( raise FeatureServiceNotFoundException(name, project=project) +def get_any_feature_view( + registry_proto: RegistryProto, name: str, project: str +) -> BaseFeatureView: + for feature_view_proto in registry_proto.feature_views: + if ( + feature_view_proto.spec.name == name + and feature_view_proto.spec.project == project + ): + return FeatureView.from_proto(feature_view_proto) + + for feature_view_proto in registry_proto.stream_feature_views: + if ( + feature_view_proto.spec.name == name + and feature_view_proto.spec.project == project + ): + return StreamFeatureView.from_proto(feature_view_proto) + + for on_demand_feature_view in registry_proto.on_demand_feature_views: + if ( + on_demand_feature_view.spec.project == project + and on_demand_feature_view.spec.name == name + ): + return OnDemandFeatureView.from_proto(on_demand_feature_view) + + raise FeatureViewNotFoundException(name, project) + + def get_feature_view( registry_proto: RegistryProto, name: str, project: str ) -> FeatureView: @@ -179,6 +207,17 @@ def list_feature_services( return feature_services +@registry_proto_cache_with_tags +def list_all_feature_views( + registry_proto: RegistryProto, project: str, tags: Optional[dict[str, str]] +) -> List[BaseFeatureView]: + return ( + list_feature_views(registry_proto, project, tags) + + list_stream_feature_views(registry_proto, project, tags) + + list_on_demand_feature_views(registry_proto, project, tags) + ) + + @registry_proto_cache_with_tags def list_feature_views( registry_proto: RegistryProto, project: str, tags: Optional[dict[str, str]] diff --git a/sdk/python/feast/infra/registry/registry.py b/sdk/python/feast/infra/registry/registry.py index 634d6fa7ac..bf5dfbe24f 100644 --- a/sdk/python/feast/infra/registry/registry.py +++ b/sdk/python/feast/infra/registry/registry.py @@ -585,7 +585,26 @@ def apply_materialization( self.commit() return - raise FeatureViewNotFoundException(feature_view.name, project) + def list_all_feature_views( + self, + project: str, + allow_cache: bool = False, + tags: Optional[dict[str, str]] = None, + ) -> List[BaseFeatureView]: + registry_proto = self._get_registry_proto( + project=project, allow_cache=allow_cache + ) + return proto_registry_utils.list_all_feature_views( + registry_proto, project, tags + ) + + def get_any_feature_view( + self, name: str, project: str, allow_cache: bool = False + ) -> BaseFeatureView: + registry_proto = self._get_registry_proto( + project=project, allow_cache=allow_cache + ) + return proto_registry_utils.get_any_feature_view(registry_proto, name, project) def list_feature_views( self, diff --git a/sdk/python/feast/infra/registry/remote.py b/sdk/python/feast/infra/registry/remote.py index ba25ef7dbe..cdb45f0363 100644 --- a/sdk/python/feast/infra/registry/remote.py +++ b/sdk/python/feast/infra/registry/remote.py @@ -30,6 +30,24 @@ from feast.stream_feature_view import StreamFeatureView +def extract_base_feature_view( + any_feature_view: RegistryServer_pb2.AnyFeatureView, +) -> BaseFeatureView: + feature_view_type = any_feature_view.WhichOneof("any_feature_view") + if feature_view_type == "feature_view": + feature_view = FeatureView.from_proto(any_feature_view.feature_view) + elif feature_view_type == "on_demand_feature_view": + feature_view = OnDemandFeatureView.from_proto( + any_feature_view.on_demand_feature_view + ) + elif feature_view_type == "stream_feature_view": + feature_view = StreamFeatureView.from_proto( + any_feature_view.stream_feature_view + ) + + return feature_view + + class RemoteRegistryConfig(RegistryConfig): registry_type: StrictStr = "remote" """ str: Provider name or a class name that implements Registry.""" @@ -249,6 +267,37 @@ def list_on_demand_feature_views( for on_demand_feature_view in response.on_demand_feature_views ] + def get_any_feature_view( + self, name: str, project: str, allow_cache: bool = False + ) -> BaseFeatureView: + request = RegistryServer_pb2.GetAnyFeatureViewRequest( + name=name, project=project, allow_cache=allow_cache + ) + + response: RegistryServer_pb2.GetAnyFeatureViewResponse = ( + self.stub.GetAnyFeatureView(request) + ) + any_feature_view = response.any_feature_view + return extract_base_feature_view(any_feature_view) + + def list_all_feature_views( + self, + project: str, + allow_cache: bool = False, + tags: Optional[dict[str, str]] = None, + ) -> List[BaseFeatureView]: + request = RegistryServer_pb2.ListAllFeatureViewsRequest( + project=project, allow_cache=allow_cache, tags=tags + ) + + response: RegistryServer_pb2.ListAllFeatureViewsResponse = ( + self.stub.ListAllFeatureViews(request) + ) + return [ + extract_base_feature_view(any_feature_view) + for any_feature_view in response.feature_views + ] + def get_feature_view( self, name: str, project: str, allow_cache: bool = False ) -> FeatureView: diff --git a/sdk/python/feast/infra/registry/snowflake.py b/sdk/python/feast/infra/registry/snowflake.py index accfa42e12..f9dd37e516 100644 --- a/sdk/python/feast/infra/registry/snowflake.py +++ b/sdk/python/feast/infra/registry/snowflake.py @@ -5,7 +5,7 @@ from datetime import datetime, timedelta, timezone from enum import Enum from threading import Lock -from typing import Any, Callable, List, Literal, Optional, Union +from typing import Any, Callable, List, Literal, Optional, Union, cast from pydantic import ConfigDict, Field, StrictStr @@ -575,6 +575,76 @@ def get_feature_view( FeatureViewNotFoundException, ) + def get_any_feature_view( + self, name: str, project: str, allow_cache: bool = False + ) -> BaseFeatureView: + if allow_cache: + self._refresh_cached_registry_if_necessary() + return proto_registry_utils.get_any_feature_view( + self.cached_registry_proto, name, project + ) + fv = self._get_object( + "FEATURE_VIEWS", + name, + project, + FeatureViewProto, + FeatureView, + "FEATURE_VIEW_NAME", + "FEATURE_VIEW_PROTO", + None, + ) + + if not fv: + fv = self._get_object( + "STREAM_FEATURE_VIEWS", + name, + project, + StreamFeatureViewProto, + StreamFeatureView, + "STREAM_FEATURE_VIEW_NAME", + "STREAM_FEATURE_VIEW_PROTO", + None, + ) + if not fv: + fv = self._get_object( + "ON_DEMAND_FEATURE_VIEWS", + name, + project, + OnDemandFeatureViewProto, + OnDemandFeatureView, + "ON_DEMAND_FEATURE_VIEW_NAME", + "ON_DEMAND_FEATURE_VIEW_PROTO", + FeatureViewNotFoundException, + ) + return fv + + def list_all_feature_views( + self, + project: str, + allow_cache: bool = False, + tags: Optional[dict[str, str]] = None, + ) -> List[BaseFeatureView]: + if allow_cache: + self._refresh_cached_registry_if_necessary() + return proto_registry_utils.list_all_feature_views( + self.cached_registry_proto, project, tags + ) + + return ( + cast( + list[BaseFeatureView], + self.list_feature_views(project, allow_cache, tags), + ) + + cast( + list[BaseFeatureView], + self.list_stream_feature_views(project, allow_cache, tags), + ) + + cast( + list[BaseFeatureView], + self.list_on_demand_feature_views(project, allow_cache, tags), + ) + ) + def get_infra(self, project: str, allow_cache: bool = False) -> Infra: infra_object = self._get_object( "MANAGED_INFRA", diff --git a/sdk/python/feast/infra/registry/sql.py b/sdk/python/feast/infra/registry/sql.py index 2b4a58266c..9ce3fbe5dd 100644 --- a/sdk/python/feast/infra/registry/sql.py +++ b/sdk/python/feast/infra/registry/sql.py @@ -4,7 +4,7 @@ from datetime import datetime, timezone from enum import Enum from pathlib import Path -from typing import Any, Callable, Dict, List, Optional, Union +from typing import Any, Callable, Dict, List, Optional, Union, cast from pydantic import StrictInt, StrictStr from sqlalchemy import ( # type: ignore @@ -334,6 +334,61 @@ def _get_entity(self, name: str, project: str) -> Entity: not_found_exception=EntityNotFoundException, ) + def _get_any_feature_view(self, name: str, project: str) -> BaseFeatureView: + fv = self._get_object( + table=feature_views, + name=name, + project=project, + proto_class=FeatureViewProto, + python_class=FeatureView, + id_field_name="feature_view_name", + proto_field_name="feature_view_proto", + not_found_exception=None, + ) + + if not fv: + fv = self._get_object( + table=on_demand_feature_views, + name=name, + project=project, + proto_class=OnDemandFeatureViewProto, + python_class=OnDemandFeatureView, + id_field_name="feature_view_name", + proto_field_name="feature_view_proto", + not_found_exception=None, + ) + + if not fv: + fv = self._get_object( + table=stream_feature_views, + name=name, + project=project, + proto_class=StreamFeatureViewProto, + python_class=StreamFeatureView, + id_field_name="feature_view_name", + proto_field_name="feature_view_proto", + not_found_exception=FeatureViewNotFoundException, + ) + return fv + + def _list_all_feature_views( + self, project: str, tags: Optional[dict[str, str]] + ) -> List[BaseFeatureView]: + return ( + cast( + list[BaseFeatureView], + self._list_feature_views(project=project, tags=tags), + ) + + cast( + list[BaseFeatureView], + self._list_stream_feature_views(project=project, tags=tags), + ) + + cast( + list[BaseFeatureView], + self._list_on_demand_feature_views(project=project, tags=tags), + ) + ) + def _get_feature_view(self, name: str, project: str) -> FeatureView: return self._get_object( table=feature_views, diff --git a/sdk/python/feast/registry_server.py b/sdk/python/feast/registry_server.py index 2661f25882..c2f4a688d3 100644 --- a/sdk/python/feast/registry_server.py +++ b/sdk/python/feast/registry_server.py @@ -8,6 +8,7 @@ from grpc_reflection.v1alpha import reflection from feast import FeatureService, FeatureStore +from feast.base_feature_view import BaseFeatureView from feast.data_source import DataSource from feast.entity import Entity from feast.errors import FeatureViewNotFoundException @@ -38,6 +39,28 @@ from feast.stream_feature_view import StreamFeatureView +def _build_any_feature_view_proto(feature_view: BaseFeatureView): + if isinstance(feature_view, StreamFeatureView): + arg_name = "stream_feature_view" + feature_view_proto = feature_view.to_proto() + elif isinstance(feature_view, FeatureView): + arg_name = "feature_view" + feature_view_proto = feature_view.to_proto() + elif isinstance(feature_view, OnDemandFeatureView): + arg_name = "on_demand_feature_view" + feature_view_proto = feature_view.to_proto() + + return RegistryServer_pb2.AnyFeatureView( + feature_view=feature_view_proto if arg_name == "feature_view" else None, + stream_feature_view=feature_view_proto + if arg_name == "stream_feature_view" + else None, + on_demand_feature_view=feature_view_proto + if arg_name == "on_demand_feature_view" + else None, + ) + + class RegistryServer(RegistryServer_pb2_grpc.RegistryServerServicer): def __init__(self, registry: BaseRegistry) -> None: super().__init__() @@ -178,6 +201,27 @@ def GetFeatureView( actions=[AuthzedAction.DESCRIBE], ).to_proto() + def GetAnyFeatureView( + self, request: RegistryServer_pb2.GetAnyFeatureViewRequest, context + ): + feature_view = assert_permissions( + cast( + FeastObject, + self.proxied_registry.get_any_feature_view( + name=request.name, + project=request.project, + allow_cache=request.allow_cache, + ), + ), + actions=[AuthzedAction.DESCRIBE], + ) + + return RegistryServer_pb2.GetAnyFeatureViewResponse( + any_feature_view=_build_any_feature_view_proto( + cast(BaseFeatureView, feature_view) + ) + ) + def ApplyFeatureView( self, request: RegistryServer_pb2.ApplyFeatureViewRequest, context ): @@ -228,6 +272,26 @@ def ListFeatureViews( ] ) + def ListAllFeatureViews( + self, request: RegistryServer_pb2.ListAllFeatureViewsRequest, context + ): + return RegistryServer_pb2.ListAllFeatureViewsResponse( + feature_views=[ + _build_any_feature_view_proto(cast(BaseFeatureView, feature_view)) + for feature_view in permitted_resources( + resources=cast( + list[FeastObject], + self.proxied_registry.list_all_feature_views( + project=request.project, + allow_cache=request.allow_cache, + tags=dict(request.tags), + ), + ), + actions=AuthzedAction.DESCRIBE, + ) + ] + ) + def DeleteFeatureView( self, request: RegistryServer_pb2.DeleteFeatureViewRequest, context ): diff --git a/sdk/python/feast/utils.py b/sdk/python/feast/utils.py index 5862cd4630..992869557a 100644 --- a/sdk/python/feast/utils.py +++ b/sdk/python/feast/utils.py @@ -1,6 +1,5 @@ import copy import itertools -import logging import os import typing import warnings @@ -746,10 +745,6 @@ def _list_feature_views( ) -> List["FeatureView"]: from feast.feature_view import DUMMY_ENTITY_NAME - logging.warning( - "_list_feature_views will make breaking changes. Please use _list_batch_feature_views instead. " - "_list_feature_views will behave like _list_all_feature_views in the future." - ) feature_views = [] for fv in registry.list_feature_views(project, allow_cache=allow_cache, tags=tags): if hide_dummy_entity and fv.entities and fv.entities[0] == DUMMY_ENTITY_NAME: diff --git a/sdk/python/tests/integration/online_store/test_universal_online.py b/sdk/python/tests/integration/online_store/test_universal_online.py index 2ffe869ef5..308201590d 100644 --- a/sdk/python/tests/integration/online_store/test_universal_online.py +++ b/sdk/python/tests/integration/online_store/test_universal_online.py @@ -163,7 +163,6 @@ def test_write_to_online_store_event_check(environment): fs.apply([fv1, e]) assert len(fs.list_all_feature_views(tags=TAGS)) == 1 assert len(fs.list_feature_views(tags=TAGS)) == 1 - assert len(fs.list_batch_feature_views(tags=TAGS)) == 1 # data to ingest into Online Store (recent) data = { @@ -421,7 +420,7 @@ def setup_feature_store_universal_feature_views( feature_views = construct_universal_feature_views(data_sources) fs.apply([driver(), feature_views.driver, feature_views.global_fv]) - assert len(fs.list_batch_feature_views(TAGS)) == 2 + assert len(fs.list_all_feature_views(TAGS)) == 2 data = { "driver_id": [1, 2], @@ -518,7 +517,7 @@ def test_online_list_retrieval(environment, universal_data_sources): environment, universal_data_sources ) - assert len(fs.list_batch_feature_views(tags=TAGS)) == 2 + assert len(fs.list_all_feature_views(tags=TAGS)) == 2 @pytest.mark.integration diff --git a/sdk/python/tests/integration/registration/test_universal_registry.py b/sdk/python/tests/integration/registration/test_universal_registry.py index aee93b4705..6e4a208d4b 100644 --- a/sdk/python/tests/integration/registration/test_universal_registry.py +++ b/sdk/python/tests/integration/registration/test_universal_registry.py @@ -36,6 +36,7 @@ from feast.field import Field from feast.infra.infra_object import Infra from feast.infra.online_stores.sqlite import SqliteTable +from feast.infra.registry.base_registry import BaseRegistry from feast.infra.registry.registry import Registry from feast.infra.registry.remote import RemoteRegistry, RemoteRegistryConfig from feast.infra.registry.sql import SqlRegistry, SqlRegistryConfig @@ -415,7 +416,7 @@ def assert_project(project_name, test_registry, allow_cache=False): "test_registry", all_fixtures, ) -def test_apply_feature_view_success(test_registry): +def test_apply_feature_view_success(test_registry: BaseRegistry): # Create Feature Views batch_source = FileSource( file_format=ParquetFormat(), @@ -464,6 +465,8 @@ def test_apply_feature_view_success(test_registry): ) feature_view = test_registry.get_feature_view("my_feature_view_1", project) + any_feature_view = test_registry.get_any_feature_view("my_feature_view_1", project) + assert ( feature_view.name == "my_feature_view_1" and feature_view.features[0].name == "fs1_my_feature_1" @@ -475,6 +478,7 @@ def test_apply_feature_view_success(test_registry): and feature_view.features[3].name == "fs1_my_feature_4" and feature_view.features[3].dtype == Array(Bytes) and feature_view.entities[0] == "fs1_my_entity_1" + and feature_view == any_feature_view ) assert feature_view.ttl == timedelta(minutes=5) @@ -502,7 +506,7 @@ def test_apply_feature_view_success(test_registry): "test_registry", sql_fixtures, ) -def test_apply_on_demand_feature_view_success(test_registry): +def test_apply_on_demand_feature_view_success(test_registry: BaseRegistry): # Create Feature Views driver_stats = FileSource( name="driver_stats_source", @@ -545,6 +549,7 @@ def location_features_from_push(inputs: pd.DataFrame) -> pd.DataFrame: test_registry.get_user_metadata(project, location_features_from_push) # Register Feature View + test_registry.apply_feature_view(driver_daily_features_view, project) test_registry.apply_feature_view(location_features_from_push, project) assert not test_registry.get_user_metadata(project, location_features_from_push) @@ -563,13 +568,21 @@ def location_features_from_push(inputs: pd.DataFrame) -> pd.DataFrame: and feature_views[0].features[0].dtype == String ) + all_feature_views = test_registry.list_all_feature_views(project) + + assert len(all_feature_views) == 2 + feature_view = test_registry.get_on_demand_feature_view( "location_features_from_push", project ) + any_feature_view = test_registry.get_any_feature_view( + "location_features_from_push", project + ) assert ( feature_view.name == "location_features_from_push" and feature_view.features[0].name == "first_char" and feature_view.features[0].dtype == String + and feature_view == any_feature_view ) test_registry.delete_feature_view("location_features_from_push", project) @@ -1110,7 +1123,7 @@ def test_registry_cache_thread_async(test_registry): "test_registry", all_fixtures, ) -def test_apply_stream_feature_view_success(test_registry): +def test_apply_stream_feature_view_success(test_registry: BaseRegistry): # Create Feature Views def simple_udf(x: int): return x + 3 @@ -1163,8 +1176,11 @@ def simple_udf(x: int): project, tags=sfv.tags ) + all_feature_views = test_registry.list_all_feature_views(project, tags=sfv.tags) + # List Feature Views assert len(stream_feature_views) == 1 + assert len(all_feature_views) == 1 assert stream_feature_views[0] == sfv test_registry.delete_feature_view("test kafka stream feature view", project) From 87e7ca4f55bd23fe5bf38b010c6b5a43d1780fce Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 7 Sep 2024 16:31:14 +0400 Subject: [PATCH 061/185] chore: Bump actions/download-artifact from 2 to 4.1.7 in /.github/workflows (#4482) chore: Bump actions/download-artifact in /.github/workflows Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 2 to 4.1.7. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v2...v4.1.7) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build_wheels.yml | 2 +- .github/workflows/publish.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index df8534d078..d8e5e484bb 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -159,7 +159,7 @@ jobs: with: python-version: ${{ matrix.python-version }} architecture: x64 - - uses: actions/download-artifact@v2 + - uses: actions/download-artifact@v4.1.7 with: name: wheels path: dist diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e56296ec4b..0342943313 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -134,7 +134,7 @@ jobs: runs-on: ubuntu-latest needs: [build_wheels] steps: - - uses: actions/download-artifact@v2 + - uses: actions/download-artifact@v4.1.7 with: name: wheels path: dist From d793c77d923df95a186b9d4829b167f1a5a304e6 Mon Sep 17 00:00:00 2001 From: Bhargav Dodla <13788369+EXPEbdodla@users.noreply.github.com> Date: Sat, 7 Sep 2024 12:29:43 -0700 Subject: [PATCH 062/185] feat: Added support for reading from Reader Endpoints for AWS Aurora use cases (#4494) fix: Resovled merge conflicts associated to new changes Signed-off-by: Bhargav Dodla Co-authored-by: Bhargav Dodla --- sdk/python/feast/infra/registry/sql.py | 51 +++++++++++-------- .../registration/test_universal_registry.py | 43 ++++++++++++++++ 2 files changed, 74 insertions(+), 20 deletions(-) diff --git a/sdk/python/feast/infra/registry/sql.py b/sdk/python/feast/infra/registry/sql.py index 9ce3fbe5dd..b049adc898 100644 --- a/sdk/python/feast/infra/registry/sql.py +++ b/sdk/python/feast/infra/registry/sql.py @@ -202,6 +202,10 @@ class SqlRegistryConfig(RegistryConfig): """ str: Path to metadata store. If registry_type is 'sql', then this is a database URL as expected by SQLAlchemy """ + read_path: Optional[StrictStr] = None + """ str: Read Path to metadata store if different from path. + If registry_type is 'sql', then this is a Read Endpoint for database URL. If not set, path will be used for read and write. """ + sqlalchemy_config_kwargs: Dict[str, Any] = {"echo": False} """ Dict[str, Any]: Extra arguments to pass to SQLAlchemy.create_engine. """ @@ -223,13 +227,20 @@ def __init__( registry_config, SqlRegistryConfig ), "SqlRegistry needs a valid registry_config" - self.engine: Engine = create_engine( + self.write_engine: Engine = create_engine( registry_config.path, **registry_config.sqlalchemy_config_kwargs ) + if registry_config.read_path: + self.read_engine: Engine = create_engine( + registry_config.read_path, + **registry_config.sqlalchemy_config_kwargs, + ) + else: + self.read_engine = self.write_engine + metadata.create_all(self.write_engine) self.thread_pool_executor_worker_count = ( registry_config.thread_pool_executor_worker_count ) - metadata.create_all(self.engine) self.purge_feast_metadata = registry_config.purge_feast_metadata # Sync feast_metadata to projects table # when purge_feast_metadata is set to True, Delete data from @@ -246,7 +257,7 @@ def __init__( def _sync_feast_metadata_to_projects_table(self): feast_metadata_projects: set = [] projects_set: set = [] - with self.engine.begin() as conn: + with self.write_engine.begin() as conn: stmt = select(feast_metadata).where( feast_metadata.c.metadata_key == FeastMetadataKeys.PROJECT_UUID.value ) @@ -255,7 +266,7 @@ def _sync_feast_metadata_to_projects_table(self): feast_metadata_projects.append(row._mapping["project_id"]) if len(feast_metadata_projects) > 0: - with self.engine.begin() as conn: + with self.write_engine.begin() as conn: stmt = select(projects) rows = conn.execute(stmt).all() for row in rows: @@ -267,7 +278,7 @@ def _sync_feast_metadata_to_projects_table(self): self.apply_project(Project(name=project_name), commit=True) if self.purge_feast_metadata: - with self.engine.begin() as conn: + with self.write_engine.begin() as conn: for project_name in feast_metadata_projects: stmt = delete(feast_metadata).where( feast_metadata.c.project_id == project_name @@ -285,7 +296,7 @@ def teardown(self): validation_references, permissions, }: - with self.engine.begin() as conn: + with self.write_engine.begin() as conn: stmt = delete(t) conn.execute(stmt) @@ -549,7 +560,7 @@ def apply_feature_service( ) def delete_data_source(self, name: str, project: str, commit: bool = True): - with self.engine.begin() as conn: + with self.write_engine.begin() as conn: stmt = delete(data_sources).where( data_sources.c.data_source_name == name, data_sources.c.project_id == project, @@ -607,7 +618,7 @@ def _list_on_demand_feature_views( ) def _list_project_metadata(self, project: str) -> List[ProjectMetadata]: - with self.engine.begin() as conn: + with self.read_engine.begin() as conn: stmt = select(feast_metadata).where( feast_metadata.c.project_id == project, ) @@ -726,7 +737,7 @@ def apply_user_metadata( table = self._infer_fv_table(feature_view) name = feature_view.name - with self.engine.begin() as conn: + with self.write_engine.begin() as conn: stmt = select(table).where( getattr(table.c, "feature_view_name") == name, table.c.project_id == project, @@ -781,7 +792,7 @@ def get_user_metadata( table = self._infer_fv_table(feature_view) name = feature_view.name - with self.engine.begin() as conn: + with self.read_engine.begin() as conn: stmt = select(table).where(getattr(table.c, "feature_view_name") == name) row = conn.execute(stmt).first() if row: @@ -885,7 +896,7 @@ def _apply_object( name = name or (obj.name if hasattr(obj, "name") else None) assert name, f"name needs to be provided for {obj}" - with self.engine.begin() as conn: + with self.write_engine.begin() as conn: update_datetime = _utc_now() update_time = int(update_datetime.timestamp()) stmt = select(table).where( @@ -961,7 +972,7 @@ def _apply_object( def _maybe_init_project_metadata(self, project): # Initialize project metadata if needed - with self.engine.begin() as conn: + with self.write_engine.begin() as conn: update_datetime = _utc_now() update_time = int(update_datetime.timestamp()) stmt = select(feast_metadata).where( @@ -988,7 +999,7 @@ def _delete_object( id_field_name: str, not_found_exception: Optional[Callable], ): - with self.engine.begin() as conn: + with self.write_engine.begin() as conn: stmt = delete(table).where( getattr(table.c, id_field_name) == name, table.c.project_id == project ) @@ -1014,7 +1025,7 @@ def _get_object( proto_field_name: str, not_found_exception: Optional[Callable], ): - with self.engine.begin() as conn: + with self.read_engine.begin() as conn: stmt = select(table).where( getattr(table.c, id_field_name) == name, table.c.project_id == project ) @@ -1036,7 +1047,7 @@ def _list_objects( proto_field_name: str, tags: Optional[dict[str, str]] = None, ): - with self.engine.begin() as conn: + with self.read_engine.begin() as conn: stmt = select(table).where(table.c.project_id == project) rows = conn.execute(stmt).all() if rows: @@ -1051,7 +1062,7 @@ def _list_objects( return [] def _set_last_updated_metadata(self, last_updated: datetime, project: str): - with self.engine.begin() as conn: + with self.write_engine.begin() as conn: stmt = select(feast_metadata).where( feast_metadata.c.metadata_key == FeastMetadataKeys.LAST_UPDATED_TIMESTAMP.value, @@ -1085,7 +1096,7 @@ def _set_last_updated_metadata(self, last_updated: datetime, project: str): conn.execute(insert_stmt) def _get_last_updated_metadata(self, project: str): - with self.engine.begin() as conn: + with self.read_engine.begin() as conn: stmt = select(feast_metadata).where( feast_metadata.c.metadata_key == FeastMetadataKeys.LAST_UPDATED_TIMESTAMP.value, @@ -1130,7 +1141,7 @@ def apply_permission( ) def delete_permission(self, name: str, project: str, commit: bool = True): - with self.engine.begin() as conn: + with self.write_engine.begin() as conn: stmt = delete(permissions).where( permissions.c.permission_name == name, permissions.c.project_id == project, @@ -1143,7 +1154,7 @@ def _list_projects( self, tags: Optional[dict[str, str]], ) -> List[Project]: - with self.engine.begin() as conn: + with self.read_engine.begin() as conn: stmt = select(projects) rows = conn.execute(stmt).all() if rows: @@ -1188,7 +1199,7 @@ def delete_project( ): project = self.get_project(name, allow_cache=False) if project: - with self.engine.begin() as conn: + with self.write_engine.begin() as conn: for t in { managed_infra, saved_datasets, diff --git a/sdk/python/tests/integration/registration/test_universal_registry.py b/sdk/python/tests/integration/registration/test_universal_registry.py index 6e4a208d4b..5dc2509333 100644 --- a/sdk/python/tests/integration/registration/test_universal_registry.py +++ b/sdk/python/tests/integration/registration/test_universal_registry.py @@ -118,9 +118,43 @@ def minio_registry(minio_server): yield Registry("project", registry_config, None) +POSTGRES_READONLY_USER = "read_only_user" +POSTGRES_READONLY_PASSWORD = "readonly_password" + logger = logging.getLogger(__name__) +def add_pg_read_only_user( + container_host, container_port, db_name, postgres_user, postgres_password +): + # Connect to PostgreSQL as an admin + import psycopg + + conn_string = f"dbname={db_name} user={postgres_user} password={postgres_password} host={container_host} port={container_port}" + + with psycopg.connect(conn_string) as conn: + user_exists = conn.execute( + f"SELECT 1 FROM pg_catalog.pg_user WHERE usename = '{POSTGRES_READONLY_USER}'" + ).fetchone() + if not user_exists: + conn.execute( + f"CREATE USER {POSTGRES_READONLY_USER} WITH PASSWORD '{POSTGRES_READONLY_PASSWORD}';" + ) + + conn.execute( + f"REVOKE ALL PRIVILEGES ON DATABASE {db_name} FROM {POSTGRES_READONLY_USER};" + ) + conn.execute( + f"GRANT CONNECT ON DATABASE {db_name} TO {POSTGRES_READONLY_USER};" + ) + conn.execute( + f"GRANT SELECT ON ALL TABLES IN SCHEMA public TO {POSTGRES_READONLY_USER};" + ) + conn.execute( + f"ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO {POSTGRES_READONLY_USER};" + ) + + @pytest.fixture(scope="function") def pg_registry(postgres_server): db_name = "".join(random.choices(string.ascii_lowercase, k=10)) @@ -130,6 +164,14 @@ def pg_registry(postgres_server): container_port = postgres_server.get_exposed_port(5432) container_host = postgres_server.get_container_host_ip() + add_pg_read_only_user( + container_host, + container_port, + db_name, + postgres_server.username, + postgres_server.password, + ) + registry_config = SqlRegistryConfig( registry_type="sql", cache_ttl_seconds=2, @@ -137,6 +179,7 @@ def pg_registry(postgres_server): # The `path` must include `+psycopg` in order for `sqlalchemy.create_engine()` # to understand that we are using psycopg3. path=f"postgresql+psycopg://{postgres_server.username}:{postgres_server.password}@{container_host}:{container_port}/{db_name}", + read_path=f"postgresql+psycopg://{POSTGRES_READONLY_USER}:{POSTGRES_READONLY_PASSWORD}@{container_host}:{container_port}/{db_name}", sqlalchemy_config_kwargs={"echo": False, "pool_pre_ping": True}, thread_pool_executor_worker_count=0, purge_feast_metadata=False, From 7ecc615945b7bb48e103ca6eb278b39759d71c5a Mon Sep 17 00:00:00 2001 From: Shuchu Han Date: Sun, 8 Sep 2024 04:34:38 -0400 Subject: [PATCH 063/185] fix: Fix the mypy type check issue. (#4498) Signed-off-by: Shuchu Han --- .../infra/utils/postgres/connection_utils.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/sdk/python/feast/infra/utils/postgres/connection_utils.py b/sdk/python/feast/infra/utils/postgres/connection_utils.py index 3749fc2fc1..70148f3ee0 100644 --- a/sdk/python/feast/infra/utils/postgres/connection_utils.py +++ b/sdk/python/feast/infra/utils/postgres/connection_utils.py @@ -56,14 +56,14 @@ async def _get_connection_pool_async(config: PostgreSQLConfig) -> AsyncConnectio def _get_conninfo(config: PostgreSQLConfig) -> str: """Get the `conninfo` argument required for connection objects.""" - psycopg_config = { - "user": config.user, - "password": config.password, - "host": config.host, - "port": int(config.port), - "dbname": config.database, - } - return make_conninfo(conninfo="", **psycopg_config) + return make_conninfo( + conninfo="", + user=config.user, + password=config.password, + host=config.host, + port=int(config.port), + dbname=config.database, + ) def _get_conn_kwargs(config: PostgreSQLConfig) -> Dict[str, Any]: From c94f32f2b637c7b7d917d2456432180af7569cf5 Mon Sep 17 00:00:00 2001 From: Jiwon Park Date: Mon, 9 Sep 2024 18:24:52 +0900 Subject: [PATCH 064/185] fix: Disable active_timer When registry_ttl_sec is 0 (#4499) * fix: Disable active_timer When registry_ttl_sec is 0 Signed-off-by: Jiwon Park * feat: Add delete mark Signed-off-by: Jiwon Park --------- Signed-off-by: Jiwon Park --- sdk/python/feast/feature_server.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/sdk/python/feast/feature_server.py b/sdk/python/feast/feature_server.py index 4f8de1eef5..9757e95143 100644 --- a/sdk/python/feast/feature_server.py +++ b/sdk/python/feast/feature_server.py @@ -90,9 +90,11 @@ def async_refresh(): registry_proto = store.registry.proto() if shutting_down: return - nonlocal active_timer - active_timer = threading.Timer(registry_ttl_sec, async_refresh) - active_timer.start() + + if registry_ttl_sec: + nonlocal active_timer + active_timer = threading.Timer(registry_ttl_sec, async_refresh) + active_timer.start() @asynccontextmanager async def lifespan(app: FastAPI): From 867f532154977790e3bb11f2a94baa4f2289de99 Mon Sep 17 00:00:00 2001 From: Shuchu Han Date: Mon, 9 Sep 2024 06:14:43 -0400 Subject: [PATCH 065/185] fix: Ignore the type check as both functions calls are not belonging to Feast code. (#4500) Signed-off-by: Shuchu Han --- sdk/python/feast/ui_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/python/feast/ui_server.py b/sdk/python/feast/ui_server.py index 35b51a8021..7e8591e2aa 100644 --- a/sdk/python/feast/ui_server.py +++ b/sdk/python/feast/ui_server.py @@ -51,7 +51,7 @@ def shutdown_event(): async_refresh() - ui_dir_ref = importlib_resources.files(__spec__.parent) / "ui/build/" # type: ignore[name-defined] + ui_dir_ref = importlib_resources.files(__spec__.parent) / "ui/build/" # type: ignore[name-defined, arg-type] with importlib_resources.as_file(ui_dir_ref) as ui_dir: # Initialize with the projects-list.json file with ui_dir.joinpath("projects-list.json").open(mode="w") as f: From 0c90137f628917914cb9373a2b4aa5c21c5a4457 Mon Sep 17 00:00:00 2001 From: Abdul Hameed Date: Mon, 9 Sep 2024 10:00:35 -0400 Subject: [PATCH 066/185] chore: Added rbac examples (#4450) * added rbac examples Signed-off-by: Abdul Hameed * Fixed the intra service communication Signed-off-by: Abdul Hameed * remove the rbac-local example Signed-off-by: Abdul Hameed * updated readme with suggestions from code review Co-authored-by: Francisco Arceo --------- Signed-off-by: Abdul Hameed Co-authored-by: Francisco Arceo --- examples/rbac-remote/README.md | 171 ++++++++++++++++++ examples/rbac-remote/cleanup_feast.sh | 24 +++ .../client/k8s/admin_user_resources.yaml | 56 ++++++ .../k8s/feature_repo/feature_store.yaml | 14 ++ .../client/k8s/feature_repo/test.py | 140 ++++++++++++++ .../client/k8s/readonly_user_resources.yaml | 57 ++++++ .../k8s/unauthorized_user_resources.yaml | 36 ++++ .../client/oidc/admin_user_resources.yaml | 34 ++++ .../oidc/feature_repo/feature_store.yaml | 19 ++ .../client/oidc/feature_repo/test.py | 140 ++++++++++++++ .../client/oidc/readonly_user_resources.yaml | 34 ++++ .../oidc/unauthorized_user_resources.yaml | 35 ++++ examples/rbac-remote/demo.jpg | Bin 0 -> 115961 bytes examples/rbac-remote/deployment.png | Bin 0 -> 98226 bytes examples/rbac-remote/install_feast.sh | 109 +++++++++++ .../server/feature_repo/example_repo.py | 130 +++++++++++++ .../server/feature_repo/feature_store.yaml | 26 +++ .../server/feature_repo/permissions_apply.py | 21 +++ .../server/k8s/feature_store_offline.yaml | 16 ++ .../server/k8s/feature_store_online.yaml | 20 ++ .../server/k8s/feature_store_registry.yaml | 12 ++ .../server/k8s/server_resources.yaml | 27 +++ .../server/oidc/feature_store_offline.yaml | 18 ++ .../server/oidc/feature_store_online.yaml | 22 +++ .../server/oidc/feature_store_registry.yaml | 14 ++ .../templates/deployment.yaml | 1 + infra/charts/feast-feature-server/values.yaml | 3 + .../client/auth_client_manager_factory.py | 15 +- 28 files changed, 1192 insertions(+), 2 deletions(-) create mode 100644 examples/rbac-remote/README.md create mode 100755 examples/rbac-remote/cleanup_feast.sh create mode 100644 examples/rbac-remote/client/k8s/admin_user_resources.yaml create mode 100644 examples/rbac-remote/client/k8s/feature_repo/feature_store.yaml create mode 100644 examples/rbac-remote/client/k8s/feature_repo/test.py create mode 100644 examples/rbac-remote/client/k8s/readonly_user_resources.yaml create mode 100644 examples/rbac-remote/client/k8s/unauthorized_user_resources.yaml create mode 100644 examples/rbac-remote/client/oidc/admin_user_resources.yaml create mode 100644 examples/rbac-remote/client/oidc/feature_repo/feature_store.yaml create mode 100644 examples/rbac-remote/client/oidc/feature_repo/test.py create mode 100644 examples/rbac-remote/client/oidc/readonly_user_resources.yaml create mode 100644 examples/rbac-remote/client/oidc/unauthorized_user_resources.yaml create mode 100644 examples/rbac-remote/demo.jpg create mode 100644 examples/rbac-remote/deployment.png create mode 100755 examples/rbac-remote/install_feast.sh create mode 100644 examples/rbac-remote/server/feature_repo/example_repo.py create mode 100644 examples/rbac-remote/server/feature_repo/feature_store.yaml create mode 100644 examples/rbac-remote/server/feature_repo/permissions_apply.py create mode 100644 examples/rbac-remote/server/k8s/feature_store_offline.yaml create mode 100644 examples/rbac-remote/server/k8s/feature_store_online.yaml create mode 100644 examples/rbac-remote/server/k8s/feature_store_registry.yaml create mode 100644 examples/rbac-remote/server/k8s/server_resources.yaml create mode 100644 examples/rbac-remote/server/oidc/feature_store_offline.yaml create mode 100644 examples/rbac-remote/server/oidc/feature_store_online.yaml create mode 100644 examples/rbac-remote/server/oidc/feature_store_registry.yaml diff --git a/examples/rbac-remote/README.md b/examples/rbac-remote/README.md new file mode 100644 index 0000000000..118800db55 --- /dev/null +++ b/examples/rbac-remote/README.md @@ -0,0 +1,171 @@ +# Feast Deployment with RBAC + +## Demo Summary +This demo showcases how to enable Role-Based Access Control (RBAC) for Feast using Kubernetes or [OIDC](https://openid.net/developers/how-connect-works/) Authentication type. +The demo steps involve deploying server components (registry, offline, online) and client examples within a Kubernetes environment. +The goal is to ensure secure access control based on user roles and permissions. For understanding the Feast RBAC framework +Please read these reference documents. +- [RBAC Architecture](https://docs.feast.dev/v/master/getting-started/architecture/rbac) +- [RBAC Permission](https://docs.feast.dev/v/master/getting-started/concepts/permission). +- [RBAC Authorization Manager](https://docs.feast.dev/v/master/getting-started/components/authz_manager) + +## Tools and Projects +- Kubernetes +- Feast +- PostgreSQL Database +- [Keycloak](https://www.keycloak.org) (if OIDC) + +## Application Environment + +This demo contains the following components: + +1. Feast Remote Server components (online, offline, registry). +2. Feast Remote Client RBAC example. +3. Yaml Configuration and installation related scripts files. + +![demo.jpg](demo.jpg) + +## Setup Instructions + +The application works with Kubernetes or OpenShift and the instructions assume that you are using a Kubernetes or OpenShift cluster. + +### Prerequisites + +1. Kubernetes Cluster and Kubernetes CLI (kubectl). +2. Helm: Ensure you have Helm installed for deploying the Feast components. +3. Python environment. +4. Feast CLI latest version. + +## 1. Prerequisites Step + + - **Step 1 : Create the Feast project with PostgreSQL.** + + * Install the PostgreSQL on a Kubernetes cluster if you are using OpenShift you can install using [OpenShift Template](https://github.com/RHEcosystemAppEng/feast-workshop-team-share/tree/main/feast_postgres#1-install-postgresql-on-openshift-using-openshift-template) + * Port Forward the PostgreSQL Database to your local machine. Since we are setting up the Feast project locally using the Feast CLI, we need to port forward PostgreSQL: + ``` kubectl port-forward svc/postgresql 5432:5432``` + * Create a feature repository/project using the cli with PostgreSQL. Please see the instructions for more details [here](https://docs.feast.dev/reference/offline-stores/postgres#getting-started). + For this (local) example setup, we create a project with name server using these settings for the [feature_store.yaml](server/feature_repo/feature_store.yaml). + +## 2. Authorization Setup + +### A. Kubernetes Authorization +- **Step 1: Create Remote configuration Files** + - Set the auth type to `kubernetes` in the respective `feature_store` files + + ```yaml + auth: + type: kubernetes + ``` + - For each server, feature store YAML files can be created for example like below: + + **Registry Server:** [feature_store_registry.yaml](server/k8s/feature_store_registry.yaml) + + **Offline Server :** [feature_store_offline.yaml](server/k8s/feature_store_offline.yaml) + + **Online Server :** [feature_store_online.yaml](server/k8s/feature_store_online.yaml) + +- **Step 2: Deploy the Server Components** + - Run the installation script. The setup script will deploy the server components based on the user's confirmation, enter `k8s` for kubernetes authentication deployment. The script will deploy all the components with the namespace `feast-dev`. + + ```sh + ./install_feast.sh + ``` + +### B. OIDC Authorization +- **Step 1: Setup Keycloak** + - See the documentation [here](https://www.keycloak.org/getting-started/getting-started-kube) and install Keycloak. + - Create a new realm with the name `feast-rbac` from the admin console. + - Under the `feast-rbac` realm, create a new client with the name `feast-client` + - Generate the secret for the `feast-client`. +- **Step 2: Create the Server Feature Store Files** + - Set the auth type to `oidc` in the respective `feature_store` files + + ```yaml + auth: + type: oidc + client_id: _CLIENT_ID__ + auth_discovery_url: _OIDC_SERVER_URL_/realms/feast-rbac/.well-known/openid-configuration + ``` + - For each server the feature store YAML files can be created for example like below: + + **Registry Server:** [feature_store_registry.yaml](server/oidc/feature_store_registry.yaml) + + **Offline Server :** [feature_store_offline.yaml](server/oidc/feature_store_offline.yaml) + + **Online Server :** [feature_store_online.yaml](server/oidc/feature_store_online.yaml) + +- **Step 3: Deploy the Server Components** + - Run the installation script. Enter `oidc` for the Keycloak authentication deployment. The script will deploy all of the components with the namespace `feast-dev`. + + ```sh + ./install_feast.sh + ``` + +## 3. Client Setup + +### A. Kubernetes Authorization +- **Step 1: Create the Client Feature Store YAML** + - Set up the client feature store with remote connection details for the registry, online, and offline store with auth type `kuberentes` . See the client remote setting example here: [feature_store.yaml](client/k8s/feature_repo/feature_store.yaml) +- **Step 2: Deploy the Client Examples** + - As an example, we created 3 different users: 1. [admin_user](client/k8s/admin_user_resources.yaml), 2. [readonly_user](client/k8s/readonly_user_resources.yaml) and 3. [unauthorized_user](client/k8s/unauthorized_user_resources.yaml) . + - Each user is assigned their own service account and roles, as shown in the table below. + ##### Roles and Permissions for Examples (Admin and User) + | **User** | **Service Account** | **Roles** | **Permission** | **Feast Resources** | **Actions** | + |-----------------|----------------------------|------------------|--------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------| + | admin | feast-admin-sa | feast-admin-role | feast_admin_permission | FeatureView, OnDemandFeatureView, BatchFeatureView, StreamFeatureView, Entity, FeatureService, DataSource, ValidationReference, SavedDataset, Permission | CREATE, DESCRIBE, UPDATE, DELETE, READ_ONLINE, READY_OFFLINE, WRITE_ONLINE, WRITE_OFFLINE | + | user | feast-user-sa | feast-user-role | feast_user_permission | FeatureView, OnDemandFeatureView, BatchFeatureView, StreamFeatureView, Entity, FeatureService, DataSource, ValidationReference, SavedDataset, Permission | READ, READ_OFFLINE, READ_ONLINE | + |unauthorized-user| feast-unauthorized-user-sa | | + - To deploy the client confirm `Apply client creation examples` `Y` + - The Deployment of the overall setup looks like : + + ![Deployment.png](deployment.png) + +### B. OIDC Authorization +- **Step 1: Create the Client Feature Store YAML** + - Set up the client feature store with the remote connection details for the registry, online, and offline store. + - Set the `Auth type` to `oidc` + - update the client secret in client side `feature_store.yaml` or if required any other settings as show below. + ``` + auth_discovery_url: https://keycloak-feast-dev.apps.com/realms/feast-rbac/.well-known/openid-configuration + client_id: feast-client + client_secret: update-this-value + username: ${FEAST_USERNAME} + password: ${FEAST_PASSWORD} + ``` + - See the client remote setting example here: [feature_store.yaml](client/oidc/feature_repo/feature_store.yaml) +- **Step 2: Create the Roles and Users** + - Under the `feast-client` create the two roles `feast-admin-role` and `feast-user-role` + - Under the `feast-rbac` realm, create 3 different users: `admin-user`, `readonly-user`, and `unauthorized-user`. Assign the password `feast` to each user. + - Map the roles to users: select the `admin-user`, go to `Role mapping`, and assign the `feast-admin-role`. Select the `readonly-user` and assign the `feast-user-role`. For the `unauthorized-user`, do not assign any roles. +- **Step 3: Deploy the Client Examples** + - For OIDC, similar to the k8s examples, create different deployments and add the username and password as environment variables: 1. [admin_user](client/oidc/admin_user_resources.yaml), 2. [readonly_user](client/oidc/readonly_user_resources.yaml) and 3. [unauthorized_user](client/oidc/unauthorized_user_resources.yaml) . + - To deploy the client confirm `Apply client creation examples` `Y` + +## 4. Permissions Management +- **Step 1: Apply the Permissions** + - See the code example in [permissions_apply.py](server/feature_repo/permissions_apply.py) for applying the permissions for both Kubernetes and OIDC setup. + - The `install_feast.sh` has the option to apply permission from the pod with the user's confirmation `Do you want to copy files and execute 'feast apply in the pod? (y/n)`. +- **Step 2: Validate the Permissions** + - use the Feast cli to validate the permissions with the command `feast permissions list` for more details use `feast permissions list -v`. Additionally, there are other commands such as: + `feast permissions check / describe / list-roles` +## 5. Validating the Permissions/RBAC results +- **Run the Examples** + - As outlined in the [test.py](client/k8s/feature_repo/test.py) script, the example attempts to fetch Historical Features, perform Materialization, fetch Online Features, and push to the online/offline store based on user roles. + - The `admin-user` can perform all actions on all objects. + - The `readonly-user` can only read or query all objects. + - `unauthorized user` should not able to read or write any resources as no role is defined for this user. + - From each user's pod run the example `python feature_repo/test.py` + +## 6. Local Testing and Cleanup +- **Local Testing** + - For local testing, port forward the services PostgreSQL Service and Feast Servers with the commands below: + ``` + kubectl port-forward svc/postgresql 5432:5432 + kubectl port-forward svc/feast-offline-server-feast-feature-server 8815:80 + kubectl port-forward svc/feast-registry-server-feast-feature-server 6570:80 + kubectl port-forward svc/feast-feature-server 6566:80 + ``` + - When testing in Kubernetes, users can set the environment variable `LOCAL_K8S_TOKEN` in each example. The token can be obtained from the service account. +- **Cleanup** + - Run the command + - ```./cleanup_feast.sh``` \ No newline at end of file diff --git a/examples/rbac-remote/cleanup_feast.sh b/examples/rbac-remote/cleanup_feast.sh new file mode 100755 index 0000000000..18acf6727c --- /dev/null +++ b/examples/rbac-remote/cleanup_feast.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +DEFAULT_HELM_RELEASES=("feast-feature-server" "feast-offline-server" "feast-registry-server") +NAMESPACE="feast-dev" + +HELM_RELEASES=(${1:-${DEFAULT_HELM_RELEASES[@]}}) +NAMESPACE=${2:-$NAMESPACE} + +echo "Deleting Helm releases..." +for release in "${HELM_RELEASES[@]}"; do + helm uninstall $release -n $NAMESPACE +done + +echo "Deleting Kubernetes roles, role bindings, and service accounts for clients" +kubectl delete -f client/k8s/admin_user_resources.yaml +kubectl delete -f client/k8s/readonly_user_resources.yaml +kubectl delete -f client/k8s/unauthorized_user_resources.yaml +kubectl delete -f client/oidc/admin_user_resources.yaml +kubectl delete -f client/oidc/readonly_user_resources.yaml +kubectl delete -f client/oidc/unauthorized_user_resources.yaml +kubectl delete -f server/k8s/server_resources.yaml +kubectl delete configmap client-feature-repo-config + +echo "Cleanup completed." diff --git a/examples/rbac-remote/client/k8s/admin_user_resources.yaml b/examples/rbac-remote/client/k8s/admin_user_resources.yaml new file mode 100644 index 0000000000..d5df8bcbf2 --- /dev/null +++ b/examples/rbac-remote/client/k8s/admin_user_resources.yaml @@ -0,0 +1,56 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: feast-admin-sa + namespace: feast-dev +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: feast-admin-role + namespace: feast-dev +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: feast-admin-rolebinding + namespace: feast-dev +subjects: + - kind: ServiceAccount + name: feast-admin-sa + namespace: feast-dev +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: feast-admin-role +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: client-admin-user + namespace: feast-dev + labels: + app: client-admin +spec: + replicas: 1 + selector: + matchLabels: + app: client-admin + template: + metadata: + labels: + app: client-admin + spec: + serviceAccountName: feast-admin-sa + containers: + - name: client-admin-container + image: feastdev/feature-server:latest + imagePullPolicy: Always + command: ["sleep", "infinity"] + volumeMounts: + - name: client-feature-repo-config + mountPath: /feature_repo + volumes: + - name: client-feature-repo-config + configMap: + name: client-feature-repo-config diff --git a/examples/rbac-remote/client/k8s/feature_repo/feature_store.yaml b/examples/rbac-remote/client/k8s/feature_repo/feature_store.yaml new file mode 100644 index 0000000000..d316005098 --- /dev/null +++ b/examples/rbac-remote/client/k8s/feature_repo/feature_store.yaml @@ -0,0 +1,14 @@ +project: server +registry: + registry_type: remote + path: feast-registry-server-feast-feature-server.feast-dev.svc.cluster.local:80 +offline_store: + type: remote + host: feast-offline-server-feast-feature-server.feast-dev.svc.cluster.local + port: 80 +online_store: + type: remote + path: http://feast-feature-server.feast-dev.svc.cluster.local:80 +auth: + type: kubernetes + diff --git a/examples/rbac-remote/client/k8s/feature_repo/test.py b/examples/rbac-remote/client/k8s/feature_repo/test.py new file mode 100644 index 0000000000..6e1480bc94 --- /dev/null +++ b/examples/rbac-remote/client/k8s/feature_repo/test.py @@ -0,0 +1,140 @@ +import os +from datetime import datetime + +import pandas as pd +from feast import FeatureStore +from feast.data_source import PushMode + + +def run_demo(): + try: + os.environ["LOCAL_K8S_TOKEN"] = "" + + store = FeatureStore(repo_path="/feature_repo") + + print("\n--- Historical features for training ---") + fetch_historical_features_entity_df(store, for_batch_scoring=False) + + print("\n--- Historical features for batch scoring ---") + fetch_historical_features_entity_df(store, for_batch_scoring=True) + + try: + print("\n--- Load features into online store/materialize_incremental ---") + feature_views= store.list_feature_views() + if not feature_views: + raise PermissionError("No access to feature-views or no feature-views available.") + store.materialize_incremental(end_date=datetime.now()) + except PermissionError as pe: + print(f"Permission error: {pe}") + except Exception as e: + print(f"An occurred while performing materialize incremental: {e}") + + print("\n--- Online features ---") + fetch_online_features(store) + + print("\n--- Online features retrieved (instead) through a feature service---") + fetch_online_features(store, source="feature_service") + + print( + "\n--- Online features retrieved (using feature service v3, which uses a feature view with a push source---" + ) + fetch_online_features(store, source="push") + + print("\n--- Simulate a stream event ingestion of the hourly stats df ---") + event_df = pd.DataFrame.from_dict( + { + "driver_id": [1001], + "event_timestamp": [datetime.now()], + "created": [datetime.now()], + "conv_rate": [1.0], + "acc_rate": [1.0], + "avg_daily_trips": [1000], + } + ) + store.push("driver_stats_push_source", event_df, to=PushMode.ONLINE_AND_OFFLINE) + + print("\n--- Online features again with updated values from a stream push---") + fetch_online_features(store, source="push") + + except Exception as e: + print(f"An error occurred: {e}") + + +def fetch_historical_features_entity_df(store: FeatureStore, for_batch_scoring: bool): + try: + entity_df = pd.DataFrame.from_dict( + { + "driver_id": [1001, 1002, 1003], + "event_timestamp": [ + datetime(2021, 4, 12, 10, 59, 42), + datetime(2021, 4, 12, 8, 12, 10), + datetime(2021, 4, 12, 16, 40, 26), + ], + "label_driver_reported_satisfaction": [1, 5, 3], + # values we're using for an on-demand transformation + "val_to_add": [1, 2, 3], + "val_to_add_2": [10, 20, 30], + + } + + ) + if for_batch_scoring: + entity_df["event_timestamp"] = pd.to_datetime("now", utc=True) + + training_df = store.get_historical_features( + entity_df=entity_df, + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "driver_hourly_stats:avg_daily_trips", + "transformed_conv_rate:conv_rate_plus_val1", + "transformed_conv_rate:conv_rate_plus_val2", + ], + ).to_df() + print(training_df.head()) + + except Exception as e: + print(f"An error occurred while fetching historical features: {e}") + + +def fetch_online_features(store, source: str = ""): + try: + entity_rows = [ + # {join_key: entity_value} + { + "driver_id": 1001, + "val_to_add": 1000, + "val_to_add_2": 2000, + }, + { + "driver_id": 1002, + "val_to_add": 1001, + "val_to_add_2": 2002, + }, + ] + if source == "feature_service": + features_to_fetch = store.get_feature_service("driver_activity_v1") + elif source == "push": + features_to_fetch = store.get_feature_service("driver_activity_v3") + else: + features_to_fetch = [ + "driver_hourly_stats:acc_rate", + "transformed_conv_rate:conv_rate_plus_val1", + "transformed_conv_rate:conv_rate_plus_val2", + ] + returned_features = store.get_online_features( + features=features_to_fetch, + entity_rows=entity_rows, + ).to_dict() + for key, value in sorted(returned_features.items()): + print(key, " : ", value) + + except Exception as e: + print(f"An error occurred while fetching online features: {e}") + + +if __name__ == "__main__": + try: + run_demo() + except Exception as e: + print(f"An error occurred in the main execution: {e}") diff --git a/examples/rbac-remote/client/k8s/readonly_user_resources.yaml b/examples/rbac-remote/client/k8s/readonly_user_resources.yaml new file mode 100644 index 0000000000..c9094e7f2f --- /dev/null +++ b/examples/rbac-remote/client/k8s/readonly_user_resources.yaml @@ -0,0 +1,57 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: feast-user-sa + namespace: feast-dev +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: feast-user-role + namespace: feast-dev +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: feast-user-rolebinding + namespace: feast-dev +subjects: + - kind: ServiceAccount + name: feast-user-sa + namespace: feast-dev +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: feast-user-role +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: client-readonly-user + namespace: feast-dev + labels: + app: client-user +spec: + replicas: 1 + selector: + matchLabels: + app: client-user + template: + metadata: + labels: + app: client-user + spec: + serviceAccountName: feast-user-sa + containers: + - name: client-user-container + image: feastdev/feature-server:latest + imagePullPolicy: Always + command: ["sleep", "infinity"] + volumeMounts: + - name: client-feature-repo-config + mountPath: /feature_repo + volumes: + - name: client-feature-repo-config + configMap: + name: client-feature-repo-config + diff --git a/examples/rbac-remote/client/k8s/unauthorized_user_resources.yaml b/examples/rbac-remote/client/k8s/unauthorized_user_resources.yaml new file mode 100644 index 0000000000..5068c94fd9 --- /dev/null +++ b/examples/rbac-remote/client/k8s/unauthorized_user_resources.yaml @@ -0,0 +1,36 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: feast-unauthorized-user-sa + namespace: feast-dev +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: client-unauthorized-user + namespace: feast-dev + labels: + app: client-unauthorized-user +spec: + replicas: 1 + selector: + matchLabels: + app: client-unauthorized-user + template: + metadata: + labels: + app: client-unauthorized-user + spec: + serviceAccountName: feast-unauthorized-user-sa + containers: + - name: client-unauthorized-user-container + image: feastdev/feature-server:latest + imagePullPolicy: Always + command: ["sleep", "infinity"] + volumeMounts: + - name: client-feature-repo-config + mountPath: /feature_repo + volumes: + - name: client-feature-repo-config + configMap: + name: client-feature-repo-config diff --git a/examples/rbac-remote/client/oidc/admin_user_resources.yaml b/examples/rbac-remote/client/oidc/admin_user_resources.yaml new file mode 100644 index 0000000000..7843ce3c9d --- /dev/null +++ b/examples/rbac-remote/client/oidc/admin_user_resources.yaml @@ -0,0 +1,34 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: client-admin-user + namespace: feast-dev + labels: + app: client-admin +spec: + replicas: 1 + selector: + matchLabels: + app: client-admin + template: + metadata: + labels: + app: client-admin + spec: + containers: + - name: client-admin-container + image: feastdev/feature-server:latest + imagePullPolicy: Always + command: ["sleep", "infinity"] + env: + - name: FEAST_USERNAME + value: admin-user + - name: FEAST_PASSWORD + value: feast + volumeMounts: + - name: client-feature-repo-config + mountPath: /feature_repo + volumes: + - name: client-feature-repo-config + configMap: + name: client-feature-repo-config diff --git a/examples/rbac-remote/client/oidc/feature_repo/feature_store.yaml b/examples/rbac-remote/client/oidc/feature_repo/feature_store.yaml new file mode 100644 index 0000000000..1454e16df9 --- /dev/null +++ b/examples/rbac-remote/client/oidc/feature_repo/feature_store.yaml @@ -0,0 +1,19 @@ +project: server +registry: + registry_type: remote + path: feast-registry-server-feast-feature-server.feast-dev.svc.cluster.local:80 +offline_store: + type: remote + host: feast-offline-server-feast-feature-server.feast-dev.svc.cluster.local + port: 80 +online_store: + type: remote + path: http://feast-feature-server.feast-dev.svc.cluster.local:80 +auth: + type: oidc + auth_discovery_url: https://keycloak-feast-dev.apps.com/realms/feast-rbac/.well-known/openid-configuration + client_id: feast-client + client_secret: update-this-value + username: ${FEAST_USERNAME} + password: ${FEAST_PASSWORD} +entity_key_serialization_version: 2 diff --git a/examples/rbac-remote/client/oidc/feature_repo/test.py b/examples/rbac-remote/client/oidc/feature_repo/test.py new file mode 100644 index 0000000000..6e1480bc94 --- /dev/null +++ b/examples/rbac-remote/client/oidc/feature_repo/test.py @@ -0,0 +1,140 @@ +import os +from datetime import datetime + +import pandas as pd +from feast import FeatureStore +from feast.data_source import PushMode + + +def run_demo(): + try: + os.environ["LOCAL_K8S_TOKEN"] = "" + + store = FeatureStore(repo_path="/feature_repo") + + print("\n--- Historical features for training ---") + fetch_historical_features_entity_df(store, for_batch_scoring=False) + + print("\n--- Historical features for batch scoring ---") + fetch_historical_features_entity_df(store, for_batch_scoring=True) + + try: + print("\n--- Load features into online store/materialize_incremental ---") + feature_views= store.list_feature_views() + if not feature_views: + raise PermissionError("No access to feature-views or no feature-views available.") + store.materialize_incremental(end_date=datetime.now()) + except PermissionError as pe: + print(f"Permission error: {pe}") + except Exception as e: + print(f"An occurred while performing materialize incremental: {e}") + + print("\n--- Online features ---") + fetch_online_features(store) + + print("\n--- Online features retrieved (instead) through a feature service---") + fetch_online_features(store, source="feature_service") + + print( + "\n--- Online features retrieved (using feature service v3, which uses a feature view with a push source---" + ) + fetch_online_features(store, source="push") + + print("\n--- Simulate a stream event ingestion of the hourly stats df ---") + event_df = pd.DataFrame.from_dict( + { + "driver_id": [1001], + "event_timestamp": [datetime.now()], + "created": [datetime.now()], + "conv_rate": [1.0], + "acc_rate": [1.0], + "avg_daily_trips": [1000], + } + ) + store.push("driver_stats_push_source", event_df, to=PushMode.ONLINE_AND_OFFLINE) + + print("\n--- Online features again with updated values from a stream push---") + fetch_online_features(store, source="push") + + except Exception as e: + print(f"An error occurred: {e}") + + +def fetch_historical_features_entity_df(store: FeatureStore, for_batch_scoring: bool): + try: + entity_df = pd.DataFrame.from_dict( + { + "driver_id": [1001, 1002, 1003], + "event_timestamp": [ + datetime(2021, 4, 12, 10, 59, 42), + datetime(2021, 4, 12, 8, 12, 10), + datetime(2021, 4, 12, 16, 40, 26), + ], + "label_driver_reported_satisfaction": [1, 5, 3], + # values we're using for an on-demand transformation + "val_to_add": [1, 2, 3], + "val_to_add_2": [10, 20, 30], + + } + + ) + if for_batch_scoring: + entity_df["event_timestamp"] = pd.to_datetime("now", utc=True) + + training_df = store.get_historical_features( + entity_df=entity_df, + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "driver_hourly_stats:avg_daily_trips", + "transformed_conv_rate:conv_rate_plus_val1", + "transformed_conv_rate:conv_rate_plus_val2", + ], + ).to_df() + print(training_df.head()) + + except Exception as e: + print(f"An error occurred while fetching historical features: {e}") + + +def fetch_online_features(store, source: str = ""): + try: + entity_rows = [ + # {join_key: entity_value} + { + "driver_id": 1001, + "val_to_add": 1000, + "val_to_add_2": 2000, + }, + { + "driver_id": 1002, + "val_to_add": 1001, + "val_to_add_2": 2002, + }, + ] + if source == "feature_service": + features_to_fetch = store.get_feature_service("driver_activity_v1") + elif source == "push": + features_to_fetch = store.get_feature_service("driver_activity_v3") + else: + features_to_fetch = [ + "driver_hourly_stats:acc_rate", + "transformed_conv_rate:conv_rate_plus_val1", + "transformed_conv_rate:conv_rate_plus_val2", + ] + returned_features = store.get_online_features( + features=features_to_fetch, + entity_rows=entity_rows, + ).to_dict() + for key, value in sorted(returned_features.items()): + print(key, " : ", value) + + except Exception as e: + print(f"An error occurred while fetching online features: {e}") + + +if __name__ == "__main__": + try: + run_demo() + except Exception as e: + print(f"An error occurred in the main execution: {e}") diff --git a/examples/rbac-remote/client/oidc/readonly_user_resources.yaml b/examples/rbac-remote/client/oidc/readonly_user_resources.yaml new file mode 100644 index 0000000000..c43137bfba --- /dev/null +++ b/examples/rbac-remote/client/oidc/readonly_user_resources.yaml @@ -0,0 +1,34 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: client-readonly-user + namespace: feast-dev + labels: + app: client-user +spec: + replicas: 1 + selector: + matchLabels: + app: client-user + template: + metadata: + labels: + app: client-user + spec: + containers: + - name: client-admin-container + image: feastdev/feature-server:latest + imagePullPolicy: Always + command: ["sleep", "infinity"] + env: + - name: FEAST_USERNAME + value: readonly-user + - name: FEAST_PASSWORD + value: feast + volumeMounts: + - name: client-feature-repo-config + mountPath: /feature_repo + volumes: + - name: client-feature-repo-config + configMap: + name: client-feature-repo-config diff --git a/examples/rbac-remote/client/oidc/unauthorized_user_resources.yaml b/examples/rbac-remote/client/oidc/unauthorized_user_resources.yaml new file mode 100644 index 0000000000..f99bb3e987 --- /dev/null +++ b/examples/rbac-remote/client/oidc/unauthorized_user_resources.yaml @@ -0,0 +1,35 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: client-unauthorized-user + namespace: feast-dev + labels: + app: client-unauthorized-user +spec: + replicas: 1 + selector: + matchLabels: + app: client-unauthorized-user + template: + metadata: + labels: + app: client-unauthorized-user + spec: + containers: + - name: client-admin-container + image: feastdev/feature-server:latest + imagePullPolicy: Always + command: ["sleep", "infinity"] + env: + - name: FEAST_USERNAME + value: unauthorized-user + - name: FEAST_PASSWORD + value: feast + volumeMounts: + - name: client-feature-repo-config + mountPath: /feature_repo + volumes: + - name: client-feature-repo-config + configMap: + name: client-feature-repo-config + diff --git a/examples/rbac-remote/demo.jpg b/examples/rbac-remote/demo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..718e49dde69a898cad82f3ad7b5d5d02b032a34e GIT binary patch literal 115961 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

>d&DZ4ntmFX=e z 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 literal 0 HcmV?d00001 diff --git a/examples/rbac-remote/deployment.png b/examples/rbac-remote/deployment.png new file mode 100644 index 0000000000000000000000000000000000000000..9b9a0d7b2a48039a1e716be1a12ab81ff87d68dd GIT binary patch literal 98226 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 literal 0 HcmV?d00001 diff --git a/examples/rbac-remote/install_feast.sh b/examples/rbac-remote/install_feast.sh new file mode 100755 index 0000000000..b87d44b335 --- /dev/null +++ b/examples/rbac-remote/install_feast.sh @@ -0,0 +1,109 @@ +#!/bin/bash + +# Specify the RBAC type (folder) +read -p "Enter RBAC type (e.g., k8s or oidc): " FOLDER + +echo "You have selected the RBAC type: $FOLDER" + +# feature_store files name for the servers +OFFLINE_YAML="feature_store_offline.yaml" +ONLINE_YAML="feature_store_online.yaml" +REGISTRY_YAML="feature_store_registry.yaml" + +# Helm chart path and service account +HELM_CHART_PATH="../../infra/charts/feast-feature-server" +SERVICE_ACCOUNT_NAME="feast-sa" +CLIENT_REPO_DIR="client/$FOLDER/feature_repo" + +# Function to check if a file exists and encode it to base64 +encode_to_base64() { + local file_path=$1 + if [ ! -f "$file_path" ]; then + echo "Error: File not found at $file_path" + exit 1 + fi + base64 < "$file_path" +} + +FEATURE_STORE_OFFLINE_YAML_PATH="server/$FOLDER/$OFFLINE_YAML" +FEATURE_STORE_ONLINE_YAML_PATH="server/$FOLDER/$ONLINE_YAML" +FEATURE_STORE_REGISTRY_YAML_PATH="server/$FOLDER/$REGISTRY_YAML" + +# Encode the YAML files to base64 +FEATURE_STORE_OFFLINE_YAML_BASE64=$(encode_to_base64 "$FEATURE_STORE_OFFLINE_YAML_PATH") +FEATURE_STORE_ONLINE_YAML_BASE64=$(encode_to_base64 "$FEATURE_STORE_ONLINE_YAML_PATH") +FEATURE_STORE_REGISTRY_YAML_BASE64=$(encode_to_base64 "$FEATURE_STORE_REGISTRY_YAML_PATH") + +# Check if base64 encoding was successful +if [ -z "$FEATURE_STORE_OFFLINE_YAML_BASE64" ] || [ -z "$FEATURE_STORE_ONLINE_YAML_BASE64" ] || [ -z "$FEATURE_STORE_REGISTRY_YAML_BASE64" ]; then + echo "Error: Failed to base64 encode one or more feature_store.yaml files in folder $FOLDER." + exit 1 +fi + +# Upgrade or install Feast components for the specified folder +read -p "Deploy Feast server components for $FOLDER? (y/n) " confirm_server +if [[ $confirm_server == [yY] ]]; then + # Apply the server service accounts and role bindings + kubectl apply -f "server/k8s/server_resources.yaml" + + # Upgrade or install Feast components + echo "Upgrading or installing Feast server components for $FOLDER" + + helm upgrade --install feast-registry-server $HELM_CHART_PATH \ + --set feast_mode=registry \ + --set feature_store_yaml_base64=$FEATURE_STORE_REGISTRY_YAML_BASE64 \ + --set serviceAccount.name=$SERVICE_ACCOUNT_NAME + + helm upgrade --install feast-feature-server $HELM_CHART_PATH \ + --set feature_store_yaml_base64=$FEATURE_STORE_ONLINE_YAML_BASE64 \ + --set serviceAccount.name=$SERVICE_ACCOUNT_NAME + + helm upgrade --install feast-offline-server $HELM_CHART_PATH \ + --set feast_mode=offline \ + --set feature_store_yaml_base64=$FEATURE_STORE_OFFLINE_YAML_BASE64 \ + --set serviceAccount.name=$SERVICE_ACCOUNT_NAME + + echo "Server components deployed for $FOLDER." +else + echo "Server components not deployed for $FOLDER." +fi + +read -p "Apply client creation examples ? (y/n) " confirm_clients +if [[ $confirm_clients == [yY] ]]; then + kubectl delete configmap client-feature-repo-config --ignore-not-found + kubectl create configmap client-feature-repo-config --from-file=$CLIENT_REPO_DIR + + kubectl apply -f "client/$FOLDER/admin_user_resources.yaml" + kubectl apply -f "client/$FOLDER/readonly_user_resources.yaml" + kubectl apply -f "client/$FOLDER/unauthorized_user_resources.yaml" + + echo "Client resources applied." +else + echo "Client resources not applied." +fi + +read -p "Apply 'feast apply' in the remote registry? (y/n) " confirm_apply +if [[ $confirm_apply == [yY] ]]; then + + POD_NAME=$(kubectl get pods --no-headers -o custom-columns=":metadata.name" | grep '^feast-registry-server-feast-feature-server') + + if [ -z "$POD_NAME" ]; then + echo "No pod found with the prefix feast-registry-server-feast-feature-server" + exit 1 + fi + + LOCAL_DIR="./server/feature_repo/" + REMOTE_DIR="/app/" + + echo "Copying files from $LOCAL_DIR to $POD_NAME:$REMOTE_DIR" + kubectl cp $LOCAL_DIR $POD_NAME:$REMOTE_DIR + + echo "Files copied successfully!" + + kubectl exec $POD_NAME -- feast -c feature_repo apply + echo "'feast apply' command executed successfully in the for remote registry." +else + echo "'feast apply' not performed ." +fi + +echo "Setup completed." diff --git a/examples/rbac-remote/server/feature_repo/example_repo.py b/examples/rbac-remote/server/feature_repo/example_repo.py new file mode 100644 index 0000000000..5b8105bb94 --- /dev/null +++ b/examples/rbac-remote/server/feature_repo/example_repo.py @@ -0,0 +1,130 @@ +# This is an example feature definition file + +from datetime import timedelta + +import pandas as pd + +from feast import Entity, FeatureService, FeatureView, Field, PushSource, RequestSource +from feast.infra.offline_stores.contrib.postgres_offline_store.postgres_source import PostgreSQLSource + +from feast.on_demand_feature_view import on_demand_feature_view +from feast.types import Float32, Float64, Int64 + +# Define an entity for the driver. You can think of an entity as a primary key used to +# fetch features. +driver = Entity(name="driver", join_keys=["driver_id"]) + +driver_stats_source = PostgreSQLSource( + name="driver_hourly_stats_source", + query="SELECT * FROM feast_driver_hourly_stats", + timestamp_field="event_timestamp", + created_timestamp_column="created", +) + +# Our parquet files contain sample data that includes a driver_id column, timestamps and +# three feature column. Here we define a Feature View that will allow us to serve this +# data to our model online. +driver_stats_fv = FeatureView( + # The unique name of this feature view. Two feature views in a single + # project cannot have the same name + name="driver_hourly_stats", + entities=[driver], + ttl=timedelta(days=1), + # The list of features defined below act as a schema to both define features + # for both materialization of features into a store, and are used as references + # during retrieval for building a training dataset or serving features + schema=[ + Field(name="conv_rate", dtype=Float32), + Field(name="acc_rate", dtype=Float32), + Field(name="avg_daily_trips", dtype=Int64), + ], + online=True, + source=driver_stats_source, + # Tags are user defined key/value pairs that are attached to each + # feature view + tags={"team": "driver_performance"}, +) + +# Define a request data source which encodes features / information only +# available at request time (e.g. part of the user initiated HTTP request) +input_request = RequestSource( + name="vals_to_add", + schema=[ + Field(name="val_to_add", dtype=Int64), + Field(name="val_to_add_2", dtype=Int64), + ], +) + + +# Define an on demand feature view which can generate new features based on +# existing feature views and RequestSource features +@on_demand_feature_view( + sources=[driver_stats_fv, input_request], + schema=[ + Field(name="conv_rate_plus_val1", dtype=Float64), + Field(name="conv_rate_plus_val2", dtype=Float64), + ], +) +def transformed_conv_rate(inputs: pd.DataFrame) -> pd.DataFrame: + df = pd.DataFrame() + df["conv_rate_plus_val1"] = inputs["conv_rate"] + inputs["val_to_add"] + df["conv_rate_plus_val2"] = inputs["conv_rate"] + inputs["val_to_add_2"] + return df + + +# This groups features into a model version +driver_activity_v1 = FeatureService( + name="driver_activity_v1", + features=[ + driver_stats_fv[["conv_rate"]], # Sub-selects a feature from a feature view + transformed_conv_rate, # Selects all features from the feature view + ], +) +driver_activity_v2 = FeatureService( + name="driver_activity_v2", features=[driver_stats_fv, transformed_conv_rate] +) + +# Defines a way to push data (to be available offline, online or both) into Feast. +driver_stats_push_source = PushSource( + name="driver_stats_push_source", + batch_source=driver_stats_source, +) + +# Defines a slightly modified version of the feature view from above, where the source +# has been changed to the push source. This allows fresh features to be directly pushed +# to the online store for this feature view. +driver_stats_fresh_fv = FeatureView( + name="driver_hourly_stats_fresh", + entities=[driver], + ttl=timedelta(days=1), + schema=[ + Field(name="conv_rate", dtype=Float32), + Field(name="acc_rate", dtype=Float32), + Field(name="avg_daily_trips", dtype=Int64), + ], + online=True, + source=driver_stats_push_source, # Changed from above + tags={"team": "driver_performance"}, +) + + +# Define an on demand feature view which can generate new features based on +# existing feature views and RequestSource features +@on_demand_feature_view( + sources=[driver_stats_fresh_fv, input_request], # relies on fresh version of FV + schema=[ + Field(name="conv_rate_plus_val1", dtype=Float64), + Field(name="conv_rate_plus_val2", dtype=Float64), + ], +) +def transformed_conv_rate_fresh(inputs: pd.DataFrame) -> pd.DataFrame: + df = pd.DataFrame() + df["conv_rate_plus_val1"] = inputs["conv_rate"] + inputs["val_to_add"] + df["conv_rate_plus_val2"] = inputs["conv_rate"] + inputs["val_to_add_2"] + return df + + +driver_activity_v3 = FeatureService( + name="driver_activity_v3", + features=[driver_stats_fresh_fv, transformed_conv_rate_fresh], +) diff --git a/examples/rbac-remote/server/feature_repo/feature_store.yaml b/examples/rbac-remote/server/feature_repo/feature_store.yaml new file mode 100644 index 0000000000..78b13c660b --- /dev/null +++ b/examples/rbac-remote/server/feature_repo/feature_store.yaml @@ -0,0 +1,26 @@ +project: server +provider: local +registry: + registry_type: sql + path: postgresql+psycopg://feast:feast@postgresql.feast-dev.svc.cluster.local:5432/feast + cache_ttl_seconds: 60 + sqlalchemy_config_kwargs: + echo: false + pool_pre_ping: true +online_store: + type: postgres + host: postgresql.feast-dev.svc.cluster.local + port: 5432 + database: feast + db_schema: public + user: feast + password: feast +offline_store: + type: postgres + host: postgresql.feast-dev.svc.cluster.local + port: 5432 + database: feast + db_schema: public + user: feast + password: feast +entity_key_serialization_version: 2 diff --git a/examples/rbac-remote/server/feature_repo/permissions_apply.py b/examples/rbac-remote/server/feature_repo/permissions_apply.py new file mode 100644 index 0000000000..93bdf2ffc6 --- /dev/null +++ b/examples/rbac-remote/server/feature_repo/permissions_apply.py @@ -0,0 +1,21 @@ +from feast.feast_object import ALL_RESOURCE_TYPES +from feast.permissions.action import READ, AuthzedAction, ALL_ACTIONS +from feast.permissions.permission import Permission +from feast.permissions.policy import RoleBasedPolicy + +admin_roles = ["feast-admin-role"] +user_roles = ["feast-user-role"] + +user_perm = Permission( + name="feast_user_permission", + types=ALL_RESOURCE_TYPES, + policy=RoleBasedPolicy(roles=user_roles), + actions=[AuthzedAction.DESCRIBE] + READ +) + +admin_perm = Permission( + name="feast_admin_permission", + types=ALL_RESOURCE_TYPES, + policy=RoleBasedPolicy(roles=admin_roles), + actions=ALL_ACTIONS +) diff --git a/examples/rbac-remote/server/k8s/feature_store_offline.yaml b/examples/rbac-remote/server/k8s/feature_store_offline.yaml new file mode 100644 index 0000000000..4fc01508bd --- /dev/null +++ b/examples/rbac-remote/server/k8s/feature_store_offline.yaml @@ -0,0 +1,16 @@ +project: server +provider: local +registry: + registry_type: remote + path: feast-registry-server-feast-feature-server.feast-dev.svc.cluster.local:80 +offline_store: + type: postgres + host: postgresql.feast-dev.svc.cluster.local + port: 5432 + database: feast + db_schema: public + user: feast + password: feast +auth: + type: kubernetes +entity_key_serialization_version: 2 diff --git a/examples/rbac-remote/server/k8s/feature_store_online.yaml b/examples/rbac-remote/server/k8s/feature_store_online.yaml new file mode 100644 index 0000000000..aa167731b2 --- /dev/null +++ b/examples/rbac-remote/server/k8s/feature_store_online.yaml @@ -0,0 +1,20 @@ +project: server +provider: local +registry: + registry_type: remote + path: feast-registry-server-feast-feature-server.feast-dev.svc.cluster.local:80 +online_store: + type: postgres + host: postgresql.feast-dev.svc.cluster.local + port: 5432 + database: feast + db_schema: public + user: feast + password: feast +offline_store: + type: remote + host: feast-offline-server-feast-feature-server.feast-dev.svc.cluster.local + port: 80 +auth: + type: kubernetes +entity_key_serialization_version: 2 diff --git a/examples/rbac-remote/server/k8s/feature_store_registry.yaml b/examples/rbac-remote/server/k8s/feature_store_registry.yaml new file mode 100644 index 0000000000..579141fb01 --- /dev/null +++ b/examples/rbac-remote/server/k8s/feature_store_registry.yaml @@ -0,0 +1,12 @@ +project: server +provider: local +registry: + registry_type: sql + path: postgresql+psycopg://feast:feast@postgresql.feast-dev.svc.cluster.local:5432/feast + cache_ttl_seconds: 60 + sqlalchemy_config_kwargs: + echo: false + pool_pre_ping: true +auth: + type: kubernetes +entity_key_serialization_version: 2 diff --git a/examples/rbac-remote/server/k8s/server_resources.yaml b/examples/rbac-remote/server/k8s/server_resources.yaml new file mode 100644 index 0000000000..03e35495d6 --- /dev/null +++ b/examples/rbac-remote/server/k8s/server_resources.yaml @@ -0,0 +1,27 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: feast-sa + namespace: feast-dev +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: feast-cluster-role +rules: + - apiGroups: ["rbac.authorization.k8s.io"] + resources: ["roles", "rolebindings", "clusterrolebindings"] + verbs: ["get", "list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: feast-cluster-rolebinding +subjects: + - kind: ServiceAccount + name: feast-sa + namespace: feast-dev +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: feast-cluster-role diff --git a/examples/rbac-remote/server/oidc/feature_store_offline.yaml b/examples/rbac-remote/server/oidc/feature_store_offline.yaml new file mode 100644 index 0000000000..8ed4cc1ff3 --- /dev/null +++ b/examples/rbac-remote/server/oidc/feature_store_offline.yaml @@ -0,0 +1,18 @@ +project: server +provider: local +registry: + registry_type: remote + path: feast-registry-server-feast-feature-server.feast-dev.svc.cluster.local:80 +offline_store: + type: postgres + host: postgresql.feast-dev.svc.cluster.local + port: 5432 + database: feast + db_schema: public + user: feast + password: feast +auth: + type: oidc + auth_discovery_url: https://keycloak-feast-dev.apps.com/realms/feast-rbac/.well-known/openid-configuration + client_id: feast-client +entity_key_serialization_version: 2 diff --git a/examples/rbac-remote/server/oidc/feature_store_online.yaml b/examples/rbac-remote/server/oidc/feature_store_online.yaml new file mode 100644 index 0000000000..c47c3a0662 --- /dev/null +++ b/examples/rbac-remote/server/oidc/feature_store_online.yaml @@ -0,0 +1,22 @@ +project: server +provider: local +registry: + registry_type: remote + path: feast-registry-server-feast-feature-server.feast-dev.svc.cluster.local:80 +online_store: + type: postgres + host: postgresql.feast-dev.svc.cluster.local + port: 5432 + database: feast + db_schema: public + user: feast + password: feast +offline_store: + type: remote + host: feast-offline-server-feast-feature-server.feast-dev.svc.cluster.local + port: 80 +auth: + type: oidc + auth_discovery_url: https://keycloak-feast-dev.apps.com/realms/feast-rbac/.well-known/openid-configuration + client_id: feast-client +entity_key_serialization_version: 2 diff --git a/examples/rbac-remote/server/oidc/feature_store_registry.yaml b/examples/rbac-remote/server/oidc/feature_store_registry.yaml new file mode 100644 index 0000000000..a661d9dc56 --- /dev/null +++ b/examples/rbac-remote/server/oidc/feature_store_registry.yaml @@ -0,0 +1,14 @@ +project: server +provider: local +registry: + registry_type: sql + path: postgresql+psycopg://feast:feast@postgresql.feast-dev.svc.cluster.local:5432/feast + cache_ttl_seconds: 60 + sqlalchemy_config_kwargs: + echo: false + pool_pre_ping: true +auth: + type: oidc + auth_discovery_url: https://keycloak-feast-dev.apps.com/realms/feast-rbac/.well-known/openid-configuration + client_id: feast-client +entity_key_serialization_version: 2 diff --git a/infra/charts/feast-feature-server/templates/deployment.yaml b/infra/charts/feast-feature-server/templates/deployment.yaml index 8dddeed6fd..dc62be8b95 100644 --- a/infra/charts/feast-feature-server/templates/deployment.yaml +++ b/infra/charts/feast-feature-server/templates/deployment.yaml @@ -21,6 +21,7 @@ spec: labels: {{- include "feast-feature-server.selectorLabels" . | nindent 8 }} spec: + serviceAccountName: {{ .Values.serviceAccount.name | default "default" }} {{- with .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} diff --git a/infra/charts/feast-feature-server/values.yaml b/infra/charts/feast-feature-server/values.yaml index 64d805a66c..22bbdeace0 100644 --- a/infra/charts/feast-feature-server/values.yaml +++ b/infra/charts/feast-feature-server/values.yaml @@ -44,6 +44,9 @@ service: type: ClusterIP port: 80 +serviceAccount: + name: "" + resources: {} # We usually recommend not to specify default resources and to leave this as a conscious # choice for the user. This also increases chances charts run on environments with little diff --git a/sdk/python/feast/permissions/client/auth_client_manager_factory.py b/sdk/python/feast/permissions/client/auth_client_manager_factory.py index 3dff5fb45d..359072f38e 100644 --- a/sdk/python/feast/permissions/client/auth_client_manager_factory.py +++ b/sdk/python/feast/permissions/client/auth_client_manager_factory.py @@ -1,7 +1,11 @@ +import os +from typing import cast + from feast.permissions.auth.auth_type import AuthType from feast.permissions.auth_model import ( AuthConfig, KubernetesAuthConfig, + OidcAuthConfig, OidcClientAuthConfig, ) from feast.permissions.client.auth_client_manager import AuthenticationClientManager @@ -15,8 +19,15 @@ def get_auth_client_manager(auth_config: AuthConfig) -> AuthenticationClientManager: if auth_config.type == AuthType.OIDC.value: - assert isinstance(auth_config, OidcClientAuthConfig) - return OidcAuthClientManager(auth_config) + intra_communication_base64 = os.getenv("INTRA_COMMUNICATION_BASE64") + # If intra server communication call + if intra_communication_base64: + assert isinstance(auth_config, OidcAuthConfig) + client_auth_config = cast(OidcClientAuthConfig, auth_config) + else: + assert isinstance(auth_config, OidcClientAuthConfig) + client_auth_config = auth_config + return OidcAuthClientManager(client_auth_config) elif auth_config.type == AuthType.KUBERNETES.value: assert isinstance(auth_config, KubernetesAuthConfig) return KubernetesAuthClientManager(auth_config) From def863360bf0e553d242900ee915e953c6c3f9b6 Mon Sep 17 00:00:00 2001 From: Theodor Mihalache <84387487+tmihalac@users.noreply.github.com> Date: Thu, 12 Sep 2024 09:28:47 -0400 Subject: [PATCH 067/185] =?UTF-8?q?fix:=20Refactor=20auth=5Fclient=5Fmanag?= =?UTF-8?q?er=5Ffactory.py=20in=20function=20get=5Fauth=5Fclient=5Fm?= =?UTF-8?q?=E2=80=A6=20(#4505)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactor auth_client_manager_factory.py in function get_auth_client_manager Signed-off-by: Theodor Mihalache * Refactor auth_client_manager_factory.py in function get_auth_client_manager -Added test Signed-off-by: Theodor Mihalache * Refactor auth_client_manager_factory.py in function get_auth_client_manager -updated test following review Signed-off-by: Theodor Mihalache * Refactor auth_client_manager_factory.py in function get_auth_client_manager -fixed linter Signed-off-by: Theodor Mihalache --------- Signed-off-by: Theodor Mihalache --- .../client/arrow_flight_auth_interceptor.py | 2 +- .../permissions/client/auth_client_manager.py | 41 ++++++++++++++ .../client/auth_client_manager_factory.py | 41 -------------- .../permissions/client/client_auth_token.py | 14 +++++ .../client/grpc_client_auth_interceptor.py | 2 +- .../client/http_auth_requests_wrapper.py | 2 +- ...ntra_comm_authentication_client_manager.py | 31 +++++++++++ ...t_authentication_client_manager_factory.py | 55 +++++++++++++++++++ 8 files changed, 144 insertions(+), 44 deletions(-) delete mode 100644 sdk/python/feast/permissions/client/auth_client_manager_factory.py create mode 100644 sdk/python/feast/permissions/client/client_auth_token.py create mode 100644 sdk/python/feast/permissions/client/intra_comm_authentication_client_manager.py create mode 100644 sdk/python/tests/unit/permissions/auth/client/test_authentication_client_manager_factory.py diff --git a/sdk/python/feast/permissions/client/arrow_flight_auth_interceptor.py b/sdk/python/feast/permissions/client/arrow_flight_auth_interceptor.py index 724c7df5ca..7ef84fbeae 100644 --- a/sdk/python/feast/permissions/client/arrow_flight_auth_interceptor.py +++ b/sdk/python/feast/permissions/client/arrow_flight_auth_interceptor.py @@ -2,7 +2,7 @@ from feast.permissions.auth.auth_type import AuthType from feast.permissions.auth_model import AuthConfig -from feast.permissions.client.auth_client_manager_factory import get_auth_token +from feast.permissions.client.client_auth_token import get_auth_token class FlightBearerTokenInterceptor(fl.ClientMiddleware): diff --git a/sdk/python/feast/permissions/client/auth_client_manager.py b/sdk/python/feast/permissions/client/auth_client_manager.py index 82f9b7433e..2151cfb409 100644 --- a/sdk/python/feast/permissions/client/auth_client_manager.py +++ b/sdk/python/feast/permissions/client/auth_client_manager.py @@ -1,8 +1,49 @@ +import os from abc import ABC, abstractmethod +from feast.permissions.auth.auth_type import AuthType +from feast.permissions.auth_model import ( + AuthConfig, + KubernetesAuthConfig, + OidcClientAuthConfig, +) + class AuthenticationClientManager(ABC): @abstractmethod def get_token(self) -> str: """Retrieves the token based on the authentication type configuration""" pass + + +class AuthenticationClientManagerFactory(ABC): + def __init__(self, auth_config: AuthConfig): + self.auth_config = auth_config + + def get_auth_client_manager(self) -> AuthenticationClientManager: + from feast.permissions.client.intra_comm_authentication_client_manager import ( + IntraCommAuthClientManager, + ) + from feast.permissions.client.kubernetes_auth_client_manager import ( + KubernetesAuthClientManager, + ) + from feast.permissions.client.oidc_authentication_client_manager import ( + OidcAuthClientManager, + ) + + intra_communication_base64 = os.getenv("INTRA_COMMUNICATION_BASE64") + if intra_communication_base64: + return IntraCommAuthClientManager( + self.auth_config, intra_communication_base64 + ) + + if self.auth_config.type == AuthType.OIDC.value: + assert isinstance(self.auth_config, OidcClientAuthConfig) + return OidcAuthClientManager(self.auth_config) + elif self.auth_config.type == AuthType.KUBERNETES.value: + assert isinstance(self.auth_config, KubernetesAuthConfig) + return KubernetesAuthClientManager(self.auth_config) + else: + raise RuntimeError( + f"No Auth client manager implemented for the auth type:${self.auth_config.type}" + ) diff --git a/sdk/python/feast/permissions/client/auth_client_manager_factory.py b/sdk/python/feast/permissions/client/auth_client_manager_factory.py deleted file mode 100644 index 359072f38e..0000000000 --- a/sdk/python/feast/permissions/client/auth_client_manager_factory.py +++ /dev/null @@ -1,41 +0,0 @@ -import os -from typing import cast - -from feast.permissions.auth.auth_type import AuthType -from feast.permissions.auth_model import ( - AuthConfig, - KubernetesAuthConfig, - OidcAuthConfig, - OidcClientAuthConfig, -) -from feast.permissions.client.auth_client_manager import AuthenticationClientManager -from feast.permissions.client.kubernetes_auth_client_manager import ( - KubernetesAuthClientManager, -) -from feast.permissions.client.oidc_authentication_client_manager import ( - OidcAuthClientManager, -) - - -def get_auth_client_manager(auth_config: AuthConfig) -> AuthenticationClientManager: - if auth_config.type == AuthType.OIDC.value: - intra_communication_base64 = os.getenv("INTRA_COMMUNICATION_BASE64") - # If intra server communication call - if intra_communication_base64: - assert isinstance(auth_config, OidcAuthConfig) - client_auth_config = cast(OidcClientAuthConfig, auth_config) - else: - assert isinstance(auth_config, OidcClientAuthConfig) - client_auth_config = auth_config - return OidcAuthClientManager(client_auth_config) - elif auth_config.type == AuthType.KUBERNETES.value: - assert isinstance(auth_config, KubernetesAuthConfig) - return KubernetesAuthClientManager(auth_config) - else: - raise RuntimeError( - f"No Auth client manager implemented for the auth type:${auth_config.type}" - ) - - -def get_auth_token(auth_config: AuthConfig) -> str: - return get_auth_client_manager(auth_config).get_token() diff --git a/sdk/python/feast/permissions/client/client_auth_token.py b/sdk/python/feast/permissions/client/client_auth_token.py new file mode 100644 index 0000000000..68821e3f9c --- /dev/null +++ b/sdk/python/feast/permissions/client/client_auth_token.py @@ -0,0 +1,14 @@ +from feast.permissions.auth_model import ( + AuthConfig, +) +from feast.permissions.client.auth_client_manager import ( + AuthenticationClientManagerFactory, +) + + +def get_auth_token(auth_config: AuthConfig) -> str: + return ( + AuthenticationClientManagerFactory(auth_config) + .get_auth_client_manager() + .get_token() + ) diff --git a/sdk/python/feast/permissions/client/grpc_client_auth_interceptor.py b/sdk/python/feast/permissions/client/grpc_client_auth_interceptor.py index 5155b80cb5..121735e351 100644 --- a/sdk/python/feast/permissions/client/grpc_client_auth_interceptor.py +++ b/sdk/python/feast/permissions/client/grpc_client_auth_interceptor.py @@ -4,7 +4,7 @@ from feast.errors import FeastError from feast.permissions.auth_model import AuthConfig -from feast.permissions.client.auth_client_manager_factory import get_auth_token +from feast.permissions.client.client_auth_token import get_auth_token logger = logging.getLogger(__name__) diff --git a/sdk/python/feast/permissions/client/http_auth_requests_wrapper.py b/sdk/python/feast/permissions/client/http_auth_requests_wrapper.py index 3232e25025..ba02fab8d8 100644 --- a/sdk/python/feast/permissions/client/http_auth_requests_wrapper.py +++ b/sdk/python/feast/permissions/client/http_auth_requests_wrapper.py @@ -5,7 +5,7 @@ from feast.permissions.auth_model import ( AuthConfig, ) -from feast.permissions.client.auth_client_manager_factory import get_auth_token +from feast.permissions.client.client_auth_token import get_auth_token class AuthenticatedRequestsSession(Session): diff --git a/sdk/python/feast/permissions/client/intra_comm_authentication_client_manager.py b/sdk/python/feast/permissions/client/intra_comm_authentication_client_manager.py new file mode 100644 index 0000000000..678e1f39e5 --- /dev/null +++ b/sdk/python/feast/permissions/client/intra_comm_authentication_client_manager.py @@ -0,0 +1,31 @@ +import logging + +import jwt + +from feast.permissions.auth.auth_type import AuthType +from feast.permissions.auth_model import AuthConfig +from feast.permissions.client.auth_client_manager import AuthenticationClientManager + +logger = logging.getLogger(__name__) + + +class IntraCommAuthClientManager(AuthenticationClientManager): + def __init__(self, auth_config: AuthConfig, intra_communication_base64: str): + self.auth_config = auth_config + self.intra_communication_base64 = intra_communication_base64 + + def get_token(self): + if self.auth_config.type == AuthType.OIDC.value: + payload = { + "preferred_username": f"{self.intra_communication_base64}", # Subject claim + } + elif self.auth_config.type == AuthType.KUBERNETES.value: + payload = { + "sub": f":::{self.intra_communication_base64}", # Subject claim + } + else: + raise RuntimeError( + f"No Auth client manager implemented for the auth type:{self.auth_config.type}" + ) + + return jwt.encode(payload, "") diff --git a/sdk/python/tests/unit/permissions/auth/client/test_authentication_client_manager_factory.py b/sdk/python/tests/unit/permissions/auth/client/test_authentication_client_manager_factory.py new file mode 100644 index 0000000000..5a6a8d70fa --- /dev/null +++ b/sdk/python/tests/unit/permissions/auth/client/test_authentication_client_manager_factory.py @@ -0,0 +1,55 @@ +import os +from unittest import mock + +import assertpy +import jwt +import pytest +import yaml + +from feast.permissions.auth.auth_type import AuthType +from feast.permissions.auth_model import ( + AuthConfig, +) +from feast.permissions.client.auth_client_manager import ( + AuthenticationClientManagerFactory, +) +from feast.permissions.client.intra_comm_authentication_client_manager import ( + IntraCommAuthClientManager, +) + + +@mock.patch.dict(os.environ, {"INTRA_COMMUNICATION_BASE64": "server_intra_com_val"}) +def test_authentication_client_manager_factory(auth_config): + raw_config = yaml.safe_load(auth_config) + auth_config = AuthConfig(type=raw_config["auth"]["type"]) + + authentication_client_manager_factory = AuthenticationClientManagerFactory( + auth_config + ) + + authentication_client_manager = ( + authentication_client_manager_factory.get_auth_client_manager() + ) + + if auth_config.type not in [AuthType.KUBERNETES.value, AuthType.OIDC.value]: + with pytest.raises( + RuntimeError, + match=f"No Auth client manager implemented for the auth type:{auth_config.type}", + ): + authentication_client_manager.get_token() + else: + token = authentication_client_manager.get_token() + + decoded_token = jwt.decode(token, options={"verify_signature": False}) + assertpy.assert_that(authentication_client_manager).is_type_of( + IntraCommAuthClientManager + ) + + if AuthType.KUBERNETES.value == auth_config.type: + assertpy.assert_that(decoded_token["sub"]).is_equal_to( + ":::server_intra_com_val" + ) + elif AuthType.OIDC.value in auth_config.type: + assertpy.assert_that(decoded_token["preferred_username"]).is_equal_to( + "server_intra_com_val" + ) From 6a6a369eec427814f39c812f2f3da274e6100ad3 Mon Sep 17 00:00:00 2001 From: Daniele Martinoli <86618610+dmartinol@users.noreply.github.com> Date: Thu, 12 Sep 2024 18:07:59 +0200 Subject: [PATCH 068/185] chore: A full, minimal, reproducible example of the RBAC feature (#4501) A full, minimal, reproducible example of the RBAC feature Signed-off-by: Daniele Martinoli --- examples/rbac-local/01.1-start-keycloak.ipynb | 94 ++ examples/rbac-local/01.2-setup-keycloak.ipynb | 416 +++++++ examples/rbac-local/01.3-setup-feast.ipynb | 1029 +++++++++++++++++ examples/rbac-local/02-registry_server.ipynb | 73 ++ examples/rbac-local/03-online_server.ipynb | 111 ++ examples/rbac-local/04-offline_server.ipynb | 99 ++ examples/rbac-local/README.md | 57 + examples/rbac-local/client.ipynb | 607 ++++++++++ examples/rbac-local/client/feature_store.yaml | 12 + 9 files changed, 2498 insertions(+) create mode 100644 examples/rbac-local/01.1-start-keycloak.ipynb create mode 100644 examples/rbac-local/01.2-setup-keycloak.ipynb create mode 100644 examples/rbac-local/01.3-setup-feast.ipynb create mode 100644 examples/rbac-local/02-registry_server.ipynb create mode 100644 examples/rbac-local/03-online_server.ipynb create mode 100644 examples/rbac-local/04-offline_server.ipynb create mode 100644 examples/rbac-local/README.md create mode 100644 examples/rbac-local/client.ipynb create mode 100644 examples/rbac-local/client/feature_store.yaml diff --git a/examples/rbac-local/01.1-start-keycloak.ipynb b/examples/rbac-local/01.1-start-keycloak.ipynb new file mode 100644 index 0000000000..f73e699833 --- /dev/null +++ b/examples/rbac-local/01.1-start-keycloak.ipynb @@ -0,0 +1,94 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "e46a65b1-7cf0-4cc2-8aca-529d659630a4", + "metadata": {}, + "source": [ + "# Start Keycloak server" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "374e8693-7e47-4985-b7f6-a9b818b0b4d0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Updating the configuration and installing your custom providers, if any. Please wait.\n", + "2024-09-09 06:37:54,515 WARN [io.qua.dep.ind.IndexWrapper] (build-5) Failed to index org.springframework.core.io.Resource: Class does not exist in ClassLoader QuarkusClassLoader:Deployment Class Loader: PROD for keycloak@6d91790b\n", + "2024-09-09 06:37:54,518 WARN [io.qua.dep.ind.IndexWrapper] (build-5) Failed to index org.springframework.core.io.DefaultResourceLoader: Class does not exist in ClassLoader QuarkusClassLoader:Deployment Class Loader: PROD for keycloak@6d91790b\n", + "2024-09-09 06:37:54,519 WARN [io.qua.dep.ind.IndexWrapper] (build-5) Failed to index org.springframework.core.io.ResourceLoader: Class does not exist in ClassLoader QuarkusClassLoader:Deployment Class Loader: PROD for keycloak@6d91790b\n", + "2024-09-09 06:37:54,525 WARN [io.qua.dep.ind.IndexWrapper] (build-5) Failed to index org.apache.tools.ant.Task: Class does not exist in ClassLoader QuarkusClassLoader:Deployment Class Loader: PROD for keycloak@6d91790b\n", + "2024-09-09 06:37:54,568 WARN [io.qua.dep.ind.IndexWrapper] (build-5) Failed to index org.apache.activemq.artemis.core.journal.RecordInfo: Class does not exist in ClassLoader QuarkusClassLoader:Deployment Class Loader: PROD for keycloak@6d91790b\n", + "2024-09-09 06:37:54,568 WARN [io.qua.dep.ind.IndexWrapper] (build-5) Failed to index org.apache.activemq.artemis.core.journal.Journal: Class does not exist in ClassLoader QuarkusClassLoader:Deployment Class Loader: PROD for keycloak@6d91790b\n", + "2024-09-09 06:37:54,569 WARN [io.qua.dep.ind.IndexWrapper] (build-5) Failed to index io.mashona.logwriting.ArrayStore: Class does not exist in ClassLoader QuarkusClassLoader:Deployment Class Loader: PROD for keycloak@6d91790b\n", + "2024-09-09 06:37:54,573 WARN [io.qua.dep.ind.IndexWrapper] (build-5) Failed to index jakarta.jms.XAConnection: Class does not exist in ClassLoader QuarkusClassLoader:Deployment Class Loader: PROD for keycloak@6d91790b\n", + "2024-09-09 06:37:54,574 WARN [io.qua.dep.ind.IndexWrapper] (build-5) Failed to index jakarta.jms.XASession: Class does not exist in ClassLoader QuarkusClassLoader:Deployment Class Loader: PROD for keycloak@6d91790b\n", + "2024-09-09 06:37:54,574 WARN [io.qua.dep.ind.IndexWrapper] (build-5) Failed to index jakarta.jms.XAConnectionFactory: Class does not exist in ClassLoader QuarkusClassLoader:Deployment Class Loader: PROD for keycloak@6d91790b\n", + "2024-09-09 06:37:54,657 WARN [io.qua.dep.ind.IndexWrapper] (build-5) Failed to index jakarta.jms.Connection: Class does not exist in ClassLoader QuarkusClassLoader:Deployment Class Loader: PROD for keycloak@6d91790b\n", + "2024-09-09 06:37:58,410 INFO [io.qua.dep.QuarkusAugmentor] (main) Quarkus augmentation completed in 7235ms\n", + "2024-09-09 06:37:59,697 INFO [org.keycloak.quarkus.runtime.hostname.DefaultHostnameProvider] (main) Hostname settings: Base URL: , Hostname: , Strict HTTPS: false, Path: , Strict BackChannel: false, Admin URL: , Admin: , Port: -1, Proxied: false\n", + "2024-09-09 06:37:59,903 WARN [org.infinispan.CONFIG] (keycloak-cache-init) ISPN000569: Unable to persist Infinispan internal caches as no global state enabled\n", + "2024-09-09 06:37:59,949 INFO [org.infinispan.CONTAINER] (keycloak-cache-init) ISPN000556: Starting user marshaller 'org.infinispan.jboss.marshalling.core.JBossUserMarshaller'\n", + "2024-09-09 06:38:01,394 WARN [io.quarkus.agroal.runtime.DataSources] (JPA Startup Thread) Datasource enables XA but transaction recovery is not enabled. Please enable transaction recovery by setting quarkus.transaction-manager.enable-recovery=true, otherwise data may be lost if the application is terminated abruptly\n", + "2024-09-09 06:38:02,119 INFO [org.keycloak.connections.infinispan.DefaultInfinispanConnectionProviderFactory] (main) Node name: node_693934, Site name: null\n", + "2024-09-09 06:38:02,122 INFO [org.keycloak.broker.provider.AbstractIdentityProviderMapper] (main) Registering class org.keycloak.broker.provider.mappersync.ConfigSyncEventListener\n", + "2024-09-09 06:38:03,086 INFO [org.keycloak.quarkus.runtime.storage.legacy.liquibase.QuarkusJpaUpdaterProvider] (main) Initializing database schema. Using changelog META-INF/jpa-changelog-master.xml\n", + "\n", + "UPDATE SUMMARY\n", + "Run: 124\n", + "Previously run: 0\n", + "Filtered out: 0\n", + "-------------------------------\n", + "Total change sets: 124\n", + "\n", + "2024-09-09 06:38:05,143 INFO [org.keycloak.services] (main) KC-SERVICES0050: Initializing master realm\n", + "2024-09-09 06:38:06,418 INFO [org.keycloak.services] (main) KC-SERVICES0009: Added user 'admin' to realm 'master'\n", + "2024-09-09 06:38:06,492 INFO [io.quarkus] (main) Keycloak 24.0.4 on JVM (powered by Quarkus 3.8.4) started in 7.761s. Listening on: http://0.0.0.0:8080\n", + "2024-09-09 06:38:06,492 INFO [io.quarkus] (main) Profile dev activated. \n", + "2024-09-09 06:38:06,492 INFO [io.quarkus] (main) Installed features: [agroal, cdi, hibernate-orm, jdbc-h2, keycloak, logging-gelf, narayana-jta, reactive-routes, resteasy-reactive, resteasy-reactive-jackson, smallrye-context-propagation, vertx]\n", + "2024-09-09 06:38:06,495 WARN [org.keycloak.quarkus.runtime.KeycloakMain] (main) Running the server in development mode. DO NOT use this configuration in production.\n" + ] + } + ], + "source": [ + "!docker run --rm -p 9999:8080 --name my-keycloak \\\n", + "-e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin \\\n", + "quay.io/keycloak/keycloak:24.0.4 start-dev" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d2d1e035-85b3-4d77-abb3-13af5e31ef37", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/rbac-local/01.2-setup-keycloak.ipynb b/examples/rbac-local/01.2-setup-keycloak.ipynb new file mode 100644 index 0000000000..d896bd82df --- /dev/null +++ b/examples/rbac-local/01.2-setup-keycloak.ipynb @@ -0,0 +1,416 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "e8952066-7a10-4c9b-a4b7-27be074ae269", + "metadata": {}, + "source": [ + "## Create Keycloak resources" + ] + }, + { + "cell_type": "markdown", + "id": "7252812d-90eb-4752-91a7-d46b400bacd8", + "metadata": {}, + "source": [ + "Wait until Keycloak is running" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "e5d13f76-f184-44f6-8542-54a61060e531", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\u001b[36m\"Status\"\u001b[0m:\u001b[32m \"running\"\u001b[0m,\u001b[36m \"Running\"\u001b[0m:\u001b[95m true\u001b[0m,\u001b[36m \"Paused\"\u001b[0m:\u001b[95m false\u001b[0m,\u001b[36m \"Restarting\"\u001b[0m:\u001b[95m false\u001b[0m,\u001b[36m \"OOMKilled\"\u001b[0m:\u001b[95m false\u001b[0m,\u001b[36m \"Dead\"\u001b[0m:\u001b[95m false\u001b[0m,\u001b[36m \"Pid\"\u001b[0m:\u001b[95m 2838024\u001b[0m,\u001b[36m \"ExitCode\"\u001b[0m:\u001b[95m 0\u001b[0m,\u001b[36m \"Error\"\u001b[0m:\u001b[32m \"\"\u001b[0m,\u001b[36m \"StartedAt\"\u001b[0m:\u001b[32m \"2024-09-09T06:37:49.055739669Z\"\u001b[0m,\u001b[36m \"FinishedAt\"\u001b[0m:\u001b[32m \"0001-01-01T00:00:00Z\"\u001b[0m}\n" + ] + } + ], + "source": [ + "!docker inspect --format='json' my-keycloak | yq '.[0].State'" + ] + }, + { + "cell_type": "markdown", + "id": "cc9a6329-4e89-464c-ac48-dbbadaf72a2b", + "metadata": {}, + "source": [ + "Then create a sample realm and client with some roles and users matching the test environment." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "d5c60591-f41d-4a5e-9b18-93385a889495", + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "import json\n", + "from dotenv import set_key\n", + "\n", + "OIDC_SERVER_URL = \"http://0.0.0.0:9999\"\n", + "ADMIN_USERNAME = \"admin\"\n", + "ADMIN_PASSWORD = \"admin\"\n", + "\n", + "access_token: str = \"\"" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "d16969bc-423a-4d18-afa3-97a791b84b13", + "metadata": {}, + "outputs": [], + "source": [ + "def get_token():\n", + " token_url = f\"{OIDC_SERVER_URL}/realms/master/protocol/openid-connect/token\"\n", + "\n", + " token_data = {\n", + " \"grant_type\": \"password\",\n", + " \"client_id\": \"admin-cli\",\n", + " \"username\": ADMIN_USERNAME,\n", + " \"password\": ADMIN_PASSWORD,\n", + " }\n", + "\n", + " token_response = requests.post(token_url, data=token_data)\n", + " if token_response.status_code == 200:\n", + " global access_token\n", + " access_token = token_response.json()[\"access_token\"]\n", + " return access_token\n", + " else:\n", + " print(\n", + " f\"Failed to obtain access token: {token_response.status_code} - {token_response.text}\"\n", + " )\n", + " raise Exception(\"Not authenticated\")\n", + "\n", + "\n", + "def keycloak_post(endpoint, data=None):\n", + " url = f\"{OIDC_SERVER_URL}/admin/{endpoint}\"\n", + " print(f\"Creating {endpoint}\")\n", + " global access_token\n", + " headers = {\n", + " \"Content-Type\": \"application/json\",\n", + " \"Authorization\": f\"Bearer {access_token}\",\n", + " }\n", + " response = requests.request(\"POST\", url, headers=headers, data=json.dumps(data))\n", + " print(f\"POST response.status_code is {response.status_code}\")\n", + " return response.status_code\n", + "\n", + "\n", + "def keycloak_get(endpoint):\n", + " url = f\"{OIDC_SERVER_URL}/admin/{endpoint}\"\n", + " global access_token\n", + " headers = {\n", + " \"Content-Type\": \"application/json\",\n", + " \"Authorization\": f\"Bearer {access_token}\",\n", + " }\n", + " response = requests.request(\"GET\", url, headers=headers)\n", + " print(f\"GET response.status_code is {response.status_code}\")\n", + " return response.json()\n", + "\n", + "\n", + "def create_realm(realm_name):\n", + " data = {\"realm\": realm_name, \"enabled\": \"true\"}\n", + " keycloak_post(\"realms\", data=data)\n", + " response = keycloak_get(f\"realms/{realm_name}\")\n", + " return response[\"id\"]\n", + "\n", + "\n", + "def create_client(realm_name, client_name):\n", + " data = {\n", + " \"clientId\": client_name,\n", + " \"enabled\": \"true\",\n", + " \"redirectUris\": [\n", + " \"http://localhost:8000/*\",\n", + " \"http://127.0.0.1:8000/*\",\n", + " \"http://0.0.0.0:8000/*\",\n", + " ],\n", + " \"publicClient\": False,\n", + " \"authorizationServicesEnabled\": True,\n", + " \"protocol\": \"openid-connect\",\n", + " \"standardFlowEnabled\": True,\n", + " \"directAccessGrantsEnabled\": True,\n", + " \"serviceAccountsEnabled\": True,\n", + " }\n", + " keycloak_post(f\"realms/{realm_name}/clients\", data=data)\n", + " response = keycloak_get(f\"realms/{realm_name}/clients\")\n", + " client = None\n", + " for c in response:\n", + " if c[\"clientId\"] == client_name:\n", + " client = c\n", + " break\n", + " client_id = client[\"id\"]\n", + " client_secret = client[\"secret\"]\n", + " return client_id, client_secret\n", + "\n", + "\n", + "def create_client_roles(realm_name, client_id, roles):\n", + " for role_name in roles:\n", + " data = {\"name\": role_name, \"clientRole\": True}\n", + " keycloak_post(f\"realms/{realm_name}/clients/{client_id}/roles\", data=data)\n", + "\n", + " response = keycloak_get(f\"realms/{realm_name}/clients/{client_id}/roles\")\n", + " roles_by_name = dict((role[\"name\"], role[\"id\"]) for role in response)\n", + " print(roles_by_name)\n", + " return roles_by_name\n", + "\n", + "\n", + "def create_user_with_roles(\n", + " realm_name, username, password, client_id, roles_by_name, roles\n", + "):\n", + " data = {\n", + " \"username\": username,\n", + " \"enabled\": True,\n", + " \"email\": f\"{username}@poc.com\",\n", + " \"emailVerified\": True,\n", + " \"firstName\": \"user\",\n", + " \"lastName\": f\"{username}\",\n", + " \"credentials\": [{\"type\": \"password\", \"value\": password}],\n", + " \"realmRoles\": [],\n", + " }\n", + " keycloak_post(f\"realms/{realm_name}/users\", data=data)\n", + " response = keycloak_get(f\"realms/{realm_name}/users\")\n", + " user = None\n", + " for u in response:\n", + " if u[\"username\"] == username:\n", + " user = u\n", + " break\n", + " user_id = user[\"id\"]\n", + "\n", + " data = [\n", + " {\n", + " \"id\": roles_by_name[role_name],\n", + " \"name\": role_name,\n", + " }\n", + " for role_name in roles\n", + " ]\n", + " keycloak_post(\n", + " f\"realms/{realm_name}/users/{user_id}/role-mappings/clients/{client_id}\",\n", + " data=data,\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e097fac1-f2c3-4afe-b78c-2c8279e3a84e", + "metadata": {}, + "outputs": [], + "source": [ + "get_token()" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "f114fa41-8cea-486f-baf4-998cbf69fea4", + "metadata": {}, + "outputs": [], + "source": [ + "realm_name = \"rbac_example\"\n", + "client_name = \"app\"\n", + "password = \"password\"" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "0f889548-9b60-448b-beed-ac3fc1890b13", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Creating realms\n", + "POST response.status_code is 201\n", + "GET response.status_code is 200\n", + "Creating realms/rbac_example/clients\n", + "POST response.status_code is 201\n", + "GET response.status_code is 200\n", + "Creating realms/rbac_example/clients/c3475e89-27c3-41ac-a3d1-0bbcaf68083b/roles\n", + "POST response.status_code is 201\n", + "Creating realms/rbac_example/clients/c3475e89-27c3-41ac-a3d1-0bbcaf68083b/roles\n", + "POST response.status_code is 201\n", + "Creating realms/rbac_example/clients/c3475e89-27c3-41ac-a3d1-0bbcaf68083b/roles\n", + "POST response.status_code is 201\n", + "Creating realms/rbac_example/clients/c3475e89-27c3-41ac-a3d1-0bbcaf68083b/roles\n", + "POST response.status_code is 201\n", + "GET response.status_code is 200\n", + "{'store_admin': '2d7a675f-031d-42b1-aba6-eb28a95561af', 'batch_admin': '8664084a-4e3c-42b0-8e37-70a8fea012b3', 'reader': '6cbf4473-c165-48bd-b572-d20133ae2b2b', 'uma_protection': '172d464d-92c7-4055-95af-3e048d8077b2', 'fresh_writer': '9e2abf47-a7af-414e-bf14-2c9897933532'}\n" + ] + } + ], + "source": [ + "realm_id = create_realm(realm_name)\n", + "client_id, client_secret = create_client(realm_name, client_name)\n", + "\n", + "roles_by_name = create_client_roles(\n", + " realm_name, client_id, [\"reader\", \"fresh_writer\", \"store_admin\", \"batch_admin\"]\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "a3430d83-107d-44ad-acf2-0df810dff0ff", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Creating realms/rbac_example/users\n", + "POST response.status_code is 201\n", + "GET response.status_code is 200\n", + "Creating realms/rbac_example/users/a87b4ca8-e1a9-40f7-a166-f48fe45beec2/role-mappings/clients/c3475e89-27c3-41ac-a3d1-0bbcaf68083b\n", + "POST response.status_code is 204\n", + "Creating realms/rbac_example/users\n", + "POST response.status_code is 201\n", + "GET response.status_code is 200\n", + "Creating realms/rbac_example/users/eb343a9b-d800-4fff-96b6-4588c7db08de/role-mappings/clients/c3475e89-27c3-41ac-a3d1-0bbcaf68083b\n", + "POST response.status_code is 204\n", + "Creating realms/rbac_example/users\n", + "POST response.status_code is 201\n", + "GET response.status_code is 200\n", + "Creating realms/rbac_example/users/91bfbaae-e1fd-4167-9432-2d1d8ca8c838/role-mappings/clients/c3475e89-27c3-41ac-a3d1-0bbcaf68083b\n", + "POST response.status_code is 204\n", + "Creating realms/rbac_example/users\n", + "POST response.status_code is 201\n", + "GET response.status_code is 200\n", + "Creating realms/rbac_example/users/4d67e8ca-6c2a-48b7-b511-c3f6197aa5ae/role-mappings/clients/c3475e89-27c3-41ac-a3d1-0bbcaf68083b\n", + "POST response.status_code is 204\n" + ] + } + ], + "source": [ + "create_user_with_roles(\n", + " realm_name, \"reader\", password, client_id, roles_by_name, [\"reader\"]\n", + ")\n", + "create_user_with_roles(\n", + " realm_name,\n", + " \"writer\",\n", + " password,\n", + " client_id,\n", + " roles_by_name,\n", + " [\"fresh_writer\"],\n", + ")\n", + "create_user_with_roles(\n", + " realm_name,\n", + " \"batch_admin\",\n", + " password,\n", + " client_id,\n", + " roles_by_name,\n", + " [\"batch_admin\"],\n", + ")\n", + "create_user_with_roles(\n", + " realm_name,\n", + " \"admin\",\n", + " password,\n", + " client_id,\n", + " roles_by_name,\n", + " [\"store_admin\"],\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "54317f9e-476b-4b8e-864a-a07c54b549f4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Realm rbac_example setup completed.\n", + "Client app created with ID c3475e89-27c3-41ac-a3d1-0bbcaf68083b and secret REDACTED\n", + "Settings configured in .env\n" + ] + } + ], + "source": [ + "print(f\"Realm {realm_name} setup completed.\")\n", + "print(\n", + " f\"Client {client_name} created with ID {client_id} and secret {client_secret}\"\n", + ")\n", + "\n", + "env_file = \".env\"\n", + "with open(env_file, \"w\") as file:\n", + " pass\n", + "\n", + "# Write property P=1 to the .env file\n", + "set_key(env_file, \"OIDC_SERVER_URL\", OIDC_SERVER_URL)\n", + "set_key(env_file, \"REALM\", realm_name)\n", + "set_key(env_file, \"CLIENT_ID\", client_name)\n", + "set_key(env_file, \"CLIENT_SECRET\", client_secret)\n", + "set_key(env_file, \"PASSWORD\", password)\n", + "print(f\"Settings configured in {env_file}\")" + ] + }, + { + "cell_type": "markdown", + "id": "35dcd5ed-4004-4570-965f-0f68668605d8", + "metadata": {}, + "source": [ + "The [.env](.env) file contains the settings of the created realm, including the client secret to be used to connect the server." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "46a1e2c7-e379-461d-b0bf-82354378e830", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "OIDC_SERVER_URL='http://0.0.0.0:9999'\n", + "REALM='rbac_example'\n", + "CLIENT_ID='app'\n", + "CLIENT_SECRET='REDACTED'\n", + "PASSWORD='password'\n" + ] + } + ], + "source": [ + "!cat .env" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d035826b-64d6-47cc-a48e-26eb29b31fc7", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/rbac-local/01.3-setup-feast.ipynb b/examples/rbac-local/01.3-setup-feast.ipynb new file mode 100644 index 0000000000..e7e0943094 --- /dev/null +++ b/examples/rbac-local/01.3-setup-feast.ipynb @@ -0,0 +1,1029 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a7b2570a-bdf1-477a-8799-0aefe81a0e28", + "metadata": {}, + "source": [ + "## Setup Feast\n", + "Create a sample `rbac` project with local storage." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "74c1ee91-1816-4338-aabf-7851b655b061", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Creating a new Feast repository in \u001b[1m\u001b[32m/Users/dmartino/projects/AI/feast/feast/examples/rbac-local/rbac\u001b[0m.\n", + "\n" + ] + } + ], + "source": [ + "!rm -rf rbac\n", + "!feast init rbac" + ] + }, + { + "cell_type": "markdown", + "id": "e3215797-198a-49af-a241-7e0117634897", + "metadata": {}, + "source": [ + "Update the `feature_store.yaml` with an `auth` section derived from the Keycloak setup file [.env](.env)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "a09d2198-9e3a-48f6-8c9d-72d62d20cd57", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "OIDC_SERVER_URL='http://0.0.0.0:9999'\n", + "REALM='rbac_example'\n", + "CLIENT_ID='app'\n", + "CLIENT_SECRET='REDACTED'\n", + "PASSWORD='password'\n" + ] + } + ], + "source": [ + "!cat .env" + ] + }, + { + "cell_type": "markdown", + "id": "6cd89872-a6c6-4be0-a6e3-8fd60d448b7b", + "metadata": {}, + "source": [ + "### Update the server YAML\n", + "Update the server YAML to use OIDC authorization" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "e16d5a44-ab0c-4ca8-8491-e7d9073469f8", + "metadata": {}, + "outputs": [], + "source": [ + "from dotenv import load_dotenv\n", + "import os\n", + "import yaml\n", + "\n", + "def load_config_file(path):\n", + " load_dotenv()\n", + "\n", + " with open(path, 'r') as file:\n", + " config = yaml.safe_load(file) or {}\n", + " return config" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "cd30523b-4e1c-4d56-9c72-84aacb46b29d", + "metadata": {}, + "outputs": [], + "source": [ + "def update_config_with_auth(config, is_client=False):\n", + " config['auth']={}\n", + " config['auth']['type']='oidc'\n", + " config['auth']['auth_discovery_url']=f\"{os.getenv('OIDC_SERVER_URL')}/realms/{os.getenv('REALM')}/.well-known/openid-configuration\"\n", + " config['auth']['client_id']=os.getenv('CLIENT_ID')\n", + " if is_client:\n", + " config['auth']['client_secret']=os.getenv('CLIENT_SECRET')\n", + " config['auth']['username']=''\n", + " config['auth']['password']='password'" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "1631a8c8-f635-4970-8653-06c147b1c128", + "metadata": {}, + "outputs": [], + "source": [ + "def update_config_file(path):\n", + " with open(path, 'w') as file:\n", + " yaml.safe_dump(config, file, default_flow_style=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "78898d46-1185-4528-8f08-b137dd49246a", + "metadata": {}, + "outputs": [], + "source": [ + "config = load_config_file('rbac/feature_repo/feature_store.yaml')\n", + "update_config_with_auth(config)\n", + "update_config_file('rbac/feature_repo/feature_store.yaml')" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "e2437286-2907-4818-87ad-a2293f21311e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "auth:\n", + " auth_discovery_url: http://0.0.0.0:9999/realms/rbac_example/.well-known/openid-configuration\n", + " client_id: app\n", + " type: oidc\n", + "entity_key_serialization_version: 2\n", + "online_store:\n", + " path: data/online_store.db\n", + " type: sqlite\n", + "project: rbac\n", + "provider: local\n", + "registry: data/registry.db\n" + ] + } + ], + "source": [ + "!cat rbac/feature_repo/feature_store.yaml" + ] + }, + { + "cell_type": "markdown", + "id": "fa715453-8c41-4f57-8cf2-c96f6a211cde", + "metadata": {}, + "source": [ + "### Update the client YAML\n", + "Update the client YAML to use OIDC authorization" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "886a558a-1746-44fa-9e38-0e381b3b3deb", + "metadata": {}, + "outputs": [], + "source": [ + "config = load_config_file('client/feature_store.yaml')\n", + "update_config_with_auth(config, is_client=True)\n", + "update_config_file('client/feature_store.yaml')" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "267a72e4-443a-4b08-bd59-84d475a29e2a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "auth:\n", + " auth_discovery_url: http://0.0.0.0:9999/realms/rbac_example/.well-known/openid-configuration\n", + " client_id: app\n", + " client_secret: REDACTED\n", + " password: password\n", + " type: oidc\n", + " username: ''\n", + "entity_key_serialization_version: 2\n", + "offline_store:\n", + " host: localhost\n", + " port: 8815\n", + " type: remote\n", + "online_store:\n", + " path: http://localhost:6566\n", + " type: remote\n", + "project: rbac\n", + "registry:\n", + " path: localhost:6570\n", + " registry_type: remote\n" + ] + } + ], + "source": [ + "!cat client/feature_store.yaml" + ] + }, + { + "cell_type": "markdown", + "id": "f71f5189-4423-4720-bbd2-fcb9b778a26b", + "metadata": {}, + "source": [ + "### Apply the configuration" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "e0c24e05-6e38-4ff1-9c39-73818fe41f18", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Applying changes for project rbac\n", + "/Users/dmartino/projects/AI/feast/feast/sdk/python/feast/feature_store.py:562: RuntimeWarning: On demand feature view is an experimental feature. This API is stable, but the functionality does not scale well for offline retrieval\n", + " warnings.warn(\n", + "Created project \u001b[1m\u001b[32mrbac\u001b[0m\n", + "Created entity \u001b[1m\u001b[32mdriver\u001b[0m\n", + "Created feature view \u001b[1m\u001b[32mdriver_hourly_stats\u001b[0m\n", + "Created feature view \u001b[1m\u001b[32mdriver_hourly_stats_fresh\u001b[0m\n", + "Created on demand feature view \u001b[1m\u001b[32mtransformed_conv_rate_fresh\u001b[0m\n", + "Created on demand feature view \u001b[1m\u001b[32mtransformed_conv_rate\u001b[0m\n", + "Created feature service \u001b[1m\u001b[32mdriver_activity_v1\u001b[0m\n", + "Created feature service \u001b[1m\u001b[32mdriver_activity_v3\u001b[0m\n", + "Created feature service \u001b[1m\u001b[32mdriver_activity_v2\u001b[0m\n", + "\n", + "Created sqlite table \u001b[1m\u001b[32mrbac_driver_hourly_stats_fresh\u001b[0m\n", + "Created sqlite table \u001b[1m\u001b[32mrbac_driver_hourly_stats\u001b[0m\n", + "\n" + ] + } + ], + "source": [ + "!feast -c rbac/feature_repo apply" + ] + }, + { + "cell_type": "markdown", + "id": "69b9857a-e32b-47ed-a120-57919ecb6b5d", + "metadata": {}, + "source": [ + "### Validate permissions" + ] + }, + { + "cell_type": "markdown", + "id": "867f565d-9740-4790-8d11-31001d920358", + "metadata": {}, + "source": [ + "There are no permissions after applying the example:" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "004f16bf-d125-4aec-b683-3e9653815a27", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "NAME TYPES NAME_PATTERN ACTIONS ROLES REQUIRED_TAGS\n" + ] + } + ], + "source": [ + "!feast -c rbac/feature_repo permissions list" + ] + }, + { + "cell_type": "markdown", + "id": "f2276488-39ec-4ae8-bb69-08dce7ad1bd4", + "metadata": {}, + "source": [ + "The `permissions check` command identifies the resources that have no permissions matching their type, name or tags." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "9fdd2660-c0f5-4dc9-a2da-d45751dcfa01", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1m\u001b[31mThe following resources are not secured by any permission configuration:\u001b[0m\n", + "NAME TYPE\n", + "driver Entity\n", + "driver_hourly_stats FeatureView\n", + "driver_hourly_stats_fresh FeatureView\n", + "transformed_conv_rate_fresh OnDemandFeatureView\n", + "transformed_conv_rate OnDemandFeatureView\n", + "driver_activity_v1 FeatureService\n", + "driver_activity_v3 FeatureService\n", + "driver_activity_v2 FeatureService\n", + "vals_to_add RequestSource\n", + "driver_stats_push_source PushSource\n", + "driver_hourly_stats_source FileSource\n", + "\u001b[1m\u001b[31mThe following actions are not secured by any permission configuration (Note: this might not be a security concern, depending on the used APIs):\u001b[0m\n", + "NAME TYPE UNSECURED ACTIONS\n", + "driver Entity CREATE\n", + " DESCRIBE\n", + " UPDATE\n", + " DELETE\n", + " READ_ONLINE\n", + " READ_OFFLINE\n", + " WRITE_ONLINE\n", + " WRITE_OFFLINE\n", + "driver_hourly_stats FeatureView CREATE\n", + " DESCRIBE\n", + " UPDATE\n", + " DELETE\n", + " READ_ONLINE\n", + " READ_OFFLINE\n", + " WRITE_ONLINE\n", + " WRITE_OFFLINE\n", + "driver_hourly_stats_fresh FeatureView CREATE\n", + " DESCRIBE\n", + " UPDATE\n", + " DELETE\n", + " READ_ONLINE\n", + " READ_OFFLINE\n", + " WRITE_ONLINE\n", + " WRITE_OFFLINE\n", + "transformed_conv_rate_fresh OnDemandFeatureView CREATE\n", + " DESCRIBE\n", + " UPDATE\n", + " DELETE\n", + " READ_ONLINE\n", + " READ_OFFLINE\n", + " WRITE_ONLINE\n", + " WRITE_OFFLINE\n", + "transformed_conv_rate OnDemandFeatureView CREATE\n", + " DESCRIBE\n", + " UPDATE\n", + " DELETE\n", + " READ_ONLINE\n", + " READ_OFFLINE\n", + " WRITE_ONLINE\n", + " WRITE_OFFLINE\n", + "driver_activity_v1 FeatureService CREATE\n", + " DESCRIBE\n", + " UPDATE\n", + " DELETE\n", + " READ_ONLINE\n", + " READ_OFFLINE\n", + " WRITE_ONLINE\n", + " WRITE_OFFLINE\n", + "driver_activity_v3 FeatureService CREATE\n", + " DESCRIBE\n", + " UPDATE\n", + " DELETE\n", + " READ_ONLINE\n", + " READ_OFFLINE\n", + " WRITE_ONLINE\n", + " WRITE_OFFLINE\n", + "driver_activity_v2 FeatureService CREATE\n", + " DESCRIBE\n", + " UPDATE\n", + " DELETE\n", + " READ_ONLINE\n", + " READ_OFFLINE\n", + " WRITE_ONLINE\n", + " WRITE_OFFLINE\n", + "vals_to_add RequestSource CREATE\n", + " DESCRIBE\n", + " UPDATE\n", + " DELETE\n", + " READ_ONLINE\n", + " READ_OFFLINE\n", + " WRITE_ONLINE\n", + " WRITE_OFFLINE\n", + "driver_stats_push_source PushSource CREATE\n", + " DESCRIBE\n", + " UPDATE\n", + " DELETE\n", + " READ_ONLINE\n", + " READ_OFFLINE\n", + " WRITE_ONLINE\n", + " WRITE_OFFLINE\n", + "driver_hourly_stats_source FileSource CREATE\n", + " DESCRIBE\n", + " UPDATE\n", + " DELETE\n", + " READ_ONLINE\n", + " READ_OFFLINE\n", + " WRITE_ONLINE\n", + " WRITE_OFFLINE\n" + ] + } + ], + "source": [ + "!feast -c rbac/feature_repo permissions check" + ] + }, + { + "cell_type": "markdown", + "id": "eb65649d-7ba7-494f-9e01-772842304ca1", + "metadata": {}, + "source": [ + "### Applying permissions\n", + "Let's create some Permissions to cover basic scenarios.\n", + "\n", + "First a simple permission to read the status of all the objects." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "3e910c5d-2f27-4f19-b324-c00347133da7", + "metadata": {}, + "outputs": [], + "source": [ + "from feast import FeatureStore\n", + "from feast.feast_object import ALL_RESOURCE_TYPES\n", + "from feast.permissions.action import CRUD, AuthzedAction, ALL_ACTIONS\n", + "from feast.permissions.permission import Permission\n", + "from feast.permissions.policy import RoleBasedPolicy" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "9e85bb35-cf12-4860-90d6-d1cd4830049c", + "metadata": {}, + "outputs": [], + "source": [ + "store = FeatureStore(\"rbac/feature_repo\")" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "87cc7c4f-48af-4158-adee-b1ccd8a72ea7", + "metadata": {}, + "outputs": [], + "source": [ + "read_permission = Permission(\n", + " name=\"read_permission\",\n", + " types=ALL_RESOURCE_TYPES,\n", + " policy=RoleBasedPolicy(roles=[\"reader\"]),\n", + " actions=AuthzedAction.DESCRIBE\n", + ")\n", + "store.registry.apply_permission(read_permission, store.project)" + ] + }, + { + "cell_type": "markdown", + "id": "e1dcb0d3-21e3-44b7-9ad5-c6b2b1e45b33", + "metadata": {}, + "source": [ + "Now a specific permission to write online data (e.g. `materialize`) the `FeatureView`s whose name ends by `fresh`" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "1c2fecdd-056e-4462-b1ad-eec123e282dd", + "metadata": {}, + "outputs": [], + "source": [ + "from feast.feature_view import FeatureView\n", + "write_fresh_permission = Permission(\n", + " name=\"write_fresh_permission\",\n", + " types=FeatureView,\n", + " name_pattern=\".*_fresh\",\n", + " policy=RoleBasedPolicy(roles=[\"fresh_writer\"]),\n", + " actions=AuthzedAction.WRITE_ONLINE\n", + ")\n", + "store.registry.apply_permission(write_fresh_permission, store.project)" + ] + }, + { + "cell_type": "markdown", + "id": "71edd0ea-67b5-4845-b8ae-602ed3883bb7", + "metadata": {}, + "source": [ + "Another one to match allow access to OFFLINE functions." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "c74e677c-3959-4963-b683-a5289c8238c9", + "metadata": {}, + "outputs": [], + "source": [ + "from feast.feature_view import FeatureView\n", + "from feast.feature_service import FeatureService\n", + "from feast.on_demand_feature_view import OnDemandFeatureView\n", + "offline_permission = Permission(\n", + " name=\"offline_permission\",\n", + " types=[FeatureView, OnDemandFeatureView, FeatureService],\n", + " policy=RoleBasedPolicy(roles=[\"batch_admin\"]),\n", + " actions= CRUD + [AuthzedAction.WRITE_OFFLINE, AuthzedAction.READ_OFFLINE]\n", + ")\n", + "store.registry.apply_permission(offline_permission, store.project)" + ] + }, + { + "cell_type": "markdown", + "id": "3edc08f5-40e1-488a-b749-9b1f5fc31061", + "metadata": {}, + "source": [ + "Finally, ad `admin` permission to manage all the resources" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "739a26ee-e08e-461a-9f75-59158328fc90", + "metadata": {}, + "outputs": [], + "source": [ + "admin_permission = Permission(\n", + " name=\"admin_permission\",\n", + " types=ALL_RESOURCE_TYPES,\n", + " policy=RoleBasedPolicy(roles=[\"store_admin\"]),\n", + " actions=ALL_ACTIONS\n", + ")\n", + "store.registry.apply_permission(admin_permission, store.project)" + ] + }, + { + "cell_type": "markdown", + "id": "916c9399-866e-4796-9858-a890ceb29e48", + "metadata": {}, + "source": [ + "## Validate registered permissions" + ] + }, + { + "cell_type": "markdown", + "id": "aed869b3-c567-428f-8a69-9c322b62f7c6", + "metadata": {}, + "source": [ + "List all the permissions." + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "cd284369-1cef-4cf6-859f-ea79d1450ed2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "NAME TYPES NAME_PATTERN ACTIONS ROLES REQUIRED_TAGS\n", + "read_permission Project DESCRIBE reader -\n", + " FeatureView\n", + " OnDemandFeatureView\n", + " BatchFeatureView\n", + " StreamFeatureView\n", + " Entity\n", + " FeatureService\n", + " DataSource\n", + " ValidationReference\n", + " SavedDataset\n", + " Permission\n", + "write_fresh_permission FeatureView .*_fresh WRITE_ONLINE fresh_writer -\n", + "offline_permission FeatureView CREATE batch_admin -\n", + " OnDemandFeatureView DESCRIBE\n", + " FeatureService UPDATE\n", + " DELETE\n", + " WRITE_OFFLINE\n", + " READ_OFFLINE\n", + "admin_permission Project CREATE store_admin -\n", + " FeatureView DESCRIBE\n", + " OnDemandFeatureView UPDATE\n", + " BatchFeatureView DELETE\n", + " StreamFeatureView READ_ONLINE\n", + " Entity READ_OFFLINE\n", + " FeatureService WRITE_ONLINE\n", + " DataSource WRITE_OFFLINE\n", + " ValidationReference\n", + " SavedDataset\n", + " Permission\n" + ] + } + ], + "source": [ + "!feast -c rbac/feature_repo permissions list" + ] + }, + { + "cell_type": "markdown", + "id": "be3873ee-2514-4aec-8fe8-8b54a3602651", + "metadata": {}, + "source": [ + "List all the resources matching each configured permission." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "419df226-36df-4d19-be0d-ba82813fef80", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\u001b[1m\u001b[32mThe structure of the \u001b[1m\u001b[37mfeast-permissions list --verbose \u001b[1m\u001b[32mcommand will be as in the following example:\n", + "\n", + "\u001b[2mFor example: \u001b[0m\u001b[1m\u001b[32m\n", + "\n", + "permissions\n", + "├── permission_1 ['role names list']\n", + "│ ├── FeatureView: ['feature view names']\n", + "│ ├── FeatureService: none\n", + "│ └── ..\n", + "├── permission_2 ['role names list']\n", + "└── ..\n", + "\n", + "-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------\u001b[0m\n", + " \n", + "Permissions:\n", + "\n", + "permissions\n", + "├── read_permission ['reader']\n", + "│ ├── FeatureView ['driver_hourly_stats_fresh', 'transformed_conv_rate_fresh', 'transformed_conv_rate', 'driver_hourly_stats']\n", + "│ ├── OnDemandFeatureView ['transformed_conv_rate_fresh', 'transformed_conv_rate']\n", + "│ ├── BatchFeatureView ['driver_hourly_stats_fresh', 'driver_hourly_stats']\n", + "│ ├── StreamFeatureView: none\n", + "│ ├── Entity: ['driver']\n", + "│ ├── FeatureService: ['driver_activity_v3', 'driver_activity_v2', 'driver_activity_v1']\n", + "│ ├── DataSource: ['driver_stats_push_source', 'driver_hourly_stats_source', 'vals_to_add']\n", + "│ ├── ValidationReference: none\n", + "│ └── SavedDataset: none\n", + "├── write_fresh_permission ['fresh_writer']\n", + "│ └── FeatureView ['driver_hourly_stats_fresh']\n", + "├── offline_permission ['batch_admin']\n", + "│ ├── FeatureView ['driver_hourly_stats_fresh', 'transformed_conv_rate_fresh', 'transformed_conv_rate', 'driver_hourly_stats']\n", + "│ ├── OnDemandFeatureView ['transformed_conv_rate_fresh', 'transformed_conv_rate']\n", + "│ └── FeatureService: ['driver_activity_v3', 'driver_activity_v2', 'driver_activity_v1']\n", + "└── admin_permission ['store_admin']\n", + " ├── FeatureView ['driver_hourly_stats_fresh', 'transformed_conv_rate_fresh', 'transformed_conv_rate', 'driver_hourly_stats']\n", + " ├── OnDemandFeatureView ['transformed_conv_rate_fresh', 'transformed_conv_rate']\n", + " ├── BatchFeatureView ['driver_hourly_stats_fresh', 'driver_hourly_stats']\n", + " ├── StreamFeatureView: none\n", + " ├── Entity: ['driver']\n", + " ├── FeatureService: ['driver_activity_v3', 'driver_activity_v2', 'driver_activity_v1']\n", + " ├── DataSource: ['driver_stats_push_source', 'driver_hourly_stats_source', 'vals_to_add']\n", + " ├── ValidationReference: none\n", + " └── SavedDataset: none\n" + ] + } + ], + "source": [ + "!feast -c rbac/feature_repo permissions list -v" + ] + }, + { + "cell_type": "markdown", + "id": "90319f10-abce-4a18-9891-7428c8781187", + "metadata": {}, + "source": [ + "Describe one of the permissions." + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "cec436ce-5d1c-455e-a6d7-80f84380e83a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "spec:\n", + " name: admin_permission\n", + " types:\n", + " - PROJECT\n", + " - FEATURE_VIEW\n", + " - ON_DEMAND_FEATURE_VIEW\n", + " - BATCH_FEATURE_VIEW\n", + " - STREAM_FEATURE_VIEW\n", + " - ENTITY\n", + " - FEATURE_SERVICE\n", + " - DATA_SOURCE\n", + " - VALIDATION_REFERENCE\n", + " - SAVED_DATASET\n", + " - PERMISSION\n", + " actions:\n", + " - CREATE\n", + " - DESCRIBE\n", + " - UPDATE\n", + " - DELETE\n", + " - READ_ONLINE\n", + " - READ_OFFLINE\n", + " - WRITE_ONLINE\n", + " - WRITE_OFFLINE\n", + " policy:\n", + " roleBasedPolicy:\n", + " roles:\n", + " - store_admin\n", + "meta:\n", + " createdTimestamp: '2024-09-09T06:41:28.335684Z'\n", + " lastUpdatedTimestamp: '2024-09-09T06:41:28.335684Z'\n", + "\n" + ] + } + ], + "source": [ + "!feast -c rbac/feature_repo permissions describe admin_permission" + ] + }, + { + "cell_type": "markdown", + "id": "a267a3bb-9861-43eb-9f7b-33f5d5a23e81", + "metadata": {}, + "source": [ + "List the roles specified by these permissions." + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "b6a3f4a6-e3ab-4aaa-9a15-69ea63246b45", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+--------------+\n", + "| ROLE NAME |\n", + "+==============+\n", + "| batch_admin |\n", + "+--------------+\n", + "| fresh_writer |\n", + "+--------------+\n", + "| reader |\n", + "+--------------+\n", + "| store_admin |\n", + "+--------------+\n" + ] + } + ], + "source": [ + "!feast -c rbac/feature_repo permissions list-roles" + ] + }, + { + "cell_type": "markdown", + "id": "0dbb31d3-edc9-4146-a46c-146d7f59532a", + "metadata": {}, + "source": [ + "For each configured role, list all the resources and operations that are allowed to a user impersonating this role." + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "45832f21-43c6-4784-ba88-1e65fa8479b5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ROLE NAME RESOURCE NAME RESOURCE TYPE PERMITTED ACTIONS\n", + "batch_admin driver Entity -\n", + "batch_admin driver_hourly_stats FeatureView CREATE\n", + " DESCRIBE\n", + " UPDATE\n", + " DELETE\n", + " READ_OFFLINE\n", + " WRITE_OFFLINE\n", + "batch_admin driver_hourly_stats_fresh FeatureView CREATE\n", + " DESCRIBE\n", + " UPDATE\n", + " DELETE\n", + " READ_OFFLINE\n", + " WRITE_OFFLINE\n", + "batch_admin transformed_conv_rate_fresh OnDemandFeatureView CREATE\n", + " DESCRIBE\n", + " UPDATE\n", + " DELETE\n", + " READ_OFFLINE\n", + " WRITE_OFFLINE\n", + "batch_admin transformed_conv_rate OnDemandFeatureView CREATE\n", + " DESCRIBE\n", + " UPDATE\n", + " DELETE\n", + " READ_OFFLINE\n", + " WRITE_OFFLINE\n", + "batch_admin driver_activity_v1 FeatureService CREATE\n", + " DESCRIBE\n", + " UPDATE\n", + " DELETE\n", + " READ_OFFLINE\n", + " WRITE_OFFLINE\n", + "batch_admin driver_activity_v3 FeatureService CREATE\n", + " DESCRIBE\n", + " UPDATE\n", + " DELETE\n", + " READ_OFFLINE\n", + " WRITE_OFFLINE\n", + "batch_admin driver_activity_v2 FeatureService CREATE\n", + " DESCRIBE\n", + " UPDATE\n", + " DELETE\n", + " READ_OFFLINE\n", + " WRITE_OFFLINE\n", + "batch_admin vals_to_add RequestSource -\n", + "batch_admin driver_stats_push_source PushSource -\n", + "batch_admin driver_hourly_stats_source FileSource -\n", + "batch_admin read_permission Permission -\n", + "batch_admin write_fresh_permission Permission -\n", + "batch_admin offline_permission Permission -\n", + "batch_admin admin_permission Permission -\n", + "fresh_writer driver Entity -\n", + "fresh_writer driver_hourly_stats FeatureView -\n", + "fresh_writer driver_hourly_stats_fresh FeatureView WRITE_ONLINE\n", + "fresh_writer transformed_conv_rate_fresh OnDemandFeatureView -\n", + "fresh_writer transformed_conv_rate OnDemandFeatureView -\n", + "fresh_writer driver_activity_v1 FeatureService -\n", + "fresh_writer driver_activity_v3 FeatureService -\n", + "fresh_writer driver_activity_v2 FeatureService -\n", + "fresh_writer vals_to_add RequestSource -\n", + "fresh_writer driver_stats_push_source PushSource -\n", + "fresh_writer driver_hourly_stats_source FileSource -\n", + "fresh_writer read_permission Permission -\n", + "fresh_writer write_fresh_permission Permission -\n", + "fresh_writer offline_permission Permission -\n", + "fresh_writer admin_permission Permission -\n", + "reader driver Entity DESCRIBE\n", + "reader driver_hourly_stats FeatureView DESCRIBE\n", + "reader driver_hourly_stats_fresh FeatureView DESCRIBE\n", + "reader transformed_conv_rate_fresh OnDemandFeatureView DESCRIBE\n", + "reader transformed_conv_rate OnDemandFeatureView DESCRIBE\n", + "reader driver_activity_v1 FeatureService DESCRIBE\n", + "reader driver_activity_v3 FeatureService DESCRIBE\n", + "reader driver_activity_v2 FeatureService DESCRIBE\n", + "reader vals_to_add RequestSource DESCRIBE\n", + "reader driver_stats_push_source PushSource DESCRIBE\n", + "reader driver_hourly_stats_source FileSource DESCRIBE\n", + "reader read_permission Permission DESCRIBE\n", + "reader write_fresh_permission Permission DESCRIBE\n", + "reader offline_permission Permission DESCRIBE\n", + "reader admin_permission Permission DESCRIBE\n", + "store_admin driver Entity CREATE\n", + " DESCRIBE\n", + " UPDATE\n", + " DELETE\n", + " READ_ONLINE\n", + " READ_OFFLINE\n", + " WRITE_ONLINE\n", + " WRITE_OFFLINE\n", + "store_admin driver_hourly_stats FeatureView CREATE\n", + " DESCRIBE\n", + " UPDATE\n", + " DELETE\n", + " READ_ONLINE\n", + " READ_OFFLINE\n", + " WRITE_ONLINE\n", + " WRITE_OFFLINE\n", + "store_admin driver_hourly_stats_fresh FeatureView CREATE\n", + " DESCRIBE\n", + " UPDATE\n", + " DELETE\n", + " READ_ONLINE\n", + " READ_OFFLINE\n", + " WRITE_ONLINE\n", + " WRITE_OFFLINE\n", + "store_admin transformed_conv_rate_fresh OnDemandFeatureView CREATE\n", + " DESCRIBE\n", + " UPDATE\n", + " DELETE\n", + " READ_ONLINE\n", + " READ_OFFLINE\n", + " WRITE_ONLINE\n", + " WRITE_OFFLINE\n", + "store_admin transformed_conv_rate OnDemandFeatureView CREATE\n", + " DESCRIBE\n", + " UPDATE\n", + " DELETE\n", + " READ_ONLINE\n", + " READ_OFFLINE\n", + " WRITE_ONLINE\n", + " WRITE_OFFLINE\n", + "store_admin driver_activity_v1 FeatureService CREATE\n", + " DESCRIBE\n", + " UPDATE\n", + " DELETE\n", + " READ_ONLINE\n", + " READ_OFFLINE\n", + " WRITE_ONLINE\n", + " WRITE_OFFLINE\n", + "store_admin driver_activity_v3 FeatureService CREATE\n", + " DESCRIBE\n", + " UPDATE\n", + " DELETE\n", + " READ_ONLINE\n", + " READ_OFFLINE\n", + " WRITE_ONLINE\n", + " WRITE_OFFLINE\n", + "store_admin driver_activity_v2 FeatureService CREATE\n", + " DESCRIBE\n", + " UPDATE\n", + " DELETE\n", + " READ_ONLINE\n", + " READ_OFFLINE\n", + " WRITE_ONLINE\n", + " WRITE_OFFLINE\n", + "store_admin vals_to_add RequestSource CREATE\n", + " DESCRIBE\n", + " UPDATE\n", + " DELETE\n", + " READ_ONLINE\n", + " READ_OFFLINE\n", + " WRITE_ONLINE\n", + " WRITE_OFFLINE\n", + "store_admin driver_stats_push_source PushSource CREATE\n", + " DESCRIBE\n", + " UPDATE\n", + " DELETE\n", + " READ_ONLINE\n", + " READ_OFFLINE\n", + " WRITE_ONLINE\n", + " WRITE_OFFLINE\n", + "store_admin driver_hourly_stats_source FileSource CREATE\n", + " DESCRIBE\n", + " UPDATE\n", + " DELETE\n", + " READ_ONLINE\n", + " READ_OFFLINE\n", + " WRITE_ONLINE\n", + " WRITE_OFFLINE\n", + "store_admin read_permission Permission CREATE\n", + " DESCRIBE\n", + " UPDATE\n", + " DELETE\n", + " READ_ONLINE\n", + " READ_OFFLINE\n", + " WRITE_ONLINE\n", + " WRITE_OFFLINE\n", + "store_admin write_fresh_permission Permission CREATE\n", + " DESCRIBE\n", + " UPDATE\n", + " DELETE\n", + " READ_ONLINE\n", + " READ_OFFLINE\n", + " WRITE_ONLINE\n", + " WRITE_OFFLINE\n", + "store_admin offline_permission Permission CREATE\n", + " DESCRIBE\n", + " UPDATE\n", + " DELETE\n", + " READ_ONLINE\n", + " READ_OFFLINE\n", + " WRITE_ONLINE\n", + " WRITE_OFFLINE\n", + "store_admin admin_permission Permission CREATE\n", + " DESCRIBE\n", + " UPDATE\n", + " DELETE\n", + " READ_ONLINE\n", + " READ_OFFLINE\n", + " WRITE_ONLINE\n", + " WRITE_OFFLINE\n" + ] + } + ], + "source": [ + "!feast -c rbac/feature_repo permissions list-roles -v" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c7960d2c-e43f-46b4-8cb3-5c6fc9dbaba8", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/rbac-local/02-registry_server.ipynb b/examples/rbac-local/02-registry_server.ipynb new file mode 100644 index 0000000000..43a5ead908 --- /dev/null +++ b/examples/rbac-local/02-registry_server.ipynb @@ -0,0 +1,73 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "70df4877-177b-441a-a745-f0cd091e0a3a", + "metadata": {}, + "source": [ + "## Registry server" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "ef9f796d-f9d7-47d0-96c2-03b38a219d83", + "metadata": {}, + "outputs": [], + "source": [ + "!lsof -i :6570\n", + "# !kill -9 64859 98087" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "bd303508-9f32-4bdb-87c2-729e3ab62b4f", + "metadata": {}, + "outputs": [], + "source": [ + "from feast import FeatureStore\n", + "store = FeatureStore(repo_path=\"rbac/feature_repo\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29127952-f9d5-44c4-b7c3-437e0b55c4b0", + "metadata": {}, + "outputs": [], + "source": [ + "store.serve_registry(6570)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5c285fb3-442b-4bb4-bf34-2a61ae5fe76a", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/rbac-local/03-online_server.ipynb b/examples/rbac-local/03-online_server.ipynb new file mode 100644 index 0000000000..f80ef35a17 --- /dev/null +++ b/examples/rbac-local/03-online_server.ipynb @@ -0,0 +1,111 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d75bb824-a6cf-493e-87a8-2ae1095cf918", + "metadata": {}, + "source": [ + "## Online server" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "27a17dd4-08f5-4f01-b5a4-a76aa99952a1", + "metadata": {}, + "outputs": [], + "source": [ + "!lsof -i :6566\n", + "# !kill -9 64859 98087" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "897f5979-da53-4441-ac31-f5cd40abf6cd", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "YES\n" + ] + } + ], + "source": [ + "# This must be YES on MacOS\n", + "!echo $OBJC_DISABLE_INITIALIZE_FORK_SAFETY" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "73b219c3-7782-4e09-9897-d01f44ccae2d", + "metadata": {}, + "outputs": [], + "source": [ + "# from feast import FeatureStore\n", + "# store = FeatureStore(repo_path=\"rbac/feature_repo\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d1619739-f763-45bb-a1f1-53f6452bc60a", + "metadata": {}, + "outputs": [], + "source": [ + "# store.serve(\n", + "# host=\"localhost\",\n", + "# port=6566,\n", + "# type_=\"http\",\n", + "# no_access_log=False,\n", + "# workers=1,\n", + "# metrics=False,\n", + "# keep_alive_timeout=5,\n", + "# registry_ttl_sec=5,\n", + "# )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bc804040-9cd0-4dbc-a63d-a81de9422605", + "metadata": {}, + "outputs": [], + "source": [ + "!feast -c rbac/feature_repo serve" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c3bc63e7-cf7c-4132-b39b-3cd75a1d6755", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/rbac-local/04-offline_server.ipynb b/examples/rbac-local/04-offline_server.ipynb new file mode 100644 index 0000000000..62ad8b1a78 --- /dev/null +++ b/examples/rbac-local/04-offline_server.ipynb @@ -0,0 +1,99 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "36f5d04f-b456-4e65-91a8-482c91f854c1", + "metadata": {}, + "source": [ + "## Offline server" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "86924e3b-d7dc-46e1-a9f4-05c8abee4da8", + "metadata": {}, + "outputs": [], + "source": [ + "!lsof -i :8815\n", + "# !kill -9 64859 98087" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "73b219c3-7782-4e09-9897-d01f44ccae2d", + "metadata": {}, + "outputs": [], + "source": [ + "from feast import FeatureStore\n", + "store = FeatureStore(repo_path=\"rbac/feature_repo\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "75967678-0573-410e-b9dd-09743b67eac3", + "metadata": {}, + "outputs": [], + "source": [ + "import logging\n", + "import sys\n", + "from io import StringIO\n", + "logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')\n", + "logger = logging.getLogger() " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5400ee1b-de0a-4fe9-9003-83d0af0863e6", + "metadata": {}, + "outputs": [], + "source": [ + "store.serve_offline(\"localhost\", 8815)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8b822d5c-41d9-477a-8b42-c4701784bac2", + "metadata": {}, + "outputs": [], + "source": [ + "# Run this in case it's needed to force materialize from offline server\n", + "from datetime import datetime\n", + "store.materialize_incremental(end_date=datetime.now())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ff854a14-4649-4d40-94fa-b6e2b8577afa", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/rbac-local/README.md b/examples/rbac-local/README.md new file mode 100644 index 0000000000..dd1128e94a --- /dev/null +++ b/examples/rbac-local/README.md @@ -0,0 +1,57 @@ +# RBAC demo +RBAC demo with local environment. + +## System Requirements +* Clone of the Feast repo +* Docker +* yq + +## Architecture +The demo creates the following components: +* An OIDC authorization server using a Keycloak docker container and initialized for demo purposes with a sample realm. +* A sample feature store using `feast init`, later adapted to use the `oidc` authorization against the sample realm. +* Three servers running the registry, online and offline stores. +* A client application connected to the servers to run test code. + +## Setup the environment +Run the sample notebooks to setup the environment: +* [01.1-startkeycloak](./01.1-startkeycloak.ipynb) to start a Keycloak container. +* [01.2-setup-keycloak.ipynb](./01.2-setup-keycloak.ipynb) to configure Keycloak with all the needed resources for the next steps. +* [01.3-setup-feast.ipynb](./01.3-setup-feast.ipynb) to create the sample Feast store and inject the authoprization settings +* [02-registry_server.ipynb](./02-registry_server.ipynb) to start the Registry server +* [03-online_server.ipynb](./03-online_server.ipynb) to start the Online store server +* [04-offline_server.ipynb](04-offline_server.ipynb) to start the Offline store server + +**Note**: For MacOs users, you must set this environment variable before launching the notebook server: +```bash +OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES +``` + +## Goal +Once the environment is defined, we can use the [client.ipynb](./client.ipynb) notebook to verify how the behavior changes +according to the configured user. + +In particular, given the configured permissions: +| Permission | Types | Name pattern | Actions | Roles | +|------------|-------|--------------|---------|-------| +| read_permission | ALL | | DESCRIBE | reader | +| write_fresh_permission | FeatureView1 | .*_fresh | WRITE_ONLINE | fresh_writer | +| offline_permission | FeatureView, OnDemandFeatureView, FeatureService | | CRUD, WRITE_OFFLINE, QUERY_OFFLINE | batch_admin | +| admin_permission | ALL | | ALL | store_admin | + +and the user roles defined in Keycloak: +| User | Roles | +|------|-------| +| reader | reader | +| writer | fresh_writer | +| batch_admin | batch_admin | +| admin | store_admin | + +We should expect the following behavior for each test section of the [client notebook](./client.ipynb): +| User | Basic validation | Historical | Materialization | Online | Stream push | +|------|------------------|------------|-------------------|--------|-------------| +| reader | Ok | Denied | Denied | Denied | Denied | +| writer | Empty | Denied | Ok | Denied | Denied | +| batch_admin | No Entities and Permissions | Ok | Denied | Denied | Denied | +| admin | Ok | Ok | Ok | Ok | Ok | + diff --git a/examples/rbac-local/client.ipynb b/examples/rbac-local/client.ipynb new file mode 100644 index 0000000000..7e5561f5f7 --- /dev/null +++ b/examples/rbac-local/client.ipynb @@ -0,0 +1,607 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "bee9388f-8ffc-4fcd-930f-197ec3c2dd96", + "metadata": {}, + "source": [ + "# Test client" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "id": "eceb50b4-c516-4224-a0b1-efd31bb78c29", + "metadata": {}, + "outputs": [], + "source": [ + "import yaml\n", + "def update_username(username):\n", + " path = 'client/feature_store.yaml'\n", + " with open(path, 'r') as file:\n", + " config = yaml.safe_load(file) or {}\n", + " config['auth']['username'] = username\n", + " with open(path, 'w') as file:\n", + " yaml.safe_dump(config, file, default_flow_style=False)" + ] + }, + { + "cell_type": "markdown", + "id": "08a4020a-10ad-476a-af25-26a09d3d4786", + "metadata": {}, + "source": [ + "# Update test user\n", + "Use one of `reader`, `writer`, `batch_admin` or `admin` (password is fixed) as the current `username`." + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "id": "564849f9-c95a-4278-9fa7-fa09694e5d93", + "metadata": {}, + "outputs": [], + "source": [ + "username = 'reader'\n", + "update_username(username)" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "id": "6ffb2c42-5a5d-495c-92c5-0729f0144fb8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "auth:\n", + " auth_discovery_url: http://0.0.0.0:9999/realms/rbac_example/.well-known/openid-configuration\n", + " client_id: app\n", + " client_secret: REDACTED\n", + " password: password\n", + " type: oidc\n", + " username: reader\n", + "entity_key_serialization_version: 2\n", + "offline_store:\n", + " host: localhost\n", + " port: 8815\n", + " type: remote\n", + "online_store:\n", + " path: http://localhost:6566\n", + " type: remote\n", + "project: rbac\n", + "registry:\n", + " path: localhost:6570\n", + " registry_type: remote\n" + ] + } + ], + "source": [ + "!cat client/feature_store.yaml" + ] + }, + { + "cell_type": "markdown", + "id": "664b6f52-d8cf-4145-bf7a-fcce111a34da", + "metadata": {}, + "source": [ + "## Updating logger\n", + "The following is needed to log in the notebook the output the messages logged by th Feast application." + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "id": "3a6fe206-63f8-486f-88cb-b4e888cb6855", + "metadata": {}, + "outputs": [], + "source": [ + "import logging\n", + "import sys\n", + "from io import StringIO\n", + "logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')\n", + "logger = logging.getLogger()" + ] + }, + { + "cell_type": "markdown", + "id": "a1eb1495-1f38-4165-a6a4-26a2087f1635", + "metadata": {}, + "source": [ + "## Setup Feast client\n", + "Initialize the Feast store from the [client configuration](./client/feature_store.yaml)" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "id": "b2292e78-cf30-441c-b67f-36e1f1a81923", + "metadata": {}, + "outputs": [], + "source": [ + "from feast.feature_store import FeatureStore" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "id": "bb653327-9eb3-448f-b320-625337851522", + "metadata": {}, + "outputs": [], + "source": [ + "store = FeatureStore(repo_path=\"client\")" + ] + }, + { + "cell_type": "markdown", + "id": "7e826371-3df5-483a-878d-ce79e8b907e3", + "metadata": {}, + "source": [ + "## Basic validation\n", + "Verify the authorization config and run some GET APIs on the registry." + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "id": "a59979af-a438-436d-918c-3174d94ade5b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Authorization config is: {'auth_discovery_url': 'http://0.0.0.0:9999/realms/rbac_example/.well-known/openid-configuration', 'client_id': 'app', 'client_secret': 'REDACTED', 'password': 'password', 'type': 'oidc', 'username': 'reader'}\n" + ] + } + ], + "source": [ + "print(f\"Authorization config is: {store.config.auth}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "id": "bf0af19c-6609-4cb4-86f3-a976528c3966", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Entity found driver\n" + ] + } + ], + "source": [ + "for e in store.list_entities():\n", + " print(f\"Entity found {e.name}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "id": "0494a65f-64bf-45f0-a772-ee6d8b89c91e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "FeatureView found driver_hourly_stats of type FeatureView\n", + "FeatureView found driver_hourly_stats_fresh of type FeatureView\n", + "FeatureView found transformed_conv_rate_fresh of type OnDemandFeatureView\n", + "FeatureView found transformed_conv_rate of type OnDemandFeatureView\n" + ] + } + ], + "source": [ + "for fv in store.list_all_feature_views():\n", + " print(f\"FeatureView found {fv.name} of type {type(fv).__name__}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "id": "0832822f-e954-4d43-a96f-de5cf05acb2b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "FeatureService found driver_activity_v1 of type FeatureService\n", + "FeatureService found driver_activity_v3 of type FeatureService\n", + "FeatureService found driver_activity_v2 of type FeatureService\n" + ] + } + ], + "source": [ + "for fs in store.list_feature_services():\n", + " print(f\"FeatureService found {fs.name} of type {type(fs).__name__}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "id": "98fd0767-4305-4b18-a50b-298fa7103815", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "NAME TYPES NAME_PATTERN ACTIONS ROLES REQUIRED_TAGS\n", + "read_permission Project DESCRIBE reader -\n", + " FeatureView\n", + " OnDemandFeatureView\n", + " BatchFeatureView\n", + " StreamFeatureView\n", + " Entity\n", + " FeatureService\n", + " DataSource\n", + " ValidationReference\n", + " SavedDataset\n", + " Permission\n", + "write_fresh_permission FeatureView .*_fresh WRITE_ONLINE fresh_writer -\n", + "offline_permission FeatureView CREATE batch_admin -\n", + " OnDemandFeatureView DESCRIBE\n", + " FeatureService UPDATE\n", + " DELETE\n", + " WRITE_OFFLINE\n", + " READ_OFFLINE\n", + "admin_permission Project CREATE store_admin -\n", + " FeatureView DESCRIBE\n", + " OnDemandFeatureView UPDATE\n", + " BatchFeatureView DELETE\n", + " StreamFeatureView READ_ONLINE\n", + " Entity READ_OFFLINE\n", + " FeatureService WRITE_ONLINE\n", + " DataSource WRITE_OFFLINE\n", + " ValidationReference\n", + " SavedDataset\n", + " Permission\n" + ] + } + ], + "source": [ + "!feast -c client permissions list" + ] + }, + { + "cell_type": "markdown", + "id": "ad2d56ee-e7a9-463e-a597-932c10f8df1c", + "metadata": {}, + "source": [ + "## Validating with test_workflow.py\n", + "The following test functions were copied from the `test_workflow.py` template but we added `try` blocks to print only \n", + "the relevant error messages, since we expect to receive errors from the permission enforcement modules." + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "id": "930f7e8c-c2a0-4425-99c2-c9958a5a7632", + "metadata": {}, + "outputs": [], + "source": [ + "import subprocess\n", + "from datetime import datetime\n", + "\n", + "import pandas as pd\n", + "\n", + "from feast import FeatureStore\n", + "from feast.data_source import PushMode\n", + "\n", + "def fetch_historical_features_entity_df(store: FeatureStore, for_batch_scoring: bool):\n", + " # Note: see https://docs.feast.dev/getting-started/concepts/feature-retrieval for more details on how to retrieve\n", + " # for all entities in the offline store instead\n", + " entity_df = pd.DataFrame.from_dict(\n", + " {\n", + " # entity's join key -> entity values\n", + " \"driver_id\": [1001, 1002, 1003],\n", + " # \"event_timestamp\" (reserved key) -> timestamps\n", + " \"event_timestamp\": [\n", + " datetime(2021, 4, 12, 10, 59, 42),\n", + " datetime(2021, 4, 12, 8, 12, 10),\n", + " datetime(2021, 4, 12, 16, 40, 26),\n", + " ],\n", + " # (optional) label name -> label values. Feast does not process these\n", + " \"label_driver_reported_satisfaction\": [1, 5, 3],\n", + " # values we're using for an on-demand transformation\n", + " \"val_to_add\": [1, 2, 3],\n", + " \"val_to_add_2\": [10, 20, 30],\n", + " }\n", + " )\n", + " # For batch scoring, we want the latest timestamps\n", + " if for_batch_scoring:\n", + " entity_df[\"event_timestamp\"] = pd.to_datetime(\"now\", utc=True)\n", + "\n", + " try:\n", + " training_df = store.get_historical_features(\n", + " entity_df=entity_df,\n", + " features=[\n", + " \"driver_hourly_stats:conv_rate\",\n", + " \"driver_hourly_stats:acc_rate\",\n", + " \"driver_hourly_stats:avg_daily_trips\",\n", + " \"transformed_conv_rate:conv_rate_plus_val1\",\n", + " \"transformed_conv_rate:conv_rate_plus_val2\",\n", + " ],\n", + " ).to_df()\n", + " print(training_df.head())\n", + " except Exception as e:\n", + " print(f\"Failed to run `store.get_historical_features`: {e}\")\n", + "\n", + "\n", + "def fetch_online_features(store, source: str = \"\"):\n", + " entity_rows = [\n", + " # {join_key: entity_value}\n", + " {\n", + " \"driver_id\": 1001,\n", + " \"val_to_add\": 1000,\n", + " \"val_to_add_2\": 2000,\n", + " },\n", + " {\n", + " \"driver_id\": 1002,\n", + " \"val_to_add\": 1001,\n", + " \"val_to_add_2\": 2002,\n", + " },\n", + " ]\n", + " if source == \"feature_service\":\n", + " try:\n", + " features_to_fetch = store.get_feature_service(\"driver_activity_v1\")\n", + " except Exception as e:\n", + " print(f\"Failed to run `store.get_feature_service`: {e}\")\n", + " elif source == \"push\":\n", + " try:\n", + " features_to_fetch = store.get_feature_service(\"driver_activity_v3\")\n", + " except Exception as e:\n", + " print(f\"Failed to run `store.get_feature_service`: {e}\")\n", + " else:\n", + " features_to_fetch = [\n", + " \"driver_hourly_stats:acc_rate\",\n", + " \"transformed_conv_rate:conv_rate_plus_val1\",\n", + " \"transformed_conv_rate:conv_rate_plus_val2\",\n", + " ]\n", + " try:\n", + " returned_features = store.get_online_features(\n", + " features=features_to_fetch,\n", + " entity_rows=entity_rows,\n", + " ).to_dict()\n", + " for key, value in sorted(returned_features.items()):\n", + " print(key, \" : \", value)\n", + " except Exception as e:\n", + " print(f\"Failed to run `store.get_online_features`: {e}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "id": "86359ae5-e723-4976-89bb-e772f597ed60", + "metadata": {}, + "outputs": [], + "source": [ + "store = FeatureStore(repo_path=\"client\")" + ] + }, + { + "cell_type": "markdown", + "id": "c0fed355-a1ac-4515-ae27-9d0feca886f4", + "metadata": {}, + "source": [ + "### Historical features" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "id": "e18dba03-6199-4b48-a9cb-23e3fa51a505", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "--- Historical features for training ---\n", + "Failed to run `store.get_historical_features`: Permission error:\n", + "Permission offline_permission denied execution of ['READ_OFFLINE'] to FeatureView:driver_hourly_stats: Requires roles ['batch_admin'],Permission admin_permission denied execution of ['READ_OFFLINE'] to FeatureView:driver_hourly_stats: Requires roles ['store_admin']. Detail: Python exception: FeastPermissionError. gRPC client debug context: UNKNOWN:Error received from peer ipv6:%5B::1%5D:8815 {grpc_message:\"Permission error:\\nPermission offline_permission denied execution of [\\'READ_OFFLINE\\'] to FeatureView:driver_hourly_stats: Requires roles [\\'batch_admin\\'],Permission admin_permission denied execution of [\\'READ_OFFLINE\\'] to FeatureView:driver_hourly_stats: Requires roles [\\'store_admin\\']. Detail: Python exception: FeastPermissionError\", grpc_status:2, created_time:\"2024-09-09T08:52:22.529654+02:00\"}. Client context: IOError: Server never sent a data message. Detail: Internal\n", + "\n", + "--- Historical features for batch scoring ---\n", + "Failed to run `store.get_historical_features`: Permission error:\n", + "Permission offline_permission denied execution of ['READ_OFFLINE'] to FeatureView:driver_hourly_stats: Requires roles ['batch_admin'],Permission admin_permission denied execution of ['READ_OFFLINE'] to FeatureView:driver_hourly_stats: Requires roles ['store_admin']. Detail: Python exception: FeastPermissionError. gRPC client debug context: UNKNOWN:Error received from peer ipv6:%5B::1%5D:8815 {created_time:\"2024-09-09T08:52:23.51953+02:00\", grpc_status:2, grpc_message:\"Permission error:\\nPermission offline_permission denied execution of [\\'READ_OFFLINE\\'] to FeatureView:driver_hourly_stats: Requires roles [\\'batch_admin\\'],Permission admin_permission denied execution of [\\'READ_OFFLINE\\'] to FeatureView:driver_hourly_stats: Requires roles [\\'store_admin\\']. Detail: Python exception: FeastPermissionError\"}. Client context: IOError: Server never sent a data message. Detail: Internal\n" + ] + } + ], + "source": [ + "print(\"\\n--- Historical features for training ---\")\n", + "fetch_historical_features_entity_df(store, for_batch_scoring=False)\n", + "\n", + "print(\"\\n--- Historical features for batch scoring ---\")\n", + "fetch_historical_features_entity_df(store, for_batch_scoring=True)" + ] + }, + { + "cell_type": "markdown", + "id": "83bdd1a1-7071-4c51-bf69-9b2bade572a1", + "metadata": {}, + "source": [ + "### Materialization" + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "id": "baeed80c-d2bf-4ac2-ae97-dc689c32e797", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "--- Load features into online store ---\n", + "Materializing \u001b[1m\u001b[32m2\u001b[0m feature views to \u001b[1m\u001b[32m2024-09-09 08:52:23+02:00\u001b[0m into the \u001b[1m\u001b[32mremote\u001b[0m online store.\n", + "\n", + "\u001b[1m\u001b[32mdriver_hourly_stats\u001b[0m from \u001b[1m\u001b[32m2024-09-09 10:50:53+02:00\u001b[0m to \u001b[1m\u001b[32m2024-09-09 08:52:23+02:00\u001b[0m:\n", + "Failed to run `store.materialize_incremental`: Permission error:\n", + "Permission admin_permission denied execution of ['READ_OFFLINE'] to FileSource:driver_hourly_stats_source: Requires roles ['store_admin']. Detail: Python exception: FeastPermissionError. gRPC client debug context: UNKNOWN:Error received from peer ipv6:%5B::1%5D:8815 {created_time:\"2024-09-09T08:52:24.551895+02:00\", grpc_status:2, grpc_message:\"Permission error:\\nPermission admin_permission denied execution of [\\'READ_OFFLINE\\'] to FileSource:driver_hourly_stats_source: Requires roles [\\'store_admin\\']. Detail: Python exception: FeastPermissionError\"}. Client context: IOError: Server never sent a data message. Detail: Internal\n" + ] + } + ], + "source": [ + "print(\"\\n--- Load features into online store ---\")\n", + "try:\n", + " store.materialize_incremental(end_date=datetime.now())\n", + "except Exception as e:\n", + " print(f\"Failed to run `store.materialize_incremental`: {e}\")" + ] + }, + { + "cell_type": "markdown", + "id": "f3ef1e87-a98e-447e-893a-d10e205d87c5", + "metadata": {}, + "source": [ + "### Online features" + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "id": "feb552de-77da-4177-bc4e-4c882ca91fe8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "--- Online features ---\n", + "Failed to run `store.get_online_features`: Permission error:\n", + "Permission admin_permission denied execution of ['READ_ONLINE'] to FeatureView:driver_hourly_stats: Requires roles ['store_admin']\n", + "\n", + "--- Online features retrieved (instead) through a feature service---\n", + "Failed to run `store.get_online_features`: Permission error:\n", + "Permission admin_permission denied execution of ['READ_ONLINE'] to FeatureView:driver_hourly_stats: Requires roles ['store_admin']\n", + "\n", + "--- Online features retrieved (using feature service v3, which uses a feature view with a push source---\n", + "Failed to run `store.get_online_features`: Permission error:\n", + "Permission admin_permission denied execution of ['READ_ONLINE'] to FeatureView:driver_hourly_stats: Requires roles ['store_admin']\n" + ] + } + ], + "source": [ + "print(\"\\n--- Online features ---\")\n", + "fetch_online_features(store)\n", + "\n", + "print(\"\\n--- Online features retrieved (instead) through a feature service---\")\n", + "fetch_online_features(store, source=\"feature_service\")\n", + "\n", + "print(\n", + " \"\\n--- Online features retrieved (using feature service v3, which uses a feature view with a push source---\"\n", + ")\n", + "fetch_online_features(store, source=\"push\")" + ] + }, + { + "cell_type": "markdown", + "id": "7ce5704c-86ef-4d00-a111-b86e853f2cca", + "metadata": {}, + "source": [ + "### Stream push" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "id": "e53317fc-8e6b-4dc3-89ca-28d6be04b98a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "--- Simulate a stream event ingestion of the hourly stats df ---\n", + " driver_id event_timestamp created conv_rate \\\n", + "0 1001 2024-09-09 08:52:33.038542 2024-09-09 08:52:33.038547 1.0 \n", + "\n", + " acc_rate avg_daily_trips \n", + "0 1.0 1000 \n", + "Failed to run `store.push`: \n", + "\n", + "--- Online features again with updated values from a stream push---\n", + "Failed to run `store.get_online_features`: Permission error:\n", + "Permission admin_permission denied execution of ['READ_ONLINE'] to FeatureView:driver_hourly_stats: Requires roles ['store_admin']\n" + ] + } + ], + "source": [ + "print(\"\\n--- Simulate a stream event ingestion of the hourly stats df ---\")\n", + "event_df = pd.DataFrame.from_dict(\n", + " {\n", + " \"driver_id\": [1001],\n", + " \"event_timestamp\": [\n", + " datetime.now(),\n", + " ],\n", + " \"created\": [\n", + " datetime.now(),\n", + " ],\n", + " \"conv_rate\": [1.0],\n", + " \"acc_rate\": [1.0],\n", + " \"avg_daily_trips\": [1000],\n", + " }\n", + ")\n", + "print(event_df)\n", + "try:\n", + " store.push(\"driver_stats_push_source\", event_df, to=PushMode.ONLINE_AND_OFFLINE)\n", + "except Exception as e:\n", + " print(f\"Failed to run `store.push`: {e}\") \n", + "\n", + "print(\"\\n--- Online features again with updated values from a stream push---\")\n", + "fetch_online_features(store, source=\"push\")" + ] + }, + { + "cell_type": "markdown", + "id": "5709f71b-ddff-4048-9db1-98d4090326e1", + "metadata": {}, + "source": [ + "**Note** If you see the following error, it is likely due to the issue [#4392: Remote registry client does not map application errors](https://github.com/feast-dev/feast/issues/4392):\n", + "```\n", + "Feature view driver_hourly_stats_fresh does not exist in project rbac\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "573d9e29-4ba8-41f4-b6a1-82a24d4550b5", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/rbac-local/client/feature_store.yaml b/examples/rbac-local/client/feature_store.yaml new file mode 100644 index 0000000000..d428adf671 --- /dev/null +++ b/examples/rbac-local/client/feature_store.yaml @@ -0,0 +1,12 @@ +entity_key_serialization_version: 2 +offline_store: + host: localhost + port: 8815 + type: remote +online_store: + path: http://localhost:6566 + type: remote +project: rbac +registry: + path: localhost:6570 + registry_type: remote From ddecae89a4d5e6cdb6e49c7514ba6ae0c354350d Mon Sep 17 00:00:00 2001 From: lokeshrangineni <19699092+lokeshrangineni@users.noreply.github.com> Date: Thu, 12 Sep 2024 23:49:58 -0400 Subject: [PATCH 069/185] docs: Adding the missed documentation for the RBAC (#4515) * Adding the missed documentation - * OIDC Token requirement or assumptions * Added `feast permissions check` cli command documentation. Signed-off-by: Lokesh Rangineni <19699092+lokeshrangineni@users.noreply.github.com> * Adding the missed documentation - * OIDC Token requirement or assumptions * Added `feast permissions check` cli command documentation. Signed-off-by: Lokesh Rangineni <19699092+lokeshrangineni@users.noreply.github.com> * Fixed code review comments. Signed-off-by: Lokesh Rangineni <19699092+lokeshrangineni@users.noreply.github.com> --------- Signed-off-by: Lokesh Rangineni <19699092+lokeshrangineni@users.noreply.github.com> --- .../components/authz_manager.md | 10 +++-- docs/reference/feast-cli-commands.md | 42 +++++++++++++++++++ 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/docs/getting-started/components/authz_manager.md b/docs/getting-started/components/authz_manager.md index 0d011fbf2b..20fcdca107 100644 --- a/docs/getting-started/components/authz_manager.md +++ b/docs/getting-started/components/authz_manager.md @@ -1,5 +1,5 @@ # Authorization Manager -An Authorization Manager is an instance of the `AuthManager` class that is plugged into one of the Feast servers to extract user details from the current request and inject them into the [permissions](../../getting-started/concepts/permissions.md) framework. +An Authorization Manager is an instance of the `AuthManager` class that is plugged into one of the Feast servers to extract user details from the current request and inject them into the [permission](../../getting-started/concepts/permission.md) framework. {% hint style="info" %} **Note**: Feast does not provide authentication capabilities; it is the client's responsibility to manage the authentication token and pass it to @@ -44,7 +44,10 @@ The server, in turn, uses the same OIDC server to validate the token and extract Some assumptions are made in the OIDC server configuration: * The OIDC token refers to a client with roles matching the RBAC roles of the configured `Permission`s (*) -* The roles are exposed in the access token passed to the server +* The roles are exposed in the access token that is passed to the server +* The JWT token is expected to have a verified signature and not be expired. The Feast OIDC token parser logic validates for `verify_signature` and `verify_exp` so make sure that the given OIDC provider is configured to meet these requirements. +* The preferred_username should be part of the JWT token claim. + (*) Please note that **the role match is case-sensitive**, e.g. the name of the role in the OIDC server and in the `Permission` configuration must be exactly the same. @@ -57,7 +60,8 @@ For example, the access token for a client `app` of a user with `reader` role sh "roles": [ "reader" ] - }, + } + } } ``` diff --git a/docs/reference/feast-cli-commands.md b/docs/reference/feast-cli-commands.md index be31720034..b32db3215a 100644 --- a/docs/reference/feast-cli-commands.md +++ b/docs/reference/feast-cli-commands.md @@ -224,6 +224,48 @@ tags: key2: value2 ``` +### Permission check +The `permissions check` command is used to identify resources that lack the appropriate permissions based on their type, name, or tags. + +This command is particularly useful for administrators when roles, actions, or permissions have been modified or newly configured. By running this command, administrators can easily verify which resources and actions are not protected by any permission configuration, ensuring that proper security measures are in place. + +```text +> feast permissions check + + +The following resources are not secured by any permission configuration: +NAME TYPE +driver Entity +driver_hourly_stats_fresh FeatureView +The following actions are not secured by any permission configuration (Note: this might not be a security concern, depending on the used APIs): +NAME TYPE UNSECURED ACTIONS +driver Entity CREATE + DESCRIBE + UPDATE + DELETE + READ_ONLINE + READ_OFFLINE + WRITE_ONLINE + WRITE_OFFLINE +driver_hourly_stats_fresh FeatureView CREATE + DESCRIBE + UPDATE + DELETE + READ_ONLINE + READ_OFFLINE + WRITE_ONLINE + WRITE_OFFLINE + +Based on the above results, the administrator can reassess the permissions configuration and make any necessary adjustments to meet their security requirements. + +If no resources are accessible publicly, the permissions check command will return the following response: +> feast permissions check +The following resources are not secured by any permission configuration: +NAME TYPE +The following actions are not secured by any permission configuration (Note: this might not be a security concern, depending on the used APIs): +NAME TYPE UNSECURED ACTIONS +``` + ### List of the configured roles List all the configured roles From 06eade3b83d5e481fe158dd323411a3d92d75c98 Mon Sep 17 00:00:00 2001 From: cburroughs Date: Mon, 16 Sep 2024 10:55:02 -0400 Subject: [PATCH 070/185] chore: Remove bump upper bound on fsspec requirement (#4512) Alternative to #4461 Signed-off-by: Chris Burroughs --- .../requirements/py3.10-ci-requirements.txt | 305 ++++++++---------- .../requirements/py3.10-requirements.txt | 129 +++----- .../requirements/py3.11-ci-requirements.txt | 302 ++++++++--------- .../requirements/py3.11-requirements.txt | 127 +++----- .../requirements/py3.9-ci-requirements.txt | 289 ++++++++--------- .../requirements/py3.9-requirements.txt | 127 +++----- setup.py | 4 +- 7 files changed, 545 insertions(+), 738 deletions(-) diff --git a/sdk/python/requirements/py3.10-ci-requirements.txt b/sdk/python/requirements/py3.10-ci-requirements.txt index bfe855a2d8..b8798d96c6 100644 --- a/sdk/python/requirements/py3.10-ci-requirements.txt +++ b/sdk/python/requirements/py3.10-ci-requirements.txt @@ -1,9 +1,11 @@ # This file was autogenerated by uv via the following command: # uv pip compile --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.10-ci-requirements.txt -aiobotocore==2.13.1 -aiohttp==3.9.5 +aiobotocore==2.15.0 +aiohappyeyeballs==2.4.0 + # via aiohttp +aiohttp==3.10.5 # via aiobotocore -aioitertools==0.11.0 +aioitertools==0.12.0 # via aiobotocore aiosignal==1.3.1 # via aiohttp @@ -38,9 +40,9 @@ async-timeout==4.0.3 # via # aiohttp # redis -atpublic==4.1.0 +atpublic==5.0 # via ibis-framework -attrs==23.2.0 +attrs==24.2.0 # via # aiohttp # jsonschema @@ -50,34 +52,32 @@ azure-core==1.30.2 # azure-identity # azure-storage-blob azure-identity==1.17.1 -azure-storage-blob==12.20.0 -babel==2.15.0 +azure-storage-blob==12.22.0 +babel==2.16.0 # via # jupyterlab-server # sphinx beautifulsoup4==4.12.3 # via nbconvert -bidict==0.23.1 - # via ibis-framework -bigtree==0.19.2 +bigtree==0.21.1 bleach==6.1.0 # via nbconvert -boto3==1.34.131 +boto3==1.35.16 # via moto -botocore==1.34.131 +botocore==1.35.16 # via # aiobotocore # boto3 # moto # s3transfer -build==1.2.1 +build==1.2.2 # via # pip-tools # singlestoredb -cachetools==5.3.3 +cachetools==5.5.0 # via google-auth -cassandra-driver==3.29.1 -certifi==2024.7.4 +cassandra-driver==3.29.2 +certifi==2024.8.30 # via # elastic-transport # httpcore @@ -86,7 +86,7 @@ certifi==2024.7.4 # minio # requests # snowflake-connector-python -cffi==1.16.0 +cffi==1.17.1 # via # argon2-cffi-bindings # cryptography @@ -103,7 +103,6 @@ click==8.1.7 # geomet # great-expectations # pip-tools - # typer # uvicorn cloudpickle==3.0.0 # via dask @@ -113,9 +112,9 @@ comm==0.2.2 # via # ipykernel # ipywidgets -coverage[toml]==7.5.4 +coverage[toml]==7.6.1 # via pytest-cov -cryptography==43.0.1 +cryptography==42.0.8 # via # azure-identity # azure-storage-blob @@ -128,54 +127,50 @@ cryptography==43.0.1 # snowflake-connector-python # types-pyopenssl # types-redis -dask[dataframe]==2024.6.2 +cython==3.0.11 + # via thriftpy2 +dask[dataframe]==2024.8.2 # via dask-expr -dask-expr==1.1.6 +dask-expr==1.1.13 # via dask -db-dtypes==1.2.0 +db-dtypes==1.3.0 # via google-cloud-bigquery -debugpy==1.8.2 +debugpy==1.8.5 # via ipykernel decorator==5.1.1 # via ipython defusedxml==0.7.1 # via nbconvert -deltalake==0.18.1 +deltalake==0.19.2 deprecation==2.1.0 # via python-keycloak dill==0.3.8 distlib==0.3.8 # via virtualenv -dnspython==2.6.1 - # via email-validator docker==7.1.0 # via testcontainers docutils==0.19 # via sphinx -duckdb==0.10.3 +duckdb==1.1.0 # via ibis-framework -elastic-transport==8.13.1 +elastic-transport==8.15.0 # via elasticsearch -elasticsearch==8.14.0 -email-validator==2.2.0 - # via fastapi +elasticsearch==8.15.1 entrypoints==0.4 # via altair -exceptiongroup==1.2.1 +exceptiongroup==1.2.2 # via # anyio # ipython # pytest execnet==2.1.1 # via pytest-xdist -executing==2.0.1 +executing==2.1.0 # via stack-data -fastapi==0.111.0 -fastapi-cli==0.0.4 - # via fastapi +fastapi==0.114.1 fastjsonschema==2.20.0 # via nbformat -filelock==3.15.4 +filelock==3.16.0 # via # snowflake-connector-python # virtualenv @@ -185,11 +180,11 @@ frozenlist==1.4.1 # via # aiohttp # aiosignal -fsspec==2023.12.2 +fsspec==2024.9.0 # via dask geomet==0.2.1.post1 # via cassandra-driver -google-api-core[grpc]==2.19.1 +google-api-core[grpc]==2.19.2 # via # google-cloud-bigquery # google-cloud-bigquery-storage @@ -197,46 +192,48 @@ google-api-core[grpc]==2.19.1 # google-cloud-core # google-cloud-datastore # google-cloud-storage -google-auth==2.30.0 +google-auth==2.34.0 # via # google-api-core + # google-cloud-bigquery # google-cloud-bigquery-storage + # google-cloud-bigtable # google-cloud-core + # google-cloud-datastore # google-cloud-storage # kubernetes -google-cloud-bigquery[pandas]==3.13.0 -google-cloud-bigquery-storage==2.25.0 -google-cloud-bigtable==2.24.0 +google-cloud-bigquery[pandas]==3.25.0 +google-cloud-bigquery-storage==2.26.0 +google-cloud-bigtable==2.26.0 google-cloud-core==2.4.1 # via # google-cloud-bigquery # google-cloud-bigtable # google-cloud-datastore # google-cloud-storage -google-cloud-datastore==2.19.0 -google-cloud-storage==2.17.0 -google-crc32c==1.5.0 +google-cloud-datastore==2.20.1 +google-cloud-storage==2.18.2 +google-crc32c==1.6.0 # via # google-cloud-storage # google-resumable-media -google-resumable-media==2.7.1 +google-resumable-media==2.7.2 # via # google-cloud-bigquery # google-cloud-storage -googleapis-common-protos[grpc]==1.63.2 +googleapis-common-protos[grpc]==1.65.0 # via # google-api-core # grpc-google-iam-v1 # grpcio-status -great-expectations==0.18.16 -greenlet==3.0.3 +great-expectations==0.18.20 +greenlet==3.1.0 # via sqlalchemy grpc-google-iam-v1==0.13.1 # via google-cloud-bigtable -grpcio==1.64.1 +grpcio==1.66.1 # via # google-api-core - # google-cloud-bigquery # googleapis-common-protos # grpc-google-iam-v1 # grpcio-health-checking @@ -244,38 +241,36 @@ grpcio==1.64.1 # grpcio-status # grpcio-testing # grpcio-tools -grpcio-health-checking==1.62.2 -grpcio-reflection==1.62.2 -grpcio-status==1.62.2 +grpcio-health-checking==1.62.3 +grpcio-reflection==1.62.3 +grpcio-status==1.62.3 # via google-api-core -grpcio-testing==1.62.2 -grpcio-tools==1.62.2 -gunicorn==22.0.0 +grpcio-testing==1.62.3 +grpcio-tools==1.62.3 +gunicorn==23.0.0 h11==0.14.0 # via # httpcore # uvicorn happybase==1.2.0 -hazelcast-python-client==5.4.0 -hiredis==2.3.2 +hazelcast-python-client==5.5.0 +hiredis==2.4.0 httpcore==1.0.5 # via httpx httptools==0.6.1 # via uvicorn -httpx==0.27.0 +httpx==0.27.2 # via - # fastapi # jupyterlab # python-keycloak -ibis-framework[duckdb]==9.1.0 +ibis-framework[duckdb]==9.4.0 # via ibis-substrait -ibis-substrait==4.0.0 -identify==2.5.36 +ibis-substrait==4.0.1 +identify==2.6.0 # via pre-commit -idna==3.7 +idna==3.8 # via # anyio - # email-validator # httpx # jsonschema # requests @@ -283,18 +278,18 @@ idna==3.7 # yarl imagesize==1.4.1 # via sphinx -importlib-metadata==8.0.0 +importlib-metadata==8.5.0 # via dask iniconfig==2.0.0 # via pytest -ipykernel==6.29.4 +ipykernel==6.29.5 # via jupyterlab -ipython==8.25.0 +ipython==8.27.0 # via # great-expectations # ipykernel # ipywidgets -ipywidgets==8.1.3 +ipywidgets==8.1.5 # via great-expectations isodate==0.6.1 # via azure-storage-blob @@ -305,7 +300,6 @@ jedi==0.19.1 jinja2==3.1.4 # via # altair - # fastapi # great-expectations # jupyter-server # jupyterlab @@ -325,7 +319,7 @@ jsonpointer==3.0.0 # via # jsonpatch # jsonschema -jsonschema[format-nongpl]==4.22.0 +jsonschema[format-nongpl]==4.23.0 # via # altair # great-expectations @@ -352,7 +346,7 @@ jupyter-events==0.10.0 # via jupyter-server jupyter-lsp==2.2.5 # via jupyterlab -jupyter-server==2.14.1 +jupyter-server==2.14.2 # via # jupyter-lsp # jupyterlab @@ -365,18 +359,18 @@ jupyterlab==4.2.5 # via notebook jupyterlab-pygments==0.3.0 # via nbconvert -jupyterlab-server==2.27.2 +jupyterlab-server==2.27.3 # via # jupyterlab # notebook -jupyterlab-widgets==3.0.11 +jupyterlab-widgets==3.0.13 # via ipywidgets jwcrypto==1.5.6 # via python-keycloak kubernetes==20.13.0 locket==1.0.0 # via partd -makefun==1.15.2 +makefun==1.15.4 # via great-expectations markdown-it-py==3.0.0 # via rich @@ -385,7 +379,7 @@ markupsafe==2.1.5 # jinja2 # nbconvert # werkzeug -marshmallow==3.21.3 +marshmallow==3.22.0 # via great-expectations matplotlib-inline==0.1.7 # via @@ -401,17 +395,17 @@ mistune==3.0.2 mmh3==4.1.0 mock==2.0.0 moto==4.2.14 -msal==1.29.0 +msal==1.31.0 # via # azure-identity # msal-extensions msal-extensions==1.2.0 # via azure-identity -multidict==6.0.5 +multidict==6.1.0 # via # aiohttp # yarl -mypy==1.10.1 +mypy==1.11.2 # via sqlalchemy mypy-extensions==1.0.0 # via mypy @@ -448,8 +442,6 @@ numpy==1.26.4 # scipy oauthlib==3.2.2 # via requests-oauthlib -orjson==3.10.5 - # via fastapi overrides==7.7.0 # via jupyter-server packaging==24.1 @@ -491,11 +483,11 @@ parsy==2.1 # via ibis-framework partd==1.4.2 # via dask -pbr==6.0.0 +pbr==6.1.0 # via mock pexpect==4.9.0 # via ipython -pip==24.1.1 +pip==24.2 # via pip-tools pip-tools==7.4.1 platformdirs==3.11.0 @@ -507,7 +499,7 @@ pluggy==1.5.0 # via pytest ply==3.11 # via thriftpy2 -portalocker==2.10.0 +portalocker==2.10.1 # via msal-extensions pre-commit==3.3.1 prometheus-client==0.20.0 @@ -517,14 +509,12 @@ prompt-toolkit==3.0.47 proto-plus==1.24.0 # via # google-api-core - # google-cloud-bigquery # google-cloud-bigquery-storage # google-cloud-bigtable # google-cloud-datastore -protobuf==4.25.3 +protobuf==4.25.4 # via # google-api-core - # google-cloud-bigquery # google-cloud-bigquery-storage # google-cloud-bigtable # google-cloud-datastore @@ -540,8 +530,8 @@ protobuf==4.25.3 # substrait psutil==5.9.0 # via ipykernel -psycopg[binary, pool]==3.1.19 -psycopg-binary==3.1.19 +psycopg[binary, pool]==3.2.1 +psycopg-binary==3.2.1 # via psycopg psycopg-pool==3.2.2 # via psycopg @@ -549,14 +539,14 @@ ptyprocess==0.7.0 # via # pexpect # terminado -pure-eval==0.2.2 +pure-eval==0.2.3 # via stack-data py==1.11.0 py-cpuinfo==9.0.0 # via pytest-benchmark py4j==0.10.9.7 # via pyspark -pyarrow==15.0.2 +pyarrow==17.0.0 # via # dask-expr # db-dtypes @@ -565,23 +555,21 @@ pyarrow==15.0.2 # ibis-framework # snowflake-connector-python pyarrow-hotfix==0.6 - # via - # deltalake - # ibis-framework -pyasn1==0.6.0 + # via ibis-framework +pyasn1==0.6.1 # via # pyasn1-modules # rsa -pyasn1-modules==0.4.0 +pyasn1-modules==0.4.1 # via google-auth pybindgen==0.22.1 pycparser==2.22 # via cffi -pydantic==2.7.4 +pydantic==2.9.1 # via # fastapi # great-expectations -pydantic-core==2.18.4 +pydantic-core==2.23.3 # via pydantic pygments==2.18.0 # via @@ -589,23 +577,23 @@ pygments==2.18.0 # nbconvert # rich # sphinx -pyjwt[crypto]==2.8.0 +pyjwt[crypto]==2.9.0 # via # msal # singlestoredb # snowflake-connector-python -pymssql==2.3.0 +pymssql==2.3.1 pymysql==1.1.1 pyodbc==5.1.0 -pyopenssl==24.1.0 +pyopenssl==24.2.1 # via snowflake-connector-python -pyparsing==3.1.2 +pyparsing==3.1.4 # via great-expectations pyproject-hooks==1.1.0 # via # build # pip-tools -pyspark==3.5.1 +pyspark==3.5.2 pytest==7.4.4 # via # pytest-benchmark @@ -641,16 +629,14 @@ python-dotenv==1.0.1 python-json-logger==2.0.7 # via jupyter-events python-keycloak==4.2.2 -python-multipart==0.0.9 - # via fastapi -pytz==2024.1 +pytz==2024.2 # via # great-expectations # ibis-framework # pandas # snowflake-connector-python # trino -pyyaml==6.0.1 +pyyaml==6.0.2 # via # dask # ibis-substrait @@ -659,7 +645,7 @@ pyyaml==6.0.1 # pre-commit # responses # uvicorn -pyzmq==26.0.3 +pyzmq==26.2.0 # via # ipykernel # jupyter-client @@ -670,7 +656,7 @@ referencing==0.35.1 # jsonschema # jsonschema-specifications # jupyter-events -regex==2024.5.15 +regex==2024.7.24 # via parsimonious requests==2.32.3 # via @@ -706,35 +692,33 @@ rfc3986-validator==0.1.1 # via # jsonschema # jupyter-events -rich==13.7.1 - # via - # ibis-framework - # typer -rpds-py==0.18.1 +rich==13.8.1 + # via ibis-framework +rpds-py==0.20.0 # via # jsonschema # referencing rsa==4.9 # via google-auth -ruamel-yaml==0.17.17 +ruamel-yaml==0.17.40 # via great-expectations -ruff==0.4.10 +ruamel-yaml-clib==0.2.8 + # via ruamel-yaml +ruff==0.6.4 s3transfer==0.10.2 # via boto3 -scipy==1.14.0 +scipy==1.14.1 # via great-expectations send2trash==1.8.3 # via jupyter-server -setuptools==70.1.1 +setuptools==74.1.2 # via # grpcio-tools # jupyterlab # kubernetes # pip-tools # singlestoredb -shellingham==1.5.4 - # via typer -singlestoredb==1.4.0 +singlestoredb==1.6.3 six==1.16.0 # via # asttokens @@ -754,44 +738,44 @@ sniffio==1.3.1 # httpx snowballstemmer==2.2.0 # via sphinx -snowflake-connector-python[pandas]==3.11.0 +snowflake-connector-python[pandas]==3.12.1 sortedcontainers==2.4.0 # via snowflake-connector-python -soupsieve==2.5 +soupsieve==2.6 # via beautifulsoup4 sphinx==6.2.1 -sphinxcontrib-applehelp==1.0.8 +sphinxcontrib-applehelp==2.0.0 # via sphinx -sphinxcontrib-devhelp==1.0.6 +sphinxcontrib-devhelp==2.0.0 # via sphinx -sphinxcontrib-htmlhelp==2.0.5 +sphinxcontrib-htmlhelp==2.1.0 # via sphinx sphinxcontrib-jsmath==1.0.1 # via sphinx -sphinxcontrib-qthelp==1.0.7 +sphinxcontrib-qthelp==2.0.0 # via sphinx -sphinxcontrib-serializinghtml==1.1.10 +sphinxcontrib-serializinghtml==2.0.0 # via sphinx -sqlalchemy[mypy]==2.0.31 -sqlglot==25.1.0 +sqlalchemy[mypy]==2.0.34 +sqlglot==25.18.0 # via ibis-framework sqlite-vec==0.1.1 -sqlparams==6.0.1 +sqlparams==6.1.0 # via singlestoredb stack-data==0.6.3 # via ipython -starlette==0.37.2 +starlette==0.38.5 # via fastapi -substrait==0.19.0 +substrait==0.22.0 # via ibis-substrait tabulate==0.9.0 -tenacity==8.4.2 +tenacity==8.5.0 terminado==0.18.1 # via # jupyter-server # jupyter-server-terminals testcontainers==4.4.0 -thriftpy2==0.5.1 +thriftpy2==0.5.2 # via happybase tinycss2==1.3.0 # via nbconvert @@ -806,7 +790,7 @@ tomli==2.0.1 # pytest # pytest-env # singlestoredb -tomlkit==0.12.5 +tomlkit==0.13.2 # via snowflake-connector-python toolz==0.12.1 # via @@ -822,7 +806,7 @@ tornado==6.4.1 # jupyterlab # notebook # terminado -tqdm==4.66.4 +tqdm==4.66.5 # via great-expectations traitlets==5.14.3 # via @@ -839,24 +823,22 @@ traitlets==5.14.3 # nbclient # nbconvert # nbformat -trino==0.328.0 +trino==0.329.0 typeguard==4.3.0 -typer==0.12.3 - # via fastapi-cli types-cffi==1.16.0.20240331 # via types-pyopenssl types-protobuf==3.19.22 # via mypy-protobuf types-pymysql==1.1.0.20240524 -types-pyopenssl==24.1.0.20240425 +types-pyopenssl==24.1.0.20240722 # via types-redis -types-python-dateutil==2.9.0.20240316 +types-python-dateutil==2.9.0.20240906 # via arrow types-pytz==2024.1.0.20240417 -types-pyyaml==6.0.12.20240311 -types-redis==4.6.0.20240425 +types-pyyaml==6.0.12.20240808 +types-redis==4.6.0.20240903 types-requests==2.30.0.0 -types-setuptools==70.1.0.20240627 +types-setuptools==74.1.0.20240907 # via types-cffi types-tabulate==0.9.0.20240106 types-urllib3==1.26.25.14 @@ -873,6 +855,7 @@ typing-extensions==4.12.2 # ibis-framework # ipython # jwcrypto + # multidict # mypy # psycopg # psycopg-pool @@ -882,7 +865,6 @@ typing-extensions==4.12.2 # sqlalchemy # testcontainers # typeguard - # typer # uvicorn tzdata==2024.1 # via pandas @@ -890,11 +872,9 @@ tzlocal==5.2 # via # great-expectations # trino -ujson==5.10.0 - # via fastapi uri-template==1.3.0 # via jsonschema -urllib3==1.26.19 +urllib3==2.2.2 # via # botocore # docker @@ -905,17 +885,16 @@ urllib3==1.26.19 # requests # responses # testcontainers -uvicorn[standard]==0.30.1 - # via fastapi -uvloop==0.19.0 +uvicorn[standard]==0.30.6 +uvloop==0.20.0 # via uvicorn virtualenv==20.23.0 # via pre-commit -watchfiles==0.22.0 +watchfiles==0.24.0 # via uvicorn wcwidth==0.2.13 # via prompt-toolkit -webcolors==24.6.0 +webcolors==24.8.0 # via jsonschema webencodings==0.5.1 # via @@ -925,15 +904,15 @@ websocket-client==1.8.0 # via # jupyter-server # kubernetes -websockets==12.0 +websockets==13.0.1 # via uvicorn -werkzeug==3.0.3 +werkzeug==3.0.4 # via moto -wheel==0.43.0 +wheel==0.44.0 # via # pip-tools # singlestoredb -widgetsnbextension==4.0.11 +widgetsnbextension==4.0.13 # via ipywidgets wrapt==1.16.0 # via @@ -941,7 +920,7 @@ wrapt==1.16.0 # testcontainers xmltodict==0.13.0 # via moto -yarl==1.9.4 +yarl==1.11.1 # via aiohttp -zipp==3.19.1 +zipp==3.20.1 # via importlib-metadata diff --git a/sdk/python/requirements/py3.10-requirements.txt b/sdk/python/requirements/py3.10-requirements.txt index eed2baaefe..a3006e4555 100644 --- a/sdk/python/requirements/py3.10-requirements.txt +++ b/sdk/python/requirements/py3.10-requirements.txt @@ -1,23 +1,20 @@ # This file was autogenerated by uv via the following command: # uv pip compile --system --no-strip-extras setup.py --output-file sdk/python/requirements/py3.10-requirements.txt -annotated-types==0.6.0 +annotated-types==0.7.0 # via pydantic -anyio==4.3.0 +anyio==4.4.0 # via - # httpx # starlette # watchfiles -attrs==23.2.0 +attrs==24.2.0 # via # jsonschema # referencing -bigtree==0.19.2 +bigtree==0.21.1 cachetools==5.5.0 # via google-auth -certifi==2024.7.4 +certifi==2024.8.30 # via - # httpcore - # httpx # kubernetes # requests charset-normalizer==3.3.2 @@ -25,67 +22,46 @@ charset-normalizer==3.3.2 click==8.1.7 # via # dask - # typer # uvicorn cloudpickle==3.0.0 # via dask colorama==0.4.6 -dask[dataframe]==2024.5.0 +dask[dataframe]==2024.8.2 # via dask-expr -dask-expr==1.1.0 +dask-expr==1.1.13 # via dask dill==0.3.8 -dnspython==2.6.1 - # via email-validator -email-validator==2.1.1 - # via fastapi -exceptiongroup==1.2.1 +exceptiongroup==1.2.2 # via anyio -fastapi==0.111.0 - # via fastapi-cli -fastapi-cli==0.0.2 - # via fastapi -fsspec==2024.3.1 +fastapi==0.114.1 +fsspec==2024.9.0 # via dask google-auth==2.34.0 # via kubernetes -greenlet==3.0.3 +greenlet==3.1.0 # via sqlalchemy -gunicorn==22.0.0 +gunicorn==23.0.0 h11==0.14.0 - # via - # httpcore - # uvicorn -httpcore==1.0.5 - # via httpx + # via uvicorn httptools==0.6.1 # via uvicorn -httpx==0.27.0 - # via fastapi -idna==3.7 +idna==3.8 # via # anyio - # email-validator - # httpx # requests -importlib-metadata==7.1.0 +importlib-metadata==8.5.0 # via dask jinja2==3.1.4 - # via fastapi -jsonschema==4.22.0 +jsonschema==4.23.0 jsonschema-specifications==2023.12.1 # via jsonschema kubernetes==20.13.0 locket==1.0.0 # via partd -markdown-it-py==3.0.0 - # via rich markupsafe==2.1.5 # via jinja2 -mdurl==0.1.2 - # via markdown-it-py mmh3==4.1.0 -mypy==1.10.0 +mypy==1.11.2 # via sqlalchemy mypy-extensions==1.0.0 # via mypy @@ -97,9 +73,7 @@ numpy==1.26.4 # pyarrow oauthlib==3.2.2 # via requests-oauthlib -orjson==3.10.3 - # via fastapi -packaging==24.0 +packaging==24.1 # via # dask # gunicorn @@ -110,23 +84,22 @@ pandas==2.2.2 partd==1.4.2 # via dask prometheus-client==0.20.0 -protobuf==4.25.3 +protobuf==4.25.4 # via mypy-protobuf psutil==6.0.0 -pyarrow==16.0.0 +pyarrow==17.0.0 # via dask-expr -pyasn1==0.6.0 +pyasn1==0.6.1 # via # pyasn1-modules # rsa -pyasn1-modules==0.4.0 +pyasn1-modules==0.4.1 # via google-auth -pydantic==2.7.1 +pydantic==2.9.1 # via fastapi -pydantic-core==2.18.2 +pydantic-core==2.23.3 # via pydantic pygments==2.18.0 - # via rich pyjwt==2.9.0 python-dateutil==2.9.0.post0 # via @@ -134,11 +107,9 @@ python-dateutil==2.9.0.post0 # pandas python-dotenv==1.0.1 # via uvicorn -python-multipart==0.0.9 - # via fastapi -pytz==2024.1 +pytz==2024.2 # via pandas -pyyaml==6.0.1 +pyyaml==6.0.2 # via # dask # kubernetes @@ -147,37 +118,31 @@ referencing==0.35.1 # via # jsonschema # jsonschema-specifications -requests==2.31.0 +requests==2.32.3 # via # kubernetes # requests-oauthlib requests-oauthlib==2.0.0 # via kubernetes -rich==13.7.1 - # via typer -rpds-py==0.18.1 +rpds-py==0.20.0 # via # jsonschema # referencing rsa==4.9 # via google-auth -setuptools==73.0.1 +setuptools==74.1.2 # via kubernetes -shellingham==1.5.4 - # via typer six==1.16.0 # via # kubernetes # python-dateutil sniffio==1.3.1 - # via - # anyio - # httpx -sqlalchemy[mypy]==2.0.30 -starlette==0.37.2 + # via anyio +sqlalchemy[mypy]==2.0.34 +starlette==0.38.5 # via fastapi tabulate==0.9.0 -tenacity==8.3.0 +tenacity==8.5.0 toml==0.10.2 tomli==2.0.1 # via mypy @@ -185,13 +150,11 @@ toolz==0.12.1 # via # dask # partd -tqdm==4.66.4 -typeguard==4.2.1 -typer==0.12.3 - # via fastapi-cli -types-protobuf==5.26.0.20240422 +tqdm==4.66.5 +typeguard==4.3.0 +types-protobuf==5.27.0.20240907 # via mypy-protobuf -typing-extensions==4.11.0 +typing-extensions==4.12.2 # via # anyio # fastapi @@ -200,27 +163,21 @@ typing-extensions==4.11.0 # pydantic-core # sqlalchemy # typeguard - # typer # uvicorn tzdata==2024.1 # via pandas -ujson==5.9.0 - # via fastapi -urllib3==2.2.1 +urllib3==2.2.2 # via # kubernetes # requests -uvicorn[standard]==0.29.0 - # via - # fastapi - # fastapi-cli -uvloop==0.19.0 +uvicorn[standard]==0.30.6 +uvloop==0.20.0 # via uvicorn -watchfiles==0.21.0 +watchfiles==0.24.0 # via uvicorn websocket-client==1.8.0 # via kubernetes -websockets==12.0 +websockets==13.0.1 # via uvicorn -zipp==3.19.1 +zipp==3.20.1 # via importlib-metadata diff --git a/sdk/python/requirements/py3.11-ci-requirements.txt b/sdk/python/requirements/py3.11-ci-requirements.txt index 6a097526d7..cd78247a23 100644 --- a/sdk/python/requirements/py3.11-ci-requirements.txt +++ b/sdk/python/requirements/py3.11-ci-requirements.txt @@ -1,9 +1,11 @@ # This file was autogenerated by uv via the following command: # uv pip compile --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.11-ci-requirements.txt -aiobotocore==2.13.1 -aiohttp==3.9.5 +aiobotocore==2.15.0 +aiohappyeyeballs==2.4.0 + # via aiohttp +aiohttp==3.10.5 # via aiobotocore -aioitertools==0.11.0 +aioitertools==0.12.0 # via aiobotocore aiosignal==1.3.1 # via aiohttp @@ -34,9 +36,9 @@ async-lru==2.0.4 # via jupyterlab async-property==0.2.2 # via python-keycloak -atpublic==4.1.0 +atpublic==5.0 # via ibis-framework -attrs==23.2.0 +attrs==24.2.0 # via # aiohttp # jsonschema @@ -46,34 +48,32 @@ azure-core==1.30.2 # azure-identity # azure-storage-blob azure-identity==1.17.1 -azure-storage-blob==12.20.0 -babel==2.15.0 +azure-storage-blob==12.22.0 +babel==2.16.0 # via # jupyterlab-server # sphinx beautifulsoup4==4.12.3 # via nbconvert -bidict==0.23.1 - # via ibis-framework -bigtree==0.19.2 +bigtree==0.21.1 bleach==6.1.0 # via nbconvert -boto3==1.34.131 +boto3==1.35.16 # via moto -botocore==1.34.131 +botocore==1.35.16 # via # aiobotocore # boto3 # moto # s3transfer -build==1.2.1 +build==1.2.2 # via # pip-tools # singlestoredb -cachetools==5.3.3 +cachetools==5.5.0 # via google-auth -cassandra-driver==3.29.1 -certifi==2024.7.4 +cassandra-driver==3.29.2 +certifi==2024.8.30 # via # elastic-transport # httpcore @@ -82,7 +82,7 @@ certifi==2024.7.4 # minio # requests # snowflake-connector-python -cffi==1.16.0 +cffi==1.17.1 # via # argon2-cffi-bindings # cryptography @@ -99,7 +99,6 @@ click==8.1.7 # geomet # great-expectations # pip-tools - # typer # uvicorn cloudpickle==3.0.0 # via dask @@ -109,9 +108,9 @@ comm==0.2.2 # via # ipykernel # ipywidgets -coverage[toml]==7.5.4 +coverage[toml]==7.6.1 # via pytest-cov -cryptography==43.0.1 +cryptography==42.0.8 # via # azure-identity # azure-storage-blob @@ -124,49 +123,45 @@ cryptography==43.0.1 # snowflake-connector-python # types-pyopenssl # types-redis -dask[dataframe]==2024.6.2 +cython==3.0.11 + # via thriftpy2 +dask[dataframe]==2024.8.2 # via dask-expr -dask-expr==1.1.6 +dask-expr==1.1.13 # via dask -db-dtypes==1.2.0 +db-dtypes==1.3.0 # via google-cloud-bigquery -debugpy==1.8.2 +debugpy==1.8.5 # via ipykernel decorator==5.1.1 # via ipython defusedxml==0.7.1 # via nbconvert -deltalake==0.18.1 +deltalake==0.19.2 deprecation==2.1.0 # via python-keycloak dill==0.3.8 distlib==0.3.8 # via virtualenv -dnspython==2.6.1 - # via email-validator docker==7.1.0 # via testcontainers docutils==0.19 # via sphinx -duckdb==0.10.3 +duckdb==1.1.0 # via ibis-framework -elastic-transport==8.13.1 +elastic-transport==8.15.0 # via elasticsearch -elasticsearch==8.14.0 -email-validator==2.2.0 - # via fastapi +elasticsearch==8.15.1 entrypoints==0.4 # via altair execnet==2.1.1 # via pytest-xdist -executing==2.0.1 +executing==2.1.0 # via stack-data -fastapi==0.111.0 -fastapi-cli==0.0.4 - # via fastapi +fastapi==0.114.1 fastjsonschema==2.20.0 # via nbformat -filelock==3.15.4 +filelock==3.16.0 # via # snowflake-connector-python # virtualenv @@ -176,11 +171,11 @@ frozenlist==1.4.1 # via # aiohttp # aiosignal -fsspec==2023.12.2 +fsspec==2024.9.0 # via dask geomet==0.2.1.post1 # via cassandra-driver -google-api-core[grpc]==2.19.1 +google-api-core[grpc]==2.19.2 # via # google-cloud-bigquery # google-cloud-bigquery-storage @@ -188,46 +183,48 @@ google-api-core[grpc]==2.19.1 # google-cloud-core # google-cloud-datastore # google-cloud-storage -google-auth==2.30.0 +google-auth==2.34.0 # via # google-api-core + # google-cloud-bigquery # google-cloud-bigquery-storage + # google-cloud-bigtable # google-cloud-core + # google-cloud-datastore # google-cloud-storage # kubernetes -google-cloud-bigquery[pandas]==3.13.0 -google-cloud-bigquery-storage==2.25.0 -google-cloud-bigtable==2.24.0 +google-cloud-bigquery[pandas]==3.25.0 +google-cloud-bigquery-storage==2.26.0 +google-cloud-bigtable==2.26.0 google-cloud-core==2.4.1 # via # google-cloud-bigquery # google-cloud-bigtable # google-cloud-datastore # google-cloud-storage -google-cloud-datastore==2.19.0 -google-cloud-storage==2.17.0 -google-crc32c==1.5.0 +google-cloud-datastore==2.20.1 +google-cloud-storage==2.18.2 +google-crc32c==1.6.0 # via # google-cloud-storage # google-resumable-media -google-resumable-media==2.7.1 +google-resumable-media==2.7.2 # via # google-cloud-bigquery # google-cloud-storage -googleapis-common-protos[grpc]==1.63.2 +googleapis-common-protos[grpc]==1.65.0 # via # google-api-core # grpc-google-iam-v1 # grpcio-status -great-expectations==0.18.16 -greenlet==3.0.3 +great-expectations==0.18.20 +greenlet==3.1.0 # via sqlalchemy grpc-google-iam-v1==0.13.1 # via google-cloud-bigtable -grpcio==1.64.1 +grpcio==1.66.1 # via # google-api-core - # google-cloud-bigquery # googleapis-common-protos # grpc-google-iam-v1 # grpcio-health-checking @@ -235,38 +232,36 @@ grpcio==1.64.1 # grpcio-status # grpcio-testing # grpcio-tools -grpcio-health-checking==1.62.2 -grpcio-reflection==1.62.2 -grpcio-status==1.62.2 +grpcio-health-checking==1.62.3 +grpcio-reflection==1.62.3 +grpcio-status==1.62.3 # via google-api-core -grpcio-testing==1.62.2 -grpcio-tools==1.62.2 -gunicorn==22.0.0 +grpcio-testing==1.62.3 +grpcio-tools==1.62.3 +gunicorn==23.0.0 h11==0.14.0 # via # httpcore # uvicorn happybase==1.2.0 -hazelcast-python-client==5.4.0 -hiredis==2.3.2 +hazelcast-python-client==5.5.0 +hiredis==2.4.0 httpcore==1.0.5 # via httpx httptools==0.6.1 # via uvicorn -httpx==0.27.0 +httpx==0.27.2 # via - # fastapi # jupyterlab # python-keycloak -ibis-framework[duckdb]==9.1.0 +ibis-framework[duckdb]==9.4.0 # via ibis-substrait -ibis-substrait==4.0.0 -identify==2.5.36 +ibis-substrait==4.0.1 +identify==2.6.0 # via pre-commit -idna==3.7 +idna==3.8 # via # anyio - # email-validator # httpx # jsonschema # requests @@ -274,18 +269,18 @@ idna==3.7 # yarl imagesize==1.4.1 # via sphinx -importlib-metadata==8.0.0 +importlib-metadata==8.5.0 # via dask iniconfig==2.0.0 # via pytest -ipykernel==6.29.4 +ipykernel==6.29.5 # via jupyterlab -ipython==8.25.0 +ipython==8.27.0 # via # great-expectations # ipykernel # ipywidgets -ipywidgets==8.1.3 +ipywidgets==8.1.5 # via great-expectations isodate==0.6.1 # via azure-storage-blob @@ -296,7 +291,6 @@ jedi==0.19.1 jinja2==3.1.4 # via # altair - # fastapi # great-expectations # jupyter-server # jupyterlab @@ -316,7 +310,7 @@ jsonpointer==3.0.0 # via # jsonpatch # jsonschema -jsonschema[format-nongpl]==4.22.0 +jsonschema[format-nongpl]==4.23.0 # via # altair # great-expectations @@ -343,7 +337,7 @@ jupyter-events==0.10.0 # via jupyter-server jupyter-lsp==2.2.5 # via jupyterlab -jupyter-server==2.14.1 +jupyter-server==2.14.2 # via # jupyter-lsp # jupyterlab @@ -356,18 +350,18 @@ jupyterlab==4.2.5 # via notebook jupyterlab-pygments==0.3.0 # via nbconvert -jupyterlab-server==2.27.2 +jupyterlab-server==2.27.3 # via # jupyterlab # notebook -jupyterlab-widgets==3.0.11 +jupyterlab-widgets==3.0.13 # via ipywidgets jwcrypto==1.5.6 # via python-keycloak kubernetes==20.13.0 locket==1.0.0 # via partd -makefun==1.15.2 +makefun==1.15.4 # via great-expectations markdown-it-py==3.0.0 # via rich @@ -376,7 +370,7 @@ markupsafe==2.1.5 # jinja2 # nbconvert # werkzeug -marshmallow==3.21.3 +marshmallow==3.22.0 # via great-expectations matplotlib-inline==0.1.7 # via @@ -392,17 +386,17 @@ mistune==3.0.2 mmh3==4.1.0 mock==2.0.0 moto==4.2.14 -msal==1.29.0 +msal==1.31.0 # via # azure-identity # msal-extensions msal-extensions==1.2.0 # via azure-identity -multidict==6.0.5 +multidict==6.1.0 # via # aiohttp # yarl -mypy==1.10.1 +mypy==1.11.2 # via sqlalchemy mypy-extensions==1.0.0 # via mypy @@ -439,8 +433,6 @@ numpy==1.26.4 # scipy oauthlib==3.2.2 # via requests-oauthlib -orjson==3.10.5 - # via fastapi overrides==7.7.0 # via jupyter-server packaging==24.1 @@ -482,11 +474,11 @@ parsy==2.1 # via ibis-framework partd==1.4.2 # via dask -pbr==6.0.0 +pbr==6.1.0 # via mock pexpect==4.9.0 # via ipython -pip==24.1.1 +pip==24.2 # via pip-tools pip-tools==7.4.1 platformdirs==3.11.0 @@ -498,7 +490,7 @@ pluggy==1.5.0 # via pytest ply==3.11 # via thriftpy2 -portalocker==2.10.0 +portalocker==2.10.1 # via msal-extensions pre-commit==3.3.1 prometheus-client==0.20.0 @@ -508,14 +500,12 @@ prompt-toolkit==3.0.47 proto-plus==1.24.0 # via # google-api-core - # google-cloud-bigquery # google-cloud-bigquery-storage # google-cloud-bigtable # google-cloud-datastore -protobuf==4.25.3 +protobuf==4.25.4 # via # google-api-core - # google-cloud-bigquery # google-cloud-bigquery-storage # google-cloud-bigtable # google-cloud-datastore @@ -531,8 +521,8 @@ protobuf==4.25.3 # substrait psutil==5.9.0 # via ipykernel -psycopg[binary, pool]==3.1.19 -psycopg-binary==3.1.19 +psycopg[binary, pool]==3.2.1 +psycopg-binary==3.2.1 # via psycopg psycopg-pool==3.2.2 # via psycopg @@ -540,14 +530,14 @@ ptyprocess==0.7.0 # via # pexpect # terminado -pure-eval==0.2.2 +pure-eval==0.2.3 # via stack-data py==1.11.0 py-cpuinfo==9.0.0 # via pytest-benchmark py4j==0.10.9.7 # via pyspark -pyarrow==15.0.2 +pyarrow==17.0.0 # via # dask-expr # db-dtypes @@ -556,23 +546,21 @@ pyarrow==15.0.2 # ibis-framework # snowflake-connector-python pyarrow-hotfix==0.6 - # via - # deltalake - # ibis-framework -pyasn1==0.6.0 + # via ibis-framework +pyasn1==0.6.1 # via # pyasn1-modules # rsa -pyasn1-modules==0.4.0 +pyasn1-modules==0.4.1 # via google-auth pybindgen==0.22.1 pycparser==2.22 # via cffi -pydantic==2.7.4 +pydantic==2.9.1 # via # fastapi # great-expectations -pydantic-core==2.18.4 +pydantic-core==2.23.3 # via pydantic pygments==2.18.0 # via @@ -580,23 +568,23 @@ pygments==2.18.0 # nbconvert # rich # sphinx -pyjwt[crypto]==2.8.0 +pyjwt[crypto]==2.9.0 # via # msal # singlestoredb # snowflake-connector-python -pymssql==2.3.0 +pymssql==2.3.1 pymysql==1.1.1 pyodbc==5.1.0 -pyopenssl==24.1.0 +pyopenssl==24.2.1 # via snowflake-connector-python -pyparsing==3.1.2 +pyparsing==3.1.4 # via great-expectations pyproject-hooks==1.1.0 # via # build # pip-tools -pyspark==3.5.1 +pyspark==3.5.2 pytest==7.4.4 # via # pytest-benchmark @@ -632,16 +620,14 @@ python-dotenv==1.0.1 python-json-logger==2.0.7 # via jupyter-events python-keycloak==4.2.2 -python-multipart==0.0.9 - # via fastapi -pytz==2024.1 +pytz==2024.2 # via # great-expectations # ibis-framework # pandas # snowflake-connector-python # trino -pyyaml==6.0.1 +pyyaml==6.0.2 # via # dask # ibis-substrait @@ -650,7 +636,7 @@ pyyaml==6.0.1 # pre-commit # responses # uvicorn -pyzmq==26.0.3 +pyzmq==26.2.0 # via # ipykernel # jupyter-client @@ -661,7 +647,7 @@ referencing==0.35.1 # jsonschema # jsonschema-specifications # jupyter-events -regex==2024.5.15 +regex==2024.7.24 # via parsimonious requests==2.32.3 # via @@ -697,35 +683,33 @@ rfc3986-validator==0.1.1 # via # jsonschema # jupyter-events -rich==13.7.1 - # via - # ibis-framework - # typer -rpds-py==0.18.1 +rich==13.8.1 + # via ibis-framework +rpds-py==0.20.0 # via # jsonschema # referencing rsa==4.9 # via google-auth -ruamel-yaml==0.17.17 +ruamel-yaml==0.17.40 # via great-expectations -ruff==0.4.10 +ruamel-yaml-clib==0.2.8 + # via ruamel-yaml +ruff==0.6.4 s3transfer==0.10.2 # via boto3 -scipy==1.14.0 +scipy==1.14.1 # via great-expectations send2trash==1.8.3 # via jupyter-server -setuptools==70.1.1 +setuptools==74.1.2 # via # grpcio-tools # jupyterlab # kubernetes # pip-tools # singlestoredb -shellingham==1.5.4 - # via typer -singlestoredb==1.4.0 +singlestoredb==1.6.3 six==1.16.0 # via # asttokens @@ -745,49 +729,49 @@ sniffio==1.3.1 # httpx snowballstemmer==2.2.0 # via sphinx -snowflake-connector-python[pandas]==3.11.0 +snowflake-connector-python[pandas]==3.12.1 sortedcontainers==2.4.0 # via snowflake-connector-python -soupsieve==2.5 +soupsieve==2.6 # via beautifulsoup4 sphinx==6.2.1 -sphinxcontrib-applehelp==1.0.8 +sphinxcontrib-applehelp==2.0.0 # via sphinx -sphinxcontrib-devhelp==1.0.6 +sphinxcontrib-devhelp==2.0.0 # via sphinx -sphinxcontrib-htmlhelp==2.0.5 +sphinxcontrib-htmlhelp==2.1.0 # via sphinx sphinxcontrib-jsmath==1.0.1 # via sphinx -sphinxcontrib-qthelp==1.0.7 +sphinxcontrib-qthelp==2.0.0 # via sphinx -sphinxcontrib-serializinghtml==1.1.10 +sphinxcontrib-serializinghtml==2.0.0 # via sphinx -sqlalchemy[mypy]==2.0.31 -sqlglot==25.1.0 +sqlalchemy[mypy]==2.0.34 +sqlglot==25.18.0 # via ibis-framework sqlite-vec==0.1.1 -sqlparams==6.0.1 +sqlparams==6.1.0 # via singlestoredb stack-data==0.6.3 # via ipython -starlette==0.37.2 +starlette==0.38.5 # via fastapi -substrait==0.19.0 +substrait==0.22.0 # via ibis-substrait tabulate==0.9.0 -tenacity==8.4.2 +tenacity==8.5.0 terminado==0.18.1 # via # jupyter-server # jupyter-server-terminals testcontainers==4.4.0 -thriftpy2==0.5.1 +thriftpy2==0.5.2 # via happybase tinycss2==1.3.0 # via nbconvert toml==0.10.2 -tomlkit==0.12.5 +tomlkit==0.13.2 # via snowflake-connector-python toolz==0.12.1 # via @@ -803,7 +787,7 @@ tornado==6.4.1 # jupyterlab # notebook # terminado -tqdm==4.66.4 +tqdm==4.66.5 # via great-expectations traitlets==5.14.3 # via @@ -820,24 +804,22 @@ traitlets==5.14.3 # nbclient # nbconvert # nbformat -trino==0.328.0 +trino==0.329.0 typeguard==4.3.0 -typer==0.12.3 - # via fastapi-cli types-cffi==1.16.0.20240331 # via types-pyopenssl types-protobuf==3.19.22 # via mypy-protobuf types-pymysql==1.1.0.20240524 -types-pyopenssl==24.1.0.20240425 +types-pyopenssl==24.1.0.20240722 # via types-redis -types-python-dateutil==2.9.0.20240316 +types-python-dateutil==2.9.0.20240906 # via arrow types-pytz==2024.1.0.20240417 -types-pyyaml==6.0.12.20240311 -types-redis==4.6.0.20240425 +types-pyyaml==6.0.12.20240808 +types-redis==4.6.0.20240903 types-requests==2.30.0.0 -types-setuptools==70.1.0.20240627 +types-setuptools==74.1.0.20240907 # via types-cffi types-tabulate==0.9.0.20240106 types-urllib3==1.26.25.14 @@ -861,18 +843,15 @@ typing-extensions==4.12.2 # sqlalchemy # testcontainers # typeguard - # typer tzdata==2024.1 # via pandas tzlocal==5.2 # via # great-expectations # trino -ujson==5.10.0 - # via fastapi uri-template==1.3.0 # via jsonschema -urllib3==1.26.19 +urllib3==2.2.2 # via # botocore # docker @@ -883,17 +862,16 @@ urllib3==1.26.19 # requests # responses # testcontainers -uvicorn[standard]==0.30.1 - # via fastapi -uvloop==0.19.0 +uvicorn[standard]==0.30.6 +uvloop==0.20.0 # via uvicorn virtualenv==20.23.0 # via pre-commit -watchfiles==0.22.0 +watchfiles==0.24.0 # via uvicorn wcwidth==0.2.13 # via prompt-toolkit -webcolors==24.6.0 +webcolors==24.8.0 # via jsonschema webencodings==0.5.1 # via @@ -903,15 +881,15 @@ websocket-client==1.8.0 # via # jupyter-server # kubernetes -websockets==12.0 +websockets==13.0.1 # via uvicorn -werkzeug==3.0.3 +werkzeug==3.0.4 # via moto -wheel==0.43.0 +wheel==0.44.0 # via # pip-tools # singlestoredb -widgetsnbextension==4.0.11 +widgetsnbextension==4.0.13 # via ipywidgets wrapt==1.16.0 # via @@ -919,7 +897,7 @@ wrapt==1.16.0 # testcontainers xmltodict==0.13.0 # via moto -yarl==1.9.4 +yarl==1.11.1 # via aiohttp -zipp==3.19.1 +zipp==3.20.1 # via importlib-metadata diff --git a/sdk/python/requirements/py3.11-requirements.txt b/sdk/python/requirements/py3.11-requirements.txt index 9f6dff962b..611a0cedca 100644 --- a/sdk/python/requirements/py3.11-requirements.txt +++ b/sdk/python/requirements/py3.11-requirements.txt @@ -1,23 +1,20 @@ # This file was autogenerated by uv via the following command: # uv pip compile --system --no-strip-extras setup.py --output-file sdk/python/requirements/py3.11-requirements.txt -annotated-types==0.6.0 +annotated-types==0.7.0 # via pydantic -anyio==4.3.0 +anyio==4.4.0 # via - # httpx # starlette # watchfiles -attrs==23.2.0 +attrs==24.2.0 # via # jsonschema # referencing -bigtree==0.19.2 +bigtree==0.21.1 cachetools==5.5.0 # via google-auth -certifi==2024.7.4 +certifi==2024.8.30 # via - # httpcore - # httpx # kubernetes # requests charset-normalizer==3.3.2 @@ -25,65 +22,44 @@ charset-normalizer==3.3.2 click==8.1.7 # via # dask - # typer # uvicorn cloudpickle==3.0.0 # via dask colorama==0.4.6 -dask[dataframe]==2024.5.0 +dask[dataframe]==2024.8.2 # via dask-expr -dask-expr==1.1.0 +dask-expr==1.1.13 # via dask dill==0.3.8 -dnspython==2.6.1 - # via email-validator -email-validator==2.1.1 - # via fastapi -fastapi==0.111.0 - # via fastapi-cli -fastapi-cli==0.0.2 - # via fastapi -fsspec==2024.3.1 +fastapi==0.114.1 +fsspec==2024.9.0 # via dask google-auth==2.34.0 # via kubernetes -greenlet==3.0.3 +greenlet==3.1.0 # via sqlalchemy -gunicorn==22.0.0 +gunicorn==23.0.0 h11==0.14.0 - # via - # httpcore - # uvicorn -httpcore==1.0.5 - # via httpx + # via uvicorn httptools==0.6.1 # via uvicorn -httpx==0.27.0 - # via fastapi -idna==3.7 +idna==3.8 # via # anyio - # email-validator - # httpx # requests -importlib-metadata==7.1.0 +importlib-metadata==8.5.0 # via dask jinja2==3.1.4 - # via fastapi -jsonschema==4.22.0 +jsonschema==4.23.0 jsonschema-specifications==2023.12.1 # via jsonschema kubernetes==20.13.0 locket==1.0.0 # via partd -markdown-it-py==3.0.0 - # via rich markupsafe==2.1.5 # via jinja2 -mdurl==0.1.2 - # via markdown-it-py mmh3==4.1.0 -mypy==1.10.0 +mypy==1.11.2 # via sqlalchemy mypy-extensions==1.0.0 # via mypy @@ -95,9 +71,7 @@ numpy==1.26.4 # pyarrow oauthlib==3.2.2 # via requests-oauthlib -orjson==3.10.3 - # via fastapi -packaging==24.0 +packaging==24.1 # via # dask # gunicorn @@ -108,23 +82,22 @@ pandas==2.2.2 partd==1.4.2 # via dask prometheus-client==0.20.0 -protobuf==4.25.3 +protobuf==4.25.4 # via mypy-protobuf psutil==6.0.0 -pyarrow==16.0.0 +pyarrow==17.0.0 # via dask-expr -pyasn1==0.6.0 +pyasn1==0.6.1 # via # pyasn1-modules # rsa -pyasn1-modules==0.4.0 +pyasn1-modules==0.4.1 # via google-auth -pydantic==2.7.1 +pydantic==2.9.1 # via fastapi -pydantic-core==2.18.2 +pydantic-core==2.23.3 # via pydantic pygments==2.18.0 - # via rich pyjwt==2.9.0 python-dateutil==2.9.0.post0 # via @@ -132,11 +105,9 @@ python-dateutil==2.9.0.post0 # pandas python-dotenv==1.0.1 # via uvicorn -python-multipart==0.0.9 - # via fastapi -pytz==2024.1 +pytz==2024.2 # via pandas -pyyaml==6.0.1 +pyyaml==6.0.2 # via # dask # kubernetes @@ -145,49 +116,41 @@ referencing==0.35.1 # via # jsonschema # jsonschema-specifications -requests==2.31.0 +requests==2.32.3 # via # kubernetes # requests-oauthlib requests-oauthlib==2.0.0 # via kubernetes -rich==13.7.1 - # via typer -rpds-py==0.18.1 +rpds-py==0.20.0 # via # jsonschema # referencing rsa==4.9 # via google-auth -setuptools==73.0.1 +setuptools==74.1.2 # via kubernetes -shellingham==1.5.4 - # via typer six==1.16.0 # via # kubernetes # python-dateutil sniffio==1.3.1 - # via - # anyio - # httpx -sqlalchemy[mypy]==2.0.30 -starlette==0.37.2 + # via anyio +sqlalchemy[mypy]==2.0.34 +starlette==0.38.5 # via fastapi tabulate==0.9.0 -tenacity==8.3.0 +tenacity==8.5.0 toml==0.10.2 toolz==0.12.1 # via # dask # partd -tqdm==4.66.4 -typeguard==4.2.1 -typer==0.12.3 - # via fastapi-cli -types-protobuf==5.26.0.20240422 +tqdm==4.66.5 +typeguard==4.3.0 +types-protobuf==5.27.0.20240907 # via mypy-protobuf -typing-extensions==4.11.0 +typing-extensions==4.12.2 # via # fastapi # mypy @@ -195,26 +158,20 @@ typing-extensions==4.11.0 # pydantic-core # sqlalchemy # typeguard - # typer tzdata==2024.1 # via pandas -ujson==5.9.0 - # via fastapi -urllib3==2.2.1 +urllib3==2.2.2 # via # kubernetes # requests -uvicorn[standard]==0.29.0 - # via - # fastapi - # fastapi-cli -uvloop==0.19.0 +uvicorn[standard]==0.30.6 +uvloop==0.20.0 # via uvicorn -watchfiles==0.21.0 +watchfiles==0.24.0 # via uvicorn websocket-client==1.8.0 # via kubernetes -websockets==12.0 +websockets==13.0.1 # via uvicorn -zipp==3.19.1 +zipp==3.20.1 # via importlib-metadata diff --git a/sdk/python/requirements/py3.9-ci-requirements.txt b/sdk/python/requirements/py3.9-ci-requirements.txt index f32f6790d3..f4e7c795fa 100644 --- a/sdk/python/requirements/py3.9-ci-requirements.txt +++ b/sdk/python/requirements/py3.9-ci-requirements.txt @@ -1,9 +1,11 @@ # This file was autogenerated by uv via the following command: # uv pip compile --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.9-ci-requirements.txt -aiobotocore==2.13.1 -aiohttp==3.9.5 +aiobotocore==2.15.0 +aiohappyeyeballs==2.4.0 + # via aiohttp +aiohttp==3.10.5 # via aiobotocore -aioitertools==0.11.0 +aioitertools==0.12.0 # via aiobotocore aiosignal==1.3.1 # via aiohttp @@ -40,7 +42,7 @@ async-timeout==4.0.3 # redis atpublic==4.1.0 # via ibis-framework -attrs==23.2.0 +attrs==24.2.0 # via # aiohttp # jsonschema @@ -50,8 +52,8 @@ azure-core==1.30.2 # azure-identity # azure-storage-blob azure-identity==1.17.1 -azure-storage-blob==12.20.0 -babel==2.15.0 +azure-storage-blob==12.22.0 +babel==2.16.0 # via # jupyterlab-server # sphinx @@ -59,25 +61,25 @@ beautifulsoup4==4.12.3 # via nbconvert bidict==0.23.1 # via ibis-framework -bigtree==0.19.2 +bigtree==0.21.1 bleach==6.1.0 # via nbconvert -boto3==1.34.131 +boto3==1.35.16 # via moto -botocore==1.34.131 +botocore==1.35.16 # via # aiobotocore # boto3 # moto # s3transfer -build==1.2.1 +build==1.2.2 # via # pip-tools # singlestoredb -cachetools==5.3.3 +cachetools==5.5.0 # via google-auth -cassandra-driver==3.29.1 -certifi==2024.7.4 +cassandra-driver==3.29.2 +certifi==2024.8.30 # via # elastic-transport # httpcore @@ -86,7 +88,7 @@ certifi==2024.7.4 # minio # requests # snowflake-connector-python -cffi==1.16.0 +cffi==1.17.1 # via # argon2-cffi-bindings # cryptography @@ -103,7 +105,6 @@ click==8.1.7 # geomet # great-expectations # pip-tools - # typer # uvicorn cloudpickle==3.0.0 # via dask @@ -113,9 +114,9 @@ comm==0.2.2 # via # ipykernel # ipywidgets -coverage[toml]==7.5.4 +coverage[toml]==7.6.1 # via pytest-cov -cryptography==43.0.1 +cryptography==42.0.8 # via # azure-identity # azure-storage-blob @@ -128,54 +129,50 @@ cryptography==43.0.1 # snowflake-connector-python # types-pyopenssl # types-redis -dask[dataframe]==2024.6.2 +cython==3.0.11 + # via thriftpy2 +dask[dataframe]==2024.8.0 # via dask-expr -dask-expr==1.1.6 +dask-expr==1.1.10 # via dask -db-dtypes==1.2.0 +db-dtypes==1.3.0 # via google-cloud-bigquery -debugpy==1.8.2 +debugpy==1.8.5 # via ipykernel decorator==5.1.1 # via ipython defusedxml==0.7.1 # via nbconvert -deltalake==0.18.1 +deltalake==0.19.2 deprecation==2.1.0 # via python-keycloak dill==0.3.8 distlib==0.3.8 # via virtualenv -dnspython==2.6.1 - # via email-validator docker==7.1.0 # via testcontainers docutils==0.19 # via sphinx duckdb==0.10.3 # via ibis-framework -elastic-transport==8.13.1 +elastic-transport==8.15.0 # via elasticsearch -elasticsearch==8.14.0 -email-validator==2.2.0 - # via fastapi +elasticsearch==8.15.1 entrypoints==0.4 # via altair -exceptiongroup==1.2.1 +exceptiongroup==1.2.2 # via # anyio # ipython # pytest execnet==2.1.1 # via pytest-xdist -executing==2.0.1 +executing==2.1.0 # via stack-data -fastapi==0.111.0 -fastapi-cli==0.0.4 - # via fastapi +fastapi==0.114.1 fastjsonschema==2.20.0 # via nbformat -filelock==3.15.4 +filelock==3.16.0 # via # snowflake-connector-python # virtualenv @@ -185,11 +182,11 @@ frozenlist==1.4.1 # via # aiohttp # aiosignal -fsspec==2023.12.2 +fsspec==2024.9.0 # via dask geomet==0.2.1.post1 # via cassandra-driver -google-api-core[grpc]==2.19.1 +google-api-core[grpc]==2.19.2 # via # google-cloud-bigquery # google-cloud-bigquery-storage @@ -197,46 +194,48 @@ google-api-core[grpc]==2.19.1 # google-cloud-core # google-cloud-datastore # google-cloud-storage -google-auth==2.30.0 +google-auth==2.34.0 # via # google-api-core + # google-cloud-bigquery # google-cloud-bigquery-storage + # google-cloud-bigtable # google-cloud-core + # google-cloud-datastore # google-cloud-storage # kubernetes -google-cloud-bigquery[pandas]==3.13.0 -google-cloud-bigquery-storage==2.25.0 -google-cloud-bigtable==2.24.0 +google-cloud-bigquery[pandas]==3.25.0 +google-cloud-bigquery-storage==2.26.0 +google-cloud-bigtable==2.26.0 google-cloud-core==2.4.1 # via # google-cloud-bigquery # google-cloud-bigtable # google-cloud-datastore # google-cloud-storage -google-cloud-datastore==2.19.0 -google-cloud-storage==2.17.0 -google-crc32c==1.5.0 +google-cloud-datastore==2.20.1 +google-cloud-storage==2.18.2 +google-crc32c==1.6.0 # via # google-cloud-storage # google-resumable-media -google-resumable-media==2.7.1 +google-resumable-media==2.7.2 # via # google-cloud-bigquery # google-cloud-storage -googleapis-common-protos[grpc]==1.63.2 +googleapis-common-protos[grpc]==1.65.0 # via # google-api-core # grpc-google-iam-v1 # grpcio-status -great-expectations==0.18.16 -greenlet==3.0.3 +great-expectations==0.18.20 +greenlet==3.1.0 # via sqlalchemy grpc-google-iam-v1==0.13.1 # via google-cloud-bigtable -grpcio==1.64.1 +grpcio==1.66.1 # via # google-api-core - # google-cloud-bigquery # googleapis-common-protos # grpc-google-iam-v1 # grpcio-health-checking @@ -244,38 +243,36 @@ grpcio==1.64.1 # grpcio-status # grpcio-testing # grpcio-tools -grpcio-health-checking==1.62.2 -grpcio-reflection==1.62.2 -grpcio-status==1.62.2 +grpcio-health-checking==1.62.3 +grpcio-reflection==1.62.3 +grpcio-status==1.62.3 # via google-api-core -grpcio-testing==1.62.2 -grpcio-tools==1.62.2 -gunicorn==22.0.0 +grpcio-testing==1.62.3 +grpcio-tools==1.62.3 +gunicorn==23.0.0 h11==0.14.0 # via # httpcore # uvicorn happybase==1.2.0 -hazelcast-python-client==5.4.0 -hiredis==2.3.2 +hazelcast-python-client==5.5.0 +hiredis==2.4.0 httpcore==1.0.5 # via httpx httptools==0.6.1 # via uvicorn -httpx==0.27.0 +httpx==0.27.2 # via - # fastapi # jupyterlab # python-keycloak ibis-framework[duckdb]==9.0.0 # via ibis-substrait -ibis-substrait==4.0.0 -identify==2.5.36 +ibis-substrait==4.0.1 +identify==2.6.0 # via pre-commit -idna==3.7 +idna==3.8 # via # anyio - # email-validator # httpx # jsonschema # requests @@ -283,7 +280,7 @@ idna==3.7 # yarl imagesize==1.4.1 # via sphinx -importlib-metadata==8.0.0 +importlib-metadata==8.5.0 # via # build # dask @@ -296,14 +293,14 @@ importlib-metadata==8.0.0 # typeguard iniconfig==2.0.0 # via pytest -ipykernel==6.29.4 +ipykernel==6.29.5 # via jupyterlab ipython==8.18.1 # via # great-expectations # ipykernel # ipywidgets -ipywidgets==8.1.3 +ipywidgets==8.1.5 # via great-expectations isodate==0.6.1 # via azure-storage-blob @@ -314,7 +311,6 @@ jedi==0.19.1 jinja2==3.1.4 # via # altair - # fastapi # great-expectations # jupyter-server # jupyterlab @@ -334,7 +330,7 @@ jsonpointer==3.0.0 # via # jsonpatch # jsonschema -jsonschema[format-nongpl]==4.22.0 +jsonschema[format-nongpl]==4.23.0 # via # altair # great-expectations @@ -361,7 +357,7 @@ jupyter-events==0.10.0 # via jupyter-server jupyter-lsp==2.2.5 # via jupyterlab -jupyter-server==2.14.1 +jupyter-server==2.14.2 # via # jupyter-lsp # jupyterlab @@ -374,18 +370,18 @@ jupyterlab==4.2.5 # via notebook jupyterlab-pygments==0.3.0 # via nbconvert -jupyterlab-server==2.27.2 +jupyterlab-server==2.27.3 # via # jupyterlab # notebook -jupyterlab-widgets==3.0.11 +jupyterlab-widgets==3.0.13 # via ipywidgets jwcrypto==1.5.6 # via python-keycloak kubernetes==20.13.0 locket==1.0.0 # via partd -makefun==1.15.2 +makefun==1.15.4 # via great-expectations markdown-it-py==3.0.0 # via rich @@ -394,7 +390,7 @@ markupsafe==2.1.5 # jinja2 # nbconvert # werkzeug -marshmallow==3.21.3 +marshmallow==3.22.0 # via great-expectations matplotlib-inline==0.1.7 # via @@ -410,17 +406,17 @@ mistune==3.0.2 mmh3==4.1.0 mock==2.0.0 moto==4.2.14 -msal==1.29.0 +msal==1.31.0 # via # azure-identity # msal-extensions msal-extensions==1.2.0 # via azure-identity -multidict==6.0.5 +multidict==6.1.0 # via # aiohttp # yarl -mypy==1.10.1 +mypy==1.11.2 # via sqlalchemy mypy-extensions==1.0.0 # via mypy @@ -457,8 +453,6 @@ numpy==1.26.4 # scipy oauthlib==3.2.2 # via requests-oauthlib -orjson==3.10.5 - # via fastapi overrides==7.7.0 # via jupyter-server packaging==24.1 @@ -500,11 +494,11 @@ parsy==2.1 # via ibis-framework partd==1.4.2 # via dask -pbr==6.0.0 +pbr==6.1.0 # via mock pexpect==4.9.0 # via ipython -pip==24.1.1 +pip==24.2 # via pip-tools pip-tools==7.4.1 platformdirs==3.11.0 @@ -516,7 +510,7 @@ pluggy==1.5.0 # via pytest ply==3.11 # via thriftpy2 -portalocker==2.10.0 +portalocker==2.10.1 # via msal-extensions pre-commit==3.3.1 prometheus-client==0.20.0 @@ -526,14 +520,12 @@ prompt-toolkit==3.0.47 proto-plus==1.24.0 # via # google-api-core - # google-cloud-bigquery # google-cloud-bigquery-storage # google-cloud-bigtable # google-cloud-datastore -protobuf==4.25.3 +protobuf==4.25.4 # via # google-api-core - # google-cloud-bigquery # google-cloud-bigquery-storage # google-cloud-bigtable # google-cloud-datastore @@ -549,8 +541,8 @@ protobuf==4.25.3 # substrait psutil==5.9.0 # via ipykernel -psycopg[binary, pool]==3.1.18 -psycopg-binary==3.1.18 +psycopg[binary, pool]==3.2.1 +psycopg-binary==3.2.1 # via psycopg psycopg-pool==3.2.2 # via psycopg @@ -558,14 +550,14 @@ ptyprocess==0.7.0 # via # pexpect # terminado -pure-eval==0.2.2 +pure-eval==0.2.3 # via stack-data py==1.11.0 py-cpuinfo==9.0.0 # via pytest-benchmark py4j==0.10.9.7 # via pyspark -pyarrow==15.0.2 +pyarrow==16.1.0 # via # dask-expr # db-dtypes @@ -574,23 +566,21 @@ pyarrow==15.0.2 # ibis-framework # snowflake-connector-python pyarrow-hotfix==0.6 - # via - # deltalake - # ibis-framework -pyasn1==0.6.0 + # via ibis-framework +pyasn1==0.6.1 # via # pyasn1-modules # rsa -pyasn1-modules==0.4.0 +pyasn1-modules==0.4.1 # via google-auth pybindgen==0.22.1 pycparser==2.22 # via cffi -pydantic==2.7.4 +pydantic==2.9.1 # via # fastapi # great-expectations -pydantic-core==2.18.4 +pydantic-core==2.23.3 # via pydantic pygments==2.18.0 # via @@ -598,23 +588,23 @@ pygments==2.18.0 # nbconvert # rich # sphinx -pyjwt[crypto]==2.8.0 +pyjwt[crypto]==2.9.0 # via # msal # singlestoredb # snowflake-connector-python -pymssql==2.3.0 +pymssql==2.3.1 pymysql==1.1.1 pyodbc==5.1.0 -pyopenssl==24.1.0 +pyopenssl==24.2.1 # via snowflake-connector-python -pyparsing==3.1.2 +pyparsing==3.1.4 # via great-expectations pyproject-hooks==1.1.0 # via # build # pip-tools -pyspark==3.5.1 +pyspark==3.5.2 pytest==7.4.4 # via # pytest-benchmark @@ -650,16 +640,14 @@ python-dotenv==1.0.1 python-json-logger==2.0.7 # via jupyter-events python-keycloak==4.2.2 -python-multipart==0.0.9 - # via fastapi -pytz==2024.1 +pytz==2024.2 # via # great-expectations # ibis-framework # pandas # snowflake-connector-python # trino -pyyaml==6.0.1 +pyyaml==6.0.2 # via # dask # ibis-substrait @@ -668,7 +656,7 @@ pyyaml==6.0.1 # pre-commit # responses # uvicorn -pyzmq==26.0.3 +pyzmq==26.2.0 # via # ipykernel # jupyter-client @@ -679,7 +667,7 @@ referencing==0.35.1 # jsonschema # jsonschema-specifications # jupyter-events -regex==2024.5.15 +regex==2024.7.24 # via parsimonious requests==2.32.3 # via @@ -715,37 +703,33 @@ rfc3986-validator==0.1.1 # via # jsonschema # jupyter-events -rich==13.7.1 - # via - # ibis-framework - # typer -rpds-py==0.18.1 +rich==13.8.1 + # via ibis-framework +rpds-py==0.20.0 # via # jsonschema # referencing rsa==4.9 # via google-auth -ruamel-yaml==0.17.17 +ruamel-yaml==0.17.40 # via great-expectations ruamel-yaml-clib==0.2.8 # via ruamel-yaml -ruff==0.4.10 +ruff==0.6.4 s3transfer==0.10.2 # via boto3 scipy==1.13.1 # via great-expectations send2trash==1.8.3 # via jupyter-server -setuptools==70.1.1 +setuptools==74.1.2 # via # grpcio-tools # jupyterlab # kubernetes # pip-tools # singlestoredb -shellingham==1.5.4 - # via typer -singlestoredb==1.4.0 +singlestoredb==1.6.3 six==1.16.0 # via # asttokens @@ -765,44 +749,44 @@ sniffio==1.3.1 # httpx snowballstemmer==2.2.0 # via sphinx -snowflake-connector-python[pandas]==3.11.0 +snowflake-connector-python[pandas]==3.12.1 sortedcontainers==2.4.0 # via snowflake-connector-python -soupsieve==2.5 +soupsieve==2.6 # via beautifulsoup4 sphinx==6.2.1 -sphinxcontrib-applehelp==1.0.8 +sphinxcontrib-applehelp==2.0.0 # via sphinx -sphinxcontrib-devhelp==1.0.6 +sphinxcontrib-devhelp==2.0.0 # via sphinx -sphinxcontrib-htmlhelp==2.0.5 +sphinxcontrib-htmlhelp==2.1.0 # via sphinx sphinxcontrib-jsmath==1.0.1 # via sphinx -sphinxcontrib-qthelp==1.0.7 +sphinxcontrib-qthelp==2.0.0 # via sphinx -sphinxcontrib-serializinghtml==1.1.10 +sphinxcontrib-serializinghtml==2.0.0 # via sphinx -sqlalchemy[mypy]==2.0.31 +sqlalchemy[mypy]==2.0.34 sqlglot==23.12.2 # via ibis-framework sqlite-vec==0.1.1 -sqlparams==6.0.1 +sqlparams==6.1.0 # via singlestoredb stack-data==0.6.3 # via ipython -starlette==0.37.2 +starlette==0.38.5 # via fastapi -substrait==0.19.0 +substrait==0.22.0 # via ibis-substrait tabulate==0.9.0 -tenacity==8.4.2 +tenacity==8.5.0 terminado==0.18.1 # via # jupyter-server # jupyter-server-terminals testcontainers==4.4.0 -thriftpy2==0.5.1 +thriftpy2==0.5.2 # via happybase tinycss2==1.3.0 # via nbconvert @@ -817,7 +801,7 @@ tomli==2.0.1 # pytest # pytest-env # singlestoredb -tomlkit==0.12.5 +tomlkit==0.13.2 # via snowflake-connector-python toolz==0.12.1 # via @@ -833,7 +817,7 @@ tornado==6.4.1 # jupyterlab # notebook # terminado -tqdm==4.66.4 +tqdm==4.66.5 # via great-expectations traitlets==5.14.3 # via @@ -850,24 +834,22 @@ traitlets==5.14.3 # nbclient # nbconvert # nbformat -trino==0.328.0 +trino==0.329.0 typeguard==4.3.0 -typer==0.12.3 - # via fastapi-cli types-cffi==1.16.0.20240331 # via types-pyopenssl types-protobuf==3.19.22 # via mypy-protobuf types-pymysql==1.1.0.20240524 -types-pyopenssl==24.1.0.20240425 +types-pyopenssl==24.1.0.20240722 # via types-redis -types-python-dateutil==2.9.0.20240316 +types-python-dateutil==2.9.0.20240906 # via arrow types-pytz==2024.1.0.20240417 -types-pyyaml==6.0.12.20240311 -types-redis==4.6.0.20240425 +types-pyyaml==6.0.12.20240808 +types-redis==4.6.0.20240903 types-requests==2.30.0.0 -types-setuptools==70.1.0.20240627 +types-setuptools==74.1.0.20240907 # via types-cffi types-tabulate==0.9.0.20240106 types-urllib3==1.26.25.14 @@ -885,6 +867,7 @@ typing-extensions==4.12.2 # ibis-framework # ipython # jwcrypto + # multidict # mypy # psycopg # psycopg-pool @@ -895,7 +878,6 @@ typing-extensions==4.12.2 # starlette # testcontainers # typeguard - # typer # uvicorn tzdata==2024.1 # via pandas @@ -903,11 +885,9 @@ tzlocal==5.2 # via # great-expectations # trino -ujson==5.10.0 - # via fastapi uri-template==1.3.0 # via jsonschema -urllib3==1.26.19 +urllib3==1.26.20 # via # botocore # docker @@ -919,17 +899,16 @@ urllib3==1.26.19 # responses # snowflake-connector-python # testcontainers -uvicorn[standard]==0.30.1 - # via fastapi -uvloop==0.19.0 +uvicorn[standard]==0.30.6 +uvloop==0.20.0 # via uvicorn virtualenv==20.23.0 # via pre-commit -watchfiles==0.22.0 +watchfiles==0.24.0 # via uvicorn wcwidth==0.2.13 # via prompt-toolkit -webcolors==24.6.0 +webcolors==24.8.0 # via jsonschema webencodings==0.5.1 # via @@ -939,15 +918,15 @@ websocket-client==1.8.0 # via # jupyter-server # kubernetes -websockets==12.0 +websockets==13.0.1 # via uvicorn -werkzeug==3.0.3 +werkzeug==3.0.4 # via moto -wheel==0.43.0 +wheel==0.44.0 # via # pip-tools # singlestoredb -widgetsnbextension==4.0.11 +widgetsnbextension==4.0.13 # via ipywidgets wrapt==1.16.0 # via @@ -955,7 +934,7 @@ wrapt==1.16.0 # testcontainers xmltodict==0.13.0 # via moto -yarl==1.9.4 +yarl==1.11.1 # via aiohttp -zipp==3.19.1 +zipp==3.20.1 # via importlib-metadata diff --git a/sdk/python/requirements/py3.9-requirements.txt b/sdk/python/requirements/py3.9-requirements.txt index 960eaa6554..0ae2fcf9d6 100644 --- a/sdk/python/requirements/py3.9-requirements.txt +++ b/sdk/python/requirements/py3.9-requirements.txt @@ -1,23 +1,20 @@ # This file was autogenerated by uv via the following command: # uv pip compile --system --no-strip-extras setup.py --output-file sdk/python/requirements/py3.9-requirements.txt -annotated-types==0.6.0 +annotated-types==0.7.0 # via pydantic -anyio==4.3.0 +anyio==4.4.0 # via - # httpx # starlette # watchfiles -attrs==23.2.0 +attrs==24.2.0 # via # jsonschema # referencing -bigtree==0.19.2 +bigtree==0.21.1 cachetools==5.5.0 # via google-auth -certifi==2024.7.4 +certifi==2024.8.30 # via - # httpcore - # httpx # kubernetes # requests charset-normalizer==3.3.2 @@ -25,69 +22,48 @@ charset-normalizer==3.3.2 click==8.1.7 # via # dask - # typer # uvicorn cloudpickle==3.0.0 # via dask colorama==0.4.6 -dask[dataframe]==2024.5.0 +dask[dataframe]==2024.8.0 # via dask-expr -dask-expr==1.1.0 +dask-expr==1.1.10 # via dask dill==0.3.8 -dnspython==2.6.1 - # via email-validator -email-validator==2.1.1 - # via fastapi exceptiongroup==1.2.2 # via anyio -fastapi==0.111.0 - # via fastapi-cli -fastapi-cli==0.0.2 - # via fastapi -fsspec==2024.3.1 +fastapi==0.114.1 +fsspec==2024.9.0 # via dask google-auth==2.34.0 # via kubernetes -greenlet==3.0.3 +greenlet==3.1.0 # via sqlalchemy -gunicorn==22.0.0 +gunicorn==23.0.0 h11==0.14.0 - # via - # httpcore - # uvicorn -httpcore==1.0.5 - # via httpx + # via uvicorn httptools==0.6.1 # via uvicorn -httpx==0.27.0 - # via fastapi -idna==3.7 +idna==3.8 # via # anyio - # email-validator - # httpx # requests -importlib-metadata==8.2.0 +importlib-metadata==8.5.0 # via # dask # typeguard jinja2==3.1.4 - # via fastapi -jsonschema==4.22.0 +jsonschema==4.23.0 jsonschema-specifications==2023.12.1 # via jsonschema kubernetes==20.13.0 locket==1.0.0 # via partd -markdown-it-py==3.0.0 - # via rich markupsafe==2.1.5 # via jinja2 -mdurl==0.1.2 - # via markdown-it-py mmh3==4.1.0 -mypy==1.10.0 +mypy==1.11.2 # via sqlalchemy mypy-extensions==1.0.0 # via mypy @@ -99,9 +75,7 @@ numpy==1.26.4 # pyarrow oauthlib==3.2.2 # via requests-oauthlib -orjson==3.10.3 - # via fastapi -packaging==24.0 +packaging==24.1 # via # dask # gunicorn @@ -112,23 +86,22 @@ pandas==2.2.2 partd==1.4.2 # via dask prometheus-client==0.20.0 -protobuf==4.25.3 +protobuf==4.25.4 # via mypy-protobuf psutil==6.0.0 -pyarrow==16.0.0 +pyarrow==17.0.0 # via dask-expr -pyasn1==0.6.0 +pyasn1==0.6.1 # via # pyasn1-modules # rsa -pyasn1-modules==0.4.0 +pyasn1-modules==0.4.1 # via google-auth -pydantic==2.7.1 +pydantic==2.9.1 # via fastapi -pydantic-core==2.18.2 +pydantic-core==2.23.3 # via pydantic pygments==2.18.0 - # via rich pyjwt==2.9.0 python-dateutil==2.9.0.post0 # via @@ -136,11 +109,9 @@ python-dateutil==2.9.0.post0 # pandas python-dotenv==1.0.1 # via uvicorn -python-multipart==0.0.9 - # via fastapi -pytz==2024.1 +pytz==2024.2 # via pandas -pyyaml==6.0.1 +pyyaml==6.0.2 # via # dask # kubernetes @@ -149,37 +120,31 @@ referencing==0.35.1 # via # jsonschema # jsonschema-specifications -requests==2.31.0 +requests==2.32.3 # via # kubernetes # requests-oauthlib requests-oauthlib==2.0.0 # via kubernetes -rich==13.7.1 - # via typer -rpds-py==0.18.1 +rpds-py==0.20.0 # via # jsonschema # referencing rsa==4.9 # via google-auth -setuptools==73.0.1 +setuptools==74.1.2 # via kubernetes -shellingham==1.5.4 - # via typer six==1.16.0 # via # kubernetes # python-dateutil sniffio==1.3.1 - # via - # anyio - # httpx -sqlalchemy[mypy]==2.0.30 -starlette==0.37.2 + # via anyio +sqlalchemy[mypy]==2.0.34 +starlette==0.38.5 # via fastapi tabulate==0.9.0 -tenacity==8.3.0 +tenacity==8.5.0 toml==0.10.2 tomli==2.0.1 # via mypy @@ -187,13 +152,11 @@ toolz==0.12.1 # via # dask # partd -tqdm==4.66.4 -typeguard==4.2.1 -typer==0.12.3 - # via fastapi-cli -types-protobuf==5.26.0.20240422 +tqdm==4.66.5 +typeguard==4.3.0 +types-protobuf==5.27.0.20240907 # via mypy-protobuf -typing-extensions==4.11.0 +typing-extensions==4.12.2 # via # anyio # fastapi @@ -203,27 +166,21 @@ typing-extensions==4.11.0 # sqlalchemy # starlette # typeguard - # typer # uvicorn tzdata==2024.1 # via pandas -ujson==5.9.0 - # via fastapi -urllib3==2.2.1 +urllib3==2.2.2 # via # kubernetes # requests -uvicorn[standard]==0.29.0 - # via - # fastapi - # fastapi-cli -uvloop==0.19.0 +uvicorn[standard]==0.30.6 +uvloop==0.20.0 # via uvicorn -watchfiles==0.21.0 +watchfiles==0.24.0 # via uvicorn websocket-client==1.8.0 # via kubernetes -websockets==12.0 +websockets==13.0.1 # via uvicorn -zipp==3.19.2 +zipp==3.20.1 # via importlib-metadata diff --git a/setup.py b/setup.py index 6da5e8226a..d8bc55e334 100644 --- a/setup.py +++ b/setup.py @@ -74,7 +74,7 @@ "google-cloud-datastore>=2.16.0,<3", "google-cloud-storage>=1.34.0,<3", "google-cloud-bigtable>=2.11.0,<3", - "fsspec<=2024.1.0", + "fsspec<=2024.9.0", ] REDIS_REQUIRED = [ @@ -82,7 +82,7 @@ "hiredis>=2.0.0,<3", ] -AWS_REQUIRED = ["boto3>=1.17.0,<2", "fsspec<=2024.1.0", "aiobotocore>2,<3"] +AWS_REQUIRED = ["boto3>=1.17.0,<2", "fsspec<=2024.9.0", "aiobotocore>2,<3"] KUBERNETES_REQUIRED = ["kubernetes<=20.13.0"] From 3073ea5911339a5744be45512a9a2ee8b250292b Mon Sep 17 00:00:00 2001 From: Abdul Hameed Date: Tue, 17 Sep 2024 00:27:15 -0400 Subject: [PATCH 071/185] fix: Removed the k8s dependency from required dependencies (#4519) * Removed the k8s dependency from required dependencies Signed-off-by: Abdul Hameed * updated requirments lock files Signed-off-by: Abdul Hameed --------- Signed-off-by: Abdul Hameed --- sdk/python/feast/permissions/server/utils.py | 5 +- .../requirements/py3.10-ci-requirements.txt | 149 +++++++++++++++-- .../requirements/py3.10-requirements.txt | 82 +++++----- .../requirements/py3.11-ci-requirements.txt | 149 +++++++++++++++-- .../requirements/py3.11-requirements.txt | 82 +++++----- .../requirements/py3.9-ci-requirements.txt | 153 +++++++++++++++--- .../requirements/py3.9-requirements.txt | 82 +++++----- setup.py | 1 - 8 files changed, 525 insertions(+), 178 deletions(-) diff --git a/sdk/python/feast/permissions/server/utils.py b/sdk/python/feast/permissions/server/utils.py index ac70f187ce..9a8b319dbc 100644 --- a/sdk/python/feast/permissions/server/utils.py +++ b/sdk/python/feast/permissions/server/utils.py @@ -11,7 +11,6 @@ AuthManager, set_auth_manager, ) -from feast.permissions.auth.kubernetes_token_parser import KubernetesTokenParser from feast.permissions.auth.oidc_token_parser import OidcTokenParser from feast.permissions.auth.token_extractor import TokenExtractor from feast.permissions.auth.token_parser import TokenParser @@ -116,6 +115,10 @@ def init_auth_manager( raise ValueError(f"Unmanaged server type {server_type}") if auth_type == AuthManagerType.KUBERNETES: + from feast.permissions.auth.kubernetes_token_parser import ( + KubernetesTokenParser, + ) + token_parser = KubernetesTokenParser() elif auth_type == AuthManagerType.OIDC: assert isinstance(auth_config, OidcAuthConfig) diff --git a/sdk/python/requirements/py3.10-ci-requirements.txt b/sdk/python/requirements/py3.10-ci-requirements.txt index b8798d96c6..55df7ccb68 100644 --- a/sdk/python/requirements/py3.10-ci-requirements.txt +++ b/sdk/python/requirements/py3.10-ci-requirements.txt @@ -1,6 +1,7 @@ # This file was autogenerated by uv via the following command: # uv pip compile --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.10-ci-requirements.txt aiobotocore==2.15.0 + # via feast (setup.py) aiohappyeyeballs==2.4.0 # via aiohttp aiohttp==3.10.5 @@ -21,6 +22,8 @@ anyio==4.4.0 # jupyter-server # starlette # watchfiles +appnope==0.1.4 + # via ipykernel argon2-cffi==23.1.0 # via jupyter-server argon2-cffi-bindings==21.2.0 @@ -30,6 +33,7 @@ arrow==1.3.0 asn1crypto==1.5.1 # via snowflake-connector-python assertpy==1.1 + # via feast (setup.py) asttokens==2.4.1 # via stack-data async-lru==2.0.4 @@ -52,7 +56,9 @@ azure-core==1.30.2 # azure-identity # azure-storage-blob azure-identity==1.17.1 + # via feast (setup.py) azure-storage-blob==12.22.0 + # via feast (setup.py) babel==2.16.0 # via # jupyterlab-server @@ -60,10 +66,13 @@ babel==2.16.0 beautifulsoup4==4.12.3 # via nbconvert bigtree==0.21.1 + # via feast (setup.py) bleach==6.1.0 # via nbconvert boto3==1.35.16 - # via moto + # via + # feast (setup.py) + # moto botocore==1.35.16 # via # aiobotocore @@ -72,11 +81,13 @@ botocore==1.35.16 # s3transfer build==1.2.2 # via + # feast (setup.py) # pip-tools # singlestoredb cachetools==5.5.0 # via google-auth cassandra-driver==3.29.2 + # via feast (setup.py) certifi==2024.8.30 # via # elastic-transport @@ -99,6 +110,7 @@ charset-normalizer==3.3.2 # snowflake-connector-python click==8.1.7 # via + # feast (setup.py) # dask # geomet # great-expectations @@ -107,7 +119,9 @@ click==8.1.7 cloudpickle==3.0.0 # via dask colorama==0.4.6 - # via great-expectations + # via + # feast (setup.py) + # great-expectations comm==0.2.2 # via # ipykernel @@ -116,6 +130,7 @@ coverage[toml]==7.6.1 # via pytest-cov cryptography==42.0.8 # via + # feast (setup.py) # azure-identity # azure-storage-blob # great-expectations @@ -130,7 +145,9 @@ cryptography==42.0.8 cython==3.0.11 # via thriftpy2 dask[dataframe]==2024.8.2 - # via dask-expr + # via + # feast (setup.py) + # dask-expr dask-expr==1.1.13 # via dask db-dtypes==1.3.0 @@ -142,9 +159,11 @@ decorator==5.1.1 defusedxml==0.7.1 # via nbconvert deltalake==0.19.2 + # via feast (setup.py) deprecation==2.1.0 # via python-keycloak dill==0.3.8 + # via feast (setup.py) distlib==0.3.8 # via virtualenv docker==7.1.0 @@ -156,6 +175,7 @@ duckdb==1.1.0 elastic-transport==8.15.0 # via elasticsearch elasticsearch==8.15.1 + # via feast (setup.py) entrypoints==0.4 # via altair exceptiongroup==1.2.2 @@ -168,6 +188,7 @@ execnet==2.1.1 executing==2.1.0 # via stack-data fastapi==0.114.1 + # via feast (setup.py) fastjsonschema==2.20.0 # via nbformat filelock==3.16.0 @@ -181,11 +202,14 @@ frozenlist==1.4.1 # aiohttp # aiosignal fsspec==2024.9.0 - # via dask + # via + # feast (setup.py) + # dask geomet==0.2.1.post1 # via cassandra-driver google-api-core[grpc]==2.19.2 # via + # feast (setup.py) # google-cloud-bigquery # google-cloud-bigquery-storage # google-cloud-bigtable @@ -203,8 +227,11 @@ google-auth==2.34.0 # google-cloud-storage # kubernetes google-cloud-bigquery[pandas]==3.25.0 + # via feast (setup.py) google-cloud-bigquery-storage==2.26.0 + # via feast (setup.py) google-cloud-bigtable==2.26.0 + # via feast (setup.py) google-cloud-core==2.4.1 # via # google-cloud-bigquery @@ -212,7 +239,9 @@ google-cloud-core==2.4.1 # google-cloud-datastore # google-cloud-storage google-cloud-datastore==2.20.1 + # via feast (setup.py) google-cloud-storage==2.18.2 + # via feast (setup.py) google-crc32c==1.6.0 # via # google-cloud-storage @@ -223,16 +252,17 @@ google-resumable-media==2.7.2 # google-cloud-storage googleapis-common-protos[grpc]==1.65.0 # via + # feast (setup.py) # google-api-core # grpc-google-iam-v1 # grpcio-status great-expectations==0.18.20 -greenlet==3.1.0 - # via sqlalchemy + # via feast (setup.py) grpc-google-iam-v1==0.13.1 # via google-cloud-bigtable grpcio==1.66.1 # via + # feast (setup.py) # google-api-core # googleapis-common-protos # grpc-google-iam-v1 @@ -242,30 +272,42 @@ grpcio==1.66.1 # grpcio-testing # grpcio-tools grpcio-health-checking==1.62.3 + # via feast (setup.py) grpcio-reflection==1.62.3 + # via feast (setup.py) grpcio-status==1.62.3 # via google-api-core grpcio-testing==1.62.3 + # via feast (setup.py) grpcio-tools==1.62.3 + # via feast (setup.py) gunicorn==23.0.0 + # via feast (setup.py) h11==0.14.0 # via # httpcore # uvicorn happybase==1.2.0 + # via feast (setup.py) hazelcast-python-client==5.5.0 + # via feast (setup.py) hiredis==2.4.0 + # via feast (setup.py) httpcore==1.0.5 # via httpx httptools==0.6.1 # via uvicorn httpx==0.27.2 # via + # feast (setup.py) # jupyterlab # python-keycloak ibis-framework[duckdb]==9.4.0 - # via ibis-substrait + # via + # feast (setup.py) + # ibis-substrait ibis-substrait==4.0.1 + # via feast (setup.py) identify==2.6.0 # via pre-commit idna==3.8 @@ -299,6 +341,7 @@ jedi==0.19.1 # via ipython jinja2==3.1.4 # via + # feast (setup.py) # altair # great-expectations # jupyter-server @@ -321,6 +364,7 @@ jsonpointer==3.0.0 # jsonschema jsonschema[format-nongpl]==4.23.0 # via + # feast (setup.py) # altair # great-expectations # jupyter-events @@ -368,6 +412,7 @@ jupyterlab-widgets==3.0.13 jwcrypto==1.5.6 # via python-keycloak kubernetes==20.13.0 + # via feast (setup.py) locket==1.0.0 # via partd makefun==1.15.4 @@ -388,13 +433,17 @@ matplotlib-inline==0.1.7 mdurl==0.1.2 # via markdown-it-py minio==7.1.0 + # via feast (setup.py) mistune==3.0.2 # via # great-expectations # nbconvert mmh3==4.1.0 + # via feast (setup.py) mock==2.0.0 + # via feast (setup.py) moto==4.2.14 + # via feast (setup.py) msal==1.31.0 # via # azure-identity @@ -406,10 +455,13 @@ multidict==6.1.0 # aiohttp # yarl mypy==1.11.2 - # via sqlalchemy + # via + # feast (setup.py) + # sqlalchemy mypy-extensions==1.0.0 # via mypy mypy-protobuf==3.3.0 + # via feast (setup.py) nbclient==0.10.0 # via nbconvert nbconvert==7.16.4 @@ -432,6 +484,7 @@ notebook-shim==0.2.4 # notebook numpy==1.26.4 # via + # feast (setup.py) # altair # dask # db-dtypes @@ -465,6 +518,7 @@ packaging==24.1 # sphinx pandas==2.2.2 # via + # feast (setup.py) # altair # dask # dask-expr @@ -490,6 +544,7 @@ pexpect==4.9.0 pip==24.2 # via pip-tools pip-tools==7.4.1 + # via feast (setup.py) platformdirs==3.11.0 # via # jupyter-core @@ -502,8 +557,11 @@ ply==3.11 portalocker==2.10.1 # via msal-extensions pre-commit==3.3.1 + # via feast (setup.py) prometheus-client==0.20.0 - # via jupyter-server + # via + # feast (setup.py) + # jupyter-server prompt-toolkit==3.0.47 # via ipython proto-plus==1.24.0 @@ -514,6 +572,7 @@ proto-plus==1.24.0 # google-cloud-datastore protobuf==4.25.4 # via + # feast (setup.py) # google-api-core # google-cloud-bigquery-storage # google-cloud-bigtable @@ -529,8 +588,11 @@ protobuf==4.25.4 # proto-plus # substrait psutil==5.9.0 - # via ipykernel + # via + # feast (setup.py) + # ipykernel psycopg[binary, pool]==3.2.1 + # via feast (setup.py) psycopg-binary==3.2.1 # via psycopg psycopg-pool==3.2.2 @@ -542,12 +604,14 @@ ptyprocess==0.7.0 pure-eval==0.2.3 # via stack-data py==1.11.0 + # via feast (setup.py) py-cpuinfo==9.0.0 # via pytest-benchmark py4j==0.10.9.7 # via pyspark pyarrow==17.0.0 # via + # feast (setup.py) # dask-expr # db-dtypes # deltalake @@ -563,28 +627,35 @@ pyasn1==0.6.1 pyasn1-modules==0.4.1 # via google-auth pybindgen==0.22.1 + # via feast (setup.py) pycparser==2.22 # via cffi pydantic==2.9.1 # via + # feast (setup.py) # fastapi # great-expectations pydantic-core==2.23.3 # via pydantic pygments==2.18.0 # via + # feast (setup.py) # ipython # nbconvert # rich # sphinx pyjwt[crypto]==2.9.0 # via + # feast (setup.py) # msal # singlestoredb # snowflake-connector-python pymssql==2.3.1 + # via feast (setup.py) pymysql==1.1.1 + # via feast (setup.py) pyodbc==5.1.0 + # via feast (setup.py) pyopenssl==24.2.1 # via snowflake-connector-python pyparsing==3.1.4 @@ -594,8 +665,10 @@ pyproject-hooks==1.1.0 # build # pip-tools pyspark==3.5.2 + # via feast (setup.py) pytest==7.4.4 # via + # feast (setup.py) # pytest-benchmark # pytest-cov # pytest-env @@ -605,13 +678,21 @@ pytest==7.4.4 # pytest-timeout # pytest-xdist pytest-benchmark==3.4.1 + # via feast (setup.py) pytest-cov==5.0.0 + # via feast (setup.py) pytest-env==1.1.3 + # via feast (setup.py) pytest-lazy-fixture==0.6.3 + # via feast (setup.py) pytest-mock==1.10.4 + # via feast (setup.py) pytest-ordering==0.6 + # via feast (setup.py) pytest-timeout==1.4.2 + # via feast (setup.py) pytest-xdist==3.6.1 + # via feast (setup.py) python-dateutil==2.9.0.post0 # via # arrow @@ -629,6 +710,7 @@ python-dotenv==1.0.1 python-json-logger==2.0.7 # via jupyter-events python-keycloak==4.2.2 + # via feast (setup.py) pytz==2024.2 # via # great-expectations @@ -638,6 +720,7 @@ pytz==2024.2 # trino pyyaml==6.0.2 # via + # feast (setup.py) # dask # ibis-substrait # jupyter-events @@ -651,15 +734,19 @@ pyzmq==26.2.0 # jupyter-client # jupyter-server redis==4.6.0 + # via feast (setup.py) referencing==0.35.1 # via # jsonschema # jsonschema-specifications # jupyter-events regex==2024.7.24 - # via parsimonious + # via + # feast (setup.py) + # parsimonious requests==2.32.3 # via + # feast (setup.py) # azure-core # docker # google-api-core @@ -705,6 +792,7 @@ ruamel-yaml==0.17.40 ruamel-yaml-clib==0.2.8 # via ruamel-yaml ruff==0.6.4 + # via feast (setup.py) s3transfer==0.10.2 # via boto3 scipy==1.14.1 @@ -719,6 +807,7 @@ setuptools==74.1.2 # pip-tools # singlestoredb singlestoredb==1.6.3 + # via feast (setup.py) six==1.16.0 # via # asttokens @@ -739,11 +828,13 @@ sniffio==1.3.1 snowballstemmer==2.2.0 # via sphinx snowflake-connector-python[pandas]==3.12.1 + # via feast (setup.py) sortedcontainers==2.4.0 # via snowflake-connector-python soupsieve==2.6 # via beautifulsoup4 sphinx==6.2.1 + # via feast (setup.py) sphinxcontrib-applehelp==2.0.0 # via sphinx sphinxcontrib-devhelp==2.0.0 @@ -757,9 +848,11 @@ sphinxcontrib-qthelp==2.0.0 sphinxcontrib-serializinghtml==2.0.0 # via sphinx sqlalchemy[mypy]==2.0.34 + # via feast (setup.py) sqlglot==25.18.0 # via ibis-framework sqlite-vec==0.1.1 + # via feast (setup.py) sqlparams==6.1.0 # via singlestoredb stack-data==0.6.3 @@ -769,17 +862,21 @@ starlette==0.38.5 substrait==0.22.0 # via ibis-substrait tabulate==0.9.0 + # via feast (setup.py) tenacity==8.5.0 + # via feast (setup.py) terminado==0.18.1 # via # jupyter-server # jupyter-server-terminals testcontainers==4.4.0 + # via feast (setup.py) thriftpy2==0.5.2 # via happybase tinycss2==1.3.0 # via nbconvert toml==0.10.2 + # via feast (setup.py) tomli==2.0.1 # via # build @@ -807,7 +904,9 @@ tornado==6.4.1 # notebook # terminado tqdm==4.66.5 - # via great-expectations + # via + # feast (setup.py) + # great-expectations traitlets==5.14.3 # via # comm @@ -824,23 +923,37 @@ traitlets==5.14.3 # nbconvert # nbformat trino==0.329.0 + # via feast (setup.py) typeguard==4.3.0 + # via feast (setup.py) types-cffi==1.16.0.20240331 # via types-pyopenssl types-protobuf==3.19.22 - # via mypy-protobuf + # via + # feast (setup.py) + # mypy-protobuf types-pymysql==1.1.0.20240524 + # via feast (setup.py) types-pyopenssl==24.1.0.20240722 # via types-redis types-python-dateutil==2.9.0.20240906 - # via arrow + # via + # feast (setup.py) + # arrow types-pytz==2024.1.0.20240417 + # via feast (setup.py) types-pyyaml==6.0.12.20240808 + # via feast (setup.py) types-redis==4.6.0.20240903 + # via feast (setup.py) types-requests==2.30.0.0 + # via feast (setup.py) types-setuptools==74.1.0.20240907 - # via types-cffi + # via + # feast (setup.py) + # types-cffi types-tabulate==0.9.0.20240106 + # via feast (setup.py) types-urllib3==1.26.25.14 # via types-requests typing-extensions==4.12.2 @@ -876,6 +989,7 @@ uri-template==1.3.0 # via jsonschema urllib3==2.2.2 # via + # feast (setup.py) # botocore # docker # elastic-transport @@ -886,10 +1000,13 @@ urllib3==2.2.2 # responses # testcontainers uvicorn[standard]==0.30.6 + # via feast (setup.py) uvloop==0.20.0 # via uvicorn virtualenv==20.23.0 - # via pre-commit + # via + # feast (setup.py) + # pre-commit watchfiles==0.24.0 # via uvicorn wcwidth==0.2.13 diff --git a/sdk/python/requirements/py3.10-requirements.txt b/sdk/python/requirements/py3.10-requirements.txt index a3006e4555..3420c8a0e3 100644 --- a/sdk/python/requirements/py3.10-requirements.txt +++ b/sdk/python/requirements/py3.10-requirements.txt @@ -11,36 +11,36 @@ attrs==24.2.0 # jsonschema # referencing bigtree==0.21.1 -cachetools==5.5.0 - # via google-auth + # via feast (setup.py) certifi==2024.8.30 - # via - # kubernetes - # requests + # via requests charset-normalizer==3.3.2 # via requests click==8.1.7 # via + # feast (setup.py) # dask # uvicorn cloudpickle==3.0.0 # via dask colorama==0.4.6 + # via feast (setup.py) dask[dataframe]==2024.8.2 - # via dask-expr + # via + # feast (setup.py) + # dask-expr dask-expr==1.1.13 # via dask dill==0.3.8 + # via feast (setup.py) exceptiongroup==1.2.2 # via anyio fastapi==0.114.1 + # via feast (setup.py) fsspec==2024.9.0 # via dask -google-auth==2.34.0 - # via kubernetes -greenlet==3.1.0 - # via sqlalchemy gunicorn==23.0.0 + # via feast (setup.py) h11==0.14.0 # via uvicorn httptools==0.6.1 @@ -52,98 +52,97 @@ idna==3.8 importlib-metadata==8.5.0 # via dask jinja2==3.1.4 + # via feast (setup.py) jsonschema==4.23.0 + # via feast (setup.py) jsonschema-specifications==2023.12.1 # via jsonschema -kubernetes==20.13.0 locket==1.0.0 # via partd markupsafe==2.1.5 # via jinja2 mmh3==4.1.0 + # via feast (setup.py) mypy==1.11.2 # via sqlalchemy mypy-extensions==1.0.0 # via mypy mypy-protobuf==3.6.0 + # via feast (setup.py) numpy==1.26.4 # via + # feast (setup.py) # dask # pandas # pyarrow -oauthlib==3.2.2 - # via requests-oauthlib packaging==24.1 # via # dask # gunicorn pandas==2.2.2 # via + # feast (setup.py) # dask # dask-expr partd==1.4.2 # via dask prometheus-client==0.20.0 + # via feast (setup.py) protobuf==4.25.4 - # via mypy-protobuf + # via + # feast (setup.py) + # mypy-protobuf psutil==6.0.0 + # via feast (setup.py) pyarrow==17.0.0 - # via dask-expr -pyasn1==0.6.1 # via - # pyasn1-modules - # rsa -pyasn1-modules==0.4.1 - # via google-auth + # feast (setup.py) + # dask-expr pydantic==2.9.1 - # via fastapi + # via + # feast (setup.py) + # fastapi pydantic-core==2.23.3 # via pydantic pygments==2.18.0 + # via feast (setup.py) pyjwt==2.9.0 + # via feast (setup.py) python-dateutil==2.9.0.post0 - # via - # kubernetes - # pandas + # via pandas python-dotenv==1.0.1 # via uvicorn pytz==2024.2 # via pandas pyyaml==6.0.2 # via + # feast (setup.py) # dask - # kubernetes # uvicorn referencing==0.35.1 # via # jsonschema # jsonschema-specifications requests==2.32.3 - # via - # kubernetes - # requests-oauthlib -requests-oauthlib==2.0.0 - # via kubernetes + # via feast (setup.py) rpds-py==0.20.0 # via # jsonschema # referencing -rsa==4.9 - # via google-auth -setuptools==74.1.2 - # via kubernetes six==1.16.0 - # via - # kubernetes - # python-dateutil + # via python-dateutil sniffio==1.3.1 # via anyio sqlalchemy[mypy]==2.0.34 + # via feast (setup.py) starlette==0.38.5 # via fastapi tabulate==0.9.0 + # via feast (setup.py) tenacity==8.5.0 + # via feast (setup.py) toml==0.10.2 + # via feast (setup.py) tomli==2.0.1 # via mypy toolz==0.12.1 @@ -151,7 +150,9 @@ toolz==0.12.1 # dask # partd tqdm==4.66.5 + # via feast (setup.py) typeguard==4.3.0 + # via feast (setup.py) types-protobuf==5.27.0.20240907 # via mypy-protobuf typing-extensions==4.12.2 @@ -167,16 +168,13 @@ typing-extensions==4.12.2 tzdata==2024.1 # via pandas urllib3==2.2.2 - # via - # kubernetes - # requests + # via requests uvicorn[standard]==0.30.6 + # via feast (setup.py) uvloop==0.20.0 # via uvicorn watchfiles==0.24.0 # via uvicorn -websocket-client==1.8.0 - # via kubernetes websockets==13.0.1 # via uvicorn zipp==3.20.1 diff --git a/sdk/python/requirements/py3.11-ci-requirements.txt b/sdk/python/requirements/py3.11-ci-requirements.txt index cd78247a23..6c5fb02e06 100644 --- a/sdk/python/requirements/py3.11-ci-requirements.txt +++ b/sdk/python/requirements/py3.11-ci-requirements.txt @@ -1,6 +1,7 @@ # This file was autogenerated by uv via the following command: # uv pip compile --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.11-ci-requirements.txt aiobotocore==2.15.0 + # via feast (setup.py) aiohappyeyeballs==2.4.0 # via aiohttp aiohttp==3.10.5 @@ -21,6 +22,8 @@ anyio==4.4.0 # jupyter-server # starlette # watchfiles +appnope==0.1.4 + # via ipykernel argon2-cffi==23.1.0 # via jupyter-server argon2-cffi-bindings==21.2.0 @@ -30,6 +33,7 @@ arrow==1.3.0 asn1crypto==1.5.1 # via snowflake-connector-python assertpy==1.1 + # via feast (setup.py) asttokens==2.4.1 # via stack-data async-lru==2.0.4 @@ -48,7 +52,9 @@ azure-core==1.30.2 # azure-identity # azure-storage-blob azure-identity==1.17.1 + # via feast (setup.py) azure-storage-blob==12.22.0 + # via feast (setup.py) babel==2.16.0 # via # jupyterlab-server @@ -56,10 +62,13 @@ babel==2.16.0 beautifulsoup4==4.12.3 # via nbconvert bigtree==0.21.1 + # via feast (setup.py) bleach==6.1.0 # via nbconvert boto3==1.35.16 - # via moto + # via + # feast (setup.py) + # moto botocore==1.35.16 # via # aiobotocore @@ -68,11 +77,13 @@ botocore==1.35.16 # s3transfer build==1.2.2 # via + # feast (setup.py) # pip-tools # singlestoredb cachetools==5.5.0 # via google-auth cassandra-driver==3.29.2 + # via feast (setup.py) certifi==2024.8.30 # via # elastic-transport @@ -95,6 +106,7 @@ charset-normalizer==3.3.2 # snowflake-connector-python click==8.1.7 # via + # feast (setup.py) # dask # geomet # great-expectations @@ -103,7 +115,9 @@ click==8.1.7 cloudpickle==3.0.0 # via dask colorama==0.4.6 - # via great-expectations + # via + # feast (setup.py) + # great-expectations comm==0.2.2 # via # ipykernel @@ -112,6 +126,7 @@ coverage[toml]==7.6.1 # via pytest-cov cryptography==42.0.8 # via + # feast (setup.py) # azure-identity # azure-storage-blob # great-expectations @@ -126,7 +141,9 @@ cryptography==42.0.8 cython==3.0.11 # via thriftpy2 dask[dataframe]==2024.8.2 - # via dask-expr + # via + # feast (setup.py) + # dask-expr dask-expr==1.1.13 # via dask db-dtypes==1.3.0 @@ -138,9 +155,11 @@ decorator==5.1.1 defusedxml==0.7.1 # via nbconvert deltalake==0.19.2 + # via feast (setup.py) deprecation==2.1.0 # via python-keycloak dill==0.3.8 + # via feast (setup.py) distlib==0.3.8 # via virtualenv docker==7.1.0 @@ -152,6 +171,7 @@ duckdb==1.1.0 elastic-transport==8.15.0 # via elasticsearch elasticsearch==8.15.1 + # via feast (setup.py) entrypoints==0.4 # via altair execnet==2.1.1 @@ -159,6 +179,7 @@ execnet==2.1.1 executing==2.1.0 # via stack-data fastapi==0.114.1 + # via feast (setup.py) fastjsonschema==2.20.0 # via nbformat filelock==3.16.0 @@ -172,11 +193,14 @@ frozenlist==1.4.1 # aiohttp # aiosignal fsspec==2024.9.0 - # via dask + # via + # feast (setup.py) + # dask geomet==0.2.1.post1 # via cassandra-driver google-api-core[grpc]==2.19.2 # via + # feast (setup.py) # google-cloud-bigquery # google-cloud-bigquery-storage # google-cloud-bigtable @@ -194,8 +218,11 @@ google-auth==2.34.0 # google-cloud-storage # kubernetes google-cloud-bigquery[pandas]==3.25.0 + # via feast (setup.py) google-cloud-bigquery-storage==2.26.0 + # via feast (setup.py) google-cloud-bigtable==2.26.0 + # via feast (setup.py) google-cloud-core==2.4.1 # via # google-cloud-bigquery @@ -203,7 +230,9 @@ google-cloud-core==2.4.1 # google-cloud-datastore # google-cloud-storage google-cloud-datastore==2.20.1 + # via feast (setup.py) google-cloud-storage==2.18.2 + # via feast (setup.py) google-crc32c==1.6.0 # via # google-cloud-storage @@ -214,16 +243,17 @@ google-resumable-media==2.7.2 # google-cloud-storage googleapis-common-protos[grpc]==1.65.0 # via + # feast (setup.py) # google-api-core # grpc-google-iam-v1 # grpcio-status great-expectations==0.18.20 -greenlet==3.1.0 - # via sqlalchemy + # via feast (setup.py) grpc-google-iam-v1==0.13.1 # via google-cloud-bigtable grpcio==1.66.1 # via + # feast (setup.py) # google-api-core # googleapis-common-protos # grpc-google-iam-v1 @@ -233,30 +263,42 @@ grpcio==1.66.1 # grpcio-testing # grpcio-tools grpcio-health-checking==1.62.3 + # via feast (setup.py) grpcio-reflection==1.62.3 + # via feast (setup.py) grpcio-status==1.62.3 # via google-api-core grpcio-testing==1.62.3 + # via feast (setup.py) grpcio-tools==1.62.3 + # via feast (setup.py) gunicorn==23.0.0 + # via feast (setup.py) h11==0.14.0 # via # httpcore # uvicorn happybase==1.2.0 + # via feast (setup.py) hazelcast-python-client==5.5.0 + # via feast (setup.py) hiredis==2.4.0 + # via feast (setup.py) httpcore==1.0.5 # via httpx httptools==0.6.1 # via uvicorn httpx==0.27.2 # via + # feast (setup.py) # jupyterlab # python-keycloak ibis-framework[duckdb]==9.4.0 - # via ibis-substrait + # via + # feast (setup.py) + # ibis-substrait ibis-substrait==4.0.1 + # via feast (setup.py) identify==2.6.0 # via pre-commit idna==3.8 @@ -290,6 +332,7 @@ jedi==0.19.1 # via ipython jinja2==3.1.4 # via + # feast (setup.py) # altair # great-expectations # jupyter-server @@ -312,6 +355,7 @@ jsonpointer==3.0.0 # jsonschema jsonschema[format-nongpl]==4.23.0 # via + # feast (setup.py) # altair # great-expectations # jupyter-events @@ -359,6 +403,7 @@ jupyterlab-widgets==3.0.13 jwcrypto==1.5.6 # via python-keycloak kubernetes==20.13.0 + # via feast (setup.py) locket==1.0.0 # via partd makefun==1.15.4 @@ -379,13 +424,17 @@ matplotlib-inline==0.1.7 mdurl==0.1.2 # via markdown-it-py minio==7.1.0 + # via feast (setup.py) mistune==3.0.2 # via # great-expectations # nbconvert mmh3==4.1.0 + # via feast (setup.py) mock==2.0.0 + # via feast (setup.py) moto==4.2.14 + # via feast (setup.py) msal==1.31.0 # via # azure-identity @@ -397,10 +446,13 @@ multidict==6.1.0 # aiohttp # yarl mypy==1.11.2 - # via sqlalchemy + # via + # feast (setup.py) + # sqlalchemy mypy-extensions==1.0.0 # via mypy mypy-protobuf==3.3.0 + # via feast (setup.py) nbclient==0.10.0 # via nbconvert nbconvert==7.16.4 @@ -423,6 +475,7 @@ notebook-shim==0.2.4 # notebook numpy==1.26.4 # via + # feast (setup.py) # altair # dask # db-dtypes @@ -456,6 +509,7 @@ packaging==24.1 # sphinx pandas==2.2.2 # via + # feast (setup.py) # altair # dask # dask-expr @@ -481,6 +535,7 @@ pexpect==4.9.0 pip==24.2 # via pip-tools pip-tools==7.4.1 + # via feast (setup.py) platformdirs==3.11.0 # via # jupyter-core @@ -493,8 +548,11 @@ ply==3.11 portalocker==2.10.1 # via msal-extensions pre-commit==3.3.1 + # via feast (setup.py) prometheus-client==0.20.0 - # via jupyter-server + # via + # feast (setup.py) + # jupyter-server prompt-toolkit==3.0.47 # via ipython proto-plus==1.24.0 @@ -505,6 +563,7 @@ proto-plus==1.24.0 # google-cloud-datastore protobuf==4.25.4 # via + # feast (setup.py) # google-api-core # google-cloud-bigquery-storage # google-cloud-bigtable @@ -520,8 +579,11 @@ protobuf==4.25.4 # proto-plus # substrait psutil==5.9.0 - # via ipykernel + # via + # feast (setup.py) + # ipykernel psycopg[binary, pool]==3.2.1 + # via feast (setup.py) psycopg-binary==3.2.1 # via psycopg psycopg-pool==3.2.2 @@ -533,12 +595,14 @@ ptyprocess==0.7.0 pure-eval==0.2.3 # via stack-data py==1.11.0 + # via feast (setup.py) py-cpuinfo==9.0.0 # via pytest-benchmark py4j==0.10.9.7 # via pyspark pyarrow==17.0.0 # via + # feast (setup.py) # dask-expr # db-dtypes # deltalake @@ -554,28 +618,35 @@ pyasn1==0.6.1 pyasn1-modules==0.4.1 # via google-auth pybindgen==0.22.1 + # via feast (setup.py) pycparser==2.22 # via cffi pydantic==2.9.1 # via + # feast (setup.py) # fastapi # great-expectations pydantic-core==2.23.3 # via pydantic pygments==2.18.0 # via + # feast (setup.py) # ipython # nbconvert # rich # sphinx pyjwt[crypto]==2.9.0 # via + # feast (setup.py) # msal # singlestoredb # snowflake-connector-python pymssql==2.3.1 + # via feast (setup.py) pymysql==1.1.1 + # via feast (setup.py) pyodbc==5.1.0 + # via feast (setup.py) pyopenssl==24.2.1 # via snowflake-connector-python pyparsing==3.1.4 @@ -585,8 +656,10 @@ pyproject-hooks==1.1.0 # build # pip-tools pyspark==3.5.2 + # via feast (setup.py) pytest==7.4.4 # via + # feast (setup.py) # pytest-benchmark # pytest-cov # pytest-env @@ -596,13 +669,21 @@ pytest==7.4.4 # pytest-timeout # pytest-xdist pytest-benchmark==3.4.1 + # via feast (setup.py) pytest-cov==5.0.0 + # via feast (setup.py) pytest-env==1.1.3 + # via feast (setup.py) pytest-lazy-fixture==0.6.3 + # via feast (setup.py) pytest-mock==1.10.4 + # via feast (setup.py) pytest-ordering==0.6 + # via feast (setup.py) pytest-timeout==1.4.2 + # via feast (setup.py) pytest-xdist==3.6.1 + # via feast (setup.py) python-dateutil==2.9.0.post0 # via # arrow @@ -620,6 +701,7 @@ python-dotenv==1.0.1 python-json-logger==2.0.7 # via jupyter-events python-keycloak==4.2.2 + # via feast (setup.py) pytz==2024.2 # via # great-expectations @@ -629,6 +711,7 @@ pytz==2024.2 # trino pyyaml==6.0.2 # via + # feast (setup.py) # dask # ibis-substrait # jupyter-events @@ -642,15 +725,19 @@ pyzmq==26.2.0 # jupyter-client # jupyter-server redis==4.6.0 + # via feast (setup.py) referencing==0.35.1 # via # jsonschema # jsonschema-specifications # jupyter-events regex==2024.7.24 - # via parsimonious + # via + # feast (setup.py) + # parsimonious requests==2.32.3 # via + # feast (setup.py) # azure-core # docker # google-api-core @@ -696,6 +783,7 @@ ruamel-yaml==0.17.40 ruamel-yaml-clib==0.2.8 # via ruamel-yaml ruff==0.6.4 + # via feast (setup.py) s3transfer==0.10.2 # via boto3 scipy==1.14.1 @@ -710,6 +798,7 @@ setuptools==74.1.2 # pip-tools # singlestoredb singlestoredb==1.6.3 + # via feast (setup.py) six==1.16.0 # via # asttokens @@ -730,11 +819,13 @@ sniffio==1.3.1 snowballstemmer==2.2.0 # via sphinx snowflake-connector-python[pandas]==3.12.1 + # via feast (setup.py) sortedcontainers==2.4.0 # via snowflake-connector-python soupsieve==2.6 # via beautifulsoup4 sphinx==6.2.1 + # via feast (setup.py) sphinxcontrib-applehelp==2.0.0 # via sphinx sphinxcontrib-devhelp==2.0.0 @@ -748,9 +839,11 @@ sphinxcontrib-qthelp==2.0.0 sphinxcontrib-serializinghtml==2.0.0 # via sphinx sqlalchemy[mypy]==2.0.34 + # via feast (setup.py) sqlglot==25.18.0 # via ibis-framework sqlite-vec==0.1.1 + # via feast (setup.py) sqlparams==6.1.0 # via singlestoredb stack-data==0.6.3 @@ -760,17 +853,21 @@ starlette==0.38.5 substrait==0.22.0 # via ibis-substrait tabulate==0.9.0 + # via feast (setup.py) tenacity==8.5.0 + # via feast (setup.py) terminado==0.18.1 # via # jupyter-server # jupyter-server-terminals testcontainers==4.4.0 + # via feast (setup.py) thriftpy2==0.5.2 # via happybase tinycss2==1.3.0 # via nbconvert toml==0.10.2 + # via feast (setup.py) tomlkit==0.13.2 # via snowflake-connector-python toolz==0.12.1 @@ -788,7 +885,9 @@ tornado==6.4.1 # notebook # terminado tqdm==4.66.5 - # via great-expectations + # via + # feast (setup.py) + # great-expectations traitlets==5.14.3 # via # comm @@ -805,23 +904,37 @@ traitlets==5.14.3 # nbconvert # nbformat trino==0.329.0 + # via feast (setup.py) typeguard==4.3.0 + # via feast (setup.py) types-cffi==1.16.0.20240331 # via types-pyopenssl types-protobuf==3.19.22 - # via mypy-protobuf + # via + # feast (setup.py) + # mypy-protobuf types-pymysql==1.1.0.20240524 + # via feast (setup.py) types-pyopenssl==24.1.0.20240722 # via types-redis types-python-dateutil==2.9.0.20240906 - # via arrow + # via + # feast (setup.py) + # arrow types-pytz==2024.1.0.20240417 + # via feast (setup.py) types-pyyaml==6.0.12.20240808 + # via feast (setup.py) types-redis==4.6.0.20240903 + # via feast (setup.py) types-requests==2.30.0.0 + # via feast (setup.py) types-setuptools==74.1.0.20240907 - # via types-cffi + # via + # feast (setup.py) + # types-cffi types-tabulate==0.9.0.20240106 + # via feast (setup.py) types-urllib3==1.26.25.14 # via types-requests typing-extensions==4.12.2 @@ -853,6 +966,7 @@ uri-template==1.3.0 # via jsonschema urllib3==2.2.2 # via + # feast (setup.py) # botocore # docker # elastic-transport @@ -863,10 +977,13 @@ urllib3==2.2.2 # responses # testcontainers uvicorn[standard]==0.30.6 + # via feast (setup.py) uvloop==0.20.0 # via uvicorn virtualenv==20.23.0 - # via pre-commit + # via + # feast (setup.py) + # pre-commit watchfiles==0.24.0 # via uvicorn wcwidth==0.2.13 diff --git a/sdk/python/requirements/py3.11-requirements.txt b/sdk/python/requirements/py3.11-requirements.txt index 611a0cedca..1d0ce54cc1 100644 --- a/sdk/python/requirements/py3.11-requirements.txt +++ b/sdk/python/requirements/py3.11-requirements.txt @@ -11,34 +11,34 @@ attrs==24.2.0 # jsonschema # referencing bigtree==0.21.1 -cachetools==5.5.0 - # via google-auth + # via feast (setup.py) certifi==2024.8.30 - # via - # kubernetes - # requests + # via requests charset-normalizer==3.3.2 # via requests click==8.1.7 # via + # feast (setup.py) # dask # uvicorn cloudpickle==3.0.0 # via dask colorama==0.4.6 + # via feast (setup.py) dask[dataframe]==2024.8.2 - # via dask-expr + # via + # feast (setup.py) + # dask-expr dask-expr==1.1.13 # via dask dill==0.3.8 + # via feast (setup.py) fastapi==0.114.1 + # via feast (setup.py) fsspec==2024.9.0 # via dask -google-auth==2.34.0 - # via kubernetes -greenlet==3.1.0 - # via sqlalchemy gunicorn==23.0.0 + # via feast (setup.py) h11==0.14.0 # via uvicorn httptools==0.6.1 @@ -50,104 +50,105 @@ idna==3.8 importlib-metadata==8.5.0 # via dask jinja2==3.1.4 + # via feast (setup.py) jsonschema==4.23.0 + # via feast (setup.py) jsonschema-specifications==2023.12.1 # via jsonschema -kubernetes==20.13.0 locket==1.0.0 # via partd markupsafe==2.1.5 # via jinja2 mmh3==4.1.0 + # via feast (setup.py) mypy==1.11.2 # via sqlalchemy mypy-extensions==1.0.0 # via mypy mypy-protobuf==3.6.0 + # via feast (setup.py) numpy==1.26.4 # via + # feast (setup.py) # dask # pandas # pyarrow -oauthlib==3.2.2 - # via requests-oauthlib packaging==24.1 # via # dask # gunicorn pandas==2.2.2 # via + # feast (setup.py) # dask # dask-expr partd==1.4.2 # via dask prometheus-client==0.20.0 + # via feast (setup.py) protobuf==4.25.4 - # via mypy-protobuf + # via + # feast (setup.py) + # mypy-protobuf psutil==6.0.0 + # via feast (setup.py) pyarrow==17.0.0 - # via dask-expr -pyasn1==0.6.1 # via - # pyasn1-modules - # rsa -pyasn1-modules==0.4.1 - # via google-auth + # feast (setup.py) + # dask-expr pydantic==2.9.1 - # via fastapi + # via + # feast (setup.py) + # fastapi pydantic-core==2.23.3 # via pydantic pygments==2.18.0 + # via feast (setup.py) pyjwt==2.9.0 + # via feast (setup.py) python-dateutil==2.9.0.post0 - # via - # kubernetes - # pandas + # via pandas python-dotenv==1.0.1 # via uvicorn pytz==2024.2 # via pandas pyyaml==6.0.2 # via + # feast (setup.py) # dask - # kubernetes # uvicorn referencing==0.35.1 # via # jsonschema # jsonschema-specifications requests==2.32.3 - # via - # kubernetes - # requests-oauthlib -requests-oauthlib==2.0.0 - # via kubernetes + # via feast (setup.py) rpds-py==0.20.0 # via # jsonschema # referencing -rsa==4.9 - # via google-auth -setuptools==74.1.2 - # via kubernetes six==1.16.0 - # via - # kubernetes - # python-dateutil + # via python-dateutil sniffio==1.3.1 # via anyio sqlalchemy[mypy]==2.0.34 + # via feast (setup.py) starlette==0.38.5 # via fastapi tabulate==0.9.0 + # via feast (setup.py) tenacity==8.5.0 + # via feast (setup.py) toml==0.10.2 + # via feast (setup.py) toolz==0.12.1 # via # dask # partd tqdm==4.66.5 + # via feast (setup.py) typeguard==4.3.0 + # via feast (setup.py) types-protobuf==5.27.0.20240907 # via mypy-protobuf typing-extensions==4.12.2 @@ -161,16 +162,13 @@ typing-extensions==4.12.2 tzdata==2024.1 # via pandas urllib3==2.2.2 - # via - # kubernetes - # requests + # via requests uvicorn[standard]==0.30.6 + # via feast (setup.py) uvloop==0.20.0 # via uvicorn watchfiles==0.24.0 # via uvicorn -websocket-client==1.8.0 - # via kubernetes websockets==13.0.1 # via uvicorn zipp==3.20.1 diff --git a/sdk/python/requirements/py3.9-ci-requirements.txt b/sdk/python/requirements/py3.9-ci-requirements.txt index f4e7c795fa..ae27849f4f 100644 --- a/sdk/python/requirements/py3.9-ci-requirements.txt +++ b/sdk/python/requirements/py3.9-ci-requirements.txt @@ -1,6 +1,7 @@ # This file was autogenerated by uv via the following command: # uv pip compile --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.9-ci-requirements.txt aiobotocore==2.15.0 + # via feast (setup.py) aiohappyeyeballs==2.4.0 # via aiohttp aiohttp==3.10.5 @@ -21,6 +22,8 @@ anyio==4.4.0 # jupyter-server # starlette # watchfiles +appnope==0.1.4 + # via ipykernel argon2-cffi==23.1.0 # via jupyter-server argon2-cffi-bindings==21.2.0 @@ -30,6 +33,7 @@ arrow==1.3.0 asn1crypto==1.5.1 # via snowflake-connector-python assertpy==1.1 + # via feast (setup.py) asttokens==2.4.1 # via stack-data async-lru==2.0.4 @@ -52,7 +56,9 @@ azure-core==1.30.2 # azure-identity # azure-storage-blob azure-identity==1.17.1 + # via feast (setup.py) azure-storage-blob==12.22.0 + # via feast (setup.py) babel==2.16.0 # via # jupyterlab-server @@ -62,10 +68,13 @@ beautifulsoup4==4.12.3 bidict==0.23.1 # via ibis-framework bigtree==0.21.1 + # via feast (setup.py) bleach==6.1.0 # via nbconvert boto3==1.35.16 - # via moto + # via + # feast (setup.py) + # moto botocore==1.35.16 # via # aiobotocore @@ -74,11 +83,13 @@ botocore==1.35.16 # s3transfer build==1.2.2 # via + # feast (setup.py) # pip-tools # singlestoredb cachetools==5.5.0 # via google-auth cassandra-driver==3.29.2 + # via feast (setup.py) certifi==2024.8.30 # via # elastic-transport @@ -101,6 +112,7 @@ charset-normalizer==3.3.2 # snowflake-connector-python click==8.1.7 # via + # feast (setup.py) # dask # geomet # great-expectations @@ -109,7 +121,9 @@ click==8.1.7 cloudpickle==3.0.0 # via dask colorama==0.4.6 - # via great-expectations + # via + # feast (setup.py) + # great-expectations comm==0.2.2 # via # ipykernel @@ -118,6 +132,7 @@ coverage[toml]==7.6.1 # via pytest-cov cryptography==42.0.8 # via + # feast (setup.py) # azure-identity # azure-storage-blob # great-expectations @@ -132,7 +147,9 @@ cryptography==42.0.8 cython==3.0.11 # via thriftpy2 dask[dataframe]==2024.8.0 - # via dask-expr + # via + # feast (setup.py) + # dask-expr dask-expr==1.1.10 # via dask db-dtypes==1.3.0 @@ -144,9 +161,11 @@ decorator==5.1.1 defusedxml==0.7.1 # via nbconvert deltalake==0.19.2 + # via feast (setup.py) deprecation==2.1.0 # via python-keycloak dill==0.3.8 + # via feast (setup.py) distlib==0.3.8 # via virtualenv docker==7.1.0 @@ -158,6 +177,7 @@ duckdb==0.10.3 elastic-transport==8.15.0 # via elasticsearch elasticsearch==8.15.1 + # via feast (setup.py) entrypoints==0.4 # via altair exceptiongroup==1.2.2 @@ -170,6 +190,7 @@ execnet==2.1.1 executing==2.1.0 # via stack-data fastapi==0.114.1 + # via feast (setup.py) fastjsonschema==2.20.0 # via nbformat filelock==3.16.0 @@ -183,11 +204,14 @@ frozenlist==1.4.1 # aiohttp # aiosignal fsspec==2024.9.0 - # via dask + # via + # feast (setup.py) + # dask geomet==0.2.1.post1 # via cassandra-driver google-api-core[grpc]==2.19.2 # via + # feast (setup.py) # google-cloud-bigquery # google-cloud-bigquery-storage # google-cloud-bigtable @@ -205,8 +229,11 @@ google-auth==2.34.0 # google-cloud-storage # kubernetes google-cloud-bigquery[pandas]==3.25.0 + # via feast (setup.py) google-cloud-bigquery-storage==2.26.0 + # via feast (setup.py) google-cloud-bigtable==2.26.0 + # via feast (setup.py) google-cloud-core==2.4.1 # via # google-cloud-bigquery @@ -214,7 +241,9 @@ google-cloud-core==2.4.1 # google-cloud-datastore # google-cloud-storage google-cloud-datastore==2.20.1 + # via feast (setup.py) google-cloud-storage==2.18.2 + # via feast (setup.py) google-crc32c==1.6.0 # via # google-cloud-storage @@ -225,16 +254,17 @@ google-resumable-media==2.7.2 # google-cloud-storage googleapis-common-protos[grpc]==1.65.0 # via + # feast (setup.py) # google-api-core # grpc-google-iam-v1 # grpcio-status great-expectations==0.18.20 -greenlet==3.1.0 - # via sqlalchemy + # via feast (setup.py) grpc-google-iam-v1==0.13.1 # via google-cloud-bigtable grpcio==1.66.1 # via + # feast (setup.py) # google-api-core # googleapis-common-protos # grpc-google-iam-v1 @@ -244,30 +274,42 @@ grpcio==1.66.1 # grpcio-testing # grpcio-tools grpcio-health-checking==1.62.3 + # via feast (setup.py) grpcio-reflection==1.62.3 + # via feast (setup.py) grpcio-status==1.62.3 # via google-api-core grpcio-testing==1.62.3 + # via feast (setup.py) grpcio-tools==1.62.3 + # via feast (setup.py) gunicorn==23.0.0 + # via feast (setup.py) h11==0.14.0 # via # httpcore # uvicorn happybase==1.2.0 + # via feast (setup.py) hazelcast-python-client==5.5.0 + # via feast (setup.py) hiredis==2.4.0 + # via feast (setup.py) httpcore==1.0.5 # via httpx httptools==0.6.1 # via uvicorn httpx==0.27.2 # via + # feast (setup.py) # jupyterlab # python-keycloak ibis-framework[duckdb]==9.0.0 - # via ibis-substrait + # via + # feast (setup.py) + # ibis-substrait ibis-substrait==4.0.1 + # via feast (setup.py) identify==2.6.0 # via pre-commit idna==3.8 @@ -310,6 +352,7 @@ jedi==0.19.1 # via ipython jinja2==3.1.4 # via + # feast (setup.py) # altair # great-expectations # jupyter-server @@ -332,6 +375,7 @@ jsonpointer==3.0.0 # jsonschema jsonschema[format-nongpl]==4.23.0 # via + # feast (setup.py) # altair # great-expectations # jupyter-events @@ -379,6 +423,7 @@ jupyterlab-widgets==3.0.13 jwcrypto==1.5.6 # via python-keycloak kubernetes==20.13.0 + # via feast (setup.py) locket==1.0.0 # via partd makefun==1.15.4 @@ -399,13 +444,17 @@ matplotlib-inline==0.1.7 mdurl==0.1.2 # via markdown-it-py minio==7.1.0 + # via feast (setup.py) mistune==3.0.2 # via # great-expectations # nbconvert mmh3==4.1.0 + # via feast (setup.py) mock==2.0.0 + # via feast (setup.py) moto==4.2.14 + # via feast (setup.py) msal==1.31.0 # via # azure-identity @@ -417,10 +466,13 @@ multidict==6.1.0 # aiohttp # yarl mypy==1.11.2 - # via sqlalchemy + # via + # feast (setup.py) + # sqlalchemy mypy-extensions==1.0.0 # via mypy mypy-protobuf==3.3.0 + # via feast (setup.py) nbclient==0.10.0 # via nbconvert nbconvert==7.16.4 @@ -443,6 +495,7 @@ notebook-shim==0.2.4 # notebook numpy==1.26.4 # via + # feast (setup.py) # altair # dask # db-dtypes @@ -476,6 +529,7 @@ packaging==24.1 # sphinx pandas==2.2.2 # via + # feast (setup.py) # altair # dask # dask-expr @@ -501,6 +555,7 @@ pexpect==4.9.0 pip==24.2 # via pip-tools pip-tools==7.4.1 + # via feast (setup.py) platformdirs==3.11.0 # via # jupyter-core @@ -513,8 +568,11 @@ ply==3.11 portalocker==2.10.1 # via msal-extensions pre-commit==3.3.1 + # via feast (setup.py) prometheus-client==0.20.0 - # via jupyter-server + # via + # feast (setup.py) + # jupyter-server prompt-toolkit==3.0.47 # via ipython proto-plus==1.24.0 @@ -525,6 +583,7 @@ proto-plus==1.24.0 # google-cloud-datastore protobuf==4.25.4 # via + # feast (setup.py) # google-api-core # google-cloud-bigquery-storage # google-cloud-bigtable @@ -540,9 +599,12 @@ protobuf==4.25.4 # proto-plus # substrait psutil==5.9.0 - # via ipykernel -psycopg[binary, pool]==3.2.1 -psycopg-binary==3.2.1 + # via + # feast (setup.py) + # ipykernel +psycopg[binary, pool]==3.1.18 + # via feast (setup.py) +psycopg-binary==3.1.18 # via psycopg psycopg-pool==3.2.2 # via psycopg @@ -553,12 +615,14 @@ ptyprocess==0.7.0 pure-eval==0.2.3 # via stack-data py==1.11.0 + # via feast (setup.py) py-cpuinfo==9.0.0 # via pytest-benchmark py4j==0.10.9.7 # via pyspark pyarrow==16.1.0 # via + # feast (setup.py) # dask-expr # db-dtypes # deltalake @@ -574,28 +638,35 @@ pyasn1==0.6.1 pyasn1-modules==0.4.1 # via google-auth pybindgen==0.22.1 + # via feast (setup.py) pycparser==2.22 # via cffi pydantic==2.9.1 # via + # feast (setup.py) # fastapi # great-expectations pydantic-core==2.23.3 # via pydantic pygments==2.18.0 # via + # feast (setup.py) # ipython # nbconvert # rich # sphinx pyjwt[crypto]==2.9.0 # via + # feast (setup.py) # msal # singlestoredb # snowflake-connector-python pymssql==2.3.1 + # via feast (setup.py) pymysql==1.1.1 + # via feast (setup.py) pyodbc==5.1.0 + # via feast (setup.py) pyopenssl==24.2.1 # via snowflake-connector-python pyparsing==3.1.4 @@ -605,8 +676,10 @@ pyproject-hooks==1.1.0 # build # pip-tools pyspark==3.5.2 + # via feast (setup.py) pytest==7.4.4 # via + # feast (setup.py) # pytest-benchmark # pytest-cov # pytest-env @@ -616,13 +689,21 @@ pytest==7.4.4 # pytest-timeout # pytest-xdist pytest-benchmark==3.4.1 + # via feast (setup.py) pytest-cov==5.0.0 + # via feast (setup.py) pytest-env==1.1.3 + # via feast (setup.py) pytest-lazy-fixture==0.6.3 + # via feast (setup.py) pytest-mock==1.10.4 + # via feast (setup.py) pytest-ordering==0.6 + # via feast (setup.py) pytest-timeout==1.4.2 + # via feast (setup.py) pytest-xdist==3.6.1 + # via feast (setup.py) python-dateutil==2.9.0.post0 # via # arrow @@ -640,6 +721,7 @@ python-dotenv==1.0.1 python-json-logger==2.0.7 # via jupyter-events python-keycloak==4.2.2 + # via feast (setup.py) pytz==2024.2 # via # great-expectations @@ -649,6 +731,7 @@ pytz==2024.2 # trino pyyaml==6.0.2 # via + # feast (setup.py) # dask # ibis-substrait # jupyter-events @@ -662,15 +745,19 @@ pyzmq==26.2.0 # jupyter-client # jupyter-server redis==4.6.0 + # via feast (setup.py) referencing==0.35.1 # via # jsonschema # jsonschema-specifications # jupyter-events regex==2024.7.24 - # via parsimonious + # via + # feast (setup.py) + # parsimonious requests==2.32.3 # via + # feast (setup.py) # azure-core # docker # google-api-core @@ -716,6 +803,7 @@ ruamel-yaml==0.17.40 ruamel-yaml-clib==0.2.8 # via ruamel-yaml ruff==0.6.4 + # via feast (setup.py) s3transfer==0.10.2 # via boto3 scipy==1.13.1 @@ -730,6 +818,7 @@ setuptools==74.1.2 # pip-tools # singlestoredb singlestoredb==1.6.3 + # via feast (setup.py) six==1.16.0 # via # asttokens @@ -750,11 +839,13 @@ sniffio==1.3.1 snowballstemmer==2.2.0 # via sphinx snowflake-connector-python[pandas]==3.12.1 + # via feast (setup.py) sortedcontainers==2.4.0 # via snowflake-connector-python soupsieve==2.6 # via beautifulsoup4 sphinx==6.2.1 + # via feast (setup.py) sphinxcontrib-applehelp==2.0.0 # via sphinx sphinxcontrib-devhelp==2.0.0 @@ -768,9 +859,11 @@ sphinxcontrib-qthelp==2.0.0 sphinxcontrib-serializinghtml==2.0.0 # via sphinx sqlalchemy[mypy]==2.0.34 + # via feast (setup.py) sqlglot==23.12.2 # via ibis-framework sqlite-vec==0.1.1 + # via feast (setup.py) sqlparams==6.1.0 # via singlestoredb stack-data==0.6.3 @@ -780,17 +873,21 @@ starlette==0.38.5 substrait==0.22.0 # via ibis-substrait tabulate==0.9.0 + # via feast (setup.py) tenacity==8.5.0 + # via feast (setup.py) terminado==0.18.1 # via # jupyter-server # jupyter-server-terminals testcontainers==4.4.0 + # via feast (setup.py) thriftpy2==0.5.2 # via happybase tinycss2==1.3.0 # via nbconvert toml==0.10.2 + # via feast (setup.py) tomli==2.0.1 # via # build @@ -818,7 +915,9 @@ tornado==6.4.1 # notebook # terminado tqdm==4.66.5 - # via great-expectations + # via + # feast (setup.py) + # great-expectations traitlets==5.14.3 # via # comm @@ -835,23 +934,37 @@ traitlets==5.14.3 # nbconvert # nbformat trino==0.329.0 + # via feast (setup.py) typeguard==4.3.0 + # via feast (setup.py) types-cffi==1.16.0.20240331 # via types-pyopenssl types-protobuf==3.19.22 - # via mypy-protobuf + # via + # feast (setup.py) + # mypy-protobuf types-pymysql==1.1.0.20240524 + # via feast (setup.py) types-pyopenssl==24.1.0.20240722 # via types-redis types-python-dateutil==2.9.0.20240906 - # via arrow + # via + # feast (setup.py) + # arrow types-pytz==2024.1.0.20240417 + # via feast (setup.py) types-pyyaml==6.0.12.20240808 + # via feast (setup.py) types-redis==4.6.0.20240903 + # via feast (setup.py) types-requests==2.30.0.0 + # via feast (setup.py) types-setuptools==74.1.0.20240907 - # via types-cffi + # via + # feast (setup.py) + # types-cffi types-tabulate==0.9.0.20240106 + # via feast (setup.py) types-urllib3==1.26.25.14 # via types-requests typing-extensions==4.12.2 @@ -889,6 +1002,7 @@ uri-template==1.3.0 # via jsonschema urllib3==1.26.20 # via + # feast (setup.py) # botocore # docker # elastic-transport @@ -900,10 +1014,13 @@ urllib3==1.26.20 # snowflake-connector-python # testcontainers uvicorn[standard]==0.30.6 + # via feast (setup.py) uvloop==0.20.0 # via uvicorn virtualenv==20.23.0 - # via pre-commit + # via + # feast (setup.py) + # pre-commit watchfiles==0.24.0 # via uvicorn wcwidth==0.2.13 diff --git a/sdk/python/requirements/py3.9-requirements.txt b/sdk/python/requirements/py3.9-requirements.txt index 0ae2fcf9d6..8a7ac763c0 100644 --- a/sdk/python/requirements/py3.9-requirements.txt +++ b/sdk/python/requirements/py3.9-requirements.txt @@ -11,36 +11,36 @@ attrs==24.2.0 # jsonschema # referencing bigtree==0.21.1 -cachetools==5.5.0 - # via google-auth + # via feast (setup.py) certifi==2024.8.30 - # via - # kubernetes - # requests + # via requests charset-normalizer==3.3.2 # via requests click==8.1.7 # via + # feast (setup.py) # dask # uvicorn cloudpickle==3.0.0 # via dask colorama==0.4.6 + # via feast (setup.py) dask[dataframe]==2024.8.0 - # via dask-expr + # via + # feast (setup.py) + # dask-expr dask-expr==1.1.10 # via dask dill==0.3.8 + # via feast (setup.py) exceptiongroup==1.2.2 # via anyio fastapi==0.114.1 + # via feast (setup.py) fsspec==2024.9.0 # via dask -google-auth==2.34.0 - # via kubernetes -greenlet==3.1.0 - # via sqlalchemy gunicorn==23.0.0 + # via feast (setup.py) h11==0.14.0 # via uvicorn httptools==0.6.1 @@ -54,98 +54,97 @@ importlib-metadata==8.5.0 # dask # typeguard jinja2==3.1.4 + # via feast (setup.py) jsonschema==4.23.0 + # via feast (setup.py) jsonschema-specifications==2023.12.1 # via jsonschema -kubernetes==20.13.0 locket==1.0.0 # via partd markupsafe==2.1.5 # via jinja2 mmh3==4.1.0 + # via feast (setup.py) mypy==1.11.2 # via sqlalchemy mypy-extensions==1.0.0 # via mypy mypy-protobuf==3.6.0 + # via feast (setup.py) numpy==1.26.4 # via + # feast (setup.py) # dask # pandas # pyarrow -oauthlib==3.2.2 - # via requests-oauthlib packaging==24.1 # via # dask # gunicorn pandas==2.2.2 # via + # feast (setup.py) # dask # dask-expr partd==1.4.2 # via dask prometheus-client==0.20.0 + # via feast (setup.py) protobuf==4.25.4 - # via mypy-protobuf + # via + # feast (setup.py) + # mypy-protobuf psutil==6.0.0 + # via feast (setup.py) pyarrow==17.0.0 - # via dask-expr -pyasn1==0.6.1 # via - # pyasn1-modules - # rsa -pyasn1-modules==0.4.1 - # via google-auth + # feast (setup.py) + # dask-expr pydantic==2.9.1 - # via fastapi + # via + # feast (setup.py) + # fastapi pydantic-core==2.23.3 # via pydantic pygments==2.18.0 + # via feast (setup.py) pyjwt==2.9.0 + # via feast (setup.py) python-dateutil==2.9.0.post0 - # via - # kubernetes - # pandas + # via pandas python-dotenv==1.0.1 # via uvicorn pytz==2024.2 # via pandas pyyaml==6.0.2 # via + # feast (setup.py) # dask - # kubernetes # uvicorn referencing==0.35.1 # via # jsonschema # jsonschema-specifications requests==2.32.3 - # via - # kubernetes - # requests-oauthlib -requests-oauthlib==2.0.0 - # via kubernetes + # via feast (setup.py) rpds-py==0.20.0 # via # jsonschema # referencing -rsa==4.9 - # via google-auth -setuptools==74.1.2 - # via kubernetes six==1.16.0 - # via - # kubernetes - # python-dateutil + # via python-dateutil sniffio==1.3.1 # via anyio sqlalchemy[mypy]==2.0.34 + # via feast (setup.py) starlette==0.38.5 # via fastapi tabulate==0.9.0 + # via feast (setup.py) tenacity==8.5.0 + # via feast (setup.py) toml==0.10.2 + # via feast (setup.py) tomli==2.0.1 # via mypy toolz==0.12.1 @@ -153,7 +152,9 @@ toolz==0.12.1 # dask # partd tqdm==4.66.5 + # via feast (setup.py) typeguard==4.3.0 + # via feast (setup.py) types-protobuf==5.27.0.20240907 # via mypy-protobuf typing-extensions==4.12.2 @@ -170,16 +171,13 @@ typing-extensions==4.12.2 tzdata==2024.1 # via pandas urllib3==2.2.2 - # via - # kubernetes - # requests + # via requests uvicorn[standard]==0.30.6 + # via feast (setup.py) uvloop==0.20.0 # via uvicorn watchfiles==0.24.0 # via uvicorn -websocket-client==1.8.0 - # via kubernetes websockets==13.0.1 # via uvicorn zipp==3.20.1 diff --git a/setup.py b/setup.py index d8bc55e334..d412541b7d 100644 --- a/setup.py +++ b/setup.py @@ -63,7 +63,6 @@ "psutil", "bigtree>=0.19.2", "pyjwt", - "kubernetes<=20.13.0", ] GCP_REQUIRED = [ From c5a4d907bf34f4cf7477b212cd2820b0e7d24b7b Mon Sep 17 00:00:00 2001 From: Harri Lehtola <1781172+peruukki@users.noreply.github.com> Date: Tue, 17 Sep 2024 09:16:19 +0300 Subject: [PATCH 072/185] fix: Move tslib from devDependencies to dependencies in Feast UI (#4525) tslib is a runtime dependency, so it should be in dependencies. This ensures the specified version is installed also when the package is used as a module. Signed-off-by: Harri Lehtola --- ui/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/package.json b/ui/package.json index cd80859aa1..3a609f3c83 100644 --- a/ui/package.json +++ b/ui/package.json @@ -47,6 +47,7 @@ "react-query": "^3.34.12", "react-router-dom": "6", "react-scripts": "^5.0.0", + "tslib": "^2.3.1", "use-query-params": "^1.2.3", "zod": "^3.11.6" }, @@ -105,7 +106,6 @@ "rollup-plugin-svg": "^2.0.0", "rollup-plugin-svgo": "^1.1.0", "rollup-plugin-terser": "^7.0.2", - "tslib": "^2.3.1", "typescript": "^4.4.2" }, "description": "Web UI for the [Feast Feature Store](https://feast.dev/)", From 7535b4036ce980c9a05bc33a9e61a7938ea1303e Mon Sep 17 00:00:00 2001 From: Theodor Mihalache <84387487+tmihalac@users.noreply.github.com> Date: Tue, 17 Sep 2024 13:21:40 -0400 Subject: [PATCH 073/185] fix: Added Offline Store Arrow client errors handler (#4524) * fix: Added Offline Store Arrow client errors handler Signed-off-by: Theodor Mihalache * Added more tests Signed-off-by: Theodor Mihalache --------- Signed-off-by: Theodor Mihalache --- sdk/python/feast/arrow_error_handler.py | 49 +++++++++++++ .../feast/infra/offline_stores/remote.py | 68 ++++++++++++++++--- sdk/python/feast/offline_server.py | 54 ++++++++++----- .../client/arrow_flight_auth_interceptor.py | 9 --- sdk/python/feast/permissions/server/arrow.py | 31 ++------- .../tests/unit/test_arrow_error_decorator.py | 33 +++++++++ sdk/python/tests/unit/test_offline_server.py | 32 ++++++++- 7 files changed, 215 insertions(+), 61 deletions(-) create mode 100644 sdk/python/feast/arrow_error_handler.py create mode 100644 sdk/python/tests/unit/test_arrow_error_decorator.py diff --git a/sdk/python/feast/arrow_error_handler.py b/sdk/python/feast/arrow_error_handler.py new file mode 100644 index 0000000000..e873592bd5 --- /dev/null +++ b/sdk/python/feast/arrow_error_handler.py @@ -0,0 +1,49 @@ +import logging +from functools import wraps + +import pyarrow.flight as fl + +from feast.errors import FeastError + +logger = logging.getLogger(__name__) + + +def arrow_client_error_handling_decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except Exception as e: + mapped_error = FeastError.from_error_detail(_get_exception_data(e.args[0])) + if mapped_error is not None: + raise mapped_error + raise e + + return wrapper + + +def arrow_server_error_handling_decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except Exception as e: + if isinstance(e, FeastError): + raise fl.FlightError(e.to_error_detail()) + + return wrapper + + +def _get_exception_data(except_str) -> str: + substring = "Flight error: " + + # Find the starting index of the substring + position = except_str.find(substring) + end_json_index = except_str.find("}") + + if position != -1 and end_json_index != -1: + # Extract the part of the string after the substring + result = except_str[position + len(substring) : end_json_index + 1] + return result + + return "" diff --git a/sdk/python/feast/infra/offline_stores/remote.py b/sdk/python/feast/infra/offline_stores/remote.py index 40239c8950..8154f75f87 100644 --- a/sdk/python/feast/infra/offline_stores/remote.py +++ b/sdk/python/feast/infra/offline_stores/remote.py @@ -10,9 +10,12 @@ import pyarrow as pa import pyarrow.flight as fl import pyarrow.parquet +from pyarrow import Schema +from pyarrow._flight import FlightCallOptions, FlightDescriptor, Ticket from pydantic import StrictInt, StrictStr from feast import OnDemandFeatureView +from feast.arrow_error_handler import arrow_client_error_handling_decorator from feast.data_source import DataSource from feast.feature_logging import ( FeatureServiceLoggingSource, @@ -27,8 +30,10 @@ RetrievalMetadata, ) from feast.infra.registry.base_registry import BaseRegistry +from feast.permissions.auth.auth_type import AuthType +from feast.permissions.auth_model import AuthConfig from feast.permissions.client.arrow_flight_auth_interceptor import ( - build_arrow_flight_client, + FlightAuthInterceptorFactory, ) from feast.repo_config import FeastConfigBaseModel, RepoConfig from feast.saved_dataset import SavedDatasetStorage @@ -36,6 +41,43 @@ logger = logging.getLogger(__name__) +class FeastFlightClient(fl.FlightClient): + @arrow_client_error_handling_decorator + def get_flight_info( + self, descriptor: FlightDescriptor, options: FlightCallOptions = None + ): + return super().get_flight_info(descriptor, options) + + @arrow_client_error_handling_decorator + def do_get(self, ticket: Ticket, options: FlightCallOptions = None): + return super().do_get(ticket, options) + + @arrow_client_error_handling_decorator + def do_put( + self, + descriptor: FlightDescriptor, + schema: Schema, + options: FlightCallOptions = None, + ): + return super().do_put(descriptor, schema, options) + + @arrow_client_error_handling_decorator + def list_flights(self, criteria: bytes = b"", options: FlightCallOptions = None): + return super().list_flights(criteria, options) + + @arrow_client_error_handling_decorator + def list_actions(self, options: FlightCallOptions = None): + return super().list_actions(options) + + +def build_arrow_flight_client(host: str, port, auth_config: AuthConfig): + if auth_config.type != AuthType.NONE.value: + middlewares = [FlightAuthInterceptorFactory(auth_config)] + return FeastFlightClient(f"grpc://{host}:{port}", middleware=middlewares) + + return FeastFlightClient(f"grpc://{host}:{port}") + + class RemoteOfflineStoreConfig(FeastConfigBaseModel): type: Literal["remote"] = "remote" host: StrictStr @@ -48,7 +90,7 @@ class RemoteOfflineStoreConfig(FeastConfigBaseModel): class RemoteRetrievalJob(RetrievalJob): def __init__( self, - client: fl.FlightClient, + client: FeastFlightClient, api: str, api_parameters: Dict[str, Any], entity_df: Union[pd.DataFrame, str] = None, @@ -338,7 +380,7 @@ def _send_retrieve_remote( api_parameters: Dict[str, Any], entity_df: Union[pd.DataFrame, str], table: pa.Table, - client: fl.FlightClient, + client: FeastFlightClient, ): command_descriptor = _call_put( api, @@ -351,19 +393,19 @@ def _send_retrieve_remote( def _call_get( - client: fl.FlightClient, + client: FeastFlightClient, command_descriptor: fl.FlightDescriptor, ): flight = client.get_flight_info(command_descriptor) ticket = flight.endpoints[0].ticket reader = client.do_get(ticket) - return reader.read_all() + return read_all(reader) def _call_put( api: str, api_parameters: Dict[str, Any], - client: fl.FlightClient, + client: FeastFlightClient, entity_df: Union[pd.DataFrame, str], table: pa.Table, ): @@ -391,7 +433,7 @@ def _put_parameters( command_descriptor: fl.FlightDescriptor, entity_df: Union[pd.DataFrame, str], table: pa.Table, - client: fl.FlightClient, + client: FeastFlightClient, ): updatedTable: pa.Table @@ -404,10 +446,20 @@ def _put_parameters( writer, _ = client.do_put(command_descriptor, updatedTable.schema) - writer.write_table(updatedTable) + write_table(writer, updatedTable) + + +@arrow_client_error_handling_decorator +def write_table(writer, updated_table: pa.Table): + writer.write_table(updated_table) writer.close() +@arrow_client_error_handling_decorator +def read_all(reader): + return reader.read_all() + + def _create_empty_table(): schema = pa.schema( { diff --git a/sdk/python/feast/offline_server.py b/sdk/python/feast/offline_server.py index 839acada93..ff3db579d0 100644 --- a/sdk/python/feast/offline_server.py +++ b/sdk/python/feast/offline_server.py @@ -9,16 +9,18 @@ import pyarrow.flight as fl from feast import FeatureStore, FeatureView, utils +from feast.arrow_error_handler import arrow_server_error_handling_decorator from feast.feature_logging import FeatureServiceLoggingSource from feast.feature_view import DUMMY_ENTITY_NAME from feast.infra.offline_stores.offline_utils import get_offline_store_from_config from feast.permissions.action import AuthzedAction from feast.permissions.security_manager import assert_permissions from feast.permissions.server.arrow import ( - arrowflight_middleware, + AuthorizationMiddlewareFactory, inject_user_details_decorator, ) from feast.permissions.server.utils import ( + AuthManagerType, ServerType, init_auth_manager, init_security_manager, @@ -34,7 +36,7 @@ class OfflineServer(fl.FlightServerBase): def __init__(self, store: FeatureStore, location: str, **kwargs): super(OfflineServer, self).__init__( location, - middleware=arrowflight_middleware( + middleware=self.arrow_flight_auth_middleware( str_to_auth_manager_type(store.config.auth_config.type) ), **kwargs, @@ -45,6 +47,25 @@ def __init__(self, store: FeatureStore, location: str, **kwargs): self.store = store self.offline_store = get_offline_store_from_config(store.config.offline_store) + def arrow_flight_auth_middleware( + self, + auth_type: AuthManagerType, + ) -> dict[str, fl.ServerMiddlewareFactory]: + """ + A dictionary with the configured middlewares to support extracting the user details when the authorization manager is defined. + The authorization middleware key is `auth`. + + Returns: + dict[str, fl.ServerMiddlewareFactory]: Optional dictionary of middlewares. If the authorization type is set to `NONE`, it returns an empty dict. + """ + + if auth_type == AuthManagerType.NONE: + return {} + + return { + "auth": AuthorizationMiddlewareFactory(), + } + @classmethod def descriptor_to_key(self, descriptor: fl.FlightDescriptor): return ( @@ -61,15 +82,7 @@ def _make_flight_info(self, key: Any, descriptor: fl.FlightDescriptor): return fl.FlightInfo(schema, descriptor, endpoints, -1, -1) @inject_user_details_decorator - def get_flight_info( - self, context: fl.ServerCallContext, descriptor: fl.FlightDescriptor - ): - key = OfflineServer.descriptor_to_key(descriptor) - if key in self.flights: - return self._make_flight_info(key, descriptor) - raise KeyError("Flight not found.") - - @inject_user_details_decorator + @arrow_server_error_handling_decorator def list_flights(self, context: fl.ServerCallContext, criteria: bytes): for key, table in self.flights.items(): if key[1] is not None: @@ -79,9 +92,20 @@ def list_flights(self, context: fl.ServerCallContext, criteria: bytes): yield self._make_flight_info(key, descriptor) + @inject_user_details_decorator + @arrow_server_error_handling_decorator + def get_flight_info( + self, context: fl.ServerCallContext, descriptor: fl.FlightDescriptor + ): + key = OfflineServer.descriptor_to_key(descriptor) + if key in self.flights: + return self._make_flight_info(key, descriptor) + raise KeyError("Flight not found.") + # Expects to receive request parameters and stores them in the flights dictionary # Indexed by the unique command @inject_user_details_decorator + @arrow_server_error_handling_decorator def do_put( self, context: fl.ServerCallContext, @@ -179,6 +203,7 @@ def _validate_do_get_parameters(self, command: dict): # Extracts the API parameters from the flights dictionary, delegates the execution to the FeatureStore instance # and returns the stream of data @inject_user_details_decorator + @arrow_server_error_handling_decorator def do_get(self, context: fl.ServerCallContext, ticket: fl.Ticket): key = ast.literal_eval(ticket.ticket.decode()) if key not in self.flights: @@ -337,6 +362,7 @@ def pull_latest_from_table_or_query(self, command: dict): utils.make_tzaware(datetime.fromisoformat(command["end_date"])), ) + @arrow_server_error_handling_decorator def list_actions(self, context): return [ ( @@ -431,12 +457,6 @@ def persist(self, command: dict, key: str): traceback.print_exc() raise e - def do_action(self, context: fl.ServerCallContext, action: fl.Action): - pass - - def do_drop_dataset(self, dataset): - pass - def remove_dummies(fv: FeatureView) -> FeatureView: """ diff --git a/sdk/python/feast/permissions/client/arrow_flight_auth_interceptor.py b/sdk/python/feast/permissions/client/arrow_flight_auth_interceptor.py index 7ef84fbeae..c3281bfa51 100644 --- a/sdk/python/feast/permissions/client/arrow_flight_auth_interceptor.py +++ b/sdk/python/feast/permissions/client/arrow_flight_auth_interceptor.py @@ -1,6 +1,5 @@ import pyarrow.flight as fl -from feast.permissions.auth.auth_type import AuthType from feast.permissions.auth_model import AuthConfig from feast.permissions.client.client_auth_token import get_auth_token @@ -28,11 +27,3 @@ def __init__(self, auth_config: AuthConfig): def start_call(self, info): return FlightBearerTokenInterceptor(self.auth_config) - - -def build_arrow_flight_client(host: str, port, auth_config: AuthConfig): - if auth_config.type != AuthType.NONE.value: - middleware_factory = FlightAuthInterceptorFactory(auth_config) - return fl.FlightClient(f"grpc://{host}:{port}", middleware=[middleware_factory]) - else: - return fl.FlightClient(f"grpc://{host}:{port}") diff --git a/sdk/python/feast/permissions/server/arrow.py b/sdk/python/feast/permissions/server/arrow.py index 5eba7d0916..4f0afc3ee5 100644 --- a/sdk/python/feast/permissions/server/arrow.py +++ b/sdk/python/feast/permissions/server/arrow.py @@ -5,7 +5,7 @@ import asyncio import functools import logging -from typing import Optional, cast +from typing import cast import pyarrow.flight as fl from pyarrow.flight import ServerCallContext @@ -14,41 +14,19 @@ get_auth_manager, ) from feast.permissions.security_manager import get_security_manager -from feast.permissions.server.utils import ( - AuthManagerType, -) from feast.permissions.user import User logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) -def arrowflight_middleware( - auth_type: AuthManagerType, -) -> Optional[dict[str, fl.ServerMiddlewareFactory]]: - """ - A dictionary with the configured middlewares to support extracting the user details when the authorization manager is defined. - The authorization middleware key is `auth`. - - Returns: - dict[str, fl.ServerMiddlewareFactory]: Optional dictionary of middlewares. If the authorization type is set to `NONE`, it returns `None`. - """ - - if auth_type == AuthManagerType.NONE: - return None - - return { - "auth": AuthorizationMiddlewareFactory(), - } - - class AuthorizationMiddlewareFactory(fl.ServerMiddlewareFactory): """ A middleware factory to intercept the authorization header and propagate it to the authorization middleware. """ - def __init__(self): - pass + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) def start_call(self, info, headers): """ @@ -65,7 +43,8 @@ class AuthorizationMiddleware(fl.ServerMiddleware): A server middleware holding the authorization header and offering a method to extract the user credentials. """ - def __init__(self, access_token: str): + def __init__(self, access_token: str, *args, **kwargs): + super().__init__(*args, **kwargs) self.access_token = access_token def call_completed(self, exception): diff --git a/sdk/python/tests/unit/test_arrow_error_decorator.py b/sdk/python/tests/unit/test_arrow_error_decorator.py new file mode 100644 index 0000000000..fc350d34c0 --- /dev/null +++ b/sdk/python/tests/unit/test_arrow_error_decorator.py @@ -0,0 +1,33 @@ +import pyarrow.flight as fl +import pytest + +from feast.arrow_error_handler import arrow_client_error_handling_decorator +from feast.errors import PermissionNotFoundException + +permissionError = PermissionNotFoundException("dummy_name", "dummy_project") + + +@arrow_client_error_handling_decorator +def decorated_method(error): + raise error + + +@pytest.mark.parametrize( + "error, expected_raised_error", + [ + (fl.FlightError("Flight error: "), fl.FlightError("Flight error: ")), + ( + fl.FlightError(f"Flight error: {permissionError.to_error_detail()}"), + permissionError, + ), + (fl.FlightError("Test Error"), fl.FlightError("Test Error")), + (RuntimeError("Flight error: "), RuntimeError("Flight error: ")), + (permissionError, permissionError), + ], +) +def test_rest_error_handling_with_feast_exception(error, expected_raised_error): + with pytest.raises( + type(expected_raised_error), + match=str(expected_raised_error), + ): + decorated_method(error) diff --git a/sdk/python/tests/unit/test_offline_server.py b/sdk/python/tests/unit/test_offline_server.py index 237e2ecad4..7c38d9bfca 100644 --- a/sdk/python/tests/unit/test_offline_server.py +++ b/sdk/python/tests/unit/test_offline_server.py @@ -8,7 +8,8 @@ import pyarrow.flight as flight import pytest -from feast import FeatureStore +from feast import FeatureStore, FeatureView, FileSource +from feast.errors import FeatureViewNotFoundException from feast.feature_logging import FeatureServiceLoggingSource from feast.infra.offline_stores.remote import ( RemoteOfflineStore, @@ -120,6 +121,35 @@ def test_remote_offline_store_apis(): _test_pull_all_from_table_or_query(str(temp_dir), fs) +def test_remote_offline_store_exception_handling(): + with tempfile.TemporaryDirectory() as temp_dir: + store = default_store(str(temp_dir)) + location = "grpc+tcp://localhost:0" + + _init_auth_manager(store=store) + server = OfflineServer(store=store, location=location) + + assertpy.assert_that(server).is_not_none + assertpy.assert_that(server.port).is_not_equal_to(0) + + fs = remote_feature_store(server) + data_file = os.path.join( + temp_dir, fs.project, "feature_repo/data/driver_stats.parquet" + ) + data_df = pd.read_parquet(data_file) + + with pytest.raises( + FeatureViewNotFoundException, + match="Feature view test does not exist in project test_remote_offline", + ): + RemoteOfflineStore.offline_write_batch( + fs.config, + FeatureView(name="test", source=FileSource(path="test")), + pa.Table.from_pandas(data_df), + progress=None, + ) + + def _test_get_historical_features_returns_data(fs: FeatureStore): entity_df = pd.DataFrame.from_dict( { From 58c6fc11e690704d80a829c272400446626ba810 Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Wed, 18 Sep 2024 13:57:22 -0400 Subject: [PATCH 074/185] Update CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2bc0915002..eae34fe0c3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,3 @@

Development Guide: Main Feast Repository

-> Please see [Development Guide](https://docs.feast.dev/project/development-guide) for project level development instructions, including instructions for Maintainers. +> Please see [Development Guide](docs/project/development-guide.md) for project level development instructions, including instructions for Maintainers. From b3c93a0704856324151960b15308fc94e4765914 Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Wed, 18 Sep 2024 14:03:33 -0400 Subject: [PATCH 075/185] Update community.md --- docs/community.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/community.md b/docs/community.md index 21cca702bf..640b5238b8 100644 --- a/docs/community.md +++ b/docs/community.md @@ -2,6 +2,8 @@ ## Links & Resources +* [Come say hi on Slack!](https://communityinviter.com/apps/feastopensource/feast-the-open-source-feature-store) + * As a part of the Linux Foundation, we ask community members to adhere to the [Linux Foundation Code of Conduct](https://events.linuxfoundation.org/about/code-of-conduct/) * [GitHub Repository](https://github.com/feast-dev/feast/): Find the complete Feast codebase on GitHub. * [Community Governance Doc](https://github.com/feast-dev/feast/blob/master/community): See the governance model of Feast, including who the maintainers are and how decisions are made. * [Google Folder](https://drive.google.com/drive/u/0/folders/1jgMHOPDT2DvBlJeO9LCM79DP4lm4eOrR): This folder is used as a central repository for all Feast resources. For example: From 0fb76e9041885659c68e294b0c033c62050bd374 Mon Sep 17 00:00:00 2001 From: Bhargav Dodla <13788369+EXPEbdodla@users.noreply.github.com> Date: Wed, 18 Sep 2024 12:51:05 -0700 Subject: [PATCH 076/185] fix: Removed protobuf as a required dependency (#4535) * fix: Removed protobuf as a required dependency Signed-off-by: Bhargav Dodla * fix: Removed install-protoc-dependencies target Signed-off-by: Bhargav Dodla --------- Signed-off-by: Bhargav Dodla Co-authored-by: Bhargav Dodla --- pyproject.toml | 4 +--- sdk/python/requirements/py3.10-ci-requirements.txt | 1 - sdk/python/requirements/py3.10-requirements.txt | 4 +--- sdk/python/requirements/py3.11-ci-requirements.txt | 1 - sdk/python/requirements/py3.11-requirements.txt | 4 +--- sdk/python/requirements/py3.9-ci-requirements.txt | 1 - sdk/python/requirements/py3.9-requirements.txt | 4 +--- setup.py | 12 ++++-------- 8 files changed, 8 insertions(+), 23 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 15921e633c..283338a838 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,7 @@ [build-system] requires = [ "grpcio-tools>=1.56.2,<2", - "grpcio>=1.56.2,<2", - "mypy-protobuf==3.1", - "protobuf==4.24.0", + "mypy-protobuf>=3.1", "pybindgen==0.22.0", "setuptools>=60", "setuptools_scm>=6.2", diff --git a/sdk/python/requirements/py3.10-ci-requirements.txt b/sdk/python/requirements/py3.10-ci-requirements.txt index 55df7ccb68..f09f3dded4 100644 --- a/sdk/python/requirements/py3.10-ci-requirements.txt +++ b/sdk/python/requirements/py3.10-ci-requirements.txt @@ -572,7 +572,6 @@ proto-plus==1.24.0 # google-cloud-datastore protobuf==4.25.4 # via - # feast (setup.py) # google-api-core # google-cloud-bigquery-storage # google-cloud-bigtable diff --git a/sdk/python/requirements/py3.10-requirements.txt b/sdk/python/requirements/py3.10-requirements.txt index 3420c8a0e3..1c6f53cf69 100644 --- a/sdk/python/requirements/py3.10-requirements.txt +++ b/sdk/python/requirements/py3.10-requirements.txt @@ -89,9 +89,7 @@ partd==1.4.2 prometheus-client==0.20.0 # via feast (setup.py) protobuf==4.25.4 - # via - # feast (setup.py) - # mypy-protobuf + # via mypy-protobuf psutil==6.0.0 # via feast (setup.py) pyarrow==17.0.0 diff --git a/sdk/python/requirements/py3.11-ci-requirements.txt b/sdk/python/requirements/py3.11-ci-requirements.txt index 6c5fb02e06..bed8145e2f 100644 --- a/sdk/python/requirements/py3.11-ci-requirements.txt +++ b/sdk/python/requirements/py3.11-ci-requirements.txt @@ -563,7 +563,6 @@ proto-plus==1.24.0 # google-cloud-datastore protobuf==4.25.4 # via - # feast (setup.py) # google-api-core # google-cloud-bigquery-storage # google-cloud-bigtable diff --git a/sdk/python/requirements/py3.11-requirements.txt b/sdk/python/requirements/py3.11-requirements.txt index 1d0ce54cc1..99a994e946 100644 --- a/sdk/python/requirements/py3.11-requirements.txt +++ b/sdk/python/requirements/py3.11-requirements.txt @@ -87,9 +87,7 @@ partd==1.4.2 prometheus-client==0.20.0 # via feast (setup.py) protobuf==4.25.4 - # via - # feast (setup.py) - # mypy-protobuf + # via mypy-protobuf psutil==6.0.0 # via feast (setup.py) pyarrow==17.0.0 diff --git a/sdk/python/requirements/py3.9-ci-requirements.txt b/sdk/python/requirements/py3.9-ci-requirements.txt index ae27849f4f..1015f46cff 100644 --- a/sdk/python/requirements/py3.9-ci-requirements.txt +++ b/sdk/python/requirements/py3.9-ci-requirements.txt @@ -583,7 +583,6 @@ proto-plus==1.24.0 # google-cloud-datastore protobuf==4.25.4 # via - # feast (setup.py) # google-api-core # google-cloud-bigquery-storage # google-cloud-bigtable diff --git a/sdk/python/requirements/py3.9-requirements.txt b/sdk/python/requirements/py3.9-requirements.txt index 8a7ac763c0..4bd6a44857 100644 --- a/sdk/python/requirements/py3.9-requirements.txt +++ b/sdk/python/requirements/py3.9-requirements.txt @@ -91,9 +91,7 @@ partd==1.4.2 prometheus-client==0.20.0 # via feast (setup.py) protobuf==4.25.4 - # via - # feast (setup.py) - # mypy-protobuf + # via mypy-protobuf psutil==6.0.0 # via feast (setup.py) pyarrow==17.0.0 diff --git a/setup.py b/setup.py index d412541b7d..f332c21f71 100644 --- a/setup.py +++ b/setup.py @@ -18,10 +18,9 @@ import shutil import subprocess import sys - from pathlib import Path -from setuptools import find_packages, setup, Command +from setuptools import Command, find_packages, setup from setuptools.command.build_ext import build_ext as _build_ext from setuptools.command.build_py import build_py from setuptools.command.develop import develop @@ -43,7 +42,6 @@ "mmh3", "numpy>=1.22,<2", "pandas>=1.4.3,<3", - "protobuf>=4.24.0,<5.0.0", "pyarrow>=4", "pydantic>=2.0.0", "pygments>=2.12.0,<3", @@ -102,7 +100,7 @@ "psycopg[binary,pool]>=3.0.0,<4", ] -OPENTELEMETRY = ["prometheus_client","psutil"] +OPENTELEMETRY = ["prometheus_client", "psutil"] MYSQL_REQUIRED = ["pymysql", "types-PyMySQL"] @@ -139,7 +137,6 @@ GRPCIO_REQUIRED = [ "grpcio>=1.56.2,<2", - "grpcio-tools>=1.56.2,<2", "grpcio-reflection>=1.56.2,<2", "grpcio-health-checking>=1.56.2,<2", ] @@ -160,6 +157,7 @@ "virtualenv==20.23.0", "cryptography>=35.0,<43", "ruff>=0.3.3", + "grpcio-tools>=1.56.2,<2", "grpcio-testing>=1.56.2,<2", # FastAPI does not correctly pull starlette dependency on httpx see thread(https://github.com/tiangolo/fastapi/issues/5656). "httpx>=0.23.3", @@ -403,9 +401,7 @@ def run(self): use_scm_version=use_scm_version, setup_requires=[ "grpcio-tools>=1.56.2,<2", - "grpcio>=1.56.2,<2", - "mypy-protobuf==3.1", - "protobuf==4.24.0", + "mypy-protobuf>=3.1", "pybindgen==0.22.0", "setuptools_scm>=6.2", ], From 50b8f238b6f9adbc9ff0b20e18b78b2948c2f440 Mon Sep 17 00:00:00 2001 From: Abdul Hameed Date: Thu, 19 Sep 2024 14:04:56 -0400 Subject: [PATCH 077/185] fix: Logger settings for feature servers and updated logger for permission flow (#4531) * Logger setting for feature servers Signed-off-by: Abdul Hameed * added/updated logger for permission flow Signed-off-by: Abdul Hameed * set defualt logger level for feature servers as WARNING Signed-off-by: Abdul Hameed --------- Signed-off-by: Abdul Hameed --- .../feast-feature-server/templates/deployment.yaml | 8 ++++++++ infra/charts/feast-feature-server/values.yaml | 2 ++ .../feast/permissions/auth/kubernetes_token_parser.py | 6 ++++-- sdk/python/feast/permissions/auth/oidc_token_parser.py | 4 ++-- .../client/intra_comm_authentication_client_manager.py | 1 + sdk/python/feast/permissions/enforcer.py | 4 ++++ sdk/python/feast/permissions/server/arrow.py | 9 +++++---- sdk/python/feast/permissions/server/grpc.py | 7 ++++--- sdk/python/feast/permissions/server/utils.py | 1 - 9 files changed, 30 insertions(+), 12 deletions(-) diff --git a/infra/charts/feast-feature-server/templates/deployment.yaml b/infra/charts/feast-feature-server/templates/deployment.yaml index dc62be8b95..1f673280fe 100644 --- a/infra/charts/feast-feature-server/templates/deployment.yaml +++ b/infra/charts/feast-feature-server/templates/deployment.yaml @@ -42,20 +42,28 @@ spec: command: {{- if eq .Values.feast_mode "offline" }} - "feast" + - "--log-level" + - "{{ .Values.logLevel }}" - "serve_offline" - "-h" - "0.0.0.0" {{- else if eq .Values.feast_mode "ui" }} - "feast" + - "--log-level" + - "{{ .Values.logLevel }}" - "ui" - "-h" - "0.0.0.0" {{- else if eq .Values.feast_mode "registry" }} - "feast" + - "--log-level" + - "{{ .Values.logLevel }}" - "serve_registry" {{- else }} {{- if .Values.metrics.enlabled }} - "feast" + - "--log-level" + - "{{ .Values.logLevel }}" - "serve" - "--metrics" - "-h" diff --git a/infra/charts/feast-feature-server/values.yaml b/infra/charts/feast-feature-server/values.yaml index 22bbdeace0..f0bc55a646 100644 --- a/infra/charts/feast-feature-server/values.yaml +++ b/infra/charts/feast-feature-server/values.yaml @@ -11,6 +11,8 @@ image: # image.tag -- The Docker image tag (can be overwritten if custom feature server deps are needed for on demand transforms) tag: 0.40.0 +logLevel: "WARNING" # Set log level DEBUG, INFO, WARNING, ERROR, and CRITICAL (case-insensitive) + imagePullSecrets: [] nameOverride: "" fullnameOverride: "" diff --git a/sdk/python/feast/permissions/auth/kubernetes_token_parser.py b/sdk/python/feast/permissions/auth/kubernetes_token_parser.py index 7724163e5f..c34ebf386d 100644 --- a/sdk/python/feast/permissions/auth/kubernetes_token_parser.py +++ b/sdk/python/feast/permissions/auth/kubernetes_token_parser.py @@ -40,14 +40,16 @@ async def user_details_from_access_token(self, access_token: str) -> User: """ sa_namespace, sa_name = _decode_token(access_token) current_user = f"{sa_namespace}:{sa_name}" - logging.info(f"Received request from {sa_name} in {sa_namespace}") + logger.info( + f"Request received from ServiceAccount: {sa_name} in namespace: {sa_namespace}" + ) intra_communication_base64 = os.getenv("INTRA_COMMUNICATION_BASE64") if sa_name is not None and sa_name == intra_communication_base64: return User(username=sa_name, roles=[]) else: roles = self.get_roles(sa_namespace, sa_name) - logging.info(f"SA roles are: {roles}") + logger.info(f"Roles for ServiceAccount {sa_name}: {roles}") return User(username=current_user, roles=roles) diff --git a/sdk/python/feast/permissions/auth/oidc_token_parser.py b/sdk/python/feast/permissions/auth/oidc_token_parser.py index 28273e8c10..ffff7e7ad3 100644 --- a/sdk/python/feast/permissions/auth/oidc_token_parser.py +++ b/sdk/python/feast/permissions/auth/oidc_token_parser.py @@ -17,7 +17,6 @@ from feast.permissions.user import User logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) class OidcTokenParser(TokenParser): @@ -69,8 +68,9 @@ async def user_details_from_access_token(self, access_token: str) -> User: try: await self._validate_token(access_token) - logger.info("Validated token") + logger.debug("Token successfully validated.") except Exception as e: + logger.error(f"Token validation failed: {e}") raise AuthenticationError(f"Invalid token: {e}") optional_custom_headers = {"User-agent": "custom-user-agent"} diff --git a/sdk/python/feast/permissions/client/intra_comm_authentication_client_manager.py b/sdk/python/feast/permissions/client/intra_comm_authentication_client_manager.py index 678e1f39e5..30476316c1 100644 --- a/sdk/python/feast/permissions/client/intra_comm_authentication_client_manager.py +++ b/sdk/python/feast/permissions/client/intra_comm_authentication_client_manager.py @@ -13,6 +13,7 @@ class IntraCommAuthClientManager(AuthenticationClientManager): def __init__(self, auth_config: AuthConfig, intra_communication_base64: str): self.auth_config = auth_config self.intra_communication_base64 = intra_communication_base64 + logger.debug(f"AuthConfig type set to {self.auth_config.type}") def get_token(self): if self.auth_config.type == AuthType.OIDC.value: diff --git a/sdk/python/feast/permissions/enforcer.py b/sdk/python/feast/permissions/enforcer.py index d94a81ba04..d9855fef8c 100644 --- a/sdk/python/feast/permissions/enforcer.py +++ b/sdk/python/feast/permissions/enforcer.py @@ -67,8 +67,12 @@ def enforce_policy( if evaluator.is_decided(): grant, explanations = evaluator.grant() if not grant and not filter_only: + logger.error(f"Permission denied: {','.join(explanations)}") raise FeastPermissionError(",".join(explanations)) if grant: + logger.debug( + f"Permission granted for {type(resource).__name__}:{resource.name}" + ) _permitted_resources.append(resource) break else: diff --git a/sdk/python/feast/permissions/server/arrow.py b/sdk/python/feast/permissions/server/arrow.py index 4f0afc3ee5..bf517d94ac 100644 --- a/sdk/python/feast/permissions/server/arrow.py +++ b/sdk/python/feast/permissions/server/arrow.py @@ -17,7 +17,6 @@ from feast.permissions.user import User logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) class AuthorizationMiddlewareFactory(fl.ServerMiddlewareFactory): @@ -49,7 +48,9 @@ def __init__(self, access_token: str, *args, **kwargs): def call_completed(self, exception): if exception: - print(f"{AuthorizationMiddleware.__name__} received {exception}") + logger.exception( + f"{AuthorizationMiddleware.__name__} encountered an exception: {exception}" + ) async def extract_user(self) -> User: """ @@ -69,14 +70,14 @@ def inject_user_details(context: ServerCallContext): context: The endpoint context. """ if context.get_middleware("auth") is None: - logger.info("No `auth` middleware.") + logger.warning("No `auth` middleware.") return sm = get_security_manager() if sm is not None: auth_middleware = cast(AuthorizationMiddleware, context.get_middleware("auth")) current_user = asyncio.run(auth_middleware.extract_user()) - print(f"extracted user: {current_user}") + logger.debug(f"User extracted: {current_user}") sm.set_current_user(current_user) diff --git a/sdk/python/feast/permissions/server/grpc.py b/sdk/python/feast/permissions/server/grpc.py index 96f2690b88..9feea47a6c 100644 --- a/sdk/python/feast/permissions/server/grpc.py +++ b/sdk/python/feast/permissions/server/grpc.py @@ -9,7 +9,6 @@ from feast.permissions.security_manager import get_security_manager logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) class AuthInterceptor(grpc.ServerInterceptor): @@ -22,11 +21,13 @@ def intercept_service(self, continuation, handler_call_details): metadata=dict(handler_call_details.invocation_metadata) ) - print(f"Fetching user for token: {len(access_token)}") + logger.debug( + f"Fetching user details for token of length: {len(access_token)}" + ) current_user = asyncio.run( auth_manager.token_parser.user_details_from_access_token(access_token) ) - print(f"User is: {current_user}") + logger.debug(f"User is: {current_user}") sm.set_current_user(current_user) return continuation(handler_call_details) diff --git a/sdk/python/feast/permissions/server/utils.py b/sdk/python/feast/permissions/server/utils.py index 9a8b319dbc..cd72ae5820 100644 --- a/sdk/python/feast/permissions/server/utils.py +++ b/sdk/python/feast/permissions/server/utils.py @@ -30,7 +30,6 @@ from feast.permissions.server.rest_token_extractor import RestTokenExtractor logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) class ServerType(enum.Enum): From cecca8360bed62ab2f4fddc5d3a888247ea0a87a Mon Sep 17 00:00:00 2001 From: Bhargav Dodla <13788369+EXPEbdodla@users.noreply.github.com> Date: Thu, 19 Sep 2024 11:36:58 -0700 Subject: [PATCH 078/185] fix: Adding protobuf<5 as a required dependency due to snowflake limitations (#4537) * fix: Removed protobuf as a required dependency Signed-off-by: Bhargav Dodla * fix: Removed install-protoc-dependencies target Signed-off-by: Bhargav Dodla * fix: Ran lock python dependencies to correct dependencies Signed-off-by: Bhargav Dodla * fix: Adding protobuf<5 as a required dependency due to snowflake limitations Signed-off-by: Bhargav Dodla --------- Signed-off-by: Bhargav Dodla Co-authored-by: Bhargav Dodla --- .github/workflows/build_wheels.yml | 1 + Makefile | 19 +++-- .../requirements/py3.10-ci-requirements.txt | 72 ++++++++++--------- .../requirements/py3.10-requirements.txt | 32 ++++----- .../requirements/py3.11-ci-requirements.txt | 72 ++++++++++--------- .../requirements/py3.11-requirements.txt | 32 ++++----- .../requirements/py3.9-ci-requirements.txt | 53 +++++++------- .../requirements/py3.9-requirements.txt | 28 ++++---- setup.py | 7 +- 9 files changed, 163 insertions(+), 153 deletions(-) diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index d8e5e484bb..14d0b7d5ae 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -97,6 +97,7 @@ jobs: # There's a `git restore` in here because `make install-go-ci-dependencies` is actually messing up go.mod & go.sum. run: | pip install -U pip setuptools wheel twine + make install-protoc-dependencies make build-ui git status git restore go.mod go.sum diff --git a/Makefile b/Makefile index 78a0b6d328..8a9f643967 100644 --- a/Makefile +++ b/Makefile @@ -52,13 +52,16 @@ install-python-ci-dependencies-uv-venv: uv pip install --no-deps -e . python setup.py build_python_protos --inplace +install-protoc-dependencies: + pip install "protobuf<5" "grpcio-tools>=1.56.2,<2" "mypy-protobuf>=3.1" + lock-python-ci-dependencies: uv pip compile --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py$(PYTHON_VERSION)-ci-requirements.txt package-protos: cp -r ${ROOT_DIR}/protos ${ROOT_DIR}/sdk/python/feast/protos -compile-protos-python: +compile-protos-python: install-protoc-dependencies python setup.py build_python_protos --inplace install-python: @@ -69,12 +72,14 @@ lock-python-dependencies: uv pip compile --system --no-strip-extras setup.py --output-file sdk/python/requirements/py$(PYTHON_VERSION)-requirements.txt lock-python-dependencies-all: - pixi run --environment py39 --manifest-path infra/scripts/pixi/pixi.toml "uv pip compile --system --no-strip-extras setup.py --output-file sdk/python/requirements/py3.9-requirements.txt" - pixi run --environment py39 --manifest-path infra/scripts/pixi/pixi.toml "uv pip compile --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.9-ci-requirements.txt" - pixi run --environment py310 --manifest-path infra/scripts/pixi/pixi.toml "uv pip compile --system --no-strip-extras setup.py --output-file sdk/python/requirements/py3.10-requirements.txt" - pixi run --environment py310 --manifest-path infra/scripts/pixi/pixi.toml "uv pip compile --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.10-ci-requirements.txt" - pixi run --environment py311 --manifest-path infra/scripts/pixi/pixi.toml "uv pip compile --system --no-strip-extras setup.py --output-file sdk/python/requirements/py3.11-requirements.txt" - pixi run --environment py311 --manifest-path infra/scripts/pixi/pixi.toml "uv pip compile --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.11-ci-requirements.txt" + # Remove all existing requirements because we noticed the lock file is not always updated correctly. Removing and running the command again ensures that the lock file is always up to date. + rm -r sdk/python/requirements/* + pixi run --environment py39 --manifest-path infra/scripts/pixi/pixi.toml "uv pip compile -p 3.9 --system --no-strip-extras setup.py --output-file sdk/python/requirements/py3.9-requirements.txt" + pixi run --environment py39 --manifest-path infra/scripts/pixi/pixi.toml "uv pip compile -p 3.9 --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.9-ci-requirements.txt" + pixi run --environment py310 --manifest-path infra/scripts/pixi/pixi.toml "uv pip compile -p 3.10 --system --no-strip-extras setup.py --output-file sdk/python/requirements/py3.10-requirements.txt" + pixi run --environment py310 --manifest-path infra/scripts/pixi/pixi.toml "uv pip compile -p 3.10 --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.10-ci-requirements.txt" + pixi run --environment py311 --manifest-path infra/scripts/pixi/pixi.toml "uv pip compile -p 3.11 --system --no-strip-extras setup.py --output-file sdk/python/requirements/py3.11-requirements.txt" + pixi run --environment py311 --manifest-path infra/scripts/pixi/pixi.toml "uv pip compile -p 3.11 --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.11-ci-requirements.txt" benchmark-python: IS_TEST=True python -m pytest --integration --benchmark --benchmark-autosave --benchmark-save-data sdk/python/tests diff --git a/sdk/python/requirements/py3.10-ci-requirements.txt b/sdk/python/requirements/py3.10-ci-requirements.txt index f09f3dded4..59e799ebab 100644 --- a/sdk/python/requirements/py3.10-ci-requirements.txt +++ b/sdk/python/requirements/py3.10-ci-requirements.txt @@ -1,5 +1,5 @@ # This file was autogenerated by uv via the following command: -# uv pip compile --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.10-ci-requirements.txt +# uv pip compile -p 3.10 --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.10-ci-requirements.txt aiobotocore==2.15.0 # via feast (setup.py) aiohappyeyeballs==2.4.0 @@ -16,7 +16,7 @@ altair==4.2.2 # via great-expectations annotated-types==0.7.0 # via pydantic -anyio==4.4.0 +anyio==4.5.0 # via # httpx # jupyter-server @@ -51,13 +51,13 @@ attrs==24.2.0 # aiohttp # jsonschema # referencing -azure-core==1.30.2 +azure-core==1.31.0 # via # azure-identity # azure-storage-blob azure-identity==1.17.1 # via feast (setup.py) -azure-storage-blob==12.22.0 +azure-storage-blob==12.23.0 # via feast (setup.py) babel==2.16.0 # via @@ -144,11 +144,11 @@ cryptography==42.0.8 # types-redis cython==3.0.11 # via thriftpy2 -dask[dataframe]==2024.8.2 +dask[dataframe]==2024.9.0 # via # feast (setup.py) # dask-expr -dask-expr==1.1.13 +dask-expr==1.1.14 # via dask db-dtypes==1.3.0 # via google-cloud-bigquery @@ -158,7 +158,7 @@ decorator==5.1.1 # via ipython defusedxml==0.7.1 # via nbconvert -deltalake==0.19.2 +deltalake==0.20.0 # via feast (setup.py) deprecation==2.1.0 # via python-keycloak @@ -187,11 +187,11 @@ execnet==2.1.1 # via pytest-xdist executing==2.1.0 # via stack-data -fastapi==0.114.1 +fastapi==0.115.0 # via feast (setup.py) fastjsonschema==2.20.0 # via nbformat -filelock==3.16.0 +filelock==3.16.1 # via # snowflake-connector-python # virtualenv @@ -256,7 +256,7 @@ googleapis-common-protos[grpc]==1.65.0 # google-api-core # grpc-google-iam-v1 # grpcio-status -great-expectations==0.18.20 +great-expectations==0.18.21 # via feast (setup.py) grpc-google-iam-v1==0.13.1 # via google-cloud-bigtable @@ -302,15 +302,15 @@ httpx==0.27.2 # feast (setup.py) # jupyterlab # python-keycloak -ibis-framework[duckdb]==9.4.0 +ibis-framework[duckdb]==9.5.0 # via # feast (setup.py) # ibis-substrait ibis-substrait==4.0.1 # via feast (setup.py) -identify==2.6.0 +identify==2.6.1 # via pre-commit -idna==3.8 +idna==3.10 # via # anyio # httpx @@ -321,7 +321,9 @@ idna==3.8 imagesize==1.4.1 # via sphinx importlib-metadata==8.5.0 - # via dask + # via + # build + # dask iniconfig==2.0.0 # via pytest ipykernel==6.29.5 @@ -372,7 +374,7 @@ jsonschema[format-nongpl]==4.23.0 # nbformat jsonschema-specifications==2023.12.1 # via jsonschema -jupyter-client==8.6.2 +jupyter-client==8.6.3 # via # ipykernel # jupyter-server @@ -438,7 +440,7 @@ mistune==3.0.2 # via # great-expectations # nbconvert -mmh3==4.1.0 +mmh3==5.0.0 # via feast (setup.py) mock==2.0.0 # via feast (setup.py) @@ -506,6 +508,7 @@ packaging==24.1 # google-cloud-bigquery # great-expectations # gunicorn + # ibis-framework # ibis-substrait # ipykernel # jupyter-server @@ -570,8 +573,9 @@ proto-plus==1.24.0 # google-cloud-bigquery-storage # google-cloud-bigtable # google-cloud-datastore -protobuf==4.25.4 +protobuf==4.25.5 # via + # feast (setup.py) # google-api-core # google-cloud-bigquery-storage # google-cloud-bigtable @@ -590,11 +594,11 @@ psutil==5.9.0 # via # feast (setup.py) # ipykernel -psycopg[binary, pool]==3.2.1 +psycopg[binary, pool]==3.2.2 # via feast (setup.py) -psycopg-binary==3.2.1 +psycopg-binary==3.2.2 # via psycopg -psycopg-pool==3.2.2 +psycopg-pool==3.2.3 # via psycopg ptyprocess==0.7.0 # via @@ -629,12 +633,12 @@ pybindgen==0.22.1 # via feast (setup.py) pycparser==2.22 # via cffi -pydantic==2.9.1 +pydantic==2.9.2 # via # feast (setup.py) # fastapi # great-expectations -pydantic-core==2.23.3 +pydantic-core==2.23.4 # via pydantic pygments==2.18.0 # via @@ -739,7 +743,7 @@ referencing==0.35.1 # jsonschema # jsonschema-specifications # jupyter-events -regex==2024.7.24 +regex==2024.9.11 # via # feast (setup.py) # parsimonious @@ -790,7 +794,7 @@ ruamel-yaml==0.17.40 # via great-expectations ruamel-yaml-clib==0.2.8 # via ruamel-yaml -ruff==0.6.4 +ruff==0.6.5 # via feast (setup.py) s3transfer==0.10.2 # via boto3 @@ -798,7 +802,7 @@ scipy==1.14.1 # via great-expectations send2trash==1.8.3 # via jupyter-server -setuptools==74.1.2 +setuptools==75.1.0 # via # grpcio-tools # jupyterlab @@ -826,7 +830,7 @@ sniffio==1.3.1 # httpx snowballstemmer==2.2.0 # via sphinx -snowflake-connector-python[pandas]==3.12.1 +snowflake-connector-python[pandas]==3.12.2 # via feast (setup.py) sortedcontainers==2.4.0 # via snowflake-connector-python @@ -846,9 +850,9 @@ sphinxcontrib-qthelp==2.0.0 # via sphinx sphinxcontrib-serializinghtml==2.0.0 # via sphinx -sqlalchemy[mypy]==2.0.34 +sqlalchemy[mypy]==2.0.35 # via feast (setup.py) -sqlglot==25.18.0 +sqlglot==25.20.1 # via ibis-framework sqlite-vec==0.1.1 # via feast (setup.py) @@ -858,7 +862,7 @@ stack-data==0.6.3 # via ipython starlette==0.38.5 # via fastapi -substrait==0.22.0 +substrait==0.23.0 # via ibis-substrait tabulate==0.9.0 # via feast (setup.py) @@ -939,15 +943,15 @@ types-python-dateutil==2.9.0.20240906 # via # feast (setup.py) # arrow -types-pytz==2024.1.0.20240417 +types-pytz==2024.2.0.20240913 # via feast (setup.py) -types-pyyaml==6.0.12.20240808 +types-pyyaml==6.0.12.20240917 # via feast (setup.py) types-redis==4.6.0.20240903 # via feast (setup.py) types-requests==2.30.0.0 # via feast (setup.py) -types-setuptools==74.1.0.20240907 +types-setuptools==75.1.0.20240917 # via # feast (setup.py) # types-cffi @@ -986,7 +990,7 @@ tzlocal==5.2 # trino uri-template==1.3.0 # via jsonschema -urllib3==2.2.2 +urllib3==2.2.3 # via # feast (setup.py) # botocore @@ -1038,5 +1042,5 @@ xmltodict==0.13.0 # via moto yarl==1.11.1 # via aiohttp -zipp==3.20.1 +zipp==3.20.2 # via importlib-metadata diff --git a/sdk/python/requirements/py3.10-requirements.txt b/sdk/python/requirements/py3.10-requirements.txt index 1c6f53cf69..26eeca3529 100644 --- a/sdk/python/requirements/py3.10-requirements.txt +++ b/sdk/python/requirements/py3.10-requirements.txt @@ -1,8 +1,8 @@ # This file was autogenerated by uv via the following command: -# uv pip compile --system --no-strip-extras setup.py --output-file sdk/python/requirements/py3.10-requirements.txt +# uv pip compile -p 3.10 --system --no-strip-extras setup.py --output-file sdk/python/requirements/py3.10-requirements.txt annotated-types==0.7.0 # via pydantic -anyio==4.4.0 +anyio==4.5.0 # via # starlette # watchfiles @@ -25,17 +25,17 @@ cloudpickle==3.0.0 # via dask colorama==0.4.6 # via feast (setup.py) -dask[dataframe]==2024.8.2 +dask[dataframe]==2024.9.0 # via # feast (setup.py) # dask-expr -dask-expr==1.1.13 +dask-expr==1.1.14 # via dask dill==0.3.8 # via feast (setup.py) exceptiongroup==1.2.2 # via anyio -fastapi==0.114.1 +fastapi==0.115.0 # via feast (setup.py) fsspec==2024.9.0 # via dask @@ -45,7 +45,7 @@ h11==0.14.0 # via uvicorn httptools==0.6.1 # via uvicorn -idna==3.8 +idna==3.10 # via # anyio # requests @@ -61,14 +61,12 @@ locket==1.0.0 # via partd markupsafe==2.1.5 # via jinja2 -mmh3==4.1.0 +mmh3==5.0.0 # via feast (setup.py) mypy==1.11.2 # via sqlalchemy mypy-extensions==1.0.0 # via mypy -mypy-protobuf==3.6.0 - # via feast (setup.py) numpy==1.26.4 # via # feast (setup.py) @@ -88,19 +86,19 @@ partd==1.4.2 # via dask prometheus-client==0.20.0 # via feast (setup.py) -protobuf==4.25.4 - # via mypy-protobuf +protobuf==4.25.5 + # via feast (setup.py) psutil==6.0.0 # via feast (setup.py) pyarrow==17.0.0 # via # feast (setup.py) # dask-expr -pydantic==2.9.1 +pydantic==2.9.2 # via # feast (setup.py) # fastapi -pydantic-core==2.23.3 +pydantic-core==2.23.4 # via pydantic pygments==2.18.0 # via feast (setup.py) @@ -131,7 +129,7 @@ six==1.16.0 # via python-dateutil sniffio==1.3.1 # via anyio -sqlalchemy[mypy]==2.0.34 +sqlalchemy[mypy]==2.0.35 # via feast (setup.py) starlette==0.38.5 # via fastapi @@ -151,8 +149,6 @@ tqdm==4.66.5 # via feast (setup.py) typeguard==4.3.0 # via feast (setup.py) -types-protobuf==5.27.0.20240907 - # via mypy-protobuf typing-extensions==4.12.2 # via # anyio @@ -165,7 +161,7 @@ typing-extensions==4.12.2 # uvicorn tzdata==2024.1 # via pandas -urllib3==2.2.2 +urllib3==2.2.3 # via requests uvicorn[standard]==0.30.6 # via feast (setup.py) @@ -175,5 +171,5 @@ watchfiles==0.24.0 # via uvicorn websockets==13.0.1 # via uvicorn -zipp==3.20.1 +zipp==3.20.2 # via importlib-metadata diff --git a/sdk/python/requirements/py3.11-ci-requirements.txt b/sdk/python/requirements/py3.11-ci-requirements.txt index bed8145e2f..9f57ecd841 100644 --- a/sdk/python/requirements/py3.11-ci-requirements.txt +++ b/sdk/python/requirements/py3.11-ci-requirements.txt @@ -1,5 +1,5 @@ # This file was autogenerated by uv via the following command: -# uv pip compile --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.11-ci-requirements.txt +# uv pip compile -p 3.11 --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.11-ci-requirements.txt aiobotocore==2.15.0 # via feast (setup.py) aiohappyeyeballs==2.4.0 @@ -16,7 +16,7 @@ altair==4.2.2 # via great-expectations annotated-types==0.7.0 # via pydantic -anyio==4.4.0 +anyio==4.5.0 # via # httpx # jupyter-server @@ -40,6 +40,8 @@ async-lru==2.0.4 # via jupyterlab async-property==0.2.2 # via python-keycloak +async-timeout==4.0.3 + # via redis atpublic==5.0 # via ibis-framework attrs==24.2.0 @@ -47,13 +49,13 @@ attrs==24.2.0 # aiohttp # jsonschema # referencing -azure-core==1.30.2 +azure-core==1.31.0 # via # azure-identity # azure-storage-blob azure-identity==1.17.1 # via feast (setup.py) -azure-storage-blob==12.22.0 +azure-storage-blob==12.23.0 # via feast (setup.py) babel==2.16.0 # via @@ -140,11 +142,11 @@ cryptography==42.0.8 # types-redis cython==3.0.11 # via thriftpy2 -dask[dataframe]==2024.8.2 +dask[dataframe]==2024.9.0 # via # feast (setup.py) # dask-expr -dask-expr==1.1.13 +dask-expr==1.1.14 # via dask db-dtypes==1.3.0 # via google-cloud-bigquery @@ -154,7 +156,7 @@ decorator==5.1.1 # via ipython defusedxml==0.7.1 # via nbconvert -deltalake==0.19.2 +deltalake==0.20.0 # via feast (setup.py) deprecation==2.1.0 # via python-keycloak @@ -178,11 +180,11 @@ execnet==2.1.1 # via pytest-xdist executing==2.1.0 # via stack-data -fastapi==0.114.1 +fastapi==0.115.0 # via feast (setup.py) fastjsonschema==2.20.0 # via nbformat -filelock==3.16.0 +filelock==3.16.1 # via # snowflake-connector-python # virtualenv @@ -247,7 +249,7 @@ googleapis-common-protos[grpc]==1.65.0 # google-api-core # grpc-google-iam-v1 # grpcio-status -great-expectations==0.18.20 +great-expectations==0.18.21 # via feast (setup.py) grpc-google-iam-v1==0.13.1 # via google-cloud-bigtable @@ -293,15 +295,15 @@ httpx==0.27.2 # feast (setup.py) # jupyterlab # python-keycloak -ibis-framework[duckdb]==9.4.0 +ibis-framework[duckdb]==9.5.0 # via # feast (setup.py) # ibis-substrait ibis-substrait==4.0.1 # via feast (setup.py) -identify==2.6.0 +identify==2.6.1 # via pre-commit -idna==3.8 +idna==3.10 # via # anyio # httpx @@ -363,7 +365,7 @@ jsonschema[format-nongpl]==4.23.0 # nbformat jsonschema-specifications==2023.12.1 # via jsonschema -jupyter-client==8.6.2 +jupyter-client==8.6.3 # via # ipykernel # jupyter-server @@ -429,7 +431,7 @@ mistune==3.0.2 # via # great-expectations # nbconvert -mmh3==4.1.0 +mmh3==5.0.0 # via feast (setup.py) mock==2.0.0 # via feast (setup.py) @@ -497,6 +499,7 @@ packaging==24.1 # google-cloud-bigquery # great-expectations # gunicorn + # ibis-framework # ibis-substrait # ipykernel # jupyter-server @@ -561,8 +564,9 @@ proto-plus==1.24.0 # google-cloud-bigquery-storage # google-cloud-bigtable # google-cloud-datastore -protobuf==4.25.4 +protobuf==4.25.5 # via + # feast (setup.py) # google-api-core # google-cloud-bigquery-storage # google-cloud-bigtable @@ -581,11 +585,11 @@ psutil==5.9.0 # via # feast (setup.py) # ipykernel -psycopg[binary, pool]==3.2.1 +psycopg[binary, pool]==3.2.2 # via feast (setup.py) -psycopg-binary==3.2.1 +psycopg-binary==3.2.2 # via psycopg -psycopg-pool==3.2.2 +psycopg-pool==3.2.3 # via psycopg ptyprocess==0.7.0 # via @@ -620,12 +624,12 @@ pybindgen==0.22.1 # via feast (setup.py) pycparser==2.22 # via cffi -pydantic==2.9.1 +pydantic==2.9.2 # via # feast (setup.py) # fastapi # great-expectations -pydantic-core==2.23.3 +pydantic-core==2.23.4 # via pydantic pygments==2.18.0 # via @@ -730,7 +734,7 @@ referencing==0.35.1 # jsonschema # jsonschema-specifications # jupyter-events -regex==2024.7.24 +regex==2024.9.11 # via # feast (setup.py) # parsimonious @@ -781,7 +785,7 @@ ruamel-yaml==0.17.40 # via great-expectations ruamel-yaml-clib==0.2.8 # via ruamel-yaml -ruff==0.6.4 +ruff==0.6.5 # via feast (setup.py) s3transfer==0.10.2 # via boto3 @@ -789,7 +793,7 @@ scipy==1.14.1 # via great-expectations send2trash==1.8.3 # via jupyter-server -setuptools==74.1.2 +setuptools==75.1.0 # via # grpcio-tools # jupyterlab @@ -817,7 +821,7 @@ sniffio==1.3.1 # httpx snowballstemmer==2.2.0 # via sphinx -snowflake-connector-python[pandas]==3.12.1 +snowflake-connector-python[pandas]==3.12.2 # via feast (setup.py) sortedcontainers==2.4.0 # via snowflake-connector-python @@ -837,9 +841,9 @@ sphinxcontrib-qthelp==2.0.0 # via sphinx sphinxcontrib-serializinghtml==2.0.0 # via sphinx -sqlalchemy[mypy]==2.0.34 +sqlalchemy[mypy]==2.0.35 # via feast (setup.py) -sqlglot==25.18.0 +sqlglot==25.20.1 # via ibis-framework sqlite-vec==0.1.1 # via feast (setup.py) @@ -849,7 +853,7 @@ stack-data==0.6.3 # via ipython starlette==0.38.5 # via fastapi -substrait==0.22.0 +substrait==0.23.0 # via ibis-substrait tabulate==0.9.0 # via feast (setup.py) @@ -867,6 +871,8 @@ tinycss2==1.3.0 # via nbconvert toml==0.10.2 # via feast (setup.py) +tomli==2.0.1 + # via coverage tomlkit==0.13.2 # via snowflake-connector-python toolz==0.12.1 @@ -920,15 +926,15 @@ types-python-dateutil==2.9.0.20240906 # via # feast (setup.py) # arrow -types-pytz==2024.1.0.20240417 +types-pytz==2024.2.0.20240913 # via feast (setup.py) -types-pyyaml==6.0.12.20240808 +types-pyyaml==6.0.12.20240917 # via feast (setup.py) types-redis==4.6.0.20240903 # via feast (setup.py) types-requests==2.30.0.0 # via feast (setup.py) -types-setuptools==74.1.0.20240907 +types-setuptools==75.1.0.20240917 # via # feast (setup.py) # types-cffi @@ -963,7 +969,7 @@ tzlocal==5.2 # trino uri-template==1.3.0 # via jsonschema -urllib3==2.2.2 +urllib3==2.2.3 # via # feast (setup.py) # botocore @@ -1015,5 +1021,5 @@ xmltodict==0.13.0 # via moto yarl==1.11.1 # via aiohttp -zipp==3.20.1 +zipp==3.20.2 # via importlib-metadata diff --git a/sdk/python/requirements/py3.11-requirements.txt b/sdk/python/requirements/py3.11-requirements.txt index 99a994e946..5c20e45f07 100644 --- a/sdk/python/requirements/py3.11-requirements.txt +++ b/sdk/python/requirements/py3.11-requirements.txt @@ -1,8 +1,8 @@ # This file was autogenerated by uv via the following command: -# uv pip compile --system --no-strip-extras setup.py --output-file sdk/python/requirements/py3.11-requirements.txt +# uv pip compile -p 3.11 --system --no-strip-extras setup.py --output-file sdk/python/requirements/py3.11-requirements.txt annotated-types==0.7.0 # via pydantic -anyio==4.4.0 +anyio==4.5.0 # via # starlette # watchfiles @@ -25,15 +25,15 @@ cloudpickle==3.0.0 # via dask colorama==0.4.6 # via feast (setup.py) -dask[dataframe]==2024.8.2 +dask[dataframe]==2024.9.0 # via # feast (setup.py) # dask-expr -dask-expr==1.1.13 +dask-expr==1.1.14 # via dask dill==0.3.8 # via feast (setup.py) -fastapi==0.114.1 +fastapi==0.115.0 # via feast (setup.py) fsspec==2024.9.0 # via dask @@ -43,7 +43,7 @@ h11==0.14.0 # via uvicorn httptools==0.6.1 # via uvicorn -idna==3.8 +idna==3.10 # via # anyio # requests @@ -59,14 +59,12 @@ locket==1.0.0 # via partd markupsafe==2.1.5 # via jinja2 -mmh3==4.1.0 +mmh3==5.0.0 # via feast (setup.py) mypy==1.11.2 # via sqlalchemy mypy-extensions==1.0.0 # via mypy -mypy-protobuf==3.6.0 - # via feast (setup.py) numpy==1.26.4 # via # feast (setup.py) @@ -86,19 +84,19 @@ partd==1.4.2 # via dask prometheus-client==0.20.0 # via feast (setup.py) -protobuf==4.25.4 - # via mypy-protobuf +protobuf==4.25.5 + # via feast (setup.py) psutil==6.0.0 # via feast (setup.py) pyarrow==17.0.0 # via # feast (setup.py) # dask-expr -pydantic==2.9.1 +pydantic==2.9.2 # via # feast (setup.py) # fastapi -pydantic-core==2.23.3 +pydantic-core==2.23.4 # via pydantic pygments==2.18.0 # via feast (setup.py) @@ -129,7 +127,7 @@ six==1.16.0 # via python-dateutil sniffio==1.3.1 # via anyio -sqlalchemy[mypy]==2.0.34 +sqlalchemy[mypy]==2.0.35 # via feast (setup.py) starlette==0.38.5 # via fastapi @@ -147,8 +145,6 @@ tqdm==4.66.5 # via feast (setup.py) typeguard==4.3.0 # via feast (setup.py) -types-protobuf==5.27.0.20240907 - # via mypy-protobuf typing-extensions==4.12.2 # via # fastapi @@ -159,7 +155,7 @@ typing-extensions==4.12.2 # typeguard tzdata==2024.1 # via pandas -urllib3==2.2.2 +urllib3==2.2.3 # via requests uvicorn[standard]==0.30.6 # via feast (setup.py) @@ -169,5 +165,5 @@ watchfiles==0.24.0 # via uvicorn websockets==13.0.1 # via uvicorn -zipp==3.20.1 +zipp==3.20.2 # via importlib-metadata diff --git a/sdk/python/requirements/py3.9-ci-requirements.txt b/sdk/python/requirements/py3.9-ci-requirements.txt index 1015f46cff..bbdca890b6 100644 --- a/sdk/python/requirements/py3.9-ci-requirements.txt +++ b/sdk/python/requirements/py3.9-ci-requirements.txt @@ -1,5 +1,5 @@ # This file was autogenerated by uv via the following command: -# uv pip compile --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.9-ci-requirements.txt +# uv pip compile -p 3.9 --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.9-ci-requirements.txt aiobotocore==2.15.0 # via feast (setup.py) aiohappyeyeballs==2.4.0 @@ -16,7 +16,7 @@ altair==4.2.2 # via great-expectations annotated-types==0.7.0 # via pydantic -anyio==4.4.0 +anyio==4.5.0 # via # httpx # jupyter-server @@ -51,13 +51,13 @@ attrs==24.2.0 # aiohttp # jsonschema # referencing -azure-core==1.30.2 +azure-core==1.31.0 # via # azure-identity # azure-storage-blob azure-identity==1.17.1 # via feast (setup.py) -azure-storage-blob==12.22.0 +azure-storage-blob==12.23.0 # via feast (setup.py) babel==2.16.0 # via @@ -160,7 +160,7 @@ decorator==5.1.1 # via ipython defusedxml==0.7.1 # via nbconvert -deltalake==0.19.2 +deltalake==0.20.0 # via feast (setup.py) deprecation==2.1.0 # via python-keycloak @@ -189,11 +189,11 @@ execnet==2.1.1 # via pytest-xdist executing==2.1.0 # via stack-data -fastapi==0.114.1 +fastapi==0.115.0 # via feast (setup.py) fastjsonschema==2.20.0 # via nbformat -filelock==3.16.0 +filelock==3.16.1 # via # snowflake-connector-python # virtualenv @@ -258,7 +258,7 @@ googleapis-common-protos[grpc]==1.65.0 # google-api-core # grpc-google-iam-v1 # grpcio-status -great-expectations==0.18.20 +great-expectations==0.18.21 # via feast (setup.py) grpc-google-iam-v1==0.13.1 # via google-cloud-bigtable @@ -310,9 +310,9 @@ ibis-framework[duckdb]==9.0.0 # ibis-substrait ibis-substrait==4.0.1 # via feast (setup.py) -identify==2.6.0 +identify==2.6.1 # via pre-commit -idna==3.8 +idna==3.10 # via # anyio # httpx @@ -383,7 +383,7 @@ jsonschema[format-nongpl]==4.23.0 # nbformat jsonschema-specifications==2023.12.1 # via jsonschema -jupyter-client==8.6.2 +jupyter-client==8.6.3 # via # ipykernel # jupyter-server @@ -449,7 +449,7 @@ mistune==3.0.2 # via # great-expectations # nbconvert -mmh3==4.1.0 +mmh3==5.0.0 # via feast (setup.py) mock==2.0.0 # via feast (setup.py) @@ -581,8 +581,9 @@ proto-plus==1.24.0 # google-cloud-bigquery-storage # google-cloud-bigtable # google-cloud-datastore -protobuf==4.25.4 +protobuf==4.25.5 # via + # feast (setup.py) # google-api-core # google-cloud-bigquery-storage # google-cloud-bigtable @@ -605,7 +606,7 @@ psycopg[binary, pool]==3.1.18 # via feast (setup.py) psycopg-binary==3.1.18 # via psycopg -psycopg-pool==3.2.2 +psycopg-pool==3.2.3 # via psycopg ptyprocess==0.7.0 # via @@ -640,12 +641,12 @@ pybindgen==0.22.1 # via feast (setup.py) pycparser==2.22 # via cffi -pydantic==2.9.1 +pydantic==2.9.2 # via # feast (setup.py) # fastapi # great-expectations -pydantic-core==2.23.3 +pydantic-core==2.23.4 # via pydantic pygments==2.18.0 # via @@ -750,7 +751,7 @@ referencing==0.35.1 # jsonschema # jsonschema-specifications # jupyter-events -regex==2024.7.24 +regex==2024.9.11 # via # feast (setup.py) # parsimonious @@ -801,7 +802,7 @@ ruamel-yaml==0.17.40 # via great-expectations ruamel-yaml-clib==0.2.8 # via ruamel-yaml -ruff==0.6.4 +ruff==0.6.5 # via feast (setup.py) s3transfer==0.10.2 # via boto3 @@ -809,7 +810,7 @@ scipy==1.13.1 # via great-expectations send2trash==1.8.3 # via jupyter-server -setuptools==74.1.2 +setuptools==75.1.0 # via # grpcio-tools # jupyterlab @@ -837,7 +838,7 @@ sniffio==1.3.1 # httpx snowballstemmer==2.2.0 # via sphinx -snowflake-connector-python[pandas]==3.12.1 +snowflake-connector-python[pandas]==3.12.2 # via feast (setup.py) sortedcontainers==2.4.0 # via snowflake-connector-python @@ -857,7 +858,7 @@ sphinxcontrib-qthelp==2.0.0 # via sphinx sphinxcontrib-serializinghtml==2.0.0 # via sphinx -sqlalchemy[mypy]==2.0.34 +sqlalchemy[mypy]==2.0.35 # via feast (setup.py) sqlglot==23.12.2 # via ibis-framework @@ -869,7 +870,7 @@ stack-data==0.6.3 # via ipython starlette==0.38.5 # via fastapi -substrait==0.22.0 +substrait==0.23.0 # via ibis-substrait tabulate==0.9.0 # via feast (setup.py) @@ -950,15 +951,15 @@ types-python-dateutil==2.9.0.20240906 # via # feast (setup.py) # arrow -types-pytz==2024.1.0.20240417 +types-pytz==2024.2.0.20240913 # via feast (setup.py) -types-pyyaml==6.0.12.20240808 +types-pyyaml==6.0.12.20240917 # via feast (setup.py) types-redis==4.6.0.20240903 # via feast (setup.py) types-requests==2.30.0.0 # via feast (setup.py) -types-setuptools==74.1.0.20240907 +types-setuptools==75.1.0.20240917 # via # feast (setup.py) # types-cffi @@ -1052,5 +1053,5 @@ xmltodict==0.13.0 # via moto yarl==1.11.1 # via aiohttp -zipp==3.20.1 +zipp==3.20.2 # via importlib-metadata diff --git a/sdk/python/requirements/py3.9-requirements.txt b/sdk/python/requirements/py3.9-requirements.txt index 4bd6a44857..7ffef84b23 100644 --- a/sdk/python/requirements/py3.9-requirements.txt +++ b/sdk/python/requirements/py3.9-requirements.txt @@ -1,8 +1,8 @@ # This file was autogenerated by uv via the following command: -# uv pip compile --system --no-strip-extras setup.py --output-file sdk/python/requirements/py3.9-requirements.txt +# uv pip compile -p 3.9 --system --no-strip-extras setup.py --output-file sdk/python/requirements/py3.9-requirements.txt annotated-types==0.7.0 # via pydantic -anyio==4.4.0 +anyio==4.5.0 # via # starlette # watchfiles @@ -35,7 +35,7 @@ dill==0.3.8 # via feast (setup.py) exceptiongroup==1.2.2 # via anyio -fastapi==0.114.1 +fastapi==0.115.0 # via feast (setup.py) fsspec==2024.9.0 # via dask @@ -45,7 +45,7 @@ h11==0.14.0 # via uvicorn httptools==0.6.1 # via uvicorn -idna==3.8 +idna==3.10 # via # anyio # requests @@ -63,14 +63,12 @@ locket==1.0.0 # via partd markupsafe==2.1.5 # via jinja2 -mmh3==4.1.0 +mmh3==5.0.0 # via feast (setup.py) mypy==1.11.2 # via sqlalchemy mypy-extensions==1.0.0 # via mypy -mypy-protobuf==3.6.0 - # via feast (setup.py) numpy==1.26.4 # via # feast (setup.py) @@ -90,19 +88,19 @@ partd==1.4.2 # via dask prometheus-client==0.20.0 # via feast (setup.py) -protobuf==4.25.4 - # via mypy-protobuf +protobuf==4.25.5 + # via feast (setup.py) psutil==6.0.0 # via feast (setup.py) pyarrow==17.0.0 # via # feast (setup.py) # dask-expr -pydantic==2.9.1 +pydantic==2.9.2 # via # feast (setup.py) # fastapi -pydantic-core==2.23.3 +pydantic-core==2.23.4 # via pydantic pygments==2.18.0 # via feast (setup.py) @@ -133,7 +131,7 @@ six==1.16.0 # via python-dateutil sniffio==1.3.1 # via anyio -sqlalchemy[mypy]==2.0.34 +sqlalchemy[mypy]==2.0.35 # via feast (setup.py) starlette==0.38.5 # via fastapi @@ -153,8 +151,6 @@ tqdm==4.66.5 # via feast (setup.py) typeguard==4.3.0 # via feast (setup.py) -types-protobuf==5.27.0.20240907 - # via mypy-protobuf typing-extensions==4.12.2 # via # anyio @@ -168,7 +164,7 @@ typing-extensions==4.12.2 # uvicorn tzdata==2024.1 # via pandas -urllib3==2.2.2 +urllib3==2.2.3 # via requests uvicorn[standard]==0.30.6 # via feast (setup.py) @@ -178,5 +174,5 @@ watchfiles==0.24.0 # via uvicorn websockets==13.0.1 # via uvicorn -zipp==3.20.1 +zipp==3.20.2 # via importlib-metadata diff --git a/setup.py b/setup.py index f332c21f71..5a6f18db35 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ "click>=7.0.0,<9.0.0", "colorama>=0.3.9,<1", "dill~=0.3.0", - "mypy-protobuf>=3.1", + "protobuf<5", "Jinja2>=2,<4", "jsonschema", "mmh3", @@ -157,6 +157,8 @@ "virtualenv==20.23.0", "cryptography>=35.0,<43", "ruff>=0.3.3", + "protobuf<5", + "mypy-protobuf>=3.1", "grpcio-tools>=1.56.2,<2", "grpcio-testing>=1.56.2,<2", # FastAPI does not correctly pull starlette dependency on httpx see thread(https://github.com/tiangolo/fastapi/issues/5656). @@ -400,6 +402,9 @@ def run(self): entry_points={"console_scripts": ["feast=feast.cli:cli"]}, use_scm_version=use_scm_version, setup_requires=[ + # snowflake udf packages refer to conda packages, not pypi libraries. Conda stack is still on protobuf 4 + # So we are adding protobuf<5 as a requirement + "protobuf<5", "grpcio-tools>=1.56.2,<2", "mypy-protobuf>=3.1", "pybindgen==0.22.0", From 9688790a5e7a70f628a46021bde0201922c7e04d Mon Sep 17 00:00:00 2001 From: Bhargav Dodla <13788369+EXPEbdodla@users.noreply.github.com> Date: Thu, 19 Sep 2024 12:21:15 -0700 Subject: [PATCH 079/185] perf: Added indexes to sql tables to optimize query execution (#4538) minor: Added indexes to sql tables to optimize query execution Signed-off-by: Bhargav Dodla Co-authored-by: Bhargav Dodla --- sdk/python/feast/infra/registry/sql.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/sdk/python/feast/infra/registry/sql.py b/sdk/python/feast/infra/registry/sql.py index b049adc898..d6a716e082 100644 --- a/sdk/python/feast/infra/registry/sql.py +++ b/sdk/python/feast/infra/registry/sql.py @@ -10,6 +10,7 @@ from sqlalchemy import ( # type: ignore BigInteger, Column, + Index, LargeBinary, MetaData, String, @@ -82,6 +83,8 @@ Column("project_proto", LargeBinary, nullable=False), ) +Index("idx_projects_project_id", projects.c.project_id) + entities = Table( "entities", metadata, @@ -91,6 +94,8 @@ Column("entity_proto", LargeBinary, nullable=False), ) +Index("idx_entities_project_id", entities.c.project_id) + data_sources = Table( "data_sources", metadata, @@ -100,6 +105,8 @@ Column("data_source_proto", LargeBinary, nullable=False), ) +Index("idx_data_sources_project_id", data_sources.c.project_id) + feature_views = Table( "feature_views", metadata, @@ -111,6 +118,8 @@ Column("user_metadata", LargeBinary, nullable=True), ) +Index("idx_feature_views_project_id", feature_views.c.project_id) + stream_feature_views = Table( "stream_feature_views", metadata, @@ -121,6 +130,8 @@ Column("user_metadata", LargeBinary, nullable=True), ) +Index("idx_stream_feature_views_project_id", stream_feature_views.c.project_id) + on_demand_feature_views = Table( "on_demand_feature_views", metadata, @@ -131,6 +142,8 @@ Column("user_metadata", LargeBinary, nullable=True), ) +Index("idx_on_demand_feature_views_project_id", on_demand_feature_views.c.project_id) + feature_services = Table( "feature_services", metadata, @@ -140,6 +153,8 @@ Column("feature_service_proto", LargeBinary, nullable=False), ) +Index("idx_feature_services_project_id", feature_services.c.project_id) + saved_datasets = Table( "saved_datasets", metadata, @@ -149,6 +164,8 @@ Column("saved_dataset_proto", LargeBinary, nullable=False), ) +Index("idx_saved_datasets_project_id", saved_datasets.c.project_id) + validation_references = Table( "validation_references", metadata, @@ -157,6 +174,7 @@ Column("last_updated_timestamp", BigInteger, nullable=False), Column("validation_reference_proto", LargeBinary, nullable=False), ) +Index("idx_validation_references_project_id", validation_references.c.project_id) managed_infra = Table( "managed_infra", @@ -167,6 +185,8 @@ Column("infra_proto", LargeBinary, nullable=False), ) +Index("idx_managed_infra_project_id", managed_infra.c.project_id) + permissions = Table( "permissions", metadata, @@ -176,6 +196,8 @@ Column("permission_proto", LargeBinary, nullable=False), ) +Index("idx_permissions_project_id", permissions.c.project_id) + class FeastMetadataKeys(Enum): LAST_UPDATED_TIMESTAMP = "last_updated_timestamp" @@ -191,6 +213,8 @@ class FeastMetadataKeys(Enum): Column("last_updated_timestamp", BigInteger, nullable=False), ) +Index("idx_feast_metadata_project_id", feast_metadata.c.project_id) + logger = logging.getLogger(__name__) From 1b9280302b525084d7e1d4f99b2a8fcd68361a99 Mon Sep 17 00:00:00 2001 From: Theodor Mihalache <84387487+tmihalac@users.noreply.github.com> Date: Thu, 19 Sep 2024 15:24:56 -0400 Subject: [PATCH 080/185] docs: Add docs example of how to use tags with feature views (#4536) * Add docs example of how to use tags with feature views Signed-off-by: Theodor Mihalache * Add docs example of how to use tags with feature views - changes following review Signed-off-by: Theodor Mihalache --------- Signed-off-by: Theodor Mihalache --- docs/getting-started/concepts/README.md | 4 ++ docs/getting-started/concepts/tags.md | 59 +++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 docs/getting-started/concepts/tags.md diff --git a/docs/getting-started/concepts/README.md b/docs/getting-started/concepts/README.md index 9b967fb5af..a32c53b5f4 100644 --- a/docs/getting-started/concepts/README.md +++ b/docs/getting-started/concepts/README.md @@ -31,3 +31,7 @@ {% content-ref url="permission.md" %} [permission.md](permission.md) {% endcontent-ref %} + +{% content-ref url="tags.md" %} +[tags.md](tags.md) +{% endcontent-ref %} diff --git a/docs/getting-started/concepts/tags.md b/docs/getting-started/concepts/tags.md new file mode 100644 index 0000000000..d5b285f7c7 --- /dev/null +++ b/docs/getting-started/concepts/tags.md @@ -0,0 +1,59 @@ +# Tags + +## Overview + +Tags in Feast allow for efficient filtering of Feast objects when listing them in the UI, CLI, or querying the registry directly. + +The way to define tags on the feast objects is through the definition file or directly in the object that will be applied to the feature store. + +## Examples + +In this example we define a Feature View in a definition file that has a tag: +```python +driver_stats_fv = FeatureView( + name="driver_hourly_stats", + entities=[driver], + ttl=timedelta(days=1), + schema=[ + Field(name="conv_rate", dtype=Float32), + Field(name="acc_rate", dtype=Float32), + Field(name="avg_daily_trips", dtype=Int64, description="Average daily trips"), + ], + online=True, + source=driver_stats_source, + # Tags are user defined key/value pairs that are attached to each + # feature view + tags={"team": "driver_performance"}, +) +``` + +In this example we define a Stream Feature View that has a tag, in the code: +```python + sfv = StreamFeatureView( + name="test kafka stream feature view", + entities=[entity], + schema=[], + description="desc", + timestamp_field="event_timestamp", + source=stream_source, + tags={"team": "driver_performance"}, +``` + +An example of filtering feature-views with the tag `team:driver_performance`: +```commandline +$ feast feature-views list --tags team:driver_performance +NAME ENTITIES TYPE +driver_hourly_stats {'driver'} FeatureView +driver_hourly_stats_fresh {'driver'} FeatureView +``` + +The same example of listing feature-views without tag filtering: +```commandline +$ feast feature-views list +NAME ENTITIES TYPE +driver_hourly_stats {'driver'} FeatureView +driver_hourly_stats_fresh {'driver'} FeatureView +transformed_conv_rate_fresh {'driver'} OnDemandFeatureView +transformed_conv_rate {'driver'} OnDemandFeatureView +``` + From 4e2eacc1beea8f8866b78968abadfd42eee63d6a Mon Sep 17 00:00:00 2001 From: Bhargav Dodla <13788369+EXPEbdodla@users.noreply.github.com> Date: Thu, 19 Sep 2024 20:38:50 -0700 Subject: [PATCH 081/185] fix: Fix for SQL registry initialization fails #4543 (#4544) * fix: Fix for SQL registry initialization fails #4543 Signed-off-by: Bhargav Dodla * fix: Removed combined_sql_fixtures Signed-off-by: Bhargav Dodla * fix: Added protobuf dependency to pyproject.toml Signed-off-by: Bhargav Dodla --------- Signed-off-by: Bhargav Dodla Co-authored-by: Bhargav Dodla --- pyproject.toml | 1 + .../feast/infra/registry/caching_registry.py | 38 +++++++----- sdk/python/feast/infra/registry/sql.py | 6 +- .../registration/test_universal_registry.py | 61 +++++++++++++++++++ 4 files changed, 89 insertions(+), 17 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 283338a838..c91608b6ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,6 @@ [build-system] requires = [ + "protobuf<5", "grpcio-tools>=1.56.2,<2", "mypy-protobuf>=3.1", "pybindgen==0.22.0", diff --git a/sdk/python/feast/infra/registry/caching_registry.py b/sdk/python/feast/infra/registry/caching_registry.py index 8f47fab077..042eee06ab 100644 --- a/sdk/python/feast/infra/registry/caching_registry.py +++ b/sdk/python/feast/infra/registry/caching_registry.py @@ -19,6 +19,7 @@ from feast.permissions.permission import Permission from feast.project import Project from feast.project_metadata import ProjectMetadata +from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto from feast.saved_dataset import SavedDataset, ValidationReference from feast.stream_feature_view import StreamFeatureView from feast.utils import _utc_now @@ -28,13 +29,14 @@ class CachingRegistry(BaseRegistry): def __init__(self, project: str, cache_ttl_seconds: int, cache_mode: str): - self.cached_registry_proto = self.proto() - self.cached_registry_proto_created = _utc_now() + self.cache_mode = cache_mode + self.cached_registry_proto = RegistryProto() self._refresh_lock = Lock() self.cached_registry_proto_ttl = timedelta( seconds=cache_ttl_seconds if cache_ttl_seconds is not None else 0 ) - self.cache_mode = cache_mode + self.cached_registry_proto = self.proto() + self.cached_registry_proto_created = _utc_now() if cache_mode == "thread": self._start_thread_async_refresh(cache_ttl_seconds) atexit.register(self._exit_handler) @@ -429,20 +431,26 @@ def refresh(self, project: Optional[str] = None): def _refresh_cached_registry_if_necessary(self): if self.cache_mode == "sync": with self._refresh_lock: - expired = ( - self.cached_registry_proto is None - or self.cached_registry_proto_created is None - ) or ( - self.cached_registry_proto_ttl.total_seconds() - > 0 # 0 ttl means infinity - and ( - _utc_now() - > ( - self.cached_registry_proto_created - + self.cached_registry_proto_ttl + if self.cached_registry_proto == RegistryProto(): + # Avoids the need to refresh the registry when cache is not populated yet + # Specially during the __init__ phase + # proto() will populate the cache with project metadata if no objects are registered + expired = False + else: + expired = ( + self.cached_registry_proto is None + or self.cached_registry_proto_created is None + ) or ( + self.cached_registry_proto_ttl.total_seconds() + > 0 # 0 ttl means infinity + and ( + _utc_now() + > ( + self.cached_registry_proto_created + + self.cached_registry_proto_ttl + ) ) ) - ) if expired: logger.info("Registry cache expired, so refreshing") self.refresh() diff --git a/sdk/python/feast/infra/registry/sql.py b/sdk/python/feast/infra/registry/sql.py index d6a716e082..a6a2417c6e 100644 --- a/sdk/python/feast/infra/registry/sql.py +++ b/sdk/python/feast/infra/registry/sql.py @@ -251,6 +251,8 @@ def __init__( registry_config, SqlRegistryConfig ), "SqlRegistry needs a valid registry_config" + self.registry_config = registry_config + self.write_engine: Engine = create_engine( registry_config.path, **registry_config.sqlalchemy_config_kwargs ) @@ -281,7 +283,7 @@ def __init__( def _sync_feast_metadata_to_projects_table(self): feast_metadata_projects: set = [] projects_set: set = [] - with self.write_engine.begin() as conn: + with self.read_engine.begin() as conn: stmt = select(feast_metadata).where( feast_metadata.c.metadata_key == FeastMetadataKeys.PROJECT_UUID.value ) @@ -290,7 +292,7 @@ def _sync_feast_metadata_to_projects_table(self): feast_metadata_projects.append(row._mapping["project_id"]) if len(feast_metadata_projects) > 0: - with self.write_engine.begin() as conn: + with self.read_engine.begin() as conn: stmt = select(projects) rows = conn.execute(stmt).all() for row in rows: diff --git a/sdk/python/tests/integration/registration/test_universal_registry.py b/sdk/python/tests/integration/registration/test_universal_registry.py index 5dc2509333..0bed89ca16 100644 --- a/sdk/python/tests/integration/registration/test_universal_registry.py +++ b/sdk/python/tests/integration/registration/test_universal_registry.py @@ -1767,3 +1767,64 @@ def test_apply_entity_success_with_purge_feast_metadata(test_registry): assert len(entities) == 0 test_registry.teardown() + + +@pytest.mark.integration +@pytest.mark.parametrize( + "test_registry", + sql_fixtures + async_sql_fixtures, +) +def test_apply_entity_to_sql_registry_and_reinitialize_sql_registry(test_registry): + entity = Entity( + name="driver_car_id", + description="Car driver id", + tags={"team": "matchmaking"}, + ) + + project = "project" + + # Register Entity + test_registry.apply_entity(entity, project) + assert_project(project, test_registry) + + entities = test_registry.list_entities(project, tags=entity.tags) + assert_project(project, test_registry) + + entity = entities[0] + assert ( + len(entities) == 1 + and entity.name == "driver_car_id" + and entity.description == "Car driver id" + and "team" in entity.tags + and entity.tags["team"] == "matchmaking" + ) + + entity = test_registry.get_entity("driver_car_id", project) + assert ( + entity.name == "driver_car_id" + and entity.description == "Car driver id" + and "team" in entity.tags + and entity.tags["team"] == "matchmaking" + ) + + # After the first apply, the created_timestamp should be the same as the last_update_timestamp. + assert entity.created_timestamp == entity.last_updated_timestamp + updated_test_registry = SqlRegistry(test_registry.registry_config, "project", None) + + # Update entity + updated_entity = Entity( + name="driver_car_id", + description="Car driver Id", + tags={"team": "matchmaking"}, + ) + updated_test_registry.apply_entity(updated_entity, project) + + updated_entity = updated_test_registry.get_entity("driver_car_id", project) + updated_test_registry.delete_entity("driver_car_id", project) + assert_project(project, updated_test_registry) + entities = updated_test_registry.list_entities(project) + assert_project(project, updated_test_registry) + assert len(entities) == 0 + + updated_test_registry.teardown() + test_registry.teardown() From 5f5caf0cac539ed779692e0ec819659cf5a33a0d Mon Sep 17 00:00:00 2001 From: Hao Xu Date: Fri, 20 Sep 2024 00:18:10 -0700 Subject: [PATCH 082/185] feat: Return entity key in the retrieval document api (#4511) * update entity retrieval and add duckdb Signed-off-by: cmuhao * lint Signed-off-by: cmuhao * fix lint Signed-off-by: cmuhao * fix lint Signed-off-by: cmuhao * fix lint Signed-off-by: cmuhao * fix lint Signed-off-by: cmuhao * fix lint Signed-off-by: cmuhao * fix lint Signed-off-by: cmuhao * fix lint Signed-off-by: cmuhao * fix lint Signed-off-by: cmuhao * fix typo Signed-off-by: cmuhao * fix typo Signed-off-by: cmuhao * fix typo Signed-off-by: cmuhao * fix typo Signed-off-by: cmuhao * fix typo Signed-off-by: cmuhao * fix typo Signed-off-by: cmuhao * fix typo Signed-off-by: cmuhao * fix typo Signed-off-by: cmuhao * fix lint Signed-off-by: cmuhao * fix test Signed-off-by: cmuhao * fix test Signed-off-by: cmuhao * fix test Signed-off-by: cmuhao * fix test Signed-off-by: cmuhao * fix test Signed-off-by: cmuhao * fix test Signed-off-by: cmuhao * fix test Signed-off-by: cmuhao --------- Signed-off-by: cmuhao --- sdk/python/feast/feature_store.py | 46 ++++++++++++----- .../online_stores/contrib/elasticsearch.py | 25 +++++---- .../infra/online_stores/contrib/postgres.py | 37 ++++++-------- .../feast/infra/online_stores/online_store.py | 1 + .../feast/infra/online_stores/sqlite.py | 22 ++++---- sdk/python/feast/infra/provider.py | 1 + sdk/python/feast/utils.py | 51 ++++++++++++++++++- sdk/python/tests/conftest.py | 20 ++++++++ .../online_store/test_universal_online.py | 7 ++- 9 files changed, 149 insertions(+), 61 deletions(-) diff --git a/sdk/python/feast/feature_store.py b/sdk/python/feast/feature_store.py index 4f96cfb0fc..ab2bc6cec2 100644 --- a/sdk/python/feast/feature_store.py +++ b/sdk/python/feast/feature_store.py @@ -78,7 +78,9 @@ FieldStatus, GetOnlineFeaturesResponse, ) +from feast.protos.feast.types.EntityKey_pb2 import EntityKey from feast.protos.feast.types.Value_pb2 import RepeatedValue, Value +from feast.protos.feast.types.Value_pb2 import Value as ValueProto from feast.repo_config import RepoConfig, load_repo_config from feast.repo_contents import RepoContents from feast.saved_dataset import SavedDataset, SavedDatasetStorage, ValidationReference @@ -1666,20 +1668,29 @@ def retrieve_online_documents( distance_metric, ) - # TODO Refactor to better way of populating result - # TODO populate entity in the response after returning entity in document_features is supported # TODO currently not return the vector value since it is same as feature value, if embedding is supported, # the feature value can be raw text before embedded - document_feature_vals = [feature[2] for feature in document_features] - document_feature_distance_vals = [feature[4] for feature in document_features] + entity_key_vals = [feature[1] for feature in document_features] + join_key_values: Dict[str, List[ValueProto]] = {} + for entity_key_val in entity_key_vals: + if entity_key_val is not None: + for join_key, entity_value in zip( + entity_key_val.join_keys, entity_key_val.entity_values + ): + if join_key not in join_key_values: + join_key_values[join_key] = [] + join_key_values[join_key].append(entity_value) + + document_feature_vals = [feature[4] for feature in document_features] + document_feature_distance_vals = [feature[5] for feature in document_features] online_features_response = GetOnlineFeaturesResponse(results=[]) utils._populate_result_rows_from_columnar( online_features_response=online_features_response, - data={requested_feature: document_feature_vals}, - ) - utils._populate_result_rows_from_columnar( - online_features_response=online_features_response, - data={"distance": document_feature_distance_vals}, + data={ + **join_key_values, + requested_feature: document_feature_vals, + "distance": document_feature_distance_vals, + }, ) return OnlineResponse(online_features_response) @@ -1691,7 +1702,11 @@ def _retrieve_from_online_store( query: List[float], top_k: int, distance_metric: Optional[str], - ) -> List[Tuple[Timestamp, "FieldStatus.ValueType", Value, Value, Value]]: + ) -> List[ + Tuple[ + Timestamp, Optional[EntityKey], "FieldStatus.ValueType", Value, Value, Value + ] + ]: """ Search and return document features from the online document store. """ @@ -1707,7 +1722,7 @@ def _retrieve_from_online_store( read_row_protos = [] row_ts_proto = Timestamp() - for row_ts, feature_val, vector_value, distance_val in documents: + for row_ts, entity_key, feature_val, vector_value, distance_val in documents: # Reset timestamp to default or update if row_ts is not None if row_ts is not None: row_ts_proto.FromDatetime(row_ts) @@ -1721,7 +1736,14 @@ def _retrieve_from_online_store( status = FieldStatus.PRESENT read_row_protos.append( - (row_ts_proto, status, feature_val, vector_value, distance_val) + ( + row_ts_proto, + entity_key, + status, + feature_val, + vector_value, + distance_val, + ) ) return read_row_protos diff --git a/sdk/python/feast/infra/online_stores/contrib/elasticsearch.py b/sdk/python/feast/infra/online_stores/contrib/elasticsearch.py index c26b4199ae..a0c25b931a 100644 --- a/sdk/python/feast/infra/online_stores/contrib/elasticsearch.py +++ b/sdk/python/feast/infra/online_stores/contrib/elasticsearch.py @@ -9,12 +9,15 @@ from elasticsearch import Elasticsearch, helpers from feast import Entity, FeatureView, RepoConfig -from feast.infra.key_encoding_utils import get_list_val_str, serialize_entity_key +from feast.infra.key_encoding_utils import ( + get_list_val_str, + serialize_entity_key, +) from feast.infra.online_stores.online_store import OnlineStore from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto from feast.protos.feast.types.Value_pb2 import Value as ValueProto from feast.repo_config import FeastConfigBaseModel -from feast.utils import to_naive_utc +from feast.utils import _build_retrieve_online_document_record, to_naive_utc class ElasticSearchOnlineStoreConfig(FeastConfigBaseModel): @@ -224,6 +227,7 @@ def retrieve_online_documents( ) -> List[ Tuple[ Optional[datetime], + Optional[EntityKeyProto], Optional[ValueProto], Optional[ValueProto], Optional[ValueProto], @@ -232,6 +236,7 @@ def retrieve_online_documents( result: List[ Tuple[ Optional[datetime], + Optional[EntityKeyProto], Optional[ValueProto], Optional[ValueProto], Optional[ValueProto], @@ -247,23 +252,21 @@ def retrieve_online_documents( ) rows = response["hits"]["hits"][0:top_k] for row in rows: + entity_key = row["_source"]["entity_key"] feature_value = row["_source"]["feature_value"] vector_value = row["_source"]["vector_value"] timestamp = row["_source"]["timestamp"] distance = row["_score"] timestamp = datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S.%f") - feature_value_proto = ValueProto() - feature_value_proto.ParseFromString(base64.b64decode(feature_value)) - - vector_value_proto = ValueProto(string_val=str(vector_value)) - distance_value_proto = ValueProto(float_val=distance) result.append( - ( + _build_retrieve_online_document_record( + entity_key, + base64.b64decode(feature_value), + str(vector_value), + distance, timestamp, - feature_value_proto, - vector_value_proto, - distance_value_proto, + config.entity_key_serialization_version, ) ) return result diff --git a/sdk/python/feast/infra/online_stores/contrib/postgres.py b/sdk/python/feast/infra/online_stores/contrib/postgres.py index 8c6d3e0b99..8125da33be 100644 --- a/sdk/python/feast/infra/online_stores/contrib/postgres.py +++ b/sdk/python/feast/infra/online_stores/contrib/postgres.py @@ -37,6 +37,7 @@ from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto from feast.protos.feast.types.Value_pb2 import Value as ValueProto from feast.repo_config import RepoConfig +from feast.utils import _build_retrieve_online_document_record SUPPORTED_DISTANCE_METRICS_DICT = { "cosine": "<=>", @@ -360,6 +361,7 @@ def retrieve_online_documents( ) -> List[ Tuple[ Optional[datetime], + Optional[EntityKeyProto], Optional[ValueProto], Optional[ValueProto], Optional[ValueProto], @@ -391,12 +393,11 @@ def retrieve_online_documents( ) distance_metric_sql = SUPPORTED_DISTANCE_METRICS_DICT[distance_metric] - # Convert the embedding to a string to be used in postgres vector search - query_embedding_str = f"[{','.join(str(el) for el in embedding)}]" result: List[ Tuple[ Optional[datetime], + Optional[EntityKeyProto], Optional[ValueProto], Optional[ValueProto], Optional[ValueProto], @@ -415,45 +416,37 @@ def retrieve_online_documents( feature_name, value, vector_value, - vector_value {distance_metric_sql} %s as distance, + vector_value {distance_metric_sql} %s::vector as distance, event_ts FROM {table_name} WHERE feature_name = {feature_name} ORDER BY distance LIMIT {top_k}; """ ).format( - distance_metric_sql=distance_metric_sql, + distance_metric_sql=sql.SQL(distance_metric_sql), table_name=sql.Identifier(table_name), feature_name=sql.Literal(requested_feature), top_k=sql.Literal(top_k), ), - (query_embedding_str,), + (embedding,), ) rows = cur.fetchall() - for ( entity_key, - feature_name, - value, + _, + feature_val, vector_value, - distance, + distance_val, event_ts, ) in rows: - # TODO Deserialize entity_key to return the entity in response - # entity_key_proto = EntityKeyProto() - # entity_key_proto_bin = bytes(entity_key) - - feature_value_proto = ValueProto() - feature_value_proto.ParseFromString(bytes(value)) - - vector_value_proto = ValueProto(string_val=vector_value) - distance_value_proto = ValueProto(float_val=distance) result.append( - ( + _build_retrieve_online_document_record( + entity_key, + feature_val, + vector_value, + distance_val, event_ts, - feature_value_proto, - vector_value_proto, - distance_value_proto, + config.entity_key_serialization_version, ) ) diff --git a/sdk/python/feast/infra/online_stores/online_store.py b/sdk/python/feast/infra/online_stores/online_store.py index 9cf2ef95f6..fdb5b055cf 100644 --- a/sdk/python/feast/infra/online_stores/online_store.py +++ b/sdk/python/feast/infra/online_stores/online_store.py @@ -349,6 +349,7 @@ def retrieve_online_documents( ) -> List[ Tuple[ Optional[datetime], + Optional[EntityKeyProto], Optional[ValueProto], Optional[ValueProto], Optional[ValueProto], diff --git a/sdk/python/feast/infra/online_stores/sqlite.py b/sdk/python/feast/infra/online_stores/sqlite.py index 9896b766d4..061a766b8c 100644 --- a/sdk/python/feast/infra/online_stores/sqlite.py +++ b/sdk/python/feast/infra/online_stores/sqlite.py @@ -33,10 +33,9 @@ from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto from feast.protos.feast.core.SqliteTable_pb2 import SqliteTable as SqliteTableProto from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto -from feast.protos.feast.types.Value_pb2 import FloatList as FloatListProto from feast.protos.feast.types.Value_pb2 import Value as ValueProto from feast.repo_config import FeastConfigBaseModel, RepoConfig -from feast.utils import to_naive_utc +from feast.utils import _build_retrieve_online_document_record, to_naive_utc class SqliteOnlineStoreConfig(FeastConfigBaseModel): @@ -303,6 +302,7 @@ def retrieve_online_documents( ) -> List[ Tuple[ Optional[datetime], + Optional[EntityKeyProto], Optional[ValueProto], Optional[ValueProto], Optional[ValueProto], @@ -385,6 +385,7 @@ def retrieve_online_documents( result: List[ Tuple[ Optional[datetime], + Optional[EntityKeyProto], Optional[ValueProto], Optional[ValueProto], Optional[ValueProto], @@ -392,19 +393,14 @@ def retrieve_online_documents( ] = [] for entity_key, _, string_value, distance, event_ts in rows: - feature_value_proto = ValueProto() - feature_value_proto.ParseFromString(string_value if string_value else b"") - vector_value_proto = ValueProto( - float_list_val=FloatListProto(val=embedding) - ) - distance_value_proto = ValueProto(float_val=distance) - result.append( - ( + _build_retrieve_online_document_record( + entity_key, + string_value if string_value else b"", + embedding, + distance, event_ts, - feature_value_proto, - vector_value_proto, - distance_value_proto, + config.entity_key_serialization_version, ) ) diff --git a/sdk/python/feast/infra/provider.py b/sdk/python/feast/infra/provider.py index 9940af1d02..c0062dde02 100644 --- a/sdk/python/feast/infra/provider.py +++ b/sdk/python/feast/infra/provider.py @@ -364,6 +364,7 @@ def retrieve_online_documents( ) -> List[ Tuple[ Optional[datetime], + Optional[EntityKeyProto], Optional[ValueProto], Optional[ValueProto], Optional[ValueProto], diff --git a/sdk/python/feast/utils.py b/sdk/python/feast/utils.py index 992869557a..a6d7853e1b 100644 --- a/sdk/python/feast/utils.py +++ b/sdk/python/feast/utils.py @@ -33,11 +33,13 @@ FeatureViewNotFoundException, RequestDataNotFoundInEntityRowsException, ) +from feast.infra.key_encoding_utils import deserialize_entity_key from feast.protos.feast.serving.ServingService_pb2 import ( FieldStatus, GetOnlineFeaturesResponse, ) from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto +from feast.protos.feast.types.Value_pb2 import FloatList as FloatListProto from feast.protos.feast.types.Value_pb2 import RepeatedValue as RepeatedValueProto from feast.protos.feast.types.Value_pb2 import Value as ValueProto from feast.type_map import python_values_to_proto_values @@ -49,7 +51,6 @@ from feast.feature_view import FeatureView from feast.on_demand_feature_view import OnDemandFeatureView - APPLICATION_NAME = "feast-dev/feast" USER_AGENT = "{}/{}".format(APPLICATION_NAME, get_version()) @@ -1050,3 +1051,51 @@ def tags_str_to_dict(tags: str = "") -> dict[str, str]: def _utc_now() -> datetime: return datetime.now(tz=timezone.utc) + + +def _build_retrieve_online_document_record( + entity_key: Union[str, bytes], + feature_value: Union[str, bytes], + vector_value: Union[str, List[float]], + distance_value: float, + event_timestamp: datetime, + entity_key_serialization_version: int, +) -> Tuple[ + Optional[datetime], + Optional[EntityKeyProto], + Optional[ValueProto], + Optional[ValueProto], + Optional[ValueProto], +]: + if entity_key_serialization_version < 3: + entity_key_proto = None + else: + if isinstance(entity_key, str): + entity_key_proto_bin = entity_key.encode("utf-8") + else: + entity_key_proto_bin = entity_key + entity_key_proto = deserialize_entity_key( + entity_key_proto_bin, + entity_key_serialization_version=entity_key_serialization_version, + ) + + feature_value_proto = ValueProto() + + if isinstance(feature_value, str): + feature_value_proto.ParseFromString(feature_value.encode("utf-8")) + else: + feature_value_proto.ParseFromString(feature_value) + + if isinstance(vector_value, str): + vector_value_proto = ValueProto(string_val=vector_value) + else: + vector_value_proto = ValueProto(float_list_val=FloatListProto(val=vector_value)) + + distance_value_proto = ValueProto(float_val=distance_value) + return ( + event_timestamp, + entity_key_proto, + feature_value_proto, + vector_value_proto, + distance_value_proto, + ) diff --git a/sdk/python/tests/conftest.py b/sdk/python/tests/conftest.py index a9bb9ba9c4..08b8757b95 100644 --- a/sdk/python/tests/conftest.py +++ b/sdk/python/tests/conftest.py @@ -197,6 +197,26 @@ def environment(request, worker_id): e.teardown() +@pytest.fixture +def vectordb_environment(request, worker_id): + e = construct_test_environment( + request.param, + worker_id=worker_id, + fixture_request=request, + entity_key_serialization_version=3, + ) + + e.setup() + + if hasattr(e.data_source_creator, "mock_environ"): + with mock.patch.dict(os.environ, e.data_source_creator.mock_environ): + yield e + else: + yield e + + e.teardown() + + _config_cache: Any = {} diff --git a/sdk/python/tests/integration/online_store/test_universal_online.py b/sdk/python/tests/integration/online_store/test_universal_online.py index 308201590d..1a0803acff 100644 --- a/sdk/python/tests/integration/online_store/test_universal_online.py +++ b/sdk/python/tests/integration/online_store/test_universal_online.py @@ -846,8 +846,8 @@ def assert_feature_service_entity_mapping_correctness( @pytest.mark.integration @pytest.mark.universal_online_stores(only=["pgvector", "elasticsearch"]) -def test_retrieve_online_documents(environment, fake_document_data): - fs = environment.feature_store +def test_retrieve_online_documents(vectordb_environment, fake_document_data): + fs = vectordb_environment.feature_store df, data_source = fake_document_data item_embeddings_feature_view = create_item_embeddings_feature_view(data_source) fs.apply([item_embeddings_feature_view, item()]) @@ -861,6 +861,9 @@ def test_retrieve_online_documents(environment, fake_document_data): ).to_dict() assert len(documents["embedding_float"]) == 2 + # assert returned the entity_id + assert len(documents["item_id"]) == 2 + documents = fs.retrieve_online_documents( feature="item_embeddings:embedding_float", query=[1.0, 2.0], From 163d34f23c5fcdf5b28599ee8556d2fb31d8ac79 Mon Sep 17 00:00:00 2001 From: Tornike Gurgenidze Date: Fri, 20 Sep 2024 21:44:30 +0400 Subject: [PATCH 083/185] refactor: Use get_any_feature_view in online flow instead of listing them (#4545) --- sdk/python/feast/utils.py | 90 ++++++++++++++++++++------------------- 1 file changed, 47 insertions(+), 43 deletions(-) diff --git a/sdk/python/feast/utils.py b/sdk/python/feast/utils.py index a6d7853e1b..2ab73ae089 100644 --- a/sdk/python/feast/utils.py +++ b/sdk/python/feast/utils.py @@ -49,6 +49,7 @@ if typing.TYPE_CHECKING: from feast.feature_service import FeatureService from feast.feature_view import FeatureView + from feast.infra.registry.base_registry import BaseRegistry from feast.on_demand_feature_view import OnDemandFeatureView APPLICATION_NAME = "feast-dev/feast" @@ -756,61 +757,64 @@ def _list_feature_views( def _get_feature_views_to_use( - registry, + registry: "BaseRegistry", project, features: Optional[Union[List[str], "FeatureService"]], allow_cache=False, hide_dummy_entity: bool = True, ) -> Tuple[List["FeatureView"], List["OnDemandFeatureView"]]: from feast.feature_service import FeatureService - - fvs = { - fv.name: fv - for fv in [ - *_list_feature_views(registry, project, allow_cache, hide_dummy_entity), - *registry.list_stream_feature_views( - project=project, allow_cache=allow_cache - ), - ] - } - - od_fvs = { - fv.name: fv - for fv in registry.list_on_demand_feature_views( - project=project, allow_cache=allow_cache - ) - } + from feast.feature_view import DUMMY_ENTITY_NAME + from feast.on_demand_feature_view import OnDemandFeatureView if isinstance(features, FeatureService): - fvs_to_use, od_fvs_to_use = [], [] - for fv_name, projection in [ + feature_views = [ (projection.name, projection) for projection in features.feature_view_projections - ]: - if fv_name in fvs: - fvs_to_use.append(fvs[fv_name].with_projection(copy.copy(projection))) - elif fv_name in od_fvs: - odfv = od_fvs[fv_name].with_projection(copy.copy(projection)) - od_fvs_to_use.append(odfv) - # Let's make sure to include an FVs which the ODFV requires Features from. - for projection in odfv.source_feature_view_projections.values(): - fv = fvs[projection.name].with_projection(copy.copy(projection)) - if fv not in fvs_to_use: - fvs_to_use.append(fv) - else: - raise ValueError( - f"The provided feature service {features.name} contains a reference to a feature view" - f"{fv_name} which doesn't exist. Please make sure that you have created the feature view" - f'{fv_name} and that you have registered it by running "apply".' - ) - views_to_use = (fvs_to_use, od_fvs_to_use) + ] else: - views_to_use = ( - [*fvs.values()], - [*od_fvs.values()], - ) + assert features is not None + feature_views = [(feature.split(":")[0], None) for feature in features] # type: ignore[misc] + + fvs_to_use, od_fvs_to_use = [], [] + for name, projection in feature_views: + fv = registry.get_any_feature_view(name, project, allow_cache) + + if isinstance(fv, OnDemandFeatureView): + od_fvs_to_use.append( + fv.with_projection(copy.copy(projection)) if projection else fv + ) + + for source_projection in fv.source_feature_view_projections.values(): + source_fv = registry.get_any_feature_view( + source_projection.name, project, allow_cache + ) + # TODO better way to handler dummy entities + if ( + hide_dummy_entity + and source_fv.entities # type: ignore[attr-defined] + and source_fv.entities[0] == DUMMY_ENTITY_NAME # type: ignore[attr-defined] + ): + source_fv.entities = [] # type: ignore[attr-defined] + source_fv.entity_columns = [] # type: ignore[attr-defined] + + if source_fv not in fvs_to_use: + fvs_to_use.append( + source_fv.with_projection(copy.copy(source_projection)) + ) + else: + if ( + hide_dummy_entity + and fv.entities # type: ignore[attr-defined] + and fv.entities[0] == DUMMY_ENTITY_NAME # type: ignore[attr-defined] + ): + fv.entities = [] # type: ignore[attr-defined] + fv.entity_columns = [] # type: ignore[attr-defined] + fvs_to_use.append( + fv.with_projection(copy.copy(projection)) if projection else fv + ) - return views_to_use + return (fvs_to_use, od_fvs_to_use) def _get_online_request_context( From 9a0398e2e18585172d857cf3202a81551d31609b Mon Sep 17 00:00:00 2001 From: Julian Gesche <26026582+jgesche@users.noreply.github.com> Date: Sat, 21 Sep 2024 07:08:05 +0200 Subject: [PATCH 084/185] fix: Fixes validator field access for 'project_id' in BigQuery offline Store (#4509) Fixes validator field access for 'billing_project_id' in BigQuery Offline Store Signed-off-by: gesche <26026582+jgesche@users.noreply.github.com> --- sdk/python/feast/infra/offline_stores/bigquery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/python/feast/infra/offline_stores/bigquery.py b/sdk/python/feast/infra/offline_stores/bigquery.py index ef12eba442..3ee1717461 100644 --- a/sdk/python/feast/infra/offline_stores/bigquery.py +++ b/sdk/python/feast/infra/offline_stores/bigquery.py @@ -114,7 +114,7 @@ class BigQueryOfflineStoreConfig(FeastConfigBaseModel): @field_validator("billing_project_id") def project_id_exists(cls, v, values, **kwargs): - if v and not values["project_id"]: + if v and not values.data["project_id"]: raise ValueError( "please specify project_id if billing_project_id is specified" ) From 76b4576c2839d41a257948d0052cf11d7c5a921d Mon Sep 17 00:00:00 2001 From: Abdul Hameed Date: Sat, 21 Sep 2024 01:28:46 -0400 Subject: [PATCH 085/185] docs: Example to Deploy Feast Remote Server Components Using Podman Locally (#4516) * Added examples to deployed Feast remote server components podman container locally Signed-off-by: Abdul Hameed * removed the script and used composed files Signed-off-by: Abdul Hameed --------- Signed-off-by: Abdul Hameed --- examples/README.md | 16 ++ examples/podman_local/README.md | 72 +++++++++ examples/podman_local/__init__.py | 0 .../client/feature_repo/feature_store.yaml | 12 ++ .../podman_local/client/feature_repo/test.py | 123 +++++++++++++++ examples/podman_local/docker-compose.yml | 33 ++++ .../podman_local/feature_repo/__init__.py | 0 .../feature_repo/data/driver_stats.parquet | Bin 0 -> 35141 bytes .../podman_local/feature_repo/example_repo.py | 144 ++++++++++++++++++ .../feature_repo/feature_store.yaml | 9 ++ examples/podman_local/podman.png | Bin 0 -> 211897 bytes 11 files changed, 409 insertions(+) create mode 100644 examples/README.md create mode 100644 examples/podman_local/README.md create mode 100644 examples/podman_local/__init__.py create mode 100644 examples/podman_local/client/feature_repo/feature_store.yaml create mode 100644 examples/podman_local/client/feature_repo/test.py create mode 100644 examples/podman_local/docker-compose.yml create mode 100644 examples/podman_local/feature_repo/__init__.py create mode 100644 examples/podman_local/feature_repo/data/driver_stats.parquet create mode 100644 examples/podman_local/feature_repo/example_repo.py create mode 100644 examples/podman_local/feature_repo/feature_store.yaml create mode 100644 examples/podman_local/podman.png diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000000..91799864aa --- /dev/null +++ b/examples/README.md @@ -0,0 +1,16 @@ +# Feast Examples + +1. **[Quickstart Example](https://github.com/feast-dev/feast/tree/master/examples/quickstart)**: This is a step-by-step guide for getting started with Feast. + +2. **[Java Demo](https://github.com/feast-dev/feast/tree/master/examples/java-demo)**: Demonstrates how to use Feast with Java feature server and deployed with Kubernetes. + +3. **[Python Helm Demo](https://github.com/feast-dev/feast/tree/master/examples/python-helm-demo)**: Demonstrates Feast with Kubernetes using Helm charts and Python feature server. + +4. **[RBAC Local](https://github.com/feast-dev/feast/tree/master/examples/rbac-local)**: Demonstrates using notebooks how configure and test Role-Based Access Control (RBAC) for securing access in Feast using OIDC authorization type with in a local environment. + +5. **[RBAC Remote](https://github.com/feast-dev/feast/tree/master/examples/rbac-local)**: Demonstrates how to configure and test Role-Based Access Control (RBAC) for securing access in Feast using Kubernetes or OIDC Authentication type with in Kubernetes environment. + +6. **[Remote Offline Store](https://github.com/feast-dev/feast/tree/master/examples/remote-offline-store)**: Demonstrates how to set up and use remote offline server. + +7. **[Podman/Podman Compose_local](https://github.com/feast-dev/feast/tree/master/examples/podman_local)**: Demonstrates how to deploy Feast remote server components using Podman Compose locally. + diff --git a/examples/podman_local/README.md b/examples/podman_local/README.md new file mode 100644 index 0000000000..f5b6ad40d4 --- /dev/null +++ b/examples/podman_local/README.md @@ -0,0 +1,72 @@ + +# Feast example using Podman and Podman Compose + +This guide explains how to deploy Feast remote server components using Podman Compose locally and run an example using the client. + +## Prerequisites + +1. **Podman**: [Podman installation guide](https://podman.io/). +2. **Podman Compose**: [Podman Compose Installation guide](https://github.com/containers/podman-compose/tree/main?tab=readme-ov-file#installation]). +3. **Python 3.9+ environment** +4. **Feast CLI** + +## Setup + +### 1. **Feast Project Setup** + +- The project [feature_repo](feature_repo) already created using `feast init` command + +### 2. **Run the Podman Compose File** + +- Use the [docker-compose.yml](docker-compose.yml) file to install and run the Feast feature servers (online, offline, and registry) on podman. The docker-compose file uses the `feastdev/feature-server:latest` image. Each respective service has specific port mappings and maps the volume from the `./feature_repo` configuration. +- To start the feature servers, run the following command: + + ```bash + podman-compose up -d + ``` + +- This will launch the necessary containers for online, offline, and registry feature servers. + +### 3. **Verify the Installation** + +- Use the `podman ps` command to verify that the containers are running: + + ```bash + podman ps + ``` + + Example output: + + ``` + CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + 61442d6d6ef3 docker.io/feastdev/feature-server:latest feast -c /feature... 2 minutes ago Up 2 minutes 0.0.0.0:6566->6566/tcp online-feature-server + 1274c21716a6 docker.io/feastdev/feature-server:latest feast -c /feature... 2 minutes ago Up 2 minutes 0.0.0.0:8815->8815/tcp offline-feature-server + 4e38ca8c39db docker.io/feastdev/feature-server:latest feast -c /feature... 2 minutes ago Up 2 minutes 0.0.0.0:6570->6570/tcp registry-feature-server + ``` + +- Alternatively, you can verify the running containers through **Podman Desktop**: + ![podman.png](podman.png) + +### 4. **Run Feast Apply** + +- To apply the feature store definitions to the remote registry, run the following command: + + ```bash + podman exec registry-feature-server feast -c /feature_repo apply + ``` + +### 5. **Run Client Examples** + +- The [client](client) folder contains example client-side configurations and code: + - [feature_store.yaml](client/feature_repo/feature_store.yaml): Configuration for the feature store. + - [test.py](client/feature_repo/test.py): Example Python script to interact with the Feast server. + +### 6. **Cleanup** + +- To stop and remove the running containers, run the following command: + + ```bash + podman-compose down + ``` + +- This will stop all the feature server containers and clean up the environment. diff --git a/examples/podman_local/__init__.py b/examples/podman_local/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/podman_local/client/feature_repo/feature_store.yaml b/examples/podman_local/client/feature_repo/feature_store.yaml new file mode 100644 index 0000000000..d4ad1ccb6f --- /dev/null +++ b/examples/podman_local/client/feature_repo/feature_store.yaml @@ -0,0 +1,12 @@ +project: my_project +registry: + registry_type: remote + path: localhost:6570 +offline_store: + type: remote + host: localhost + port: 8815 +online_store: + type: remote + path: http://localhost:6566 + diff --git a/examples/podman_local/client/feature_repo/test.py b/examples/podman_local/client/feature_repo/test.py new file mode 100644 index 0000000000..13ab2444aa --- /dev/null +++ b/examples/podman_local/client/feature_repo/test.py @@ -0,0 +1,123 @@ +import subprocess +from datetime import datetime +import pandas as pd +from feast import FeatureStore +from feast.data_source import PushMode + +def run_demo(): + try: + store = FeatureStore(repo_path=".") + + print("\n--- Historical features for training ---") + fetch_historical_features_entity_df(store, for_batch_scoring=False) + + print("\n--- Historical features for batch scoring ---") + fetch_historical_features_entity_df(store, for_batch_scoring=True) + + print("\n--- Load features into online store ---") + store.materialize_incremental(end_date=datetime.now()) + + print("\n--- Online features ---") + fetch_online_features(store) + + print("\n--- Online features retrieved (instead) through a feature service---") + fetch_online_features(store, source="feature_service") + + print( + "\n--- Online features retrieved (using feature service v3, which uses a feature view with a push source---" + ) + fetch_online_features(store, source="push") + + print("\n--- Simulate a stream event ingestion of the hourly stats df ---") + event_df = pd.DataFrame.from_dict( + { + "driver_id": [1001], + "event_timestamp": [ + datetime.now(), + ], + "created": [ + datetime.now(), + ], + "conv_rate": [1.0], + "acc_rate": [1.0], + "avg_daily_trips": [1000], + } + ) + print(event_df) + store.push("driver_stats_push_source", event_df, to=PushMode.ONLINE_AND_OFFLINE) + + print("\n--- Online features again with updated values from a stream push---") + fetch_online_features(store, source="push") + except Exception as e: + print(f"An error occurred in run_demo: {e}") + +def fetch_historical_features_entity_df(store: FeatureStore, for_batch_scoring: bool): + try: + entity_df = pd.DataFrame.from_dict( + { + "driver_id": [1001, 1002, 1003], + "event_timestamp": [ + datetime(2021, 4, 12, 10, 59, 42), + datetime(2021, 4, 12, 8, 12, 10), + datetime(2021, 4, 12, 16, 40, 26), + ], + "label_driver_reported_satisfaction": [1, 5, 3], + "val_to_add": [1, 2, 3], + "val_to_add_2": [10, 20, 30], + } + ) + if for_batch_scoring: + entity_df["event_timestamp"] = pd.to_datetime("now", utc=True) + + training_df = store.get_historical_features( + entity_df=entity_df, + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "driver_hourly_stats:avg_daily_trips", + "transformed_conv_rate:conv_rate_plus_val1", + "transformed_conv_rate:conv_rate_plus_val2", + ], + ).to_df() + print(training_df.head()) + except Exception as e: + print(f"An error occurred in fetch_historical_features_entity_df: {e}") + +def fetch_online_features(store, source: str = ""): + try: + entity_rows = [ + { + "driver_id": 1001, + "val_to_add": 1000, + "val_to_add_2": 2000, + }, + { + "driver_id": 1002, + "val_to_add": 1001, + "val_to_add_2": 2002, + }, + ] + if source == "feature_service": + features_to_fetch = store.get_feature_service("driver_activity_v1") + elif source == "push": + features_to_fetch = store.get_feature_service("driver_activity_v3") + else: + features_to_fetch = [ + "driver_hourly_stats:acc_rate", + "transformed_conv_rate:conv_rate_plus_val1", + "transformed_conv_rate:conv_rate_plus_val2", + ] + returned_features = store.get_online_features( + features=features_to_fetch, + entity_rows=entity_rows, + ).to_dict() + for key, value in sorted(returned_features.items()): + print(key, " : ", value) + except Exception as e: + print(f"An error occurred in fetch_online_features: {e}") + +if __name__ == "__main__": + try: + run_demo() + except Exception as e: + print(f"An error occurred in the main block: {e}") diff --git a/examples/podman_local/docker-compose.yml b/examples/podman_local/docker-compose.yml new file mode 100644 index 0000000000..5bc1ae546a --- /dev/null +++ b/examples/podman_local/docker-compose.yml @@ -0,0 +1,33 @@ +version: '3.9' + +x-defaults: &default-settings + image: feastdev/feature-server:latest + restart: unless-stopped + +services: + online-feature-server: + <<: *default-settings + container_name: online-feature-server + command: feast -c /feature_repo serve -h 0.0.0.0 + ports: + - "6566:6566" + volumes: + - ./feature_repo:/feature_repo + + offline-feature-server: + <<: *default-settings + container_name: offline-feature-server + command: feast -c /feature_repo serve_offline -h 0.0.0.0 + ports: + - "8815:8815" + volumes: + - ./feature_repo:/feature_repo + + registry-feature-server: + <<: *default-settings + container_name: registry-feature-server + command: feast -c /feature_repo serve_registry + ports: + - "6570:6570" + volumes: + - ./feature_repo:/feature_repo diff --git a/examples/podman_local/feature_repo/__init__.py b/examples/podman_local/feature_repo/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/podman_local/feature_repo/data/driver_stats.parquet b/examples/podman_local/feature_repo/data/driver_stats.parquet new file mode 100644 index 0000000000000000000000000000000000000000..7ea02b9a9f2a2c271115468e2e81be6ff6224cae GIT binary patch literal 35141 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%0DjQYsPG1EbhwR}&6*u^J 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 literal 0 HcmV?d00001 diff --git a/examples/podman_local/feature_repo/example_repo.py b/examples/podman_local/feature_repo/example_repo.py new file mode 100644 index 0000000000..60ddd49f9c --- /dev/null +++ b/examples/podman_local/feature_repo/example_repo.py @@ -0,0 +1,144 @@ +# This is an example feature definition file + +from datetime import timedelta + +import pandas as pd + +from feast import ( + Entity, + FeatureService, + FeatureView, + Field, + FileSource, + PushSource, + RequestSource, +) +from feast.feature_logging import LoggingConfig +from feast.infra.offline_stores.file_source import FileLoggingDestination +from feast.on_demand_feature_view import on_demand_feature_view +from feast.types import Float32, Float64, Int64 + +# Define an entity for the driver. You can think of an entity as a primary key used to +# fetch features. +driver = Entity(name="driver", join_keys=["driver_id"]) + +# Read data from parquet files. Parquet is convenient for local development mode. For +# production, you can use your favorite DWH, such as BigQuery. See Feast documentation +# for more info. +driver_stats_source = FileSource( + name="driver_hourly_stats_source", + path="/feature_repo/data/driver_stats.parquet", + timestamp_field="event_timestamp", + created_timestamp_column="created", +) + +# Our parquet files contain sample data that includes a driver_id column, timestamps and +# three feature column. Here we define a Feature View that will allow us to serve this +# data to our model online. +driver_stats_fv = FeatureView( + # The unique name of this feature view. Two feature views in a single + # project cannot have the same name + name="driver_hourly_stats", + entities=[driver], + ttl=timedelta(days=1), + # The list of features defined below act as a schema to both define features + # for both materialization of features into a store, and are used as references + # during retrieval for building a training dataset or serving features + schema=[ + Field(name="conv_rate", dtype=Float32), + Field(name="acc_rate", dtype=Float32), + Field(name="avg_daily_trips", dtype=Int64, description="Average daily trips"), + ], + online=True, + source=driver_stats_source, + # Tags are user defined key/value pairs that are attached to each + # feature view + tags={"team": "driver_performance"}, +) + +# Define a request data source which encodes features / information only +# available at request time (e.g. part of the user initiated HTTP request) +input_request = RequestSource( + name="vals_to_add", + schema=[ + Field(name="val_to_add", dtype=Int64), + Field(name="val_to_add_2", dtype=Int64), + ], +) + + +# Define an on demand feature view which can generate new features based on +# existing feature views and RequestSource features +@on_demand_feature_view( + sources=[driver_stats_fv, input_request], + schema=[ + Field(name="conv_rate_plus_val1", dtype=Float64), + Field(name="conv_rate_plus_val2", dtype=Float64), + ], +) +def transformed_conv_rate(inputs: pd.DataFrame) -> pd.DataFrame: + df = pd.DataFrame() + df["conv_rate_plus_val1"] = inputs["conv_rate"] + inputs["val_to_add"] + df["conv_rate_plus_val2"] = inputs["conv_rate"] + inputs["val_to_add_2"] + return df + + +# This groups features into a model version +driver_activity_v1 = FeatureService( + name="driver_activity_v1", + features=[ + driver_stats_fv[["conv_rate"]], # Sub-selects a feature from a feature view + transformed_conv_rate, # Selects all features from the feature view + ], + logging_config=LoggingConfig( + destination=FileLoggingDestination(path="/feature_repo/data") + ), +) +driver_activity_v2 = FeatureService( + name="driver_activity_v2", features=[driver_stats_fv, transformed_conv_rate] +) + +# Defines a way to push data (to be available offline, online or both) into Feast. +driver_stats_push_source = PushSource( + name="driver_stats_push_source", + batch_source=driver_stats_source, +) + +# Defines a slightly modified version of the feature view from above, where the source +# has been changed to the push source. This allows fresh features to be directly pushed +# to the online store for this feature view. +driver_stats_fresh_fv = FeatureView( + name="driver_hourly_stats_fresh", + entities=[driver], + ttl=timedelta(days=1), + schema=[ + Field(name="conv_rate", dtype=Float32), + Field(name="acc_rate", dtype=Float32), + Field(name="avg_daily_trips", dtype=Int64), + ], + online=True, + source=driver_stats_push_source, # Changed from above + tags={"team": "driver_performance"}, +) + + +# Define an on demand feature view which can generate new features based on +# existing feature views and RequestSource features +@on_demand_feature_view( + sources=[driver_stats_fresh_fv, input_request], # relies on fresh version of FV + schema=[ + Field(name="conv_rate_plus_val1", dtype=Float64), + Field(name="conv_rate_plus_val2", dtype=Float64), + ], +) +def transformed_conv_rate_fresh(inputs: pd.DataFrame) -> pd.DataFrame: + df = pd.DataFrame() + df["conv_rate_plus_val1"] = inputs["conv_rate"] + inputs["val_to_add"] + df["conv_rate_plus_val2"] = inputs["conv_rate"] + inputs["val_to_add_2"] + return df + + +driver_activity_v3 = FeatureService( + name="driver_activity_v3", + features=[driver_stats_fresh_fv, transformed_conv_rate_fresh], +) diff --git a/examples/podman_local/feature_repo/feature_store.yaml b/examples/podman_local/feature_repo/feature_store.yaml new file mode 100644 index 0000000000..3e6a360316 --- /dev/null +++ b/examples/podman_local/feature_repo/feature_store.yaml @@ -0,0 +1,9 @@ +project: my_project +# By default, the registry is a file (but can be turned into a more scalable SQL-backed registry) +registry: data/registry.db +# The provider primarily specifies default offline / online stores & storing the registry in a given cloud +provider: local +online_store: + type: sqlite + path: data/online_store.db +entity_key_serialization_version: 2 diff --git a/examples/podman_local/podman.png b/examples/podman_local/podman.png new file mode 100644 index 0000000000000000000000000000000000000000..9aeb11f7f7f0c9c419cf8d744fd0fb812b602fe4 GIT binary patch literal 211897 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�zK=I3=!M2Tj1Rf85XL zXjqpl$m&*zm0UzW<;kV0n<0vO5UkSB^}sdX4cU|Pt(PMoGhbOG5r|je@z*U|rRv$85VqW-z**PitkooiL{`9eYIh4^- z@M>xaOI2mW`lat;*fr#KHNP78OZ+R^Ta+TJOYf2^*otO0GAfxIIh&GL`nEApV-But zs7H=^4ej#7u({Wl!9H2vP}PbjU`zm4O56DXw!Gmp8_>tusuO3YIG2*9`;u=oLNFx` z;7@JyvjGvo55=ABXC@^W4}6cz%rDJv02%n_6g2kQCn~Cr&|PRqUBe08OW<&JW(VNMnOi{AnF>)CSwov@-4!`++R}$r*@}W>>ti`5Rx)2^G#X)aF1^ zxLvbnjiI6Tgcw{{q7xs>*5}gkM_wBzEj&qzhvehEl=GNHk^)OAgwD^5^^`3Qu+bxJ zLk#Rnniz|DT!?nGI>LBYm zc8qP=nzMvirQJENm1W3$i;{%hcQGsvW!IExie5YeWga=Aa8jDvp-#@@(xCrd+YE#d zvJk4sic2>3T6#wms&_yh^M%YN8aW?oB-^JH%QS@{h0|i!q}Sm4?F%!jaTBJ7`L>aJ z6Xf*qI9@7}vSsJIh5F@NqcvJxbR2D5-mBML7Lq<00+)I1Vc<9-==febns%XwVCq>% z!L3_XaPL{>?R&9j!v5RaJ2B@cu&k4ldFycZ7`}2PRdfNPIZa8>!TewG$=0|vibmSP zmop0BTLvP-)Zy~Jy`n-paHx>p7S8YGdEl?GQpvsEdleNBPP-kGm1TGpK*6?XMK-iH zU%^jkly29Q6&*LE!`?bmak%kkx`wOYmsqiqBZSNkx~fy7Z|Sj#ia!2A=L4H?+#W)F z47#9&$S*B3l8T~omV48?YkjgBsM>1LcFPRfJmnI=@UGoGekALt>q~{ec4)fTB5lhE zoqk55fBiya(rsn&wddM=xG&z5ud2N_ws*5woVt5=kuI7KaFm{oXU%HY{XM^ zdCJrnF1H^$kh0G{-?rUl9q#OwwS8YdLj#Dv10|h6dRpSm+13>;^$I2?KS`^UTHL>y zAwi8HV}}!vngfXZ8U58yi%s^d+H>e@6Nzk0wEmcHC=9vfq+TJ9Yoi|L+WLwDQ6I?% zI}eI-@>9O`B-dk)FW&dWPBX4l0qCTd0(`E5UA)-zOyCH-EM!T6#saOW7r)Ch{K0aC>mxSid>n^Y?43kRqr-25MS!>a+uo7T%kv4jfNuRVm zS-UkKc=2uO5?wz^HwfE}IhUYWHW%1+8KeY$8ergSTx8TxuDbmK9j%#5u`&&2HcA__ z#m|DvYtf|Sh0_<8L~SizPIZ5g*~0Guigklzf8OaY#lRY>oq(yRz*ZTVsnq>e?+ytO zlR?^%@;+Wd;VF!FBV;xH9HfHg<;mPEv~@ zQs-;;^ZKpFibbZAz1`C-3tk{{Ho{NdX6wf5tF?GCx}FfW8EN3;jw;}r${)Y~AywMf zbIDR(ep-t&3@wOsa$>SZ9*!!@>9+QP&m)P`>tXf&=xEn(cha&)kQ?~olhIl_><_8y zty>9qlrE-VX^R8C=xHF)M#(|~ z_TV%xFG`6#?#nDEj5^l{4v>XT7!5M#2h`C-3R>v<`}>aR()vqlh!_oK<1lHr`clZ3 z9gJs7l6a3d5}Fh$k11Q13*(?`(=~cV$07U6<`C)4lD3e&jc)}F#0lj%q;#@ zRy_i_NTE!r$o(xr9%(hn-fN@A z6B_kZSAdOnoJ7@>Y9QgD^)-r>aE2o{^y{Tz!vro;!ukmqAZ$}whm3Qm2J^M22S z!GC_jI26Y0+f!@zx3_nI08dQ4C=Z4<*BOqZgGBARJB1vA(8u{cQ*?sKXy^dZ6Qk?8 zrl8w;qN^`Go>xTl^)2qAoA4R08#jJT%Lt^}SN@4=qwKw%_VRi&u&guvz4$<^nK)8m zXRtO;4521yDG*(_pkL?I7Y2?lJy84%=;-j^%BgdES1uB7xGzkM53C4*U4=snu{Kw2 zbaB8J?ViS>3~lkX{R@c--5}ZR%Bf)bRTm;E0VM)_^dM!X%h_;0vkz$3;VKD>Ye~yw z|MTbZqcH(o=o|OZZdKPnxIVF)8nbVk@g_I`j}O&ZMhFu+G>;|@f>AXh?E-z2TZJNZ z(Tcvyvylf)koXoq;2~O8A$>$Q-Cme#Pm1i5icNBRyZ2ihfC2Fj97r6cfI`GP{#W6f zW5OFb#hC-r0`$?P^NS}MmrGHT9xb^XC(LfspGZ{F=gfxYd!BKw0#N&7k!dM6mWLCR z(lv=!nB-UOC30?F1;D9oHX8^sx2x3oZc=J@h?NYAu9E#lgL|Mub}{3fx4d;!1+~MR z4MH4*7+;hyatmmZC8LQgMOI08XIH+JF|W=5 zN54@+YWSe^x7!EQcu_=#`W3?uT9Opl3|>U`_!&*rCwzVL$y=hy+tCM*p*^;ld2YQv zG>Uz+0=g)FB<)*EQbc^Ay6SeKQ6tZ!?8F!)Vi%t1XT$~=#gWO&GR?J0oYpWK*H+e( zH<2aw(mzfOguGFEtPk;Uts)Q>S23NF$Wra1M9&0wI+jUF47=7Y2Y)Ble1lsxn zI-)fG$^*Ek0|f9_wuLPnRw3@W&5(>rW?3XbZF8*{B%-U@x_)nTg=xPqs122*VRuXU z+d*Ct;={%?HEB}TDc|kfVH1-i3j-nO!EKQ^$`={P+ zV#|{pP06mvNka*jpUeDM8>_y0x>B_%qsa?ONK(*{ zc{uk8<-NME&c7p`yS;CxVl`xxgIK}Ek)7cQ@iUVh`RR zy*rOSUy_HYzQy!7)dp+b!rf18$;u^Cqx4qV!hO(5B?)P|4658nEk&P#2s#E?+Ob5o zlU|1Mt5d$&gb$^P!pjLG=rBtT(;shqwh1!qOnJc4se_{>p1~)0=ZyYnVcd=ZBYY`| z8!PkUc`(bCT|w(zsNM&tMWEyx%4=F6V{L|R@OHC^uP?6GdovGvoC`fRZ2rN6aA-T~ z&M)f#*h&p3VN=_x%{rV4JQ2X7%A$MP^aaRX#Cvl)_*$vT(X80)bRM$mO7bg5cj%_i z!?>2h!6a-pCas-PyEem}mv$)j8;oKp*)`q;+}NCUB~P)i0ja+)vJ+(gP>X2J{1& zE4YG+0`t1Hc;j8k5QL!7@d!1lr zpV6bnn0`IzycOs7);&w&4-Rw+!srdxEj5-d0|ub1sL+2wI{$-a{_FqpyhJ4;OQwXp z+aEpP%U>kmX^5vgMFTcz$^9Lz&RG6hD4D-v_5b<+|MzD+um5X}n2|Md-2Dgd{bbj# zUHAX0SKv8BMnh?lhe&z{q-Pw@r9eK3Na*wcnsOXvIleGzY=D>&rELln{`+g8ixi@0 zFwjKd1b?B6=Oejqj2%zL$0(i;0=DFDuAsjOnWs=-#y2$(Vb_j4)r*EsxOWgUlNVf1 zDT?O{Pww6XW&P{kRJe-<#-$0c_{5=NWH|>chH>C3_?8T58*01u_v#KXgM=LT(obw7JD+D>sos^p0=8v?117B! zfAVi16$t){^ZWKLumeG|q0RmII9w?EShKeMK3IW&<+0!_oFCN07XSrI1>(U;uZQgr ze+D43VguCj7qyyq)j9sWKmbwF<X;}(Vdw0Cn! zyoki?PLHfU^E`%3e077$S_y=O{h#1z~c3ZRJzfax2 z?~MQDh@xW@qPGA03fX-FVs}?C6M_ddr5Q?JyYS;N0=HeQd2Wf2eQeruj$XSCtPm(r zxahxtS|Rq*ex9l73FGkz`TJQxS3S9&MPz>z9}jPzQnAbVdPTMWn##4`^B>t1d}J80 zozT9>0|VC&`Y%;62nay}wgYTucbZ8sAF%;<9KcRr*Ig+Zj*m!z?=f)_=f^_Ny49zw z`{!%*#mSU6&)=%ek2Q7s%Q_jFI=}zDoDa&K-~^oCXj*X%f}KbLL~r%W$R&%Fun5Eo zcwb8soFC$j<~~0*Jgpcn$S}$9>_? z`F9olI;*!$#V=1-NUu&+W|hc`|6WXhj#cVvhZWS?MLETZSMNE+MrCG0rN6B=Oo$X-rf1-#Ujx;cS9b0T794Ldwl^L zmCqA;EcF-brX&mp(|$>B5MPUf@lb?_`GfJUVhOOn3a2RUrqPT=2v_8a6dCqsCmln< zVahuNs2k2e-qd@z|J3yAb4ee2AE1GCB&U~JLH=^C__VG*a9AsKd-wcx=p`&Nvx&j=^)|MMk zI{RukxYmeUI=_t2YnO3;wsM4A{3}RS2M2mT}+X$S_hUI!eYuXQNDYX z-emg6Q9uGTpUuAn!m>;8c%A_*zu7qv7jYoggvf%gS{~q;E;la+$u@(~T~UwMrJ#); zUOo5`)!)=9&+bvj=+Jzs<;ocfPU-7{Gr-Xs1G@9M6?xN=*Rq*1GDYi}sSAn6 z9<3D@dj)we_NiQ3o?Uo;Q!!tn%PB4rcz$kt6)}G`=y-%sAT?ocn7jr!soGww#ER17 z)j_eTVpHeFRzwDOChexJYRi3^<>ytlJ*u&_l9=`R$G7DYl#36#d&4(G_{K!&0E6HB zxW9udoN%?s*q`AOW)SpB=nq&JKA1Qj<{=>0KN#3cS&L@TY3GaR7!7+zwx2ZRLQEk8mXoe%S*+f2*Ro zBi}B%INJ+4zR;|CkAI^e|9G|?@v)Jft~yYecW3V4i5I;s=dSAYLAx~@3_HJa7C|mL zA0WS49(}qs&Io9PJvIQwJEdzjWa?i{Gq*QDeeDLuyY?CN4L8I=Efucfa$-gN9DOB{ z!6H}8ist0egy*JDjn_Le1of}a4%H)Cg$$cm*^|RB9*+Sl>3$xcs1W=#JB!XQj)($| zr2s<9HyweQ^y7<5VGXrGbgYM;`8#QAf7k$)+?vT(fYy38qWhLVH&0SQj)k^iJsGo+keC#)9-Rg8A>&;VbxX3YP=h%>C%!fh z0D{6(z(~&rb-h@-Ri4L^!;L;a_RcKGTnl;*tlo9^T=sq-6-E3w2o1t}d(CEBlsTK1 z?=?=^l+OfS`4%+D&FU5j4rS&rp4GpH6whS`j;u~)cH1THtyX3mhu+RmJUw}|gqidl zu5ACciN0Fi#i5>U(k>HDEt6o_lns6^)Vg%)=5sDb%3`|X+@rq$nI3G1C(Mh{_~nub zumDwCC{EFto(!qme*N%l=^fp(SomBnr}WCtP5n!yp|H+cH8FX3xwsqpnl=cpb;$0n z``q$g&8{L62+o9@IL|-azrGM(we+xd4i3fY?)3hF$-6%vR3lm&iZGqof}>*jtvjqo z!o4OYL9(U{S#E9)_Riebq?t(e`I8SDhLh?gk7i z+h&j?an9#+d{Hxt)}AVOB^Bv_^W%B49xMjjpaq)ZUXL~2Gfk|(Z03AE9#4mTalqvC zJ}720ug_Lay-J8W;mQ5pYS}kVDy!*iZ-mzt8hOI%s$LL)dU*f{`~4y$A~3bdOca

__Ha7k~;g);451`6NSzrjF1eGJPeox#(^4{H;sAyypF)GF(xyHfxy{nzWRR4_ zivttbc*rnVf}R~`Pp!#J6>udE(2PA5m@wXi%&`btt5V#dp@<}0=3!D{6jl}y!F*2; z|A*p6%2Xu&ME)cwP)v|uk{=mA6ILsO*32Q=*Fze&#gNDyZ} zQ3@`juJDHkDoh;)qy048SevMo*lxf3WuQ4(y)MxG1Si5d=6(^_IAbQ(ClM&ZUP6RP zHk0&B|5zJ$P2ASYU@FlB=XkVPLidoc6B{u-eZfIS2oc0ZPbANFypOHrn`#G@;C!Go zz;z5H!4z;*gKld7JVzefl2W38Hq2#1P{s#?=P$qvKRP@G^mB_&&~~1e7bfh&pxF{< zzHqIK0x*So7S^(Eyz%Q7hEElQ{Wv^X&BN>e_V?4t7kLCN0T@D#!I{#m3Sf?n>osdu zB>|nqq3(yP2PQvc(zQQ6cjRrp^_v&qUm8a%TQOOVKj}3|6f~c-*e$!^B)LPgmGCz7yA?CuGEHdqZxt} z;A?)YKqMT}8nwb}p?^b+^pHOC-hJ4k%-T1D&6*SgH1Wx~E?a*e@=HM){!z46NGp4!m}6|4uV z?x*mhJhql~RPEqub7`^#>`ej3m-hK;e!z@zP%GM~mZ_ z+`*U|+qap0saK1VcnRm71`=CQ%bgdZxZg;AROabm)mC7sgS!wkfuAthFVFj5Hqy}k zB-yvagb?JW(_SW^h?~eTDxR_I?jT2W^S6_n!wWHlI@8(nU9QY!!o|$0xu)d^aNMBP zJU(vj#5UA_2Ws-P1Ph&&gc`aHQF(okyiH{QtGi;ymEnyCl^l1Uem-a1_baWc&gdh2 zR0rZlRjUTcAqS|jKhVSmOPM~CV!VKd{n+lcTl$n`cYGooNV~ITZe%1#FgL$l{r;>Cf7Y!$CMl%x4-1DL6hVX=w<7Npeu1oBeX9v=pk-W~Oaem_@yq0qv=^ukVY zANIx-{yis5CHkgWJ~+*UOd(wq+L}!SembImy-LO~e5uLaa?N8MAN~D&!5>7_!x^B4 z5tuN&0>d|71Ww)uY(>)}qy}iP0&$MIRpdHDR+F4VIGCOor6Ff0w!+pO#bhNOUGR?5 zmd)M`UZc zchu#a*wkPipasf1<}fOp531k?4lw(!_-5#JJaJ*=Phk*vW#ecSyEnt2!*Q;iZoB5y zp`u~}>qvxehAN*JNm*OE-iw@rzbcyAP4pl|dj{#hBEiMZ>*MXh5c0Yp|2r4BdnrSj zQM(-+#>bFuf`nbl?!?2{@F2Ar9KDf$}duQ}t>Z4a~veH6JtDg;@@I z&G3-zN8Ufc-t)Z54ps#TK!E($`xJCS%;b(cEKmLxIw~gBte)@6!E*p-8z8R~RX5)+ zJ735m9E#aQOI@FVNM9hQM0#7O&-ys|BhrR$h9|+ILyx2d8i*I0xq(NZhwzQd12ME4 z=Jn`XEo!;vo+0Ow$#My-h4+UH8CM*PZ3&}gwb3Lq_ur zETI#p0ru+n)_kA+Ga#u5;Obh!o_B*;gwY@lvg$>t2SV6$5F=6$BM5zvd!IRr5;}Su zY0&Qz?^Aeu(%ur4Mrlbe30Zu>dHCd_c9s9lS>#!PSuRie$CdiPCk&9ptEgo!wwYA& zhXqF=2`Uac`L@B^8&@abv<50(3YZuPHPs~47({mINbUrORsJc{`y))CD#J zH&9JWq4{jWYxC;cAt$y~QRUaFW1>%=KygtD4#bczun47a6pw)M=Hqb#;Kv?>^C=1G zrHAv4-frLX0m6$pOABF1k@P*l`GDHA@^e8Sx;=P96=VN;5l+3~LVog4^MdlRP#dF+cR)+-S>*Q8<`aa>aT53)@gW>Qr*p>Vi=v;B3 z;lNu027d2&wTaTAfT4?)6ymzFN8s+P#{)!KS+rqU*W8HUSeG-^e5``9fFWKgnaSr*F{fmoN52`?ib<47tMh7 zORgKckQ^VKlkkJiO6meAH!l`G4xSl-O;?U1W4oskY*czoU}*&G2S!l6-Em*422`bd zL7#B|Kr;eNMbn3tq5l#I7RH`#=blB?Z1Q>NKORXIJB3LneA{)f^mX z*gb=clc_t0G9HM+~H~Z z=t)6=)2{mmLTr3e;BK5!G>McllHPxI!))ZpK@?Zn>6ShLCM$Y)4B44uXwn4chR=hK zG~Dht>sTt_u~)gT(w<^6^*TIk+```91`h9p)Ho#cr!K3=JJ0MZdWMY!;^D=!IK+Nt61t`gIWk z+H0U{GlHiu4?YrC>iN55n0ts&RsMD4g#A6q=rlyH08~*XZb598-QZ<^8lRMV-lPiL zie*o!Lj4ux?_I%-y|E;h(2L)r4@VpFtGft8PLztT(h@Q{|Cs)3N6QMp=LU3k9dA159R9H}yW{Z$E(S?`Za-Jj0#KQMi&HtQWt8T|MZMA zkaz-${+)kFKeu9hqYj;vx(tJ6uTeCKf_#`4mrw(@UJ3b`BQktcWI$ic!N-r`n<$l6 zmMjNx-!J*P8!1r;huL|X4zT~=l2JEFT`qw>N=0a7j5{!=TS+3bzv&CIAvSflprL#4`az!4q?BV>Di*_e&&mAMk01Z!}(L4B9F!?An? zvdi~xMODrWCVP_@_^3nyhC1(*7Uze~2CmYCVD+9c)RzYIcF*Gk^^aQY_&CpP4vLlrgim+S7wkA-l;n^h`L zfXDx$)MYuWFPkNW8#pM-hzf(!T9FjP)_$K~PO+S~_Q);I-61gA&0^TfVzLZDjJo2_ zpbjnfy1x*WP+iz@=@K@<2P8O4)xxW46T-nq%;BzrvA)3x7X)y?- ztJRt!JZua@i$1p3>ZhaotMtM5tr3Nnqhti1s=fdedLcOKK`8WbwC$h$A)E-)-*6;@ zjxnZk32XhE1NGu#yI&2owF$Jbw-7VK`Y4W~nO@dr73TPaiD_Uio^wkqAz#sKo}a2* z9V#^TZljL&Is7~KP5SoR%$jjyA5AXAU{`vBSS1~OuT5T|6C3gst6VvY?w3Gztm$LZ zgi&^*ZCRMm`eXy&44^Amp_imqO1J|^+YYVwG7!7`?U!sYu`ny&(LcN@E*v4pXc&jteX2=KC`+`L?0fiTv6MSjn{*EF=5U=6+L^ zsvdrU({;mdF8&MLE|KHa;SA>;6`Xq2aI|Q0dxSfjOLij@!Vlif;qa>eBGBRA+HSh> zF#YQJWep8eLH*B=mK5xi|8iUTIOrP;wuQ8MgZWVxVhOV)glYfPl5@V`kFL-#ae-`5 z2FAW)w2A3b+J{b){XHEW+dxcK|E!DNP8GE|%%7-7=H3WJNJ?Dz^lqpS)mpPEE50GJ1XQ3#CYZd0) zfo7dyWbM2)=~givK=!X*$*Yl-6QG_z9TBIy29+ka;IpHWv&UJ@pv$>-3^WnDBqnKS zMy16G0@yc40I|<`w~aWA5i=n;Wb$k4f!UAKsO!UzyroKm10ytwr)AD2W+^NI9VSOT z)3KM+yD+E$0E3nRHdTSRe&+C*H^1C|e8QNUACz_tdb%L8L3riGoVrPk&;9C)qg##Ece6)H6*N+D+vBV1%?lVJmkqQY%w zBI8CKqF(%aLfvR2VA556`HaC0-i>vaKjFCy9UKget2;NY8n!t;0SwIk-lUA@cal~Q z(aVI~q1Rdmp_g?x*Ls}VvYXbpm@MOD0oW6%m``bVegjxOTn*l;rQ@;BAEHS7F+5NT zPer1Un9)?x$d<@jt2HWm>C0K@6CXiV`6Qz>7v!NwhMS~M9;hmv1-_PWx@dI)#+E#f z5qQ>E8|y=lVa%vVF*_ZSi8U(K(MW2NPBskjXaFgymCduPgUNDIWEUnJlMthYq67>W zDabSa!?>60%2;^W{c_h6m5dI$Md)dFgSpprhLbq4_9X|co=OcEiNVGzf7yqb+lff$}EZwQnTy`(t@ zYbm=@(l(7+)c4l>A;zZoHXmP`s-QXx(u)m_#Jc6EJ^db(v5e~QSh{c zIutF&x{nW>sJJxxFKUrKvieFBQfb`4_Sfy~s{2Dg9fCT5LzX;e>Kj5YYN+qiys>*w zfG532?yDQO4|S}q&Y3hao>zKv>&i`_LJYYB5$%5Y27#SOkVW?4=5}@4NdaVdfC;V; z#W0@th_#I2t!ChZ0&r4rZFL3B83&leFta1ILCZ%L4AMkVb`{gBXneXGR-2L2HuCf? z3}n;HTLD=oKoIeC5|UHWi>_iAybCFHCmKDFGbI8C-8PRtW8!CkR+9 z3at38~2rJb*w~kMd*9 zKV8{V0(H&NpnJJ`SO(`@I0K0U4EMoY(Oa1BfEqfC_LHt&eKeHMA`IIyf0%gDNCKL0 z(E#Vi4_O$bk`w5B)>)CkSO+fC7idn2OzFXyI&S>&wCB6?_7e|Jey}K<4!OF9ih2}A z?~X0MKe8;!X7*B+~eDG;nv#+ z@2uKEHov$;IZkM50=ib8J#WmFhIzyT`{sYfc=FdbAlNXms6H8O%`gyENB!CLF>NyTfD2l*QO6j7g z;|Wj*6w}f4-aF5C_y~C3Mb8orOoH~{ckxbAE6k=4zN3(`0b!{d25s!4PCXA)!c}Q8 z%=+4ehEZ5&BO=hC8Hf4EQ8ZU``WH<=5O zS2^~d8t)2)UeaYSflvE3EBHAbK$+I`84OW*5+jwE_}!rj!@-*|<9WtcFG+>-HRaF% zxpwA_s;z;(jIe!rdJ=tR%b80pw;n1$DyDGz?%}XZ0jMJw>7@Ss1ps$3g*qh3k)Mr}J0HFAE zmEn7QHoD+4L`_%w9adu}0z|Y?jQ}>#=pH+fB_I1_C8%&k#^Qg*yru$txwm3)vkofp zk+|AUH(+#(u(|T74vWL%gN*L-t#Fc+B|tKa1(Vq<)jcq9&kTWcuz`wuJln!L&KrZuj(Sp)^x^eG)K-?@;)e{mx!Vh zcz#M`WxLR!s0t~3oT?Acad!ni*zL?mN!Z}~hW%FYYu$jO0;Cj&c5J`;Qj3o+N^UQK zIe*IzCxL5P9(2X$om27BLjvc{HmK`ya$5vhjfG6Jmq+b0saYr-?`NO$b*RZI`)t=y zoZks|MBN?Wv_(VOlsH<$HO^-MG)8xe8~Am(Ex%g@)+3i0?YqW9vd`4a%!kLrjTqB| zgQhqJ<09Slkk6TrCUyRMLY?|MkZKNbc};ZLKHb}3$n<)r%e_l8LsHh zeC$2Dke6Q<`ND;5ikyT_;W>h2!7SxDv+gnvab#+%a3;_Sd#nxr{ti_NUE@<|RAkKT zlFb!{uL^KUNh@d}WQC1rNQecP>7(RYk|w2m&)U(kF)>$Sdj0dsp{~<+Yk1d|O~LCN zyKJ%i$X$#P`_ZN{`W~9?gn`@Gq1*ZkIc~&9GHg%Z+pLQGqlT9eWW( z=nj6>{K>YC)k`ZAk9iZUXcyE89J-!T)yGLj>KAH-ZQ=2CzC_4}=BFG6<pdu6Ue9A;z1P$xIih*g~pru%2m(2Cn} z_KTw6arp|LOqOqcnGn;1uiXtU@kG-iR{ka<4SNQ-S1Sk^j5(5w&;NB4hMVnhVN=o> z+~j6FiQA4Z8L$eurh0*3c(3kSP`?+(tRq(4j~TdDLN+`^jXgU5R5D+3H6^~6at$t!?OdPYPmhptLt{eH%deiDS>n@GNE1#IUrS_II0fo>%GZ0y?D|5+;EQygr% z9?}epx-n}D&|I%hJDTkx#8^`Q5!Qc#2tK@Bk8&+3 z7;}96t}JCWwY3H1=H@EBY49e*Bvi&h>|Pgv*ey#^bzxhSNBTz$+`3TKikReK8Ivf1D!j-R;Wb-{umKGQ5 zp-I&M6WhwUMXTI~(9s_xDVi7Hfaa@`cC$kynBBPS`Z+uFbiaNF@U7~t^ovC$%}O6z zk=~%%ln!ghw21m*J=GW|#JDNcM3@T(mx(Ylm>Edt47iu!Rvd@)&sNnzy|wBL%8N}| zg+7-+1ieyeq`5J0uCGk^=pnlGC5<<^M~{H|kR?b)^`PI6O2vZFST}^kR)mqX^B16y zOx55Un3-DE=myW~msCH}Y1ce0Mh!9OjZ}wlW|YP~}NxH#gDdLLg zUL&bcG*dp`+&9dfRJ6Lt0@nuMt2G@cAp=XI2u!nE6tvOSguxI~E@Wgj%TYKKzCY|f zV_}T1qX)Wx`-`Ds=l!woYV+`LiPYpDqpwzze^~?RSF~fklue3Z8#U+90y%(1B<~#9 z5kye)iNAzM%V2zMrJrXm2h_myh&y*?2I?}@SD%Z>DoI{Q1s8V=-01X*y8S3f9U{%Sebwk5l8F6%pb*8=MtXL+tv)DmC5c^L zlhNv^)xxXj5JzKMs~vY6MA=mPXV8@az5RCK6b?<5F6>PfqiE8UtBj1J28QdpY|aiT zbi3EPhS8h zFnoF!oR6X?pybie5r|AW^I%!yyEl!ri{BSA8cHj$-$^sDSfgwLaz>_@IRhc`sw~3b zbtpLdKM~wz=*)%*QYnVG@?+KHR<-IYL-Gv(WPWgd>!G6+RG>BZ9efPJ$g5aENr1-9 ztbso_fDZWr)iPbqv&+-KFE~bF?MjZLD;K<*zYLbaF;W>>0P756pS*~W0K<)s)9g@v zN;8ltsh3~P%T7>8iSXCKsx#=sWz3waods`~u=+xUybCYc!U(J81jtiF!}f4%8SsDt z87|k8b~BQ#Fx65I{VCaHIIIWs;4%k2Xt3Y3ItPWM?zxKv(DM>l5M&d$q= zmy_N9VCMl{Yy6Bm=2Pt8B6Vm0?Ow%Jz3;G>5)Z~;oQ)SQ@#tf7J?sw&jMp%={9MRQ zjnThE!?za*CGwf}Kh_udPhYA;^TxQhVg|~oSQJc1Q7nKVGl64#>gwu?aIL9w{>?Cl zOc=lD(g9UQ3{`<5zR#I`>6Lv2-AHBg-*&uSO3KO34@2|u^N^x->cPSvX#AAV{uS@q z7`zIdSZ&F6ka7$sbr{k=+Q6a}Gys~Fq{DEPrDGB0k>s^=IZqifgjK5CVIp*9G<^Zjy>N?2q`seo2<6wEbhh@1qO9 zcEca^KRhrWCD3Xa>iT!doc! zKZeOxV*Ou{X)EOZpN3piVPPRmtG?duaK_NI*&QJ90*iYXKp?)>pJt$ z&cOzgfBC%yXv)1iUYrvZMkAcOb;mZ&_v=(9zOud4*a3e@Me0ohE&P(BqgoecR?C7h zjcxU!4ON@jRBQ8GRr4&3G4YS@I;s#J8%q@e9hE1435Jfh&xBiQ`tPs5wBeUZ2smns z?21S}d53H8!=*78c@XJ0*e!pYg8*Cgrj(zY`STF;dT*`Ri z8C~yFkqCOrRCfFY`b`IIU(5eKiU)iIqYvX6?1WoV; z6wH_Qu3wpSH$HK3aSm|Rv}tqyM@i*hjEaRJ+%nRLQ-0%Nh_?iL(ZDr0Ffe<)uB}%> z!&Pp9fHjD2G+YFQUEMyFdE>`iN8B7UNhI>?W+_tn)<5y@pDLq#3&xBFVSFLslGE39 zb=ap%I|>R4Ud6}98#Za4ULPSFZV{gel+=OKCamih^N)wypUP)pVF5;|x>K>)-0Q!l z0tbF}J_}4*OdSbXA0q3&=#LLFeFo6se$2;t!?g+|2P9Y7p;wp;1 zk1yeEVDyfVsdsaH0(;4n*6w4K6%RcqU{eb|v^41b6C)U>_)ZBz`r@5h5qE1|J9HE&!P9-TzseO+I7YvuwfMe~l*Vz;p}tHb#@uBEHVwyZ}EHlv@zE zE~?fY&;Nuhe+2Fp1pZWzZIa=)An=b!`s;-8?-98Lfg3~MPetSw>}|o`PanB0=J$6_ z+E&5)HH3d=rAPRq*~Dg0TgATd?<&Inr@U=lic~$3K>KOXu4JzyH1D*>H`w zbiO|#>Az2wTkVaX3bKv!=oak#4Qk7`1$$etxAD39zuLrBSNCt7wEt)*_}M-9eOI<% z?{96!R#*4;Nc!*pu*LSa*xt{U<)^k_@4r7_zh{FjGt1vOY5zYkvy`$h@c-ro_)iII zOXvGpK-r+Y-tzPP4Tw3l)!x`3q<*?8+-h(9o~-_dKWxF?7VQ1V@P<@xG|IhZYL*xD z7N%>nzP8e>`!&gqvxF^O1Cb4VkI7p@UDGzY(ts6gwQ<|aX*N`+|2;5YpPCTEV3f?i zIm@=Z=3Zwe{dC6BQ?oQG#bH^~b*%5w|3}(ehD9B1?Zb*Fp@f8}AfPlzNrQwUF_d(N z2uKdy2nYyB%TUr1LzhT1w1S9;Gz={b(%tVK13c&PeE47Q2fapQezEu3Yu)Q!_uBg( z7xU8rWvV~p~JM%Ll2iS*W$tT0LLGSPV0~XMr zHe|T-kK6ylT8PlhuUQ4hvQxR6HKO19`#b7|^>vw#^^V4ft(+zQ?>G51fU=qHvDKRb z_m41|d7tWKDR+wJY%)$JT7O^VZ1C8I(Ye80c8}A`GSU_~t~K{@ zJX4mbcl=9@oTV}jKibxE&-osr*_-fxccA+g3f>iI!$_>x9Z#bDdQ_+A?RV_5|Djb+ zq%CUlzG&K0VUt*9v3Mp@Uc~|uck;iRkj#j|CKS1m8>i2A`T?l}Uc35nmiOYp4rujh zYN03`{|fyd$FuQkJUL)0kWAiRj}dUW+;`SFLdh_?ivOFnJ~3Dks!z{a_XsJbl#gs z2A<`A4h@s?#ee=a?g0J|QTq^!w|2>gXI&yhO5}V-@PGc!*;nvc(V$?v6K0vX@epSX zuI7N#UwlF*vU+J2DVq%L}s(uYhfoPzeM!$dl&Aq?B z{MU2hOP~#Rho*gensxT{;LJO#>+A|x=l{CyqnG12au@#k0qWI%{<97f+$|<*v-AEy z`j51ahKy$y`j1a9IxbdZE^S#C6hQ_hf?AdT*PW6tTR$F}z5HeK&o6kP6MM)FrlAFN}_pf<5`#S2=hoEyiC6h+*>vX5rc##dRnxWVu z`g~p-@D4Z#LQl0zV&h3faoDwp?EiHqA(HW=vJR3r^QQxaMGww?Kx5%o z5KrrCt!4%*xh%I#2}iz{1eA?f+hhB5#0Y zy~JwK|Cc=^5dwR#lWu%5kp6}5KPTp|aRkd3tfZKzrwM1*$N*P;Bc*_MYKP#_Fh|G# zIV`I|U_w1KAQbIBvY&-CWiq_~w#l<=V(w}K^A^&3l5y&CsNaIGVpYt^+&H_D7u!Y5 zQB1%!BBd$a#z;3!{+ATX^T3+So_+T1yv$A5WJN@t&c z4of{Nn9yA#8h1`SZiWLB_E_w{$Kfwy7`FrFeem2X_H>|LzW`sQ9#7>kIx~3mMwGjd z;l(|*&A$gF0ZeL=7Qv+tv_@b5$H0l4^K!mAp5%zgBbt14I*)9`z+RODPyH-vI8XuP zU&Hc359NT`r1nk7EflvqpWY+>>+$}28j<>0VBXt%XdI^@hw?nI1<86b zF5Xy^`TgT&<9uLJ@0U|wc_sV#-=6p9h1TxW3obcpFf{)a02zV3W)p4w{Xk&8|2-`K zKLgU$=2Q$Hg1o{Q5!n!QGjq`srdziNETB8o3jQw$L1;I+9@EbY0k9{Fr3(1B|Q96X>t2z<7(?zl`qFb=-v4wY}BfwcV@M zwZ7Iukd?)jq<|DUklZiypfNpIW}3fnNfPXY%_bBbi24#Iu$otXjL@@XboLw{9s>WW zp4FY1e-4D{2WM~gU!VR*m<}m`^K)Cgj-GCV+pIkuTyN*eNNCISQz7u={mJnm?Awy(V=5buvBbQ_`Tzs=|Kari;tBPM?=K(c-(S7RKVLKYD zIr(l{!W>;==^##K-MjI6&JUDdL}-a!w3$k9$uLZGPE9AeR@HlVX;=B}R`+B0_NW`P zag58bjiU2buTY|3cVIMF!JRO;&OfHmYxx>D8KL^){9~qiS|jGG#Q!xoXaBqpmIE~G zE_%~h1hob;9de6dsPrT8%)&1RRml&Ptc!wsIJJiY~bEN(81P^WU0 z+(#F!=cL-FTDy3yl8j1Iu?VSSR?po^v+^80JU&q5vPn(b+e)~vQT$l2x0BsctuL`( zXRH6D(36YY4?9R597a9Z*CBZFoB^DrR?+?_YhVz`7_awS;pVgU;u?=i{csNFA|h=U zc>3nTw76Z#gs4}B@DU`(s;brYnMMma6HRx2$lfg=L zo|}R6zrg3&VBm|?UqJbx`a5jsXQQeH9MNv9QVo6n-38;X_PzZO1K-JJ$g(eql%i%_ zMALJfm~k9E)wHi8? zOfvf}HwT9!f>>bb!NHbGF<%j^zzgm4)00a`u8Q9;xM}bgBsz7zb5bbBbh!fyU`G^Y z_>X75==C-FTKLzmELzx5S7|xN6q2~JS)H>43)-oi_vkFXJ<i(JsMiVAgD zOl{AH)=8WkD}uA~V0_kZ%PjAp4syM!%C~KIh0a+tq-tz^8kUeu5rI{br^_5=(9F9W zKaSI4*o?u+qE5->G-9MZwiL(!hb!(+MWD9E9JY+x{;m!NpI7%j9$w-&4jwD6=)dBT zH8ps9Di%g3f3U2pMPr42zS(nTe9TXN(yUQmo13%bIQUo}E4pq+M(Wm3!z*@YPr&Dr zqn((@#>T8|*-6YtjQIOG-<-z8kimm%3=AmW*y`&GQUP!WzvBz(lZy9=O~)jf9j}K|YcAD)(!`sXcjKrnX%bH`a>p5!`;m ziP+DXy~Z~i%#pqut@TJz@jg%4G8ujY=}2~Bg}W~)Lg9JKEf|NhwamD3y$`mlwI=4M zHRChYI{jts?3x|j_n#H=;7Lk5uIms81si2x%{Tk|0NP%CoJ%Zh01jdC=nD-0m-`z4 z*;m7j?|i_H-B76t3L%IjVA)#^-Vwg#;VdJOXfu}rb7;hZxomdAG&OH_zx&3Arh}Dr z4xRNmoTU_@ZR@ck(8{-?jb{GbBc}{(ZeHy13BOI_8dzdeV1w%p;y0(LV*D`*XV~e) z68`}ruRoz-ks_{QO7?E`OK+@CC+Qr^?>6U`2U63yIRfW?;4t`pnG5oQrqH20V_~JZR-pjXW}J}Dy)fQXf$DVDFc7;YI(-8+ z^sgeW&R+hsdOG@N>k+*S3{*oai1+_F2F|yg4cv-LjX{ly zhe&62ol~apC|DQ--S%UJ^k>Ca#T!`!2_=55EH89?fOF99-cvfoIRrA43O+n$nKI#i z{d!?0r5qTzk&!S02m?#B;g;UB40O_))beU{UJGVe%F2?9#>rwm63cwElAR$ufz&i& zJ#q+gnN@K_f%fwdf#Go+uC`j~7{7w^BO02j`LNIL44M{T;~_-@ckz(xOXbD-bMh@A z85Gtp(($fSE;Ql%4(*AFiCu#O^j_`pH0pY+OJkm_`QVHg)%rlQLW@zvw=sIDr@$V< z2No&v2MG~ZnKh7Nr>^2ZmcBPnl~%%W35fcA1PbSWS-IBv!i!cGw=8BlC8RsSbed`0 zV0tC;A3$2rUf*v+R(#v9vV(^R(%@6Rn=MBQK=p61#xf zCsKZ8?80i6+f6)RjXbZi7P*cV4oO7sQLNwxN5 zKRc3dD@Q?WM^giSc5j}ipC8Zu#DxF#7YoV=@6gItQi0&4=+oio&gT zx8_OzjNhvQFn(!^(u-%e9$|fL(m=Xkp@V39? zJVyub3g;Ip{vqU4rq_k0`bmL| zSBSiu%iXSccpC%-;l6N<{y6=+DsbiIxmhuN`BJth^qfIU zz-%`nM+qCp>6ICyk*0>Hr)$Y*sTM?jwwY{P24{kTG2<TuCBTx8zZmR4jziOJ_nD|wXg+;_vNJaq zOiRUQh`}GzdXWhd?WOGLCv1Oyat@6JlUi!k4_jB=sZRqLlU@$XiGjPxCbMKuPqWZF zjXWA6(fibVl`42h&0vAUI5hV*WpiRnR;pG5Df>;Kk>&uA^afHfo?*($*~EV4_xZJ3 zs-|H3>Y{@?&Qumvw{R_vrXu8beG9V89g(AMi#dG9E-i6f#WqutX?3R4D+sD=BcsrR z@pHhb*Z)K`IDfhKQ%v*kREzy88_wt_-b3rz$;;@}6{ZpBW@f>)(W#4+lix(){b^xR zisF6+IaZzkj9EnYQ9D!RjfLlT&(FOcpJ^{REQqW-C`jL)CnTkJbwK4b{q&}of9z>B z0W3>FkVBsSM1=C8*CVV`w-#K|g>eN_uv?0So-Kz00%socU|Kl&ipkA&gG-6cF%N_o zOXYsNsIgA_@;_PtkO4O_x_aV>#vn)1{42z(lhTaQja1hzT*8SCj2f_ z58iz8wP^Y#IEyM?gN?qy@6x<_xtykIVe||!lVKar7_TLV-M9!tEVnF3H*Zla`d<$F7Y;V#jHOpxv%8gq? zz*xmlnqK2$ABJ&`pdsP}_3Wt=| zw_kU0?s}avtuqxj)8I%0slb=w_eycsm|;i;rQkYMHEye<%>nnFfhV20OsF;4C`t8a z1-_s|(bcU+`tpB%at`e(CTf_r+IgfS9-J$8t*o{9N(TA-IZ%WXAv#ohsK~AB7~9!U zIh#Nw#xr85%rnXaFwzRsb&ARUi&j}8L7NwFUpKpK+@(|I)c2mky>+t}Yq7xSqCxYg z47#METNfHg!&wzt{BVkwOS&yc857tZtfxK~$~tecckDK3eY}T+Y@Phz2%j>93NoS` zE-iTGH}nb^{~9WCREF-aB2~m*4B>0ImN5Slt5&e!g9DcHBjyJx6i}&STnr0Wl3w!Q zdm2!B2wpsZ-)SIyr_uW6lGUN{XW&A!!6C8VX6STU4~|yg-)78JswVebet>lA)zM8h zv_CF^QWHgeM&DgJ!^0n>>Q?|i%}T>`qApygsJ|0${DqC5*ioxg3U-epWTDv(WkoJq z-;IAD$3u>f;B$VY=}$LJ+92?y^5jjIz3D93s`qd)5B_z@w(W@b@8SKM^Tjs1)_@<9sdim%AT^u0W#0G64*-pU9>dF>X`Y@8@!-&1_05GqaMc2~F3dled>jH$x|^-W zb)r8%c_D(1=7BdVKe4uTiM1(L#DX~ZR$FS)0hEmU3;l=c*%2q{ZPQGLA#0y_9%%_9 zIC@Sljl2p?15z`*gcF5uBU4k8PZmc2NIS8-R(Hwj!)K0!(s>Ig@z!m>@eEmltZb~K zn8@(k1XrY_OtaIYqot$@bzun+gmN26yy7H?a7|ruSI6@0ib;#)ji|b#vaG63`gV%v zlq!U@>fDKL4HseVpBakRVA2-V;DELDUQ>-qMaGtp_t31d&rt<>c=`Opn^O_RdzG^m zElR)F(I+~qPH(*eLg2_uPx|W~dio2*$5EqlLZ>mEd$|1cW{w_~>KbK7OI9hKJTH$u zlvRAUF|&^dS}QA4PI~I7dX8^z>6s-9u}=?he?=m{)!Fsbweep#4~RF+BT~9jHCD*{ zAjiZupF=4<@C2L6aJxS?_&2I10S0P)H6mm9DkygdgqHkd(qC7g>0*z{=Y9hxqACOA z+UZTC<5`o1BNCvDa;j!-G|o+}E0TNPKc<8PM((1RS)u+Ah0{U~EwFKJg;d+F!o>}g=ctcfTtJad;*(JAA`!HxD4jmp z1HBXJdtrPTZOPd&hFZsm#-`7AbW9JvUrwtn4i!`__BhaL-EX&|W?*WYyIw7237m(u zy~E0APWn={)!ejR1VKjk8+XBm{D803w{<3P2>Rt_X09bnA1&o#-OLiRXAyw5A3KES8A z4E366SICI@LVO2wxDT^y#QP~m*qDJo8 z8nK%L=B)j4AaH54s{8J~Zw^#ZUe1FE-^68YjI1*bG@Z&D>FKUl`LbZR2+2tj21@nin13`5C}=7BqEqWkV)PIFj);}&8?s1+RD z4Xs(uDYE&1nf5lQ*kvOa4Yx1JHV~bWv5KtL)up7LD(uH9$JRZEkrLCc&E@K=vX#I) zQiId%#9**S%a;7{khsUj`v zf%tK6XcTMo)pHx=3egcCJS2Bd;sK?yWEYa~amlU6h5i&}S2S!HzxOUaIygD1D?a}5 zo;g$Lc^fl~!|)TXu+`C~Qg-bUcUxU}PBHcejl$z3Q>nd1Uj?k-IrTc-zC@2I z=n5lxDu9E_ikaXX71%<&W$}I0i_fBD;xppowr9O^a=wj)NuDefl~Qpfrv?zxlv?RQ z$3mF^U%mTPpm_dW3$?t~;_hlb2DP!V^mcZp6gU-462Mvi5xM*1|F0Y!z+*uTq=7|d z-#>g)yK4^z$*ibeo@6A5_!g>2U;)^7(Sc^CM2 zy1#xPLU|SakPMOXkVkpfPe*|@FNRffD%<+S3?Q}_suoK!^#BoZOlFb7Am@Dt#Nnv8 z>gBFWuNL=*_wBYGTCFXWOWS(X&fga33ECOQDRj|feh8T8p1Ms~)}mKD9IQI24uO>` z7j2v?=RoSw)_bH{K!k;ExYSBFRGS68`D*fmXrhEl1g!=+%&AjM(Rsun_3|YJ9FRFb zs$Hy-T|p3Hn-?+B?2NH$&?Vrz82d426?*y2pnH?R>W964%nM z{a00CRx%#Bbw3a`j2RM@jAed-s-xeb@P{ ztk?C;AzknEee5lU=^?GHu&&&g^vxd(3^X(_MQyTuPl;F@RfNBE0WS!m&KJNq;>3Zn_ggg`lI1OEu5@E|BL44p5%6_ zePxS&tr?ua!xVb+lk}>=L1-YzDu@mRR?nARpaS@%l=c@3hcZLvtdN6gR?p)=gDV{* zTm|3M7Z)CqTJ-WIMMh9Tva?Y{L^d3rQpeGGT>V|!wS_9x1?QE$1zFh7A$*~R!m+(k z`r2nWb^?2s%UN_}QY8haO40fwF-+Jty+Cv47^L2w`}3gqQ?$fmy!wQpb`p1KKn55W zM+@MeiMPD#hG+mkxsTY6%UVUtq|x=^K`abL)wyOUfZTrjxm}gxP`_G74Yqjb6+x=? zr=1{hv0LHf$+Onw#*F7nfr<>|*eIrwUzWH^f2Vrzz?X#jsJ||{@3Bxg*oC=hDSkZU zQ%r`@mRU9#M{`+9uqbZ0BC%maB$10hxyfzv0>b77Qm+@vBlZz%W7YW$q&S6jETu{r z0|nwG+jVmfg?q;OYgg*VrtMlyE?GU=uo~I^#%G*xdm&Bes;`y3X<#k}(jWh+Vz-6H;qwd$#vx ztI3dTp0!IErNN?jQNccV%f)SZ5pw%`VjFBhB-)K+t{?nen4Lv^6_ic5*ZjD{hCQm6 zhI?mjx5+vJGv={3p=WzK+x5;e5k+*Dcx)=f)KUBauh^(-dpag`NG1NW-B*6;F3+u8 z%fX6)weH7V2wQj-PDRF~isx9*SY#gI%0S6Lu5;b* zY_;Q;z0mXF=@CSPTw<-;(J7`T>RK)>-WxPF>Q;I@h$~$)uJOzc&tw32+s>(4V^9iV{?&Egt%o1M{1*>4YOS87OHZ;w#pd!uYo+`Z>X)84E(DI>~@ zu~a_N>R2THKFzOBgs|z;P_)|_>8g0*#eUE?jzj#RZu=!+Ny|?#UC&S+=qgmX{DlJ= z-&V8wkRguuT^oNV_UN32X?V_JVNA42?r>ZVRDe&^PjXF`$CsoQP>AjBJCI3)%gDXH zR8p%*aNgfibnVk#ERd4HGWGribjwjdS1WS+&b22IwjW+{{uDL59!Jx~9mVA4{1j&p z+F2mK>@o}K#NjbnoM+5p^YW{-gWD`?t8x2h%StW4_Y=mB(ar)o+0 z$)k&@FToM0_vP1FDnvku7s;(!?#-hqe9XHU#h3eid!{`)iV1Ez%-eP>Q)g-cvwe~dfBH=Dk(`gw2pY7VJ`KtKQk3q$qai20=M)k^$1iog~5sgmj4_YCa zV}Zn|g0TSS(0GW~1<@8BW(|U^`6zIrc*joHt;mYYqpKCO+nWocJ0Jli%M+hc>BhM7 zx=Z+20e0`+9jh-NuO*;cNwU$KKjw$@Nbjn`e}wR)x9GZ6-g~%Jt>Hcy5)I9MfluzVQ9eu zXH@=#LZx3P6DJ$61-Cc7u9SbCxxr!By*XdjzU4Aa3>k77V_AGe1=5sK6~+QY*RJSA zOAVqi8~?1?UO6daw`x=>w1pv?z&Z1_0DA&^$zj#>wsTYF<+hSjmhaT^{>a-#7L}ew z6!)vWTKie5bk?bF0~n!Ten~5ry1iC`1=4Rq) zN}a6ISP%e&h>=hs=#|$sSN&SmUW4kjnCqxrSiWCG%<=_dVnv5;*;HK9R&`|XGP zCkd&*teBX@(OFq$))>H;Id&P%O*{FVRQ%+q}^hz zHQ`(FW-54n7*2JoQeCC5W^U{N9z*SZawO_sez@16=(1E=c0_=f_*^Urs@l!0>$&x@ z0BL#PK6?zYF+Ml)O$wnyXAg24AEpr8lS>RUK%@#1NjzVx0pik8z?<7cefyH_LQ76o zeT>|y@VI~-HO+fm-|CFt)H`C_Y#`m@d32E^d&9iVX^d~b{3TLLxz^cc&IMtbUAf4D zQel+1X6d4;jAo{$Srl6hx3|jW67}RxM!MaagTL)fwme+*qfm?DJF9q~2HyZwLFQ>e zb*6DUtW}-P+Z*ypeEVfWcf1K-#-m)jnF1htX z`N0kiC$U9JEj5v4w&yf)c04!pa8pRA(rsq5G#7yT72I>BXh~=9%!?e!G9>US^w0`J zq_A_d-{@Uvyvc>}YrybNP`uj-m*J%|Y$t%ic1_e|w-Z=1oo4sm>(71r#z9e?`q0C! zL!hH9G{9Ho!XRDp-OAZyR?~wYRFE4t1eBF_8smj`MHckjdwHLy`~W_@h1~5r$3C+& zRbLWzEuc8Kb!`K-hmlp13zZ6et#s01BTg%}BiB1l6AQcP*M_5dgQklQ1K<6B?!lFEx-} zWtGnI0=##+*`c#UNcQUat?G0%qzdK(AmK=xtbT8MUl(Ppaz{O`*&vv|=~`TC5%teK z;xos<=~{lQFoDllal5PQxWn_aYx~%4BT%_?(DHs>8V`x?&4t^3xt^At?I|#&=cqB4 zP>!8AZ($kT-XY8iWIYX8?FgV?Xv{6-&dEIvQRLG1R?s1S9qu^~hk;ssw3qI}n4zq6 zwB+P)CDKmBAtwnVhFN{p?nzV!b_1!9mVCy)=!PN`wTRf&P2`V{tf{&e{Ik9WB$Wtq zgnreUhU4^Fnar$Jv0GA6!4$X*Lu`q2iadxaHOkt3pr3d!pr3~ z+nz^EM2`sa>&Z)F6)#SsGW)jMae0OO1CJX>K><;dTX;jJqHSi5w=^Jkb3W7ac>U>h zF(__90OgYpkeI>Dggb)0TjRodt#-B+GKJ^mE=m2CGd-t832T|@yU&!j_`;X0(0QOR zE+FIA87bx&eNFDR1kWeQ#dg3GOvkBX0R`A~iQuAJPk9?i8Lw1Zf4L4Bf$}VE3JCB& zd@urPnmPGeyp{=5I<^62qkY&OSfi_fnYJT<(6>lp7p=Zz_C<`4Rubq#%9i`E7ksBc zs>Yc!=G00c0VUwl?|jhY&%HrC0}YXp1Ed;WR6wk z$KsH~i6;1y9|KTfo>&ZEdd#h5KD|P(wv3aZhZ8;puI>EW>aEv#tfX^2Z$+4`008+!Dr+_r7cSgT7fOj~Rd%&V%^zcw}nv3yJnUw_`JP?ML^9EkLn7JMrGf?MRi|*BHMv7VzSXVvYpWtxHXk$d_;D zqlKsxz0-jVSqaZ?thY7S`FVg6Ak273O|P|iqN((FFGgp3gAZTa#2yqy6>X-YkRsm0 zmsukL2~}z^ABs)uMO+PfD^k#8lAbGa3-2UMudbi0VlZ462 z+X#|`FJ2cye38e;PJ_;CAIr8>UQ1Z{yr3Eq=NB*<8E+`-S0o-)ya$bga#&un$}-Mc z(8wux^TS~-Cj_X>Gsv@jwYSk=D;tQrBQE9n~w*Kf2}KC%*633BGn0I|@OBxN2eYnyZB@EzxKY z;ex~=uZme#S$$#YW3F}qFbyxcU*F$Lcf{3V%EFOGi6Ep_Mqyp=jpr6QyyD{IUxXsh z^_Dolv(d_V3X~?PwZ$_f_S`)F+!2xMRIoET|U!*-y{LK$jV1xD%|Dey2e~`8pI?1X5z{Ao_ z3$?m~hkHr7?h$U$fIqmguUM^P1EAdI<*lKe+~t(q!nRyvYezUBtEFt^Lc`q=7*@{h z-BV(QJZMy9&kyn~uJbEbH%|v$gP5(1B3(NRo~++Bm>vH~vXin@)Z3aY3@TP)rvgx6 zzT40qZ`5wn!e=&yveuWSWYHMeG0Qx{8^P5f$C+HE{tqwOWEtx$xJ0_D4&I{CEhLol z;kkz^sO&0uShR$pR<*hOo z05@qwZ{?~u)++5r=|NJkn7iCxG*A;s2u85P3-A<+1b>ywu9~>CgduI)A*>fjE8Eri zv9|*d(Ye!4i_S34*(X$S+u9X~|7Aa;r1CFOlo!4++8RD0rS?t=xA9!yJZ&dRAZYNKonhF zO*r!QYZUZ8E#_G4wFnzFiILpk%I8K*=~{cGFTD?g9s#|0R>+Gj);bF^)o-(<{SWnA zZ58eH$cJi~r^D^O@}ZJtSnZgHfJZ0ub)r&@OqaoX*~wgOt@vfVvXLMRJxt+F$_`Pl zO8t6(m}nA%5~v;PM2m=^I85ZhGKU$*=Jbac-L4ZHd@Nt1EOTFof*v*Fbw0&=VM3aU zfkD;x(+wuYO+8iwO%sIms@E*dFVrMCH+GVgKDEJNTTA&}>GD9e1dXY^**gX40d(Un z09F0U%!fYOPn&Nl22ke}dp=5Y%iS6FyVTpZ&5Ou`b` z<8c(cA25p|w98I5l8cWK8x|ud7U6QoT7W(>H^y7N-Wu8f<{?BX7HZXy=ijN{7;c$LfQ*G!n;;p!kBKP=MGQdZ6fL|9R%iyOI8< zXsHG63pY(vdWH}3wL#M?D6Ewncu0Z6*oZ6$O%*yCR4^l|bdga7$9|8HPItpVNi}+Z zGvr?+H>z9A(yS|*2}m3qeZ$3nItK!El)ea1#Kqg?b-e{$mK5&P0J$Sw7lPOq+Snaw z7C#WeX9ag870q(WY7&{UVybej5DISEptN7=g(krBQ=gYrYhkG1I_eN(nCwwiwo<_O z;k~pvp`-O{%kFsBfLfp@!PUG{nA6&?q1^V9m|`wB5eS%qyKM=;9KNu_JXRZE$oI2l z?W~DQOVROm{90N@O}->%qUb&1G2cNk!dI1-ArT5y6mZA^a^tsxGQKWZ?SV-7#FLVq zch5rd1SYRQt8brhbX@Tb0~LDnMJI~^{F`<_0%EpksdxLfHxIu6e0?DoZq&<|FT34J z=l?Y}y@V60qw?UQYt|}GHDy{p&JCy;4!Yqa#W5wo&e=*FKq~ zy9>3&I`hBW;Pw2FGDQ^gBOr@LOdPGE@=9c$;v@^oh$NOlW2$mB)!cc`?YV2sa9w~q zZz6Y2A6iQK;=)FAl=(iSsu)KTl z#{R&=gD_!apVA4r!U;=2iT&YbM4rHg%t4(RC;~E=B{#{z4USwjG6(I_lD4ktxe3+dZvgHfX#&t73aUy}w*5i3*c{Xg;RZygG%%?*gP0802H)}s9S6D5Q%aDU9^ zcV{LKUO`ZkvQ|DqNknbo4@pasp5IF?G~xV`#9WFeWa7phDU-;^h^U7IJOgt3YF_p$ znU-ICc)TBAK~2~(yX7}Ha2&8^0rm`6hu)cgcd#7F;1G{d&aKXErK`SVC0IKV!t*c% zyQ<54fAgF8y$D4v4}^lGC~sYV&WY^f`?r6xX!I%k0r-ec#PdpJ9jC-TY9h|G#!yAY z5(lS(w+~^;>M$Ml!5*`96~DZH5#Uf~Z+e(%y0B@2GI$xke^VRm*=|m&~%Wc#ja&lJ_3;1oRoJE&96b=-A1P(or zsuCn;EXmH@&^f}*F|Ko@8FqMQ1|&T+M3GN-tdjqbSjm@l1wn{93w#ZBMbcI+xDw)9Nl$Y#JaXec)mxCv01#a z*qb43bHt{Td40bL$61T?mq9&3sK;HhkH7aYe)Jlvqx*Hxv(*K(Aw6}6#oLRlS^^zp zyz;JsYcD(16Kd@#uqowPkLhS`l-9Y!zm{=pxHV#x#fvE)NqS?ItrUWA00(^oLpC|A zjajTsb5g(MnfATZU9ulhdA2fd00aVby@BCW@U8b97JSODe6iy&XA_ihFwD#hk7A9H zNikKj`HAiA#oJR6rL$%GHOh6#;Y1Pb$Aa6#9GfZ-h+LmzZ({$sr6+%U85$7V3V2xQjDTgqv*_8oOVWj-O+F@q=cItPo;qWyqH;|{2tiZ4FZWRO=O zuEMZV1A$ZO9xGt&RM$^oi+Pe_2a@t zOO&Ba7}7N6<`#^EY|NS`JNWfi9Vr0$GQ&lK7AYjF$3(@Ra~+E3&RTqroeyE5xBl^d zYRe%Y4dWgku{vPcMVs+`fKkkaY7iGSSE@ z++G4D!>pL^OSNWQy~?h_Cx~9sc6hqj5t=c45d562A&?lH>mb5O)%3EX4kHXn`#z+I7V;} zzZ?Q~7g70JYUuR{RYgh7r{q7X>bhXFpA!&qu0Eey%>^mqB(%{4_!_(-{jgS#5W zW%4@m{^|?pEx?z%GN{*({ZP#YRW(i3f)We4i%D7Igb@Vb8oHZ3f;%#()ZAdh`+cjX zMsMduhj<~50K+R)2C`^OdEPws4}@Ks{2&vh{R@6YO&v@v!Qp`8V-8a|KPq^q^TNJvuvE>>ZhWPjb2k@W^GIaF8vR7Ur>JZl zLMoPDxKC=Nmhjx(Kuf?&guaMgUd#N*spr8bU(yCrprwFpbivE)nDr{;6%V%Te7Jl3 zTxWCzi~^0^%ubrV-qvl9jW=vxYZ(en0dhqc=yxaxu`?IVPxfS4sC}pNZeQU0{*$Vd z2cX&Gd@Req!e$ldYY(y1&mg2j!M{|o-(;I3@hf1UIct-mq*6bm3h9#YOV{cPP<~B8 zueF%bw_lseg+%Yi=j_zWH!3MOaXizD%C5`19Kx?v33JOK{5X}M8AW=x>2ztvcRJ*g*b_-dM0F)%QF z#ckz=u(3SrP~p--Y_q4Ud`Ic$nU0lW;h&!FGs9%X)zU(PHsRyj`JP9)S@Q6{y2Fn) zq;hzlgr$0?Sm$ic^ujT3byQAuFu!e8_7zEy(yQZx)pc$kZ9m4T*e6g7DSsO^X z06`gIRR^t>TQJvN$kld)&)4jv@B5M7&5ab=NiZa{f6X-Ov+vyu>YsOq))hfdRdq%2 z6Yh`?pqNR22t+iupzCbz^SV$~<@jq&%#7Be(2`<66OmEY7=OVm4`K;Z>gOrt6LV+& zlr!f|Cll-nQbt2^mvnOj9_+-5b*a|6LtAJvsO)zRjNN*apv$qTs=(YOb zN%gI@kQTm+2n@M=O|o6b3COd`#yq-ar(+U1=UsIo>alY{Gum~XCtj|88A&Loi^L!^U3(o>{H5<_fH?a&*4qweScPsQKCjit0v=Of#D`GsPm}e}K z-0jCzmRXjTQjhfjWo;W@sM|u?rG?2$8Dcxb-Sc&NcW_4MfJn+AGl5{OPE2N&0#YLj zs=_m_NVLYsgg5ei7f<(&P3$qnNH)3fW?;uUHz%q(nI>7<(2O)H|{U& z_9R{e0hFYOSC++C%%GwP>L=<+=PQkqnIRe)1qrvSLLh)WdF4c+cI`{XRaDOrt4@^A zLEfaS5H0J7zwWr_GGBOldQmK?uBQ%Y@_Q#(J6p3OwQc1|>PVrAkrVR9%qK->{#}iY|7bqPW@L=SL8dN>iH1*LC_1aU;Jjr%;nFX1 z9c*p^IxmfAV$a$r>Qzw|^9F$#4?1(RNGNNxxjWw6j?Z{pUN?6CSLcbA1PP+kxbH4X z(WR4?<+iJzU5=&5RwGexDcvogmvgG}*}9>^j&icaG8vO_F|Je~EC<&$UGnQ`q z>4*YuqZ|<}Ye&rSS{SM(m!wg#5N{Qo@iBi(~nvxnDMwql=Fr@^`QD$ES86@{RXDlnVHm(mL4j428wz{#Y{MM zvr@P^q*@XoI-V5fr0qfmNh7JMSH#hRj~zvv+d15HBf+A5Ob|owBKk? z^`2h}WTAgJVywCoKf)M5j+o$jJW}IX-X{oiOnCX=QCC>+J5aN5Y#_b0*caeRZmRNd z;?BY(0Rux>6JAfxCzPx=FU^g`@E#srUzJO?SB?k<@edIjJ<5S^`1|!Di+U%F?QPKk znqk(`d#PeDWOe!Ughz!Oh^O{-!V^eK&?G&WC-hQZX+K~6HqeeDS(Hs)Vuuwmrp&Il z#Ry>I1#mj2p}HBn4nuc5=4l|{VEU!QR2_XFu4;>3sS9)D(J$BI1zj;W{GR)cKzMEe z`7Y0cZk&JMagiRB?wX(d7B4Z7_k)D+m;M?Qf?C_EZ}^klfp^>|RN~(vrYhBFDsZ0G zN8B`MS}c@aiexC8l>IOsVp93&U~1D0bl1hryR6ipw4m3#TBUaw@kOCCLFsvd6X-*Q zn^QWj*+jbT4%9m8^OxL-jzz$wWI8H=JubEOQQhUh}rDHX9UgBzdc0`6yusc}l z+T9*1OEl2IcSV3_uMKq*VG`{*Duzn1>PjX?$1U#0VvAg zk%sBa8D$WPq73-^3Zfi*Fw9BbofszvP}7+&e$4S+K1SSBE0iD)2Yp&)w^CW0(+-!) zo3Js<7dShCB6DUCXvP)Y7xUlohlK1eoy=z<<+0QJ8uJVRej!1mRE_*eiLbtS{_>&I zSlyE!>a7$)!^L<%iGUf%cdsVo$=+kr<8hyFx_%7fqK2o;Ltkp+eIr> z7c@#CGWASR!}lD4gNR)A z9mk1am5!`g^FwtJsJY9KE`l0OXb35M$;y*(>tiSo+Lidz0ui@UrO7+0yzx>%oNvD6 zZZ^UtyWaW5K7OT##A@FZ&C33Qd>zRmXZ#BpZtR8qcL$Zr`{8b*E{gU|IMtjf8wwb6 z+z>y3WuSF{#R)7I-5<7BlKmsWiqG9n{VSWgn!|(e{O34AZ`=2vs%6x zDqUNC@A?k?7rsNLY#ntcQ^VC*hg?B3UU?IY5F~o$NjRX0y5xgr)&W;nmG5}pBefA}+fdey5jqg$7F z>sD9sQ(p_$?^vcMYZ8-wOCBA-0$!=C`e~liS(WR1*CN$gVp?Y2JJ?4nY`TpXS#w;q zxm$R0PuEU`y=y#VKb8Xy>VVLLALUPdsb{D8ZmMxIT2WlO8nR6VRk}!*?S97t^z!WL z6USco$6|j$0CvOK02{ykwHuBXFCi)$;sRYCps=JhvMv@xUFx)wmG=Ufj-Qo<>1+P- zc&O+>Pybi`zWj*LWDnazvFlut!oZ3?(l7?q>ZDaszA)J-M5^guK=G+J{BP0Tz@Yf? znPk(Rb;Ov=qaqUYXgumR*Qn8Ctk1g4`tuLfNbJ&lfqVcu_< zQ}KLB%}%tC-OMASVXfm?Ps64aPpiQ)pk!56?|3Xhrf+`pYbnRgAmu+52H9NMr?5z( zkY$t{`PEGyAorfU!ccqUI8<9bc@saXwVVFeVAz1|%a(ZcMEriKxlsj!q9BxK=%IGd zC(M!iT_@d`(nz`dOw~t@fe}{Q7H!&62($#p2QWqTENDx~!$wdY{9<=7X4B2}W7+j6 znl)zX@NjwAQOgrV1zD1sb~)|Yf|O*bSn6$#RA18b;^{Toj#8PK2dm;= zwN7;ms5ryn6d+$}YRFajw zSIW5Txa{$Lyb#@=-+dgv<9i(6Ki~gdh3mRr&+(Y&c|I1s>D`ar6ZNu-PK!>rl0^=K z&OCn&k*VN}!%7bwPvu$8z@L#<>RD+?9B&16sc1Q1cklJtZ;F)jM1ehMUn^W&8#msV z*;yNxv`;c%)d(0q-&PbSOFy?#8S-&qG`{0`agoS8fAO=n%l~mExGo|N;0B$T2SwG* zU-SL^# zgyoqjlzT^}>XceZGop$>A?m58)+iDp3<$d-sq6a}n?r6ivd`;PDO@EAvI(x&0C*kkNlFcHeu2qjmwd|6%<}

Vh_B6WL~^J?f*)a0W$;4drGe9 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 qjDL0FyTafG3*L^}0ozw*HQM`@> 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

f}GT!1OQjyJ}$Z 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

~AqYypxMuAN)> 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

%~BqqFQwZ$GJ9kA%!w?m?*q7fCwmTAME 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

hn{ZlLl8jhwFh2pUB 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

L?)tJ$Iz3vgA$YtNKk>8}!>m9*nsF9}BP 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%0Daemp01#<5 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 Date: Fri, 27 Sep 2024 17:22:46 -0700 Subject: [PATCH 110/185] fix: Fix online pg import (#4581) * fix postgres import issue Signed-off-by: cmuhao * fix postgres import issue Signed-off-by: cmuhao * fix postgres import issue Signed-off-by: cmuhao * fix postgres import issue Signed-off-by: cmuhao * fix lint Signed-off-by: cmuhao --------- Signed-off-by: cmuhao --- .../feast/infra/online_stores/contrib/postgres.py | 4 +--- .../contrib/singlestore_online_store/singlestore.py | 10 ++-------- sdk/python/feast/infra/online_stores/helpers.py | 8 ++++++++ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/sdk/python/feast/infra/online_stores/contrib/postgres.py b/sdk/python/feast/infra/online_stores/contrib/postgres.py index 8125da33be..036c0b6b92 100644 --- a/sdk/python/feast/infra/online_stores/contrib/postgres.py +++ b/sdk/python/feast/infra/online_stores/contrib/postgres.py @@ -23,9 +23,7 @@ from feast import Entity from feast.feature_view import FeatureView from feast.infra.key_encoding_utils import get_list_val_str, serialize_entity_key -from feast.infra.online_stores.contrib.singlestore_online_store.singlestore import ( - _to_naive_utc, -) +from feast.infra.online_stores.helpers import _to_naive_utc from feast.infra.online_stores.online_store import OnlineStore from feast.infra.utils.postgres.connection_utils import ( _get_conn, diff --git a/sdk/python/feast/infra/online_stores/contrib/singlestore_online_store/singlestore.py b/sdk/python/feast/infra/online_stores/contrib/singlestore_online_store/singlestore.py index 3e921afcea..d78289c867 100644 --- a/sdk/python/feast/infra/online_stores/contrib/singlestore_online_store/singlestore.py +++ b/sdk/python/feast/infra/online_stores/contrib/singlestore_online_store/singlestore.py @@ -1,7 +1,7 @@ from __future__ import absolute_import from collections import defaultdict -from datetime import datetime, timezone +from datetime import datetime from typing import Any, Callable, Dict, List, Literal, Optional, Sequence, Tuple import singlestoredb @@ -11,6 +11,7 @@ from feast import Entity, FeatureView, RepoConfig from feast.infra.key_encoding_utils import serialize_entity_key +from feast.infra.online_stores.helpers import _to_naive_utc from feast.infra.online_stores.online_store import OnlineStore from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto from feast.protos.feast.types.Value_pb2 import Value as ValueProto @@ -225,10 +226,3 @@ def _drop_table_and_index(cur: Cursor, project: str, table: FeatureView) -> None def _table_id(project: str, table: FeatureView) -> str: return f"{project}_{table.name}" - - -def _to_naive_utc(ts: datetime) -> datetime: - if ts.tzinfo is None: - return ts - else: - return ts.astimezone(tz=timezone.utc).replace(tzinfo=None) diff --git a/sdk/python/feast/infra/online_stores/helpers.py b/sdk/python/feast/infra/online_stores/helpers.py index 0e2fdb3500..409a1eb212 100644 --- a/sdk/python/feast/infra/online_stores/helpers.py +++ b/sdk/python/feast/infra/online_stores/helpers.py @@ -1,4 +1,5 @@ import struct +from datetime import datetime, timezone from typing import Any, List import mmh3 @@ -62,3 +63,10 @@ def compute_entity_id( entity_key_serialization_version=entity_key_serialization_version, ) ).hex() + + +def _to_naive_utc(ts: datetime) -> datetime: + if ts.tzinfo is None: + return ts + else: + return ts.astimezone(tz=timezone.utc).replace(tzinfo=None) From 11c00d43fd1b2c5caf4d49f705bd55c704edae8a Mon Sep 17 00:00:00 2001 From: Hao Xu Date: Sat, 28 Sep 2024 01:58:48 -0700 Subject: [PATCH 111/185] fix: Fix vector store config (#4583) --- docs/reference/alpha-vector-database.md | 2 +- docs/reference/online-stores/postgres.md | 4 ++-- .../infra/online_stores/contrib/elasticsearch.py | 10 ++-------- .../infra/online_stores/contrib/postgres.py | 15 +++++---------- sdk/python/feast/infra/online_stores/sqlite.py | 15 +++++---------- .../feast/infra/online_stores/vector_store.py | 16 ++++++++++++++++ .../universal/online_store/postgres.py | 2 +- .../unit/online_store/test_online_retrieval.py | 2 +- 8 files changed, 33 insertions(+), 33 deletions(-) create mode 100644 sdk/python/feast/infra/online_stores/vector_store.py diff --git a/docs/reference/alpha-vector-database.md b/docs/reference/alpha-vector-database.md index b9ce7f408a..06909bd565 100644 --- a/docs/reference/alpha-vector-database.md +++ b/docs/reference/alpha-vector-database.md @@ -40,7 +40,7 @@ registry: path: postgresql://@localhost:5432/feast online_store: type: postgres - pgvector_enabled: true + vector_enabled: true vector_len: 384 host: 127.0.0.1 port: 5432 diff --git a/docs/reference/online-stores/postgres.md b/docs/reference/online-stores/postgres.md index 77a9408d2b..e4e2173ccd 100644 --- a/docs/reference/online-stores/postgres.md +++ b/docs/reference/online-stores/postgres.md @@ -30,7 +30,7 @@ online_store: sslkey_path: /path/to/client-key.pem sslcert_path: /path/to/client-cert.pem sslrootcert_path: /path/to/server-ca.pem - pgvector_enabled: false + vector_enabled: false vector_len: 512 ``` {% endcode %} @@ -65,7 +65,7 @@ To compare this set of functionality against other online stores, please see the ## PGVector The Postgres online store supports the use of [PGVector](https://github.com/pgvector/pgvector) for storing feature values. -To enable PGVector, set `pgvector_enabled: true` in the online store configuration. +To enable PGVector, set `vector_enabled: true` in the online store configuration. The `vector_len` parameter can be used to specify the length of the vector. The default value is 512. diff --git a/sdk/python/feast/infra/online_stores/contrib/elasticsearch.py b/sdk/python/feast/infra/online_stores/contrib/elasticsearch.py index a0c25b931a..0152ca330c 100644 --- a/sdk/python/feast/infra/online_stores/contrib/elasticsearch.py +++ b/sdk/python/feast/infra/online_stores/contrib/elasticsearch.py @@ -14,13 +14,14 @@ serialize_entity_key, ) from feast.infra.online_stores.online_store import OnlineStore +from feast.infra.online_stores.vector_store import VectorStoreConfig from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto from feast.protos.feast.types.Value_pb2 import Value as ValueProto from feast.repo_config import FeastConfigBaseModel from feast.utils import _build_retrieve_online_document_record, to_naive_utc -class ElasticSearchOnlineStoreConfig(FeastConfigBaseModel): +class ElasticSearchOnlineStoreConfig(FeastConfigBaseModel, VectorStoreConfig): """ Configuration for the ElasticSearch online store. NOTE: The class *must* end with the `OnlineStoreConfig` suffix. @@ -38,13 +39,6 @@ class ElasticSearchOnlineStoreConfig(FeastConfigBaseModel): # The number of rows to write in a single batch write_batch_size: Optional[int] = 40 - # The length of the vector value - vector_len: Optional[int] = 512 - - # The vector similarity metric to use in KNN search - # more details: https://www.elastic.co/guide/en/elasticsearch/reference/current/dense-vector.html - similarity: Optional[str] = "cosine" - class ElasticSearchOnlineStore(OnlineStore): _client: Optional[Elasticsearch] = None diff --git a/sdk/python/feast/infra/online_stores/contrib/postgres.py b/sdk/python/feast/infra/online_stores/contrib/postgres.py index 036c0b6b92..7c099c80ec 100644 --- a/sdk/python/feast/infra/online_stores/contrib/postgres.py +++ b/sdk/python/feast/infra/online_stores/contrib/postgres.py @@ -25,6 +25,7 @@ from feast.infra.key_encoding_utils import get_list_val_str, serialize_entity_key from feast.infra.online_stores.helpers import _to_naive_utc from feast.infra.online_stores.online_store import OnlineStore +from feast.infra.online_stores.vector_store import VectorStoreConfig from feast.infra.utils.postgres.connection_utils import ( _get_conn, _get_conn_async, @@ -45,15 +46,9 @@ } -class PostgreSQLOnlineStoreConfig(PostgreSQLConfig): +class PostgreSQLOnlineStoreConfig(PostgreSQLConfig, VectorStoreConfig): type: Literal["postgres"] = "postgres" - # Whether to enable the pgvector extension for vector similarity search - pgvector_enabled: Optional[bool] = False - - # If pgvector is enabled, the length of the vector field - vector_len: Optional[int] = 512 - class PostgreSQLOnlineStore(OnlineStore): _conn: Optional[Connection] = None @@ -118,7 +113,7 @@ def online_write_batch( for feature_name, val in values.items(): vector_val = None - if config.online_store.pgvector_enabled: + if config.online_store.vector_enabled: vector_val = get_list_val_str(val) insert_values.append( ( @@ -302,7 +297,7 @@ def update( for table in tables_to_keep: table_name = _table_id(project, table) - if config.online_store.pgvector_enabled: + if config.online_store.vector_enabled: vector_value_type = f"vector({config.online_store.vector_len})" else: # keep the vector_value_type as BYTEA if pgvector is not enabled, to maintain compatibility @@ -380,7 +375,7 @@ def retrieve_online_documents( """ project = config.project - if not config.online_store.pgvector_enabled: + if not config.online_store.vector_enabled: raise ValueError( "pgvector is not enabled in the online store configuration" ) diff --git a/sdk/python/feast/infra/online_stores/sqlite.py b/sdk/python/feast/infra/online_stores/sqlite.py index 061a766b8c..1b79b1a94b 100644 --- a/sdk/python/feast/infra/online_stores/sqlite.py +++ b/sdk/python/feast/infra/online_stores/sqlite.py @@ -29,6 +29,7 @@ from feast.infra.infra_object import SQLITE_INFRA_OBJECT_CLASS_TYPE, InfraObject from feast.infra.key_encoding_utils import serialize_entity_key from feast.infra.online_stores.online_store import OnlineStore +from feast.infra.online_stores.vector_store import VectorStoreConfig from feast.protos.feast.core.InfraObject_pb2 import InfraObject as InfraObjectProto from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto from feast.protos.feast.core.SqliteTable_pb2 import SqliteTable as SqliteTableProto @@ -38,7 +39,7 @@ from feast.utils import _build_retrieve_online_document_record, to_naive_utc -class SqliteOnlineStoreConfig(FeastConfigBaseModel): +class SqliteOnlineStoreConfig(FeastConfigBaseModel, VectorStoreConfig): """Online store config for local (SQLite-based) store""" type: Literal["sqlite", "feast.infra.online_stores.sqlite.SqliteOnlineStore"] = ( @@ -49,12 +50,6 @@ class SqliteOnlineStoreConfig(FeastConfigBaseModel): path: StrictStr = "data/online.db" """ (optional) Path to sqlite db """ - vec_enabled: Optional[bool] = False - """ (optional) Enable or disable sqlite-vss for vector search""" - - vector_len: Optional[int] = 512 - """ (optional) Length of the vector to be stored in the database""" - class SqliteOnlineStore(OnlineStore): """ @@ -83,7 +78,7 @@ def _get_conn(self, config: RepoConfig): if not self._conn: db_path = self._get_db_path(config) self._conn = _initialize_conn(db_path) - if sys.version_info[0:2] == (3, 10) and config.online_store.vec_enabled: + if sys.version_info[0:2] == (3, 10) and config.online_store.vector_enabled: import sqlite_vec # noqa: F401 self._conn.enable_load_extension(True) # type: ignore @@ -121,7 +116,7 @@ def online_write_batch( table_name = _table_id(project, table) for feature_name, val in values.items(): - if config.online_store.vec_enabled: + if config.online_store.vector_enabled: vector_bin = serialize_f32( val.float_list_val.val, config.online_store.vector_len ) # type: ignore @@ -321,7 +316,7 @@ def retrieve_online_documents( """ project = config.project - if not config.online_store.vec_enabled: + if not config.online_store.vector_enabled: raise ValueError("sqlite-vss is not enabled in the online store config") conn = self._get_conn(config) diff --git a/sdk/python/feast/infra/online_stores/vector_store.py b/sdk/python/feast/infra/online_stores/vector_store.py new file mode 100644 index 0000000000..051f9bcaed --- /dev/null +++ b/sdk/python/feast/infra/online_stores/vector_store.py @@ -0,0 +1,16 @@ +from typing import Optional + + +class VectorStoreConfig: + # Whether to enable the online store for vector similarity search, + # This is only applicable for online store. + vector_enabled: Optional[bool] = False + + # If vector is enabled, the length of the vector field + vector_len: Optional[int] = 512 + + # The vector similarity metric to use in KNN search + # It is helpful for vector database that does not support config at retrieval runtime + # E.g. Elasticsearch dense_vector field at + # https://www.elastic.co/guide/en/elasticsearch/reference/current/dense-vector.html + similarity: Optional[str] = "cosine" diff --git a/sdk/python/tests/integration/feature_repos/universal/online_store/postgres.py b/sdk/python/tests/integration/feature_repos/universal/online_store/postgres.py index 622ee99e14..7ff72a48a3 100644 --- a/sdk/python/tests/integration/feature_repos/universal/online_store/postgres.py +++ b/sdk/python/tests/integration/feature_repos/universal/online_store/postgres.py @@ -67,7 +67,7 @@ def create_online_store(self) -> Dict[str, Any]: "user": "root", "password": "test!@#$%", "database": "test", - "pgvector_enabled": True, + "vector_enabled": True, "vector_len": 2, "port": self.container.get_exposed_port(5432), } diff --git a/sdk/python/tests/unit/online_store/test_online_retrieval.py b/sdk/python/tests/unit/online_store/test_online_retrieval.py index 0a4880164f..83184643f3 100644 --- a/sdk/python/tests/unit/online_store/test_online_retrieval.py +++ b/sdk/python/tests/unit/online_store/test_online_retrieval.py @@ -441,7 +441,7 @@ def test_sqlite_get_online_documents() -> None: with runner.local_repo( get_example_repo("example_feature_repo_1.py"), "file" ) as store: - store.config.online_store.vec_enabled = True + store.config.online_store.vector_enabled = True store.config.online_store.vector_len = vector_length # Write some data to two tables document_embeddings_fv = store.get_feature_view(name="document_embeddings") From 2ef8dac546a7d61abae8f32ac32e355d0369919f Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Sun, 29 Sep 2024 15:06:07 -0400 Subject: [PATCH 112/185] chore: Updated unit test (#4586) updated unit test Signed-off-by: Francisco Javier Arceo --- sdk/python/tests/unit/online_store/test_online_writes.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sdk/python/tests/unit/online_store/test_online_writes.py b/sdk/python/tests/unit/online_store/test_online_writes.py index 0f7547a93b..823621abca 100644 --- a/sdk/python/tests/unit/online_store/test_online_writes.py +++ b/sdk/python/tests/unit/online_store/test_online_writes.py @@ -75,6 +75,8 @@ def setUp(self): online=True, source=driver_stats_source, ) + # Before apply() join_keys is empty + assert driver_stats_fv.join_keys == [] @on_demand_feature_view( sources=[driver_stats_fv[["conv_rate", "acc_rate"]]], @@ -100,6 +102,8 @@ def test_view(inputs: dict[str, Any]) -> dict[str, Any]: test_view, ] ) + # after apply() join_keys is [driver] + assert driver_stats_fv.join_keys == [driver] self.store.write_to_online_store( feature_view_name="driver_hourly_stats", df=driver_df ) From 43c49e25d4b722d650c4744ef08204d3b2a31e33 Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Sun, 29 Sep 2024 22:39:58 -0400 Subject: [PATCH 113/185] chore: Updated test to include join_key (#4587) Signed-off-by: Francisco Javier Arceo --- sdk/python/tests/unit/online_store/test_online_writes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/python/tests/unit/online_store/test_online_writes.py b/sdk/python/tests/unit/online_store/test_online_writes.py index 823621abca..7927b564c6 100644 --- a/sdk/python/tests/unit/online_store/test_online_writes.py +++ b/sdk/python/tests/unit/online_store/test_online_writes.py @@ -103,7 +103,7 @@ def test_view(inputs: dict[str, Any]) -> dict[str, Any]: ] ) # after apply() join_keys is [driver] - assert driver_stats_fv.join_keys == [driver] + assert driver_stats_fv.join_keys == [driver.join_key] self.store.write_to_online_store( feature_view_name="driver_hourly_stats", df=driver_df ) From ff3d59716f0160862e5510245dce017b6627c048 Mon Sep 17 00:00:00 2001 From: Tommy Hughes IV Date: Mon, 30 Sep 2024 12:24:11 -0500 Subject: [PATCH 114/185] chore: Rename Helm Operator (#4569) rename helm operator directory Signed-off-by: Tommy Hughes --- .github/workflows/publish.yml | 2 +- .releaserc.js | 2 +- Makefile | 12 ++++++------ docs/how-to-guides/running-feast-in-production.md | 2 +- .../.gitignore | 0 .../Dockerfile | 0 .../{feast-operator => feast-helm-operator}/Makefile | 4 ++-- .../{feast-operator => feast-helm-operator}/PROJECT | 2 +- .../README.md | 0 .../bases/charts.feast.dev_feastfeatureservers.yaml | 0 .../config/crd/kustomization.yaml | 0 .../config/default/kustomization.yaml | 4 ++-- .../config/manager/kustomization.yaml | 2 +- .../config/manager/manager.yaml | 10 +++++----- .../config/manifests/kustomization.yaml | 2 +- .../config/rbac/feastfeatureserver_editor_role.yaml | 4 ++-- .../rbac/feastfeatureserver_editor_rolebinding.yaml | 4 ++-- .../config/rbac/kustomization.yaml | 0 .../config/rbac/leader_election_role.yaml | 4 ++-- .../config/rbac/leader_election_role_binding.yaml | 4 ++-- .../config/rbac/role.yaml | 4 ++-- .../config/rbac/role_binding.yaml | 4 ++-- .../config/rbac/service_account.yaml | 4 ++-- .../samples/charts_v1alpha1_feastfeatureserver.yaml | 0 .../config/samples/kustomization.yaml | 0 .../config/scorecard/bases/config.yaml | 0 .../config/scorecard/kustomization.yaml | 0 .../config/scorecard/patches/basic.config.yaml | 0 .../config/scorecard/patches/olm.config.yaml | 0 .../helm-charts/feast-feature-server | 0 .../watches.yaml | 0 infra/scripts/release/files_to_bump.txt | 4 ++-- 32 files changed, 37 insertions(+), 37 deletions(-) rename infra/{feast-operator => feast-helm-operator}/.gitignore (100%) rename infra/{feast-operator => feast-helm-operator}/Dockerfile (100%) rename infra/{feast-operator => feast-helm-operator}/Makefile (98%) rename infra/{feast-operator => feast-helm-operator}/PROJECT (94%) rename infra/{feast-operator => feast-helm-operator}/README.md (100%) rename infra/{feast-operator => feast-helm-operator}/config/crd/bases/charts.feast.dev_feastfeatureservers.yaml (100%) rename infra/{feast-operator => feast-helm-operator}/config/crd/kustomization.yaml (100%) rename infra/{feast-operator => feast-helm-operator}/config/default/kustomization.yaml (85%) rename infra/{feast-operator => feast-helm-operator}/config/manager/kustomization.yaml (77%) rename infra/{feast-operator => feast-helm-operator}/config/manager/manager.yaml (92%) rename infra/{feast-operator => feast-helm-operator}/config/manifests/kustomization.yaml (76%) rename infra/{feast-operator => feast-helm-operator}/config/rbac/feastfeatureserver_editor_role.yaml (87%) rename infra/{feast-operator => feast-helm-operator}/config/rbac/feastfeatureserver_editor_rolebinding.yaml (82%) rename infra/{feast-operator => feast-helm-operator}/config/rbac/kustomization.yaml (100%) rename infra/{feast-operator => feast-helm-operator}/config/rbac/leader_election_role.yaml (86%) rename infra/{feast-operator => feast-helm-operator}/config/rbac/leader_election_role_binding.yaml (81%) rename infra/{feast-operator => feast-helm-operator}/config/rbac/role.yaml (82%) rename infra/{feast-operator => feast-helm-operator}/config/rbac/role_binding.yaml (81%) rename infra/{feast-operator => feast-helm-operator}/config/rbac/service_account.yaml (72%) rename infra/{feast-operator => feast-helm-operator}/config/samples/charts_v1alpha1_feastfeatureserver.yaml (100%) rename infra/{feast-operator => feast-helm-operator}/config/samples/kustomization.yaml (100%) rename infra/{feast-operator => feast-helm-operator}/config/scorecard/bases/config.yaml (100%) rename infra/{feast-operator => feast-helm-operator}/config/scorecard/kustomization.yaml (100%) rename infra/{feast-operator => feast-helm-operator}/config/scorecard/patches/basic.config.yaml (100%) rename infra/{feast-operator => feast-helm-operator}/config/scorecard/patches/olm.config.yaml (100%) rename infra/{feast-operator => feast-helm-operator}/helm-charts/feast-feature-server (100%) rename infra/{feast-operator => feast-helm-operator}/watches.yaml (100%) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0342943313..c2a5bd21ee 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -49,7 +49,7 @@ jobs: needs: [get-version, publish-python-sdk] strategy: matrix: - component: [feature-server, feature-server-java, feature-transformation-server, feast-operator] + component: [feature-server, feature-server-java, feature-transformation-server, feast-helm-operator] env: MAVEN_CACHE: gs://feast-templocation-kf-feast/.m2.2020-08-19.tar REGISTRY: feastdev diff --git a/.releaserc.js b/.releaserc.js index ee9c62a04d..60084dc24f 100644 --- a/.releaserc.js +++ b/.releaserc.js @@ -66,7 +66,7 @@ module.exports = { "CHANGELOG.md", "java/pom.xml", "infra/charts/**/*.*", - "infra/feast-operator/**/*", + "infra/feast-helm-operator/**/*", "ui/package.json", "sdk/python/feast/ui/package.json", "sdk/python/feast/ui/yarn.lock" diff --git a/Makefile b/Makefile index 6831a58337..10187b8ebc 100644 --- a/Makefile +++ b/Makefile @@ -430,15 +430,15 @@ build-feature-server-java-docker: -t $(REGISTRY)/feature-server-java:$(VERSION) \ -f java/infra/docker/feature-server/Dockerfile --load . -push-feast-operator-docker: - cd infra/feast-operator && \ - IMAGE_TAG_BASE=$(REGISTRY)/feast-operator \ +push-feast-helm-operator-docker: + cd infra/feast-helm-operator && \ + IMAGE_TAG_BASE=$(REGISTRY)/feast-helm-operator \ VERSION=$(VERSION) \ $(MAKE) docker-push -build-feast-operator-docker: - cd infra/feast-operator && \ - IMAGE_TAG_BASE=$(REGISTRY)/feast-operator \ +build-feast-helm-operator-docker: + cd infra/feast-helm-operator && \ + IMAGE_TAG_BASE=$(REGISTRY)/feast-helm-operator \ VERSION=$(VERSION) \ $(MAKE) docker-build diff --git a/docs/how-to-guides/running-feast-in-production.md b/docs/how-to-guides/running-feast-in-production.md index dc8b87e34f..021a10ac1c 100644 --- a/docs/how-to-guides/running-feast-in-production.md +++ b/docs/how-to-guides/running-feast-in-production.md @@ -225,7 +225,7 @@ helm install feast-release feast-charts/feast-feature-server \ This will deploy a single service. The service must have read access to the registry file on cloud storage and to the online store (e.g. via [podAnnotations](https://kubernetes-on-aws.readthedocs.io/en/latest/user-guide/iam-roles.html)). It will keep a copy of the registry in their memory and periodically refresh it, so expect some delays in update propagation in exchange for better performance. -> Alternatively, deploy the same helm chart with a [Kubernetes Operator](/infra/feast-operator). +> Alternatively, deploy the same helm chart with a [Kubernetes Operator](/infra/feast-helm-operator). ## 5. Using environment variables in your yaml configuration diff --git a/infra/feast-operator/.gitignore b/infra/feast-helm-operator/.gitignore similarity index 100% rename from infra/feast-operator/.gitignore rename to infra/feast-helm-operator/.gitignore diff --git a/infra/feast-operator/Dockerfile b/infra/feast-helm-operator/Dockerfile similarity index 100% rename from infra/feast-operator/Dockerfile rename to infra/feast-helm-operator/Dockerfile diff --git a/infra/feast-operator/Makefile b/infra/feast-helm-operator/Makefile similarity index 98% rename from infra/feast-operator/Makefile rename to infra/feast-helm-operator/Makefile index f52911f432..6712a37b4a 100644 --- a/infra/feast-operator/Makefile +++ b/infra/feast-helm-operator/Makefile @@ -28,8 +28,8 @@ BUNDLE_METADATA_OPTS ?= $(BUNDLE_CHANNELS) $(BUNDLE_DEFAULT_CHANNEL) # This variable is used to construct full image tags for bundle and catalog images. # # For example, running 'make bundle-build bundle-push catalog-build catalog-push' will build and push both -# feastdev/feast-operator-bundle:$VERSION and feastdev/feast-operator-catalog:$VERSION. -IMAGE_TAG_BASE ?= feastdev/feast-operator +# feastdev/feast-helm-operator-bundle:$VERSION and feastdev/feast-helm-operator-catalog:$VERSION. +IMAGE_TAG_BASE ?= feastdev/feast-helm-operator # BUNDLE_IMG defines the image:tag used for the bundle. # You can use it as an arg. (E.g make bundle-build BUNDLE_IMG=/:) diff --git a/infra/feast-operator/PROJECT b/infra/feast-helm-operator/PROJECT similarity index 94% rename from infra/feast-operator/PROJECT rename to infra/feast-helm-operator/PROJECT index 56b2532d85..fefe00db1e 100644 --- a/infra/feast-operator/PROJECT +++ b/infra/feast-helm-operator/PROJECT @@ -8,7 +8,7 @@ layout: plugins: manifests.sdk.operatorframework.io/v2: {} scorecard.sdk.operatorframework.io/v2: {} -projectName: feast-operator +projectName: feast-helm-operator resources: - api: crdVersion: v1 diff --git a/infra/feast-operator/README.md b/infra/feast-helm-operator/README.md similarity index 100% rename from infra/feast-operator/README.md rename to infra/feast-helm-operator/README.md diff --git a/infra/feast-operator/config/crd/bases/charts.feast.dev_feastfeatureservers.yaml b/infra/feast-helm-operator/config/crd/bases/charts.feast.dev_feastfeatureservers.yaml similarity index 100% rename from infra/feast-operator/config/crd/bases/charts.feast.dev_feastfeatureservers.yaml rename to infra/feast-helm-operator/config/crd/bases/charts.feast.dev_feastfeatureservers.yaml diff --git a/infra/feast-operator/config/crd/kustomization.yaml b/infra/feast-helm-operator/config/crd/kustomization.yaml similarity index 100% rename from infra/feast-operator/config/crd/kustomization.yaml rename to infra/feast-helm-operator/config/crd/kustomization.yaml diff --git a/infra/feast-operator/config/default/kustomization.yaml b/infra/feast-helm-operator/config/default/kustomization.yaml similarity index 85% rename from infra/feast-operator/config/default/kustomization.yaml rename to infra/feast-helm-operator/config/default/kustomization.yaml index 6cd524d519..3252e11de8 100644 --- a/infra/feast-operator/config/default/kustomization.yaml +++ b/infra/feast-helm-operator/config/default/kustomization.yaml @@ -1,12 +1,12 @@ # Adds namespace to all resources. -namespace: feast-operator-system +namespace: feast-helm-operator-system # Value of this field is prepended to the # names of all resources, e.g. a deployment named # "wordpress" becomes "alices-wordpress". # Note that it should also match with the prefix (text before '-') of the namespace # field above. -namePrefix: feast-operator- +namePrefix: feast-helm-operator- # Labels to add to all resources and selectors. #labels: diff --git a/infra/feast-operator/config/manager/kustomization.yaml b/infra/feast-helm-operator/config/manager/kustomization.yaml similarity index 77% rename from infra/feast-operator/config/manager/kustomization.yaml rename to infra/feast-helm-operator/config/manager/kustomization.yaml index aba3224be6..0d27c2149d 100644 --- a/infra/feast-operator/config/manager/kustomization.yaml +++ b/infra/feast-helm-operator/config/manager/kustomization.yaml @@ -4,5 +4,5 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization images: - name: controller - newName: feastdev/feast-operator + newName: feastdev/feast-helm-operator newTag: 0.40.0 diff --git a/infra/feast-operator/config/manager/manager.yaml b/infra/feast-helm-operator/config/manager/manager.yaml similarity index 92% rename from infra/feast-operator/config/manager/manager.yaml rename to infra/feast-helm-operator/config/manager/manager.yaml index d65e8a7890..03f1fe173d 100644 --- a/infra/feast-operator/config/manager/manager.yaml +++ b/infra/feast-helm-operator/config/manager/manager.yaml @@ -6,8 +6,8 @@ metadata: app.kubernetes.io/name: namespace app.kubernetes.io/instance: system app.kubernetes.io/component: manager - app.kubernetes.io/created-by: feast-operator - app.kubernetes.io/part-of: feast-operator + app.kubernetes.io/created-by: feast-helm-operator + app.kubernetes.io/part-of: feast-helm-operator app.kubernetes.io/managed-by: kustomize name: system --- @@ -21,8 +21,8 @@ metadata: app.kubernetes.io/name: deployment app.kubernetes.io/instance: controller-manager app.kubernetes.io/component: manager - app.kubernetes.io/created-by: feast-operator - app.kubernetes.io/part-of: feast-operator + app.kubernetes.io/created-by: feast-helm-operator + app.kubernetes.io/part-of: feast-helm-operator app.kubernetes.io/managed-by: kustomize spec: selector: @@ -68,7 +68,7 @@ spec: containers: - args: - --leader-elect - - --leader-election-id=feast-operator + - --leader-election-id=feast-helm-operator image: controller:latest name: manager securityContext: diff --git a/infra/feast-operator/config/manifests/kustomization.yaml b/infra/feast-helm-operator/config/manifests/kustomization.yaml similarity index 76% rename from infra/feast-operator/config/manifests/kustomization.yaml rename to infra/feast-helm-operator/config/manifests/kustomization.yaml index 392c30f6b6..90dccb22c3 100644 --- a/infra/feast-operator/config/manifests/kustomization.yaml +++ b/infra/feast-helm-operator/config/manifests/kustomization.yaml @@ -1,7 +1,7 @@ # These resources constitute the fully configured set of manifests # used to generate the 'manifests/' directory in a bundle. resources: -- bases/feast-operator.clusterserviceversion.yaml +- bases/feast-helm-operator.clusterserviceversion.yaml - ../default - ../samples - ../scorecard diff --git a/infra/feast-operator/config/rbac/feastfeatureserver_editor_role.yaml b/infra/feast-helm-operator/config/rbac/feastfeatureserver_editor_role.yaml similarity index 87% rename from infra/feast-operator/config/rbac/feastfeatureserver_editor_role.yaml rename to infra/feast-helm-operator/config/rbac/feastfeatureserver_editor_role.yaml index f03ac20fdd..4749204cf7 100644 --- a/infra/feast-operator/config/rbac/feastfeatureserver_editor_role.yaml +++ b/infra/feast-helm-operator/config/rbac/feastfeatureserver_editor_role.yaml @@ -6,8 +6,8 @@ metadata: app.kubernetes.io/name: clusterrole app.kubernetes.io/instance: feastfeatureserver-editor-role app.kubernetes.io/component: rbac - app.kubernetes.io/created-by: feast-operator - app.kubernetes.io/part-of: feast-operator + app.kubernetes.io/created-by: feast-helm-operator + app.kubernetes.io/part-of: feast-helm-operator app.kubernetes.io/managed-by: kustomize name: feastfeatureserver-editor-role rules: diff --git a/infra/feast-operator/config/rbac/feastfeatureserver_editor_rolebinding.yaml b/infra/feast-helm-operator/config/rbac/feastfeatureserver_editor_rolebinding.yaml similarity index 82% rename from infra/feast-operator/config/rbac/feastfeatureserver_editor_rolebinding.yaml rename to infra/feast-helm-operator/config/rbac/feastfeatureserver_editor_rolebinding.yaml index 054eb5a1a2..66c985fd00 100644 --- a/infra/feast-operator/config/rbac/feastfeatureserver_editor_rolebinding.yaml +++ b/infra/feast-helm-operator/config/rbac/feastfeatureserver_editor_rolebinding.yaml @@ -5,8 +5,8 @@ metadata: app.kubernetes.io/name: clusterrole app.kubernetes.io/instance: feastfeatureserver-editor-role app.kubernetes.io/component: rbac - app.kubernetes.io/created-by: feast-operator - app.kubernetes.io/part-of: feast-operator + app.kubernetes.io/created-by: feast-helm-operator + app.kubernetes.io/part-of: feast-helm-operator app.kubernetes.io/managed-by: kustomize name: feastfeatureserver-editor-rolebinding roleRef: diff --git a/infra/feast-operator/config/rbac/kustomization.yaml b/infra/feast-helm-operator/config/rbac/kustomization.yaml similarity index 100% rename from infra/feast-operator/config/rbac/kustomization.yaml rename to infra/feast-helm-operator/config/rbac/kustomization.yaml diff --git a/infra/feast-operator/config/rbac/leader_election_role.yaml b/infra/feast-helm-operator/config/rbac/leader_election_role.yaml similarity index 86% rename from infra/feast-operator/config/rbac/leader_election_role.yaml rename to infra/feast-helm-operator/config/rbac/leader_election_role.yaml index 0adc316dd3..f2523c129f 100644 --- a/infra/feast-operator/config/rbac/leader_election_role.yaml +++ b/infra/feast-helm-operator/config/rbac/leader_election_role.yaml @@ -6,8 +6,8 @@ metadata: app.kubernetes.io/name: role app.kubernetes.io/instance: leader-election-role app.kubernetes.io/component: rbac - app.kubernetes.io/created-by: feast-operator - app.kubernetes.io/part-of: feast-operator + app.kubernetes.io/created-by: feast-helm-operator + app.kubernetes.io/part-of: feast-helm-operator app.kubernetes.io/managed-by: kustomize name: leader-election-role rules: diff --git a/infra/feast-operator/config/rbac/leader_election_role_binding.yaml b/infra/feast-helm-operator/config/rbac/leader_election_role_binding.yaml similarity index 81% rename from infra/feast-operator/config/rbac/leader_election_role_binding.yaml rename to infra/feast-helm-operator/config/rbac/leader_election_role_binding.yaml index f745675c0e..b56c410167 100644 --- a/infra/feast-operator/config/rbac/leader_election_role_binding.yaml +++ b/infra/feast-helm-operator/config/rbac/leader_election_role_binding.yaml @@ -5,8 +5,8 @@ metadata: app.kubernetes.io/name: rolebinding app.kubernetes.io/instance: leader-election-rolebinding app.kubernetes.io/component: rbac - app.kubernetes.io/created-by: feast-operator - app.kubernetes.io/part-of: feast-operator + app.kubernetes.io/created-by: feast-helm-operator + app.kubernetes.io/part-of: feast-helm-operator app.kubernetes.io/managed-by: kustomize name: leader-election-rolebinding roleRef: diff --git a/infra/feast-operator/config/rbac/role.yaml b/infra/feast-helm-operator/config/rbac/role.yaml similarity index 82% rename from infra/feast-operator/config/rbac/role.yaml rename to infra/feast-helm-operator/config/rbac/role.yaml index 2469689484..ffc8227483 100644 --- a/infra/feast-operator/config/rbac/role.yaml +++ b/infra/feast-helm-operator/config/rbac/role.yaml @@ -5,8 +5,8 @@ metadata: app.kubernetes.io/name: clusterrole app.kubernetes.io/instance: manager-role app.kubernetes.io/component: rbac - app.kubernetes.io/created-by: feast-operator - app.kubernetes.io/part-of: feast-operator + app.kubernetes.io/created-by: feast-helm-operator + app.kubernetes.io/part-of: feast-helm-operator app.kubernetes.io/managed-by: kustomize name: manager-role rules: diff --git a/infra/feast-operator/config/rbac/role_binding.yaml b/infra/feast-helm-operator/config/rbac/role_binding.yaml similarity index 81% rename from infra/feast-operator/config/rbac/role_binding.yaml rename to infra/feast-helm-operator/config/rbac/role_binding.yaml index 3359e91169..9aa7f4264f 100644 --- a/infra/feast-operator/config/rbac/role_binding.yaml +++ b/infra/feast-helm-operator/config/rbac/role_binding.yaml @@ -5,8 +5,8 @@ metadata: app.kubernetes.io/name: clusterrolebinding app.kubernetes.io/instance: manager-rolebinding app.kubernetes.io/component: rbac - app.kubernetes.io/created-by: feast-operator - app.kubernetes.io/part-of: feast-operator + app.kubernetes.io/created-by: feast-helm-operator + app.kubernetes.io/part-of: feast-helm-operator app.kubernetes.io/managed-by: kustomize name: manager-rolebinding roleRef: diff --git a/infra/feast-operator/config/rbac/service_account.yaml b/infra/feast-helm-operator/config/rbac/service_account.yaml similarity index 72% rename from infra/feast-operator/config/rbac/service_account.yaml rename to infra/feast-helm-operator/config/rbac/service_account.yaml index 7ba6f27c60..b2ab5d5e91 100644 --- a/infra/feast-operator/config/rbac/service_account.yaml +++ b/infra/feast-helm-operator/config/rbac/service_account.yaml @@ -5,8 +5,8 @@ metadata: app.kubernetes.io/name: serviceaccount app.kubernetes.io/instance: controller-manager-sa app.kubernetes.io/component: rbac - app.kubernetes.io/created-by: feast-operator - app.kubernetes.io/part-of: feast-operator + app.kubernetes.io/created-by: feast-helm-operator + app.kubernetes.io/part-of: feast-helm-operator app.kubernetes.io/managed-by: kustomize name: controller-manager namespace: system diff --git a/infra/feast-operator/config/samples/charts_v1alpha1_feastfeatureserver.yaml b/infra/feast-helm-operator/config/samples/charts_v1alpha1_feastfeatureserver.yaml similarity index 100% rename from infra/feast-operator/config/samples/charts_v1alpha1_feastfeatureserver.yaml rename to infra/feast-helm-operator/config/samples/charts_v1alpha1_feastfeatureserver.yaml diff --git a/infra/feast-operator/config/samples/kustomization.yaml b/infra/feast-helm-operator/config/samples/kustomization.yaml similarity index 100% rename from infra/feast-operator/config/samples/kustomization.yaml rename to infra/feast-helm-operator/config/samples/kustomization.yaml diff --git a/infra/feast-operator/config/scorecard/bases/config.yaml b/infra/feast-helm-operator/config/scorecard/bases/config.yaml similarity index 100% rename from infra/feast-operator/config/scorecard/bases/config.yaml rename to infra/feast-helm-operator/config/scorecard/bases/config.yaml diff --git a/infra/feast-operator/config/scorecard/kustomization.yaml b/infra/feast-helm-operator/config/scorecard/kustomization.yaml similarity index 100% rename from infra/feast-operator/config/scorecard/kustomization.yaml rename to infra/feast-helm-operator/config/scorecard/kustomization.yaml diff --git a/infra/feast-operator/config/scorecard/patches/basic.config.yaml b/infra/feast-helm-operator/config/scorecard/patches/basic.config.yaml similarity index 100% rename from infra/feast-operator/config/scorecard/patches/basic.config.yaml rename to infra/feast-helm-operator/config/scorecard/patches/basic.config.yaml diff --git a/infra/feast-operator/config/scorecard/patches/olm.config.yaml b/infra/feast-helm-operator/config/scorecard/patches/olm.config.yaml similarity index 100% rename from infra/feast-operator/config/scorecard/patches/olm.config.yaml rename to infra/feast-helm-operator/config/scorecard/patches/olm.config.yaml diff --git a/infra/feast-operator/helm-charts/feast-feature-server b/infra/feast-helm-operator/helm-charts/feast-feature-server similarity index 100% rename from infra/feast-operator/helm-charts/feast-feature-server rename to infra/feast-helm-operator/helm-charts/feast-feature-server diff --git a/infra/feast-operator/watches.yaml b/infra/feast-helm-operator/watches.yaml similarity index 100% rename from infra/feast-operator/watches.yaml rename to infra/feast-helm-operator/watches.yaml diff --git a/infra/scripts/release/files_to_bump.txt b/infra/scripts/release/files_to_bump.txt index 505ef87b24..4b3967b23e 100644 --- a/infra/scripts/release/files_to_bump.txt +++ b/infra/scripts/release/files_to_bump.txt @@ -10,7 +10,7 @@ infra/charts/feast/README.md 11 68 69 infra/charts/feast-feature-server/Chart.yaml 5 infra/charts/feast-feature-server/README.md 3 infra/charts/feast-feature-server/values.yaml 12 -infra/feast-operator/Makefile 6 -infra/feast-operator/config/manager/kustomization.yaml 8 +infra/feast-helm-operator/Makefile 6 +infra/feast-helm-operator/config/manager/kustomization.yaml 8 java/pom.xml 38 ui/package.json 3 From f8592d86b2903ebfebc505bbf9392927aae5609c Mon Sep 17 00:00:00 2001 From: Shuchu Han Date: Mon, 30 Sep 2024 14:02:17 -0400 Subject: [PATCH 115/185] fix: Update the base image of materilization engine. (#4580) Signed-off-by: Shuchu Han --- sdk/python/feast/infra/materialization/kubernetes/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/python/feast/infra/materialization/kubernetes/Dockerfile b/sdk/python/feast/infra/materialization/kubernetes/Dockerfile index 38d4f5f188..b6ecb490af 100644 --- a/sdk/python/feast/infra/materialization/kubernetes/Dockerfile +++ b/sdk/python/feast/infra/materialization/kubernetes/Dockerfile @@ -1,7 +1,7 @@ -FROM python:3.11-slim-bullseye AS build +FROM debian:11-slim AS build RUN apt-get update && \ - apt-get install --no-install-suggests --no-install-recommends --yes git + apt-get install --no-install-suggests --no-install-recommends --yes git python3 python3-pip WORKDIR /app From 30603a2e9ec414d35045ee40022a575587c30f7f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 18:11:17 +0000 Subject: [PATCH 116/185] chore: Bump rollup from 2.68.0 to 2.79.2 in /ui (#4582) Bumps [rollup](https://github.com/rollup/rollup) from 2.68.0 to 2.79.2. - [Release notes](https://github.com/rollup/rollup/releases) - [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md) - [Commits](https://github.com/rollup/rollup/compare/v2.68.0...v2.79.2) --- updated-dependencies: - dependency-name: rollup dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- ui/yarn.lock | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/ui/yarn.lock b/ui/yarn.lock index 1f36143b67..40a4d355eb 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -9919,17 +9919,10 @@ rollup-pluginutils@^1.3.1: estree-walker "^0.2.1" minimatch "^3.0.2" -rollup@^2.43.1: - version "2.66.1" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.66.1.tgz#366b0404de353c4331d538c3ad2963934fcb4937" - integrity sha512-crSgLhSkLMnKr4s9iZ/1qJCplgAgrRY+igWv8KhG/AjKOJ0YX/WpmANyn8oxrw+zenF3BXWDLa7Xl/QZISH+7w== - optionalDependencies: - fsevents "~2.3.2" - -rollup@^2.68.0: - version "2.68.0" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.68.0.tgz#6ccabfd649447f8f21d62bf41662e5caece3bd66" - integrity sha512-XrMKOYK7oQcTio4wyTz466mucnd8LzkiZLozZ4Rz0zQD+HeX4nUK4B8GrTX/2EvN2/vBF/i2WnaXboPxo0JylA== +rollup@^2.43.1, rollup@^2.68.0: + version "2.79.2" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.2.tgz#f150e4a5db4b121a21a747d762f701e5e9f49090" + integrity sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ== optionalDependencies: fsevents "~2.3.2" From 1ba94f7e2018fea0114f1703dd3942d589071825 Mon Sep 17 00:00:00 2001 From: Aloysius Lim Date: Wed, 2 Oct 2024 23:58:35 +0800 Subject: [PATCH 117/185] fix: FeastExtrasDependencyImportError when using SparkOfflineStore without S3 (#4594) * Import aws_utils only if using s3. Signed-off-by: Aloysius Lim * Lint. Signed-off-by: Aloysius Lim --------- Signed-off-by: Aloysius Lim --- .../infra/offline_stores/contrib/spark_offline_store/spark.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/spark.py b/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/spark.py index 2896d565d3..b462607ae1 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/spark.py +++ b/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/spark.py @@ -30,7 +30,6 @@ RetrievalMetadata, ) from feast.infra.registry.base_registry import BaseRegistry -from feast.infra.utils import aws_utils from feast.repo_config import FeastConfigBaseModel, RepoConfig from feast.saved_dataset import SavedDatasetStorage from feast.type_map import spark_schema_to_np_dtypes @@ -399,6 +398,8 @@ def to_remote_storage(self) -> List[str]: return _list_files_in_folder(output_uri) elif self._config.offline_store.staging_location.startswith("s3://"): + from feast.infra.utils import aws_utils + spark_compatible_s3_staging_location = ( self._config.offline_store.staging_location.replace( "s3://", "s3a://" From cc41c9ea7322600bf733a58854456596258a3bf8 Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Thu, 3 Oct 2024 13:21:03 -0400 Subject: [PATCH 118/185] Update quickstart.md (#4598) --- docs/getting-started/quickstart.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md index 7169989e7e..4afd0086d9 100644 --- a/docs/getting-started/quickstart.md +++ b/docs/getting-started/quickstart.md @@ -475,7 +475,12 @@ We now serialize the latest values of features since the beginning of time to pr {% tab title="Bash" %} ```bash CURRENT_TIME=$(date -u +"%Y-%m-%dT%H:%M:%S") -feast materialize-incremental $CURRENT_TIME +# For mac +LAST_YEAR=$(date -u -v -1y +"%Y-%m-%dT%H:%M:%S") +# For Linux +# LAST_YEAR=$(date -u -d "last year" +"%Y-%m-%dT%H:%M:%S") + +feast materialize-incremental $LAST_YEAR $CURRENT_TIME ``` {% endtab %} {% endtabs %} From 10ce2aa0050e419acfae27971a6fff87bade3ba4 Mon Sep 17 00:00:00 2001 From: nanohanno <44575187+nanohanno@users.noreply.github.com> Date: Fri, 4 Oct 2024 16:18:43 +0200 Subject: [PATCH 119/185] feat: Add connection_name field to Snowflake config (#4600) Add connection_name field to Snowflake config Signed-off-by: hkuepers Co-authored-by: hkuepers --- sdk/python/feast/infra/materialization/snowflake_engine.py | 5 ++++- sdk/python/feast/infra/offline_stores/snowflake.py | 5 ++++- sdk/python/feast/infra/online_stores/snowflake.py | 5 ++++- sdk/python/feast/infra/registry/snowflake.py | 5 ++++- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/sdk/python/feast/infra/materialization/snowflake_engine.py b/sdk/python/feast/infra/materialization/snowflake_engine.py index 600e1b20d8..3b789a3e5d 100644 --- a/sdk/python/feast/infra/materialization/snowflake_engine.py +++ b/sdk/python/feast/infra/materialization/snowflake_engine.py @@ -46,7 +46,10 @@ class SnowflakeMaterializationEngineConfig(FeastConfigBaseModel): """ Type selector""" config_path: Optional[str] = os.path.expanduser("~/.snowsql/config") - """ Snowflake config path -- absolute path required (Cant use ~)""" + """ Snowflake snowsql config path -- absolute path required (Cant use ~)""" + + connection_name: Optional[str] = None + """ Snowflake connector connection name -- typically defined in ~/.snowflake/connections.toml """ account: Optional[str] = None """ Snowflake deployment identifier -- drop .snowflakecomputing.com""" diff --git a/sdk/python/feast/infra/offline_stores/snowflake.py b/sdk/python/feast/infra/offline_stores/snowflake.py index 9418171a96..3d23682769 100644 --- a/sdk/python/feast/infra/offline_stores/snowflake.py +++ b/sdk/python/feast/infra/offline_stores/snowflake.py @@ -83,7 +83,10 @@ class SnowflakeOfflineStoreConfig(FeastConfigBaseModel): """ Offline store type selector """ config_path: Optional[str] = os.path.expanduser("~/.snowsql/config") - """ Snowflake config path -- absolute path required (Cant use ~)""" + """ Snowflake snowsql config path -- absolute path required (Cant use ~)""" + + connection_name: Optional[str] = None + """ Snowflake connector connection name -- typically defined in ~/.snowflake/connections.toml """ account: Optional[str] = None """ Snowflake deployment identifier -- drop .snowflakecomputing.com """ diff --git a/sdk/python/feast/infra/online_stores/snowflake.py b/sdk/python/feast/infra/online_stores/snowflake.py index 6f39bdd0f6..d07066de12 100644 --- a/sdk/python/feast/infra/online_stores/snowflake.py +++ b/sdk/python/feast/infra/online_stores/snowflake.py @@ -30,7 +30,10 @@ class SnowflakeOnlineStoreConfig(FeastConfigBaseModel): """ Online store type selector """ config_path: Optional[str] = os.path.expanduser("~/.snowsql/config") - """ Snowflake config path -- absolute path required (Can't use ~)""" + """ Snowflake snowsql config path -- absolute path required (Cant use ~)""" + + connection_name: Optional[str] = None + """ Snowflake connector connection name -- typically defined in ~/.snowflake/connections.toml """ account: Optional[str] = None """ Snowflake deployment identifier -- drop .snowflakecomputing.com """ diff --git a/sdk/python/feast/infra/registry/snowflake.py b/sdk/python/feast/infra/registry/snowflake.py index e68d9d64b5..06403fe9ae 100644 --- a/sdk/python/feast/infra/registry/snowflake.py +++ b/sdk/python/feast/infra/registry/snowflake.py @@ -80,7 +80,10 @@ class SnowflakeRegistryConfig(RegistryConfig): """ Registry type selector """ config_path: Optional[str] = os.path.expanduser("~/.snowsql/config") - """ Snowflake config path -- absolute path required (Cant use ~) """ + """ Snowflake snowsql config path -- absolute path required (Cant use ~)""" + + connection_name: Optional[str] = None + """ Snowflake connector connection name -- typically defined in ~/.snowflake/connections.toml """ account: Optional[str] = None """ Snowflake deployment identifier -- drop .snowflakecomputing.com """ From 9646f031f0f853c4f6793c55d553035cf6b0c45c Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Fri, 4 Oct 2024 22:23:34 -0400 Subject: [PATCH 120/185] chore: Adding additional dummy entity field test (#4602) Signed-off-by: Francisco Javier Arceo --- sdk/python/feast/feature_view.py | 4 ++ .../test_on_demand_python_transformation.py | 40 +++++++++++++++++-- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/sdk/python/feast/feature_view.py b/sdk/python/feast/feature_view.py index 33ea761158..4aeb9a9c1d 100644 --- a/sdk/python/feast/feature_view.py +++ b/sdk/python/feast/feature_view.py @@ -49,6 +49,10 @@ name=DUMMY_ENTITY_NAME, join_keys=[DUMMY_ENTITY_ID], ) +DUMMY_ENTITY_FIELD = Field( + name=DUMMY_ENTITY_ID, + dtype=from_value_type(ValueType.STRING), +) @typechecked diff --git a/sdk/python/tests/unit/test_on_demand_python_transformation.py b/sdk/python/tests/unit/test_on_demand_python_transformation.py index ff7ad494ca..b994ea8042 100644 --- a/sdk/python/tests/unit/test_on_demand_python_transformation.py +++ b/sdk/python/tests/unit/test_on_demand_python_transformation.py @@ -17,10 +17,20 @@ RequestSource, ) from feast.driver_test_data import create_driver_hourly_stats_df +from feast.feature_view import DUMMY_ENTITY_FIELD from feast.field import Field from feast.infra.online_stores.sqlite import SqliteOnlineStoreConfig from feast.on_demand_feature_view import on_demand_feature_view -from feast.types import Array, Bool, Float32, Float64, Int64, String +from feast.types import ( + Array, + Bool, + Float32, + Float64, + Int64, + String, + ValueType, + from_value_type, +) class TestOnDemandPythonTransformation(unittest.TestCase): @@ -51,7 +61,9 @@ def setUp(self): path=driver_stats_path, allow_truncated_timestamps=True ) - driver = Entity(name="driver", join_keys=["driver_id"]) + driver = Entity( + name="driver", join_keys=["driver_id"], value_type=ValueType.INT64 + ) driver_stats_source = FileSource( name="driver_hourly_stats_source", @@ -73,6 +85,19 @@ def setUp(self): source=driver_stats_source, ) + driver_stats_entity_less_fv = FeatureView( + name="driver_hourly_stats_no_entity", + entities=[], + ttl=timedelta(days=0), + schema=[ + Field(name="conv_rate", dtype=Float32), + Field(name="acc_rate", dtype=Float32), + Field(name="avg_daily_trips", dtype=Int64), + ], + online=True, + source=driver_stats_source, + ) + @on_demand_feature_view( sources=[driver_stats_fv], schema=[Field(name="conv_rate_plus_acc_pandas", dtype=Float64)], @@ -151,6 +176,7 @@ def python_singleton_view(inputs: dict[str, Any]) -> dict[str, Any]: pandas_view, python_view, python_singleton_view, + driver_stats_entity_less_fv, ] ) @@ -162,13 +188,19 @@ def python_singleton_view(inputs: dict[str, Any]) -> dict[str, Any]: pandas_view, python_view, python_demo_view, + driver_stats_entity_less_fv, ] ) self.store.write_to_online_store( feature_view_name="driver_hourly_stats", df=driver_df ) - assert len(self.store.list_all_feature_views()) == 4 - assert len(self.store.list_feature_views()) == 1 + assert driver_stats_fv.entity_columns == [ + Field(name=driver.join_key, dtype=from_value_type(driver.value_type)) + ] + assert driver_stats_entity_less_fv.entity_columns == [DUMMY_ENTITY_FIELD] + + assert len(self.store.list_all_feature_views()) == 5 + assert len(self.store.list_feature_views()) == 2 assert len(self.store.list_on_demand_feature_views()) == 3 assert len(self.store.list_stream_feature_views()) == 0 From dd37ff1e7420901490775f8eac8e346efd3a810d Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Sat, 5 Oct 2024 06:58:00 -0400 Subject: [PATCH 121/185] chore: Updated snowflake test to be more explicit about post apply entity_columns return value (#4603) chore: updated snowflake test to be more explicit about post apply entity_column return value Signed-off-by: Francisco Javier Arceo --- .../tests/integration/materialization/test_snowflake.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sdk/python/tests/integration/materialization/test_snowflake.py b/sdk/python/tests/integration/materialization/test_snowflake.py index dc9d684ab5..5598526d2b 100644 --- a/sdk/python/tests/integration/materialization/test_snowflake.py +++ b/sdk/python/tests/integration/materialization/test_snowflake.py @@ -5,7 +5,7 @@ from feast import Field from feast.entity import Entity -from feast.feature_view import FeatureView +from feast.feature_view import DUMMY_ENTITY_FIELD, FeatureView from feast.types import Array, Bool, Bytes, Float64, Int32, Int64, String, UnixTimestamp from feast.utils import _utc_now from tests.data.data_creator import create_basic_driver_dataset @@ -189,7 +189,7 @@ def test_snowflake_materialization_consistency_internal_with_lists( snowflake_environment.data_source_creator.teardown() -@pytest.mark.integration +# @pytest.mark.integration def test_snowflake_materialization_entityless_fv(): snowflake_config = IntegrationTestRepoConfig( online_store=SNOWFLAKE_ONLINE_CONFIG, @@ -225,7 +225,7 @@ def test_snowflake_materialization_entityless_fv(): try: fs.apply([overall_stats_fv, driver]) - assert overall_stats_fv.entity_columns != [] + assert overall_stats_fv.entity_columns == [DUMMY_ENTITY_FIELD] # materialization is run in two steps and # we use timestamp from generated dataframe as a split point From a00943956fa2f4cdfda48dd8e378bf4ad0e04836 Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Sat, 5 Oct 2024 07:02:19 -0400 Subject: [PATCH 122/185] chore: Fix sf integration test (#4604) Signed-off-by: Francisco Javier Arceo --- sdk/python/tests/integration/materialization/test_snowflake.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/python/tests/integration/materialization/test_snowflake.py b/sdk/python/tests/integration/materialization/test_snowflake.py index 5598526d2b..5f01641c3b 100644 --- a/sdk/python/tests/integration/materialization/test_snowflake.py +++ b/sdk/python/tests/integration/materialization/test_snowflake.py @@ -189,7 +189,7 @@ def test_snowflake_materialization_consistency_internal_with_lists( snowflake_environment.data_source_creator.teardown() -# @pytest.mark.integration +@pytest.mark.integration def test_snowflake_materialization_entityless_fv(): snowflake_config = IntegrationTestRepoConfig( online_store=SNOWFLAKE_ONLINE_CONFIG, From c0fa9aa042b3714badad1a44835064654ebe12f4 Mon Sep 17 00:00:00 2001 From: Harri Lehtola <1781172+peruukki@users.noreply.github.com> Date: Sun, 6 Oct 2024 22:09:07 +0300 Subject: [PATCH 123/185] chore: Ignore generated protos.js file in ESLint (Feast UI) (#4599) We see lots of ESLint warnings from the generated protos.js file when running `yarn start`. No need to lint generated files. Signed-off-by: Harri Lehtola --- ui/.eslintignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 ui/.eslintignore diff --git a/ui/.eslintignore b/ui/.eslintignore new file mode 100644 index 0000000000..310a5c0688 --- /dev/null +++ b/ui/.eslintignore @@ -0,0 +1 @@ +src/protos.js From b9ddbf9351a55b1a5c102102b06ad3b2f28b3d1f Mon Sep 17 00:00:00 2001 From: Harri Lehtola <1781172+peruukki@users.noreply.github.com> Date: Sun, 6 Oct 2024 22:20:56 +0300 Subject: [PATCH 124/185] chore!: Update @elastic/eui and @emotion/react in Feast UI (#4597) * chore!: Update @elastic/eui and @emotion/react to latest versions This version of @elastic/eui supports React 18. Good to update @emotion/react to the latest version at the same time. Other dependency changes: - @emotion/css is now a peer dependency of @elastic/eui, so we need to add it to our dependencies. - prop-types is no longer a peer dependency of @elastic/eui, so we can remove it from our dependencies. - typescript needed an upgrade for TypeScript compilation to work in the `build:lib` script; it failed due to a syntax error in @types/lodash (dependency of @elastic/eui). Unfortunately this typescript version isn't within the version range of @elastic/eui's peer dependencies, but that one seems overly strict, especially since this version seems to work. Unfortunately this also introduces a warning in the `yarn start` about the typescript version being newer than what ESLint support, but again, everything seems fine. And to be honest, I don't know what else to do, this has been quite a challenge to get somehow working. :D Code changes: - EuiLoadingContent has been replaced with EuiSkeletonText. - EuiPageContent and EuiPageContentBody have been replaced with EuiPageTemplate, EuiPageTemplate.Header and EuiPageTemplate.Section. - EuiSideNav no longer takes a style prop, so it is dropped; the width seems fine without it. - EuiBasicTable now requires the field prop in its columns, and only takes objects in its items. - The `panelled` prop has been moved from Layout's EuiPageBody to each page's EuiPageTemplate, so that the page template's header gets the wanted background color. - The `sticky` prop passed to Layout's EuiPageSidebar needs to be an object with an offset, otherwise the offset is read from --euiFixedHeadersOffset that is unset if there is no fixed EuiHeader on the page. - Icons: Static class names no longer work for proper styling, we need to pass at least the className prop to the svg element. Passing all props allows possible other props to work too. Also, we no longer need separate components for differently sized icons (16, 32). - Some overview tab contents are wrapped in an additional EuiFlexGroup to add gaps between sections; they previously appeared through some component margins but not anymore. - Jest failed to parse chroma-js sources, probably something to do with it being an ES module (its support in Jest is limited: https://jest-archive-august-2023.netlify.app/docs/27.x/ecmascript-modules), so we use the build version of chroma-js with Jest, similarly to d3. BREAKING CHANGE: Consuming apps that use @elastic/eui should update it to a compatible version. If you use @elastic/eui components that have been renamed or replaced with others, you'll need to update your code accordingly. Signed-off-by: Harri Lehtola * chore: Update Node version from 17 to 20 in UI unit tests Node 17 is not an LTS (long-term support) version and apparently rejected by the latest versions of Elastic UI: > error @elastic/eui@95.12.0: The engine "node" is incompatible with > this module. Expected version "16.x || 18.x || >=20.x". Got "17.9.1" Let's try with the latest LTS version. Signed-off-by: Harri Lehtola --------- Signed-off-by: Harri Lehtola --- .github/workflows/unit_tests.yml | 2 +- ui/package.json | 13 +- ui/src/components/ExplorePanel.tsx | 4 +- ui/src/components/NoProjectGuard.tsx | 4 +- .../data-source-demo-tab/DemoCustomTab.tsx | 4 +- .../dataset-demo-tab/DemoCustomTab.tsx | 4 +- .../entity-demo-tab/DemoCustomTab.tsx | 4 +- .../feature-demo-tab/DemoCustomTab.tsx | 4 +- .../DemoCustomTab.tsx | 4 +- .../ondemand-fv-demo-tab/DemoCustomTab.tsx | 4 +- .../reguar-fv-demo-tab/DemoCustomTab.tsx | 4 +- .../stream-fv-demo-tab/DemoCustomTab.tsx | 4 +- ui/src/graphics/DataSourceIcon.tsx | 27 +- ui/src/graphics/DatasetIcon.tsx | 27 +- ui/src/graphics/EntityIcon.tsx | 27 +- ui/src/graphics/FeatureIcon.tsx | 27 +- ui/src/graphics/FeatureServiceIcon.tsx | 29 +- ui/src/graphics/FeatureViewIcon.tsx | 27 +- ui/src/pages/Layout.tsx | 10 +- ui/src/pages/ProjectOverviewPage.tsx | 23 +- ui/src/pages/RootProjectSelectionPage.tsx | 21 +- ui/src/pages/Sidebar.tsx | 21 +- .../pages/data-sources/DataSourceInstance.tsx | 38 +- ui/src/pages/data-sources/Index.tsx | 91 ++- ui/src/pages/entities/EntityInstance.tsx | 36 +- .../pages/entities/FeatureViewEdgesList.tsx | 7 +- ui/src/pages/entities/Index.tsx | 45 +- .../FeatureServiceInstance.tsx | 36 +- .../FeatureServiceOverviewTab.tsx | 4 +- ui/src/pages/feature-services/Index.tsx | 108 ++- .../ConsumingFeatureServicesList.tsx | 4 +- ui/src/pages/feature-views/Index.tsx | 108 ++- .../OnDemandFeatureViewInstance.tsx | 42 +- .../OnDemandFeatureViewOverviewTab.tsx | 4 +- .../RegularFeatureViewInstance.tsx | 42 +- .../StreamFeatureViewInstance.tsx | 43 +- .../StreamFeatureViewOverviewTab.tsx | 4 +- .../FeatureViewProjectionDisplayPanel.tsx | 5 +- .../components/RequestDataDisplayPanel.tsx | 4 +- ui/src/pages/features/FeatureInstance.tsx | 36 +- .../pages/saved-data-sets/DatasetInstance.tsx | 38 +- ui/src/pages/saved-data-sets/Index.tsx | 45 +- ui/yarn.lock | 638 +++++++++--------- 43 files changed, 699 insertions(+), 973 deletions(-) diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index dea82da44c..efce61b7b2 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -49,7 +49,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v3 with: - node-version: '17.x' + node-version: '20.x' registry-url: 'https://registry.npmjs.org' - name: Install yarn dependencies working-directory: ./ui diff --git a/ui/package.json b/ui/package.json index 9524944db5..5e427b8905 100644 --- a/ui/package.json +++ b/ui/package.json @@ -9,8 +9,8 @@ "types": "./dist/FeastUI.d.ts", "module": "./dist/feast-ui.module.js", "peerDependencies": { - "@elastic/eui": "^55.0.1", - "@emotion/react": "^11.7.1", + "@elastic/eui": "^95.12.0", + "@emotion/react": "^11.13.3", "react": "^17.0.2", "react-dom": "^17.0.2" }, @@ -24,12 +24,12 @@ }, "dependencies": { "@elastic/datemath": "^5.0.3", - "@elastic/eui": "^55.0.1", - "@emotion/react": "^11.7.1", + "@elastic/eui": "^95.12.0", + "@emotion/css": "^11.13.0", + "@emotion/react": "^11.13.3", "d3": "^7.3.0", "inter-ui": "^3.19.3", "moment": "^2.29.1", - "prop-types": "^15.8.1", "protobufjs": "^7.1.1", "query-string": "^7.1.1", "react-code-blocks": "^0.0.9-0", @@ -57,6 +57,7 @@ }, "jest": { "moduleNameMapper": { + "chroma-js": "/node_modules/chroma-js/dist/chroma.min.cjs", "d3": "/node_modules/d3/dist/d3.min.js" } }, @@ -100,7 +101,7 @@ "rollup-plugin-svg": "^2.0.0", "rollup-plugin-svgo": "^1.1.0", "rollup-plugin-terser": "^7.0.2", - "typescript": "^4.4.2" + "typescript": "^4.9.5" }, "description": "Web UI for the [Feast Feature Store](https://feast.dev/)", "repository": { diff --git a/ui/src/components/ExplorePanel.tsx b/ui/src/components/ExplorePanel.tsx index 1bcc2e9978..3527c245b1 100644 --- a/ui/src/components/ExplorePanel.tsx +++ b/ui/src/components/ExplorePanel.tsx @@ -5,7 +5,7 @@ import { EuiPanel, EuiTitle, EuiBadge, - EuiLoadingContent, + EuiSkeletonText, EuiFlexGroup, EuiFlexItem, EuiSpacer, @@ -25,7 +25,7 @@ const ExplorePanel = () => {

Explore this Project

- {isLoading && } + {isLoading && } {isSuccess && data && data.map((suggestionGroup, i) => { diff --git a/ui/src/components/NoProjectGuard.tsx b/ui/src/components/NoProjectGuard.tsx index 8501f6c931..02e70e4b61 100644 --- a/ui/src/components/NoProjectGuard.tsx +++ b/ui/src/components/NoProjectGuard.tsx @@ -1,4 +1,4 @@ -import { EuiEmptyPrompt, EuiLoadingContent } from "@elastic/eui"; +import { EuiEmptyPrompt, EuiSkeletonText } from "@elastic/eui"; import React, { useContext } from "react"; import { Outlet, useParams } from "react-router-dom"; import { @@ -14,7 +14,7 @@ const NoProjectGuard = () => { const projectListContext = useContext(ProjectListContext); if (isLoading && !data) { - return ; + return ; } if (isError) { diff --git a/ui/src/custom-tabs/data-source-demo-tab/DemoCustomTab.tsx b/ui/src/custom-tabs/data-source-demo-tab/DemoCustomTab.tsx index 79ba01d7aa..2597cae7af 100644 --- a/ui/src/custom-tabs/data-source-demo-tab/DemoCustomTab.tsx +++ b/ui/src/custom-tabs/data-source-demo-tab/DemoCustomTab.tsx @@ -6,7 +6,7 @@ import { } from "../types"; import { - EuiLoadingContent, + EuiSkeletonText, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, @@ -29,7 +29,7 @@ const DemoCustomTab = ({ id, feastObjectQuery }: DataSourceCustomTabProps) => { if (isLoading) { // Handle Loading State // https://elastic.github.io/eui/#/display/loading - return ; + return ; } if (isError) { diff --git a/ui/src/custom-tabs/dataset-demo-tab/DemoCustomTab.tsx b/ui/src/custom-tabs/dataset-demo-tab/DemoCustomTab.tsx index 37038b84a9..d275913057 100644 --- a/ui/src/custom-tabs/dataset-demo-tab/DemoCustomTab.tsx +++ b/ui/src/custom-tabs/dataset-demo-tab/DemoCustomTab.tsx @@ -6,7 +6,7 @@ import { } from "../types"; import { - EuiLoadingContent, + EuiSkeletonText, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, @@ -29,7 +29,7 @@ const DemoCustomTab = ({ id, feastObjectQuery }: DatasetCustomTabProps) => { if (isLoading) { // Handle Loading State // https://elastic.github.io/eui/#/display/loading - return ; + return ; } if (isError) { diff --git a/ui/src/custom-tabs/entity-demo-tab/DemoCustomTab.tsx b/ui/src/custom-tabs/entity-demo-tab/DemoCustomTab.tsx index 6e4a87b570..b952461a33 100644 --- a/ui/src/custom-tabs/entity-demo-tab/DemoCustomTab.tsx +++ b/ui/src/custom-tabs/entity-demo-tab/DemoCustomTab.tsx @@ -6,7 +6,7 @@ import { } from "../types"; import { - EuiLoadingContent, + EuiSkeletonText, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, @@ -29,7 +29,7 @@ const DemoCustomTab = ({ id, feastObjectQuery }: EntityCustomTabProps) => { if (isLoading) { // Handle Loading State // https://elastic.github.io/eui/#/display/loading - return ; + return ; } if (isError) { diff --git a/ui/src/custom-tabs/feature-demo-tab/DemoCustomTab.tsx b/ui/src/custom-tabs/feature-demo-tab/DemoCustomTab.tsx index fda920daf3..67f8347e18 100644 --- a/ui/src/custom-tabs/feature-demo-tab/DemoCustomTab.tsx +++ b/ui/src/custom-tabs/feature-demo-tab/DemoCustomTab.tsx @@ -6,7 +6,7 @@ import { } from "../types"; import { - EuiLoadingContent, + EuiSkeletonText, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, @@ -30,7 +30,7 @@ const DemoCustomTab = ({ id, feastObjectQuery }: FeatureCustomTabProps) => { if (isLoading) { // Handle Loading State // https://elastic.github.io/eui/#/display/loading - return ; + return ; } if (isError) { diff --git a/ui/src/custom-tabs/feature-service-demo-tab/DemoCustomTab.tsx b/ui/src/custom-tabs/feature-service-demo-tab/DemoCustomTab.tsx index 724c2504aa..bfc0604a32 100644 --- a/ui/src/custom-tabs/feature-service-demo-tab/DemoCustomTab.tsx +++ b/ui/src/custom-tabs/feature-service-demo-tab/DemoCustomTab.tsx @@ -6,7 +6,7 @@ import { } from "../types"; import { - EuiLoadingContent, + EuiSkeletonText, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, @@ -32,7 +32,7 @@ const DemoCustomTab = ({ if (isLoading) { // Handle Loading State // https://elastic.github.io/eui/#/display/loading - return ; + return ; } if (isError) { diff --git a/ui/src/custom-tabs/ondemand-fv-demo-tab/DemoCustomTab.tsx b/ui/src/custom-tabs/ondemand-fv-demo-tab/DemoCustomTab.tsx index dd6f3ab654..57c52fa078 100644 --- a/ui/src/custom-tabs/ondemand-fv-demo-tab/DemoCustomTab.tsx +++ b/ui/src/custom-tabs/ondemand-fv-demo-tab/DemoCustomTab.tsx @@ -6,7 +6,7 @@ import { } from "../types"; import { - EuiLoadingContent, + EuiSkeletonText, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, @@ -32,7 +32,7 @@ const DemoCustomTab = ({ if (isLoading) { // Handle Loading State // https://elastic.github.io/eui/#/display/loading - return ; + return ; } if (isError) { diff --git a/ui/src/custom-tabs/reguar-fv-demo-tab/DemoCustomTab.tsx b/ui/src/custom-tabs/reguar-fv-demo-tab/DemoCustomTab.tsx index 4f8d7dfcb2..1d7ba555b1 100644 --- a/ui/src/custom-tabs/reguar-fv-demo-tab/DemoCustomTab.tsx +++ b/ui/src/custom-tabs/reguar-fv-demo-tab/DemoCustomTab.tsx @@ -6,7 +6,7 @@ import { } from "../types"; import { - EuiLoadingContent, + EuiSkeletonText, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, @@ -32,7 +32,7 @@ const DemoCustomTab = ({ if (isLoading) { // Handle Loading State // https://elastic.github.io/eui/#/display/loading - return ; + return ; } if (isError) { diff --git a/ui/src/custom-tabs/stream-fv-demo-tab/DemoCustomTab.tsx b/ui/src/custom-tabs/stream-fv-demo-tab/DemoCustomTab.tsx index 86e59d10c7..baef165523 100644 --- a/ui/src/custom-tabs/stream-fv-demo-tab/DemoCustomTab.tsx +++ b/ui/src/custom-tabs/stream-fv-demo-tab/DemoCustomTab.tsx @@ -6,7 +6,7 @@ import { } from "../types"; import { - EuiLoadingContent, + EuiSkeletonText, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, @@ -32,7 +32,7 @@ const DemoCustomTab = ({ if (isLoading) { // Handle Loading State // https://elastic.github.io/eui/#/display/loading - return ; + return ; } if (isError) { diff --git a/ui/src/graphics/DataSourceIcon.tsx b/ui/src/graphics/DataSourceIcon.tsx index fdd92b8773..6c28987cc3 100644 --- a/ui/src/graphics/DataSourceIcon.tsx +++ b/ui/src/graphics/DataSourceIcon.tsx @@ -1,17 +1,9 @@ import React from "react"; -const DataSourceIcon = ({ - size, - className, -}: { - size: number; - className?: string; -}) => { +const DataSourceIcon = (props: React.SVGProps) => { return ( @@ -27,17 +19,4 @@ const DataSourceIcon = ({ ); }; -const DataSourceIcon16 = () => { - return ; -}; - -const DataSourceIcon32 = () => { - return ( - - ); -}; - -export { DataSourceIcon, DataSourceIcon16, DataSourceIcon32 }; +export { DataSourceIcon }; diff --git a/ui/src/graphics/DatasetIcon.tsx b/ui/src/graphics/DatasetIcon.tsx index 5c28f76f1d..063c6cde5d 100644 --- a/ui/src/graphics/DatasetIcon.tsx +++ b/ui/src/graphics/DatasetIcon.tsx @@ -1,17 +1,9 @@ import React from "react"; -const DatasetIcon = ({ - size, - className, -}: { - size: number; - className?: string; -}) => { +const DatasetIcon = (props: React.SVGProps) => { return ( { - return ; -}; - -const DatasetIcon32 = () => { - return ( - - ); -}; - -export { DatasetIcon, DatasetIcon16, DatasetIcon32 }; +export { DatasetIcon }; diff --git a/ui/src/graphics/EntityIcon.tsx b/ui/src/graphics/EntityIcon.tsx index d9daf542a6..6abf703d16 100644 --- a/ui/src/graphics/EntityIcon.tsx +++ b/ui/src/graphics/EntityIcon.tsx @@ -1,17 +1,9 @@ import React from "react"; -const EntityIcon = ({ - size, - className, -}: { - size: number; - className?: string; -}) => { +const EntityIcon = (props: React.SVGProps) => { return ( { - return ; -}; - -const EntityIcon32 = () => { - return ( - - ); -}; - -export { EntityIcon, EntityIcon16, EntityIcon32 }; +export { EntityIcon }; diff --git a/ui/src/graphics/FeatureIcon.tsx b/ui/src/graphics/FeatureIcon.tsx index e2e06749bc..ba275dc02e 100644 --- a/ui/src/graphics/FeatureIcon.tsx +++ b/ui/src/graphics/FeatureIcon.tsx @@ -1,17 +1,9 @@ import React from "react"; -const FeatureIcon = ({ - size, - className, -}: { - size: number; - className?: string; -}) => { +const FeatureIcon = (props: React.SVGProps) => { return ( { - return ; -}; - -const FeatureIcon32 = () => { - return ( - - ); -}; - -export { FeatureIcon, FeatureIcon16, FeatureIcon32 }; +export { FeatureIcon }; diff --git a/ui/src/graphics/FeatureServiceIcon.tsx b/ui/src/graphics/FeatureServiceIcon.tsx index 04c4de9cd6..ca7d7dacd5 100644 --- a/ui/src/graphics/FeatureServiceIcon.tsx +++ b/ui/src/graphics/FeatureServiceIcon.tsx @@ -1,17 +1,9 @@ import React from "react"; -const FeatureServiceIcon = ({ - size, - className, -}: { - size: number; - className?: string; -}) => { +const FeatureServiceIcon = (props: React.SVGProps) => { return ( { - return ( - - ); -}; - -const FeatureServiceIcon32 = () => { - return ( - - ); -}; - -export { FeatureServiceIcon, FeatureServiceIcon16, FeatureServiceIcon32 }; +export { FeatureServiceIcon }; diff --git a/ui/src/graphics/FeatureViewIcon.tsx b/ui/src/graphics/FeatureViewIcon.tsx index f536953b11..fe7a44e2c0 100644 --- a/ui/src/graphics/FeatureViewIcon.tsx +++ b/ui/src/graphics/FeatureViewIcon.tsx @@ -1,17 +1,9 @@ import React from "react"; -const FeatureViewIcon = ({ - size, - className, -}: { - size: number; - className?: string; -}) => { +const FeatureViewIcon = (props: React.SVGProps) => { return ( { - return ; -}; - -const FeatureViewIcon32 = () => { - return ( - - ); -}; - -export { FeatureViewIcon, FeatureViewIcon16, FeatureViewIcon32 }; +export { FeatureViewIcon }; diff --git a/ui/src/pages/Layout.tsx b/ui/src/pages/Layout.tsx index ff56414f35..2aee2904ae 100644 --- a/ui/src/pages/Layout.tsx +++ b/ui/src/pages/Layout.tsx @@ -2,7 +2,7 @@ import React from "react"; import { EuiPage, - EuiPageSideBar, + EuiPageSidebar, EuiPageBody, EuiErrorBoundary, EuiHorizontalRule, @@ -35,9 +35,9 @@ const Layout = () => { return ( - @@ -50,9 +50,9 @@ const Layout = () => { )} - + - + diff --git a/ui/src/pages/ProjectOverviewPage.tsx b/ui/src/pages/ProjectOverviewPage.tsx index 854af49375..427a33238a 100644 --- a/ui/src/pages/ProjectOverviewPage.tsx +++ b/ui/src/pages/ProjectOverviewPage.tsx @@ -1,14 +1,13 @@ import React, { useContext } from "react"; import { - EuiPageContent, - EuiPageContentBody, + EuiPageTemplate, EuiText, EuiFlexGroup, EuiFlexItem, EuiTitle, EuiSpacer, - EuiLoadingContent, + EuiSkeletonText, EuiEmptyPrompt, } from "@elastic/eui"; @@ -24,17 +23,11 @@ const ProjectOverviewPage = () => { const { isLoading, isSuccess, isError, data } = useLoadRegistry(registryUrl); return ( - - + +

- {isLoading && } + {isLoading && } {isSuccess && data?.project && `Project: ${data.project}`}

@@ -42,7 +35,7 @@ const ProjectOverviewPage = () => { - {isLoading && } + {isLoading && } {isError && ( { - - + + ); }; diff --git a/ui/src/pages/RootProjectSelectionPage.tsx b/ui/src/pages/RootProjectSelectionPage.tsx index d287342055..5e19b6606b 100644 --- a/ui/src/pages/RootProjectSelectionPage.tsx +++ b/ui/src/pages/RootProjectSelectionPage.tsx @@ -4,9 +4,8 @@ import { EuiFlexGrid, EuiFlexItem, EuiIcon, - EuiLoadingContent, - EuiPageContent, - EuiPageContentBody, + EuiSkeletonText, + EuiPageTemplate, EuiText, EuiTitle, EuiHorizontalRule, @@ -47,14 +46,8 @@ const RootProjectSelectionPage = () => { }); return ( - - + +

Welcome to Feast

@@ -62,14 +55,14 @@ const RootProjectSelectionPage = () => {

Select one of the projects.

- {isLoading && } + {isLoading && } {isSuccess && data?.projects && ( {projectCards} )} -
-
+ + ); }; diff --git a/ui/src/pages/Sidebar.tsx b/ui/src/pages/Sidebar.tsx index 2b652fc08d..dac02709ba 100644 --- a/ui/src/pages/Sidebar.tsx +++ b/ui/src/pages/Sidebar.tsx @@ -6,11 +6,11 @@ import { useMatchSubpath } from "../hooks/useMatchSubpath"; import useLoadRegistry from "../queries/useLoadRegistry"; import RegistryPathContext from "../contexts/RegistryPathContext"; -import { DataSourceIcon16 } from "../graphics/DataSourceIcon"; -import { EntityIcon16 } from "../graphics/EntityIcon"; -import { FeatureViewIcon16 } from "../graphics/FeatureViewIcon"; -import { FeatureServiceIcon16 } from "../graphics/FeatureServiceIcon"; -import { DatasetIcon16 } from "../graphics/DatasetIcon"; +import { DataSourceIcon } from "../graphics/DataSourceIcon"; +import { EntityIcon } from "../graphics/EntityIcon"; +import { FeatureViewIcon } from "../graphics/FeatureViewIcon"; +import { FeatureServiceIcon } from "../graphics/FeatureServiceIcon"; +import { DatasetIcon } from "../graphics/DatasetIcon"; const SideNav = () => { const registryUrl = useContext(RegistryPathContext); @@ -66,7 +66,7 @@ const SideNav = () => { { name: dataSourcesLabel, id: htmlIdGenerator("dataSources")(), - icon: , + icon: , onClick: () => { navigate(`${process.env.PUBLIC_URL || ""}/p/${projectName}/data-source`); }, @@ -75,7 +75,7 @@ const SideNav = () => { { name: entitiesLabel, id: htmlIdGenerator("entities")(), - icon: , + icon: , onClick: () => { navigate(`${process.env.PUBLIC_URL || ""}/p/${projectName}/entity`); }, @@ -84,7 +84,7 @@ const SideNav = () => { { name: featureViewsLabel, id: htmlIdGenerator("featureView")(), - icon: , + icon: , onClick: () => { navigate(`${process.env.PUBLIC_URL || ""}/p/${projectName}/feature-view`); }, @@ -93,7 +93,7 @@ const SideNav = () => { { name: featureServicesLabel, id: htmlIdGenerator("featureService")(), - icon: , + icon: , onClick: () => { navigate(`${process.env.PUBLIC_URL || ""}/p/${projectName}/feature-service`); }, @@ -102,7 +102,7 @@ const SideNav = () => { { name: savedDatasetsLabel, id: htmlIdGenerator("savedDatasets")(), - icon: , + icon: , onClick: () => { navigate(`${process.env.PUBLIC_URL || ""}/p/${projectName}/data-set`); }, @@ -118,7 +118,6 @@ const SideNav = () => { mobileTitle="Feast" toggleOpenOnMobile={() => toggleOpenOnMobile()} isOpenOnMobile={isSideNavOpenOnMobile} - style={{ width: 192 }} items={sideNav} /> ); diff --git a/ui/src/pages/data-sources/DataSourceInstance.tsx b/ui/src/pages/data-sources/DataSourceInstance.tsx index d6db4c8747..1ed2cfa5ea 100644 --- a/ui/src/pages/data-sources/DataSourceInstance.tsx +++ b/ui/src/pages/data-sources/DataSourceInstance.tsx @@ -1,12 +1,8 @@ import React from "react"; import { Route, Routes, useNavigate, useParams } from "react-router-dom"; -import { - EuiPageHeader, - EuiPageContent, - EuiPageContentBody, -} from "@elastic/eui"; +import { EuiPageTemplate } from "@elastic/eui"; -import { DataSourceIcon32 } from "../../graphics/DataSourceIcon"; +import { DataSourceIcon } from "../../graphics/DataSourceIcon"; import { useMatchExact } from "../../hooks/useMatchSubpath"; import { useDocumentTitle } from "../../hooks/useDocumentTitle"; import DataSourceRawData from "./DataSourceRawData"; @@ -39,29 +35,21 @@ const DataSourceInstance = () => { const CustomTabRoutes = useDataSourceCustomTabRoutes(); return ( - - + - - - - } /> - } /> - {CustomTabRoutes} - - - - + + + } /> + } /> + {CustomTabRoutes} + + + ); }; diff --git a/ui/src/pages/data-sources/Index.tsx b/ui/src/pages/data-sources/Index.tsx index bbccea04ac..b5cc72e654 100644 --- a/ui/src/pages/data-sources/Index.tsx +++ b/ui/src/pages/data-sources/Index.tsx @@ -1,10 +1,13 @@ import React, { useContext } from "react"; import { - EuiPageHeader, - EuiPageContent, - EuiPageContentBody, - EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem, EuiTitle, EuiFieldSearch, EuiSpacer, + EuiPageTemplate, + EuiLoadingSpinner, + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiFieldSearch, + EuiSpacer, } from "@elastic/eui"; import useLoadRegistry from "../../queries/useLoadRegistry"; @@ -12,8 +15,8 @@ import DatasourcesListingTable from "./DataSourcesListingTable"; import { useDocumentTitle } from "../../hooks/useDocumentTitle"; import RegistryPathContext from "../../contexts/RegistryPathContext"; import DataSourceIndexEmptyState from "./DataSourceIndexEmptyState"; -import { DataSourceIcon32 } from "../../graphics/DataSourceIcon"; -import { useSearchQuery} from "../../hooks/useSearchInputWithTags"; +import { DataSourceIcon } from "../../graphics/DataSourceIcon"; +import { useSearchQuery } from "../../hooks/useSearchInputWithTags"; import { feast } from "../../protos"; const useLoadDatasources = () => { @@ -52,56 +55,48 @@ const Index = () => { const { searchString, searchTokens, setSearchString } = useSearchQuery(); - const filterResult = data + const filterResult = data ? filterFn(data, searchTokens) : data; return ( - - + - - - {isLoading && ( -

- Loading -

- )} - {isError &&

We encountered an error while loading.

} - {isSuccess && !data && } - {isSuccess && data && data.length > 0 && filterResult && ( - - - - -

Search

-
- { - setSearchString(e.target.value); - }} - /> -
-
- - -
- )} -
-
-
+ + {isLoading && ( +

+ Loading +

+ )} + {isError &&

We encountered an error while loading.

} + {isSuccess && !data && } + {isSuccess && data && data.length > 0 && filterResult && ( + + + + +

Search

+
+ { + setSearchString(e.target.value); + }} + /> +
+
+ + +
+ )} +
+ ); }; diff --git a/ui/src/pages/entities/EntityInstance.tsx b/ui/src/pages/entities/EntityInstance.tsx index b44967b178..e3be0ef167 100644 --- a/ui/src/pages/entities/EntityInstance.tsx +++ b/ui/src/pages/entities/EntityInstance.tsx @@ -1,12 +1,8 @@ import React from "react"; import { Route, Routes, useNavigate, useParams } from "react-router-dom"; -import { - EuiPageHeader, - EuiPageContent, - EuiPageContentBody, -} from "@elastic/eui"; +import { EuiPageTemplate } from "@elastic/eui"; -import { EntityIcon32 } from "../../graphics/EntityIcon"; +import { EntityIcon } from "../../graphics/EntityIcon"; import { useMatchExact } from "../../hooks/useMatchSubpath"; import EntityOverviewTab from "./EntityOverviewTab"; import { useDocumentTitle } from "../../hooks/useDocumentTitle"; @@ -25,10 +21,10 @@ const EntityInstance = () => { useDocumentTitle(`${entityName} | Entity | Feast`); return ( - - + { ...customNavigationTabs, ]} /> - - - - } /> - {CustomTabRoutes} - - - - + + + } /> + {CustomTabRoutes} + + + ); }; diff --git a/ui/src/pages/entities/FeatureViewEdgesList.tsx b/ui/src/pages/entities/FeatureViewEdgesList.tsx index ab1fbfb6df..8a0b6164b4 100644 --- a/ui/src/pages/entities/FeatureViewEdgesList.tsx +++ b/ui/src/pages/entities/FeatureViewEdgesList.tsx @@ -51,7 +51,7 @@ const FeatureViewEdgesList = ({ fvNames }: FeatureViewEdgesListInterace) => { { name: "Name", field: "", - render: (name: string) => { + render: ({ name }: { name: string }) => { return ( { }, { name: "FS Consumers", - render: (name: string) => { + field: "", + render: ({ name }: { name: string }) => { return ( {isLoading && } @@ -82,7 +83,7 @@ const FeatureViewEdgesList = ({ fvNames }: FeatureViewEdgesListInterace) => { }; return ( - + ({ name }))} rowProps={getRowProps} /> ); }; diff --git a/ui/src/pages/entities/Index.tsx b/ui/src/pages/entities/Index.tsx index 43360f6866..1ce80b583f 100644 --- a/ui/src/pages/entities/Index.tsx +++ b/ui/src/pages/entities/Index.tsx @@ -1,13 +1,8 @@ import React, { useContext } from "react"; -import { - EuiPageHeader, - EuiPageContent, - EuiPageContentBody, - EuiLoadingSpinner, -} from "@elastic/eui"; +import { EuiPageTemplate, EuiLoadingSpinner } from "@elastic/eui"; -import { EntityIcon32 } from "../../graphics/EntityIcon"; +import { EntityIcon } from "../../graphics/EntityIcon"; import useLoadRegistry from "../../queries/useLoadRegistry"; import EntitiesListingTable from "./EntitiesListingTable"; @@ -36,31 +31,23 @@ const Index = () => { useDocumentTitle(`Entities | Feast`); return ( - - + - - - {isLoading && ( -

- Loading -

- )} - {isError &&

We encountered an error while loading.

} - {isSuccess && !data && } - {isSuccess && data && } -
-
-
+ + {isLoading && ( +

+ Loading +

+ )} + {isError &&

We encountered an error while loading.

} + {isSuccess && !data && } + {isSuccess && data && } +
+ ); }; diff --git a/ui/src/pages/feature-services/FeatureServiceInstance.tsx b/ui/src/pages/feature-services/FeatureServiceInstance.tsx index cf0b09a057..b88d2f4bdb 100644 --- a/ui/src/pages/feature-services/FeatureServiceInstance.tsx +++ b/ui/src/pages/feature-services/FeatureServiceInstance.tsx @@ -1,12 +1,8 @@ import React from "react"; import { Route, Routes, useNavigate, useParams } from "react-router-dom"; -import { - EuiPageHeader, - EuiPageContent, - EuiPageContentBody, -} from "@elastic/eui"; +import { EuiPageTemplate } from "@elastic/eui"; -import { FeatureServiceIcon32 } from "../../graphics/FeatureServiceIcon"; +import { FeatureServiceIcon } from "../../graphics/FeatureServiceIcon"; import { useMatchExact } from "../../hooks/useMatchSubpath"; import FeatureServiceOverviewTab from "./FeatureServiceOverviewTab"; import { useDocumentTitle } from "../../hooks/useDocumentTitle"; @@ -26,10 +22,10 @@ const FeatureServiceInstance = () => { const CustomTabRoutes = useFeatureServiceCustomTabRoutes(); return ( - - + { ...customNavigationTabs, ]} /> - - - - } /> - {CustomTabRoutes} - - - - + + + } /> + {CustomTabRoutes} + + + ); }; diff --git a/ui/src/pages/feature-services/FeatureServiceOverviewTab.tsx b/ui/src/pages/feature-services/FeatureServiceOverviewTab.tsx index f43a0cb68f..4d3d350f08 100644 --- a/ui/src/pages/feature-services/FeatureServiceOverviewTab.tsx +++ b/ui/src/pages/feature-services/FeatureServiceOverviewTab.tsx @@ -51,7 +51,7 @@ const FeatureServiceOverviewTab = () => { {isEmpty &&

No feature service with name: {featureServiceName}

} {isError &&

Error loading feature service: {featureServiceName}

} {isSuccess && data && ( - + @@ -167,7 +167,7 @@ const FeatureServiceOverviewTab = () => { - + )}
); diff --git a/ui/src/pages/feature-services/Index.tsx b/ui/src/pages/feature-services/Index.tsx index d034a24bd7..d4264882fb 100644 --- a/ui/src/pages/feature-services/Index.tsx +++ b/ui/src/pages/feature-services/Index.tsx @@ -1,9 +1,7 @@ import React, { useContext } from "react"; import { - EuiPageHeader, - EuiPageContent, - EuiPageContentBody, + EuiPageTemplate, EuiLoadingSpinner, EuiTitle, EuiSpacer, @@ -12,7 +10,7 @@ import { EuiFieldSearch, } from "@elastic/eui"; -import { FeatureServiceIcon32 } from "../../graphics/FeatureServiceIcon"; +import { FeatureServiceIcon } from "../../graphics/FeatureServiceIcon"; import useLoadRegistry from "../../queries/useLoadRegistry"; import FeatureServiceListingTable from "./FeatureServiceListingTable"; @@ -112,64 +110,56 @@ const Index = () => { : data; return ( - - + - - - {isLoading && ( -

- Loading -

- )} - {isError &&

We encountered an error while loading.

} - {isSuccess && !data && } - {isSuccess && filterResult && ( - - - - -

Search

-
- { - setSearchString(e.target.value); - }} - /> -
- - - -
- - -
- )} -
-
-
+ + {isLoading && ( +

+ Loading +

+ )} + {isError &&

We encountered an error while loading.

} + {isSuccess && !data && } + {isSuccess && filterResult && ( + + + + +

Search

+
+ { + setSearchString(e.target.value); + }} + /> +
+ + + +
+ + +
+ )} +
+ ); }; diff --git a/ui/src/pages/feature-views/ConsumingFeatureServicesList.tsx b/ui/src/pages/feature-views/ConsumingFeatureServicesList.tsx index 44df7b5111..bb9961c19c 100644 --- a/ui/src/pages/feature-views/ConsumingFeatureServicesList.tsx +++ b/ui/src/pages/feature-views/ConsumingFeatureServicesList.tsx @@ -16,7 +16,7 @@ const ConsumingFeatureServicesList = ({ { name: "Name", field: "", - render: (name: string) => { + render: ({ name }: { name: string }) => { return ( + ({ name }))} rowProps={getRowProps} /> ); }; diff --git a/ui/src/pages/feature-views/Index.tsx b/ui/src/pages/feature-views/Index.tsx index b874a53236..9b591a33d0 100644 --- a/ui/src/pages/feature-views/Index.tsx +++ b/ui/src/pages/feature-views/Index.tsx @@ -1,9 +1,7 @@ import React, { useContext } from "react"; import { - EuiPageHeader, - EuiPageContent, - EuiPageContentBody, + EuiPageTemplate, EuiLoadingSpinner, EuiSpacer, EuiTitle, @@ -12,7 +10,7 @@ import { EuiFlexItem, } from "@elastic/eui"; -import { FeatureViewIcon32 } from "../../graphics/FeatureViewIcon"; +import { FeatureViewIcon } from "../../graphics/FeatureViewIcon"; import useLoadRegistry from "../../queries/useLoadRegistry"; import FeatureViewListingTable from "./FeatureViewListingTable"; @@ -114,64 +112,56 @@ const Index = () => { : data; return ( - - + - - - {isLoading && ( -

- Loading -

- )} - {isError &&

We encountered an error while loading.

} - {isSuccess && data?.length === 0 && } - {isSuccess && data && data.length > 0 && filterResult && ( - - - - -

Search

-
- { - setSearchString(e.target.value); - }} - /> -
- - - -
- - -
- )} -
-
-
+ + {isLoading && ( +

+ Loading +

+ )} + {isError &&

We encountered an error while loading.

} + {isSuccess && data?.length === 0 && } + {isSuccess && data && data.length > 0 && filterResult && ( + + + + +

Search

+
+ { + setSearchString(e.target.value); + }} + /> +
+ + + +
+ + +
+ )} +
+ ); }; diff --git a/ui/src/pages/feature-views/OnDemandFeatureViewInstance.tsx b/ui/src/pages/feature-views/OnDemandFeatureViewInstance.tsx index 166746df22..5a4b48f6d6 100644 --- a/ui/src/pages/feature-views/OnDemandFeatureViewInstance.tsx +++ b/ui/src/pages/feature-views/OnDemandFeatureViewInstance.tsx @@ -1,13 +1,9 @@ import React from "react"; import { Route, Routes, useNavigate } from "react-router-dom"; import { useParams } from "react-router-dom"; -import { - EuiPageHeader, - EuiPageContent, - EuiPageContentBody, -} from "@elastic/eui"; +import { EuiPageTemplate } from "@elastic/eui"; -import { FeatureViewIcon32 } from "../../graphics/FeatureViewIcon"; +import { FeatureViewIcon } from "../../graphics/FeatureViewIcon"; import { useMatchExact } from "../../hooks/useMatchSubpath"; import OnDemandFeatureViewOverviewTab from "./OnDemandFeatureViewOverviewTab"; @@ -29,10 +25,10 @@ const OnDemandFeatureInstance = ({ data }: OnDemandFeatureInstanceProps) => { const CustomTabRoutes = useOnDemandFeatureViewCustomTabRoutes(); return ( - - + { ...customNavigationTabs, ]} /> - - - - } - /> - {CustomTabRoutes} - - - - + + + } + /> + {CustomTabRoutes} + + + ); }; diff --git a/ui/src/pages/feature-views/OnDemandFeatureViewOverviewTab.tsx b/ui/src/pages/feature-views/OnDemandFeatureViewOverviewTab.tsx index aac3f6ac5b..1da9cf6b2a 100644 --- a/ui/src/pages/feature-views/OnDemandFeatureViewOverviewTab.tsx +++ b/ui/src/pages/feature-views/OnDemandFeatureViewOverviewTab.tsx @@ -48,7 +48,7 @@ const OnDemandFeatureViewOverviewTab = ({ : []; return ( - + @@ -138,7 +138,7 @@ const OnDemandFeatureViewOverviewTab = ({ - + ); }; diff --git a/ui/src/pages/feature-views/RegularFeatureViewInstance.tsx b/ui/src/pages/feature-views/RegularFeatureViewInstance.tsx index 6d34745d7c..40adfca0e2 100644 --- a/ui/src/pages/feature-views/RegularFeatureViewInstance.tsx +++ b/ui/src/pages/feature-views/RegularFeatureViewInstance.tsx @@ -1,12 +1,8 @@ import React, { useContext } from "react"; import { Route, Routes, useNavigate } from "react-router-dom"; -import { - EuiPageHeader, - EuiPageContent, - EuiPageContentBody, -} from "@elastic/eui"; +import { EuiPageTemplate } from "@elastic/eui"; -import { FeatureViewIcon32 } from "../../graphics/FeatureViewIcon"; +import { FeatureViewIcon } from "../../graphics/FeatureViewIcon"; import { useMatchExact, useMatchSubpath } from "../../hooks/useMatchSubpath"; import RegularFeatureViewOverviewTab from "./RegularFeatureViewOverviewTab"; @@ -53,31 +49,23 @@ const RegularFeatureInstance = ({ data }: RegularFeatureInstanceProps) => { const TabRoutes = useRegularFeatureViewCustomTabRoutes(); return ( - - + - - - - } - /> - {TabRoutes} - - - - + + + } + /> + {TabRoutes} + + + ); }; diff --git a/ui/src/pages/feature-views/StreamFeatureViewInstance.tsx b/ui/src/pages/feature-views/StreamFeatureViewInstance.tsx index 52bc06bc5e..0e22a6c2e5 100644 --- a/ui/src/pages/feature-views/StreamFeatureViewInstance.tsx +++ b/ui/src/pages/feature-views/StreamFeatureViewInstance.tsx @@ -1,13 +1,9 @@ import React from "react"; import { Route, Routes, useNavigate } from "react-router-dom"; import { useParams } from "react-router-dom"; -import { - EuiPageHeader, - EuiPageContent, - EuiPageContentBody, -} from "@elastic/eui"; +import { EuiPageTemplate } from "@elastic/eui"; -import { FeatureViewIcon32 } from "../../graphics/FeatureViewIcon"; +import { FeatureViewIcon } from "../../graphics/FeatureViewIcon"; import { useMatchExact } from "../../hooks/useMatchSubpath"; import StreamFeatureViewOverviewTab from "./StreamFeatureViewOverviewTab"; @@ -29,10 +25,11 @@ const StreamFeatureInstance = ({ data }: StreamFeatureInstanceProps) => { const CustomTabRoutes = useStreamFeatureViewCustomTabRoutes(); return ( - - + { ...customNavigationTabs, ]} /> - - - - } - /> - {CustomTabRoutes} - - - - + + + } + /> + {CustomTabRoutes} + + + ); }; diff --git a/ui/src/pages/feature-views/StreamFeatureViewOverviewTab.tsx b/ui/src/pages/feature-views/StreamFeatureViewOverviewTab.tsx index 99f82d3e74..b4514a5edd 100644 --- a/ui/src/pages/feature-views/StreamFeatureViewOverviewTab.tsx +++ b/ui/src/pages/feature-views/StreamFeatureViewOverviewTab.tsx @@ -47,7 +47,7 @@ const StreamFeatureViewOverviewTab = ({ : []; return ( - + @@ -126,7 +126,7 @@ const StreamFeatureViewOverviewTab = ({ - + ); }; diff --git a/ui/src/pages/feature-views/components/FeatureViewProjectionDisplayPanel.tsx b/ui/src/pages/feature-views/components/FeatureViewProjectionDisplayPanel.tsx index f6856471e0..2a68cc49b5 100644 --- a/ui/src/pages/feature-views/components/FeatureViewProjectionDisplayPanel.tsx +++ b/ui/src/pages/feature-views/components/FeatureViewProjectionDisplayPanel.tsx @@ -1,6 +1,5 @@ import React from "react"; -import { EuiBasicTable, EuiPanel, EuiText, EuiTitle } from "@elastic/eui"; - +import { EuiBasicTable, EuiPanel, EuiSpacer, EuiText, EuiTitle } from "@elastic/eui"; import { useParams } from "react-router-dom"; import EuiCustomLink from "../../../components/EuiCustomLink"; import { feast } from "../../../protos"; @@ -29,6 +28,7 @@ const FeatureViewProjectionDisplayPanel = (featureViewProjection: RequestDataDis Feature View + + Request Data + + ); diff --git a/ui/src/pages/features/FeatureInstance.tsx b/ui/src/pages/features/FeatureInstance.tsx index 6eb7d0f2d6..fe81c6e619 100644 --- a/ui/src/pages/features/FeatureInstance.tsx +++ b/ui/src/pages/features/FeatureInstance.tsx @@ -1,12 +1,8 @@ import React from "react"; import { Route, Routes, useNavigate, useParams } from "react-router-dom"; -import { - EuiPageHeader, - EuiPageContent, - EuiPageContentBody, -} from "@elastic/eui"; +import { EuiPageTemplate } from "@elastic/eui"; -import { FeatureIcon32 } from "../../graphics/FeatureIcon"; +import { FeatureIcon } from "../../graphics/FeatureIcon"; import { useMatchExact } from "../../hooks/useMatchSubpath"; import FeatureOverviewTab from "./FeatureOverviewTab"; import { useDocumentTitle } from "../../hooks/useDocumentTitle"; @@ -25,10 +21,10 @@ const FeatureInstance = () => { useDocumentTitle(`${FeatureName} | ${FeatureViewName} | Feast`); return ( - - + { ...customNavigationTabs, ]} /> - - - - } /> - {CustomTabRoutes} - - - - + + + } /> + {CustomTabRoutes} + + + ); }; diff --git a/ui/src/pages/saved-data-sets/DatasetInstance.tsx b/ui/src/pages/saved-data-sets/DatasetInstance.tsx index 26df0b3eb3..0f7e3ab898 100644 --- a/ui/src/pages/saved-data-sets/DatasetInstance.tsx +++ b/ui/src/pages/saved-data-sets/DatasetInstance.tsx @@ -1,12 +1,8 @@ import React from "react"; import { Route, Routes, useNavigate, useParams } from "react-router-dom"; -import { - EuiPageHeader, - EuiPageContent, - EuiPageContentBody, -} from "@elastic/eui"; +import { EuiPageTemplate } from "@elastic/eui"; -import { DatasetIcon32 } from "../../graphics/DatasetIcon"; +import { DatasetIcon } from "../../graphics/DatasetIcon"; import { useMatchExact, useMatchSubpath } from "../../hooks/useMatchSubpath"; import DatasetOverviewTab from "./DatasetOverviewTab"; @@ -27,10 +23,10 @@ const DatasetInstance = () => { const CustomTabRoutes = useDataSourceCustomTabRoutes(); return ( - - + { ...customNavigationTabs, ]} /> - - - - } /> - } /> - {CustomTabRoutes} - - - - + + + } /> + } /> + {CustomTabRoutes} + + + ); }; diff --git a/ui/src/pages/saved-data-sets/Index.tsx b/ui/src/pages/saved-data-sets/Index.tsx index 5f20621baf..c6cc81f414 100644 --- a/ui/src/pages/saved-data-sets/Index.tsx +++ b/ui/src/pages/saved-data-sets/Index.tsx @@ -1,13 +1,8 @@ import React, { useContext } from "react"; -import { - EuiPageHeader, - EuiPageContent, - EuiPageContentBody, - EuiLoadingSpinner, -} from "@elastic/eui"; +import { EuiPageTemplate, EuiLoadingSpinner } from "@elastic/eui"; -import { DatasetIcon32 } from "../../graphics/DatasetIcon"; +import { DatasetIcon } from "../../graphics/DatasetIcon"; import useLoadRegistry from "../../queries/useLoadRegistry"; import { useDocumentTitle } from "../../hooks/useDocumentTitle"; @@ -36,31 +31,23 @@ const Index = () => { useDocumentTitle(`Saved Datasets | Feast`); return ( - - + - - - {isLoading && ( -

- Loading -

- )} - {isError &&

We encountered an error while loading.

} - {isSuccess && data && } - {isSuccess && !data && } -
-
-
+ + {isLoading && ( +

+ Loading +

+ )} + {isError &&

We encountered an error while loading.

} + {isSuccess && data && } + {isSuccess && !data && } +
+ ); }; diff --git a/ui/yarn.lock b/ui/yarn.lock index 40a4d355eb..c79ca558a9 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -1164,7 +1164,7 @@ core-js-pure "^3.20.2" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.7.tgz#03ff99f64106588c9c403c6ecb8c3bafbbdff1fa" integrity sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ== @@ -1178,6 +1178,13 @@ dependencies: regenerator-runtime "^0.13.11" +"@babel/runtime@^7.12.1", "@babel/runtime@^7.18.3", "@babel/runtime@^7.24.1": + version "7.25.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.6.tgz#9afc3289f7184d8d7f98b099884c26317b9264d2" + integrity sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.16.7", "@babel/template@^7.3.3": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155" @@ -1268,66 +1275,88 @@ dependencies: tslib "^1.9.3" -"@elastic/eui@^55.0.1": - version "55.1.2" - resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-55.1.2.tgz#dd0b42f5b26c5800d6a9cb2d4c2fe1afce9d3f07" - integrity sha512-wwZz5KxMIMFlqEsoCRiQBJDc4CrluS1d0sCOmQ5lhIzKhYc91MdxnqCk2i6YkhL4sSDf2Y9KAEuMXa+uweOWUA== - dependencies: - "@types/chroma-js" "^2.0.0" - "@types/lodash" "^4.14.160" - "@types/numeral" "^0.0.28" - "@types/react-beautiful-dnd" "^13.1.2" - "@types/react-input-autosize" "^2.2.1" - "@types/react-virtualized-auto-sizer" "^1.0.1" - "@types/react-window" "^1.8.5" - "@types/refractor" "^3.0.0" - "@types/resize-observer-browser" "^0.1.5" - "@types/vfile-message" "^2.0.0" - chroma-js "^2.1.0" - classnames "^2.2.6" +"@elastic/eui@^95.12.0": + version "95.12.0" + resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-95.12.0.tgz#862f2be8b72248a62b40704b9e62f2f5d7d43853" + integrity sha512-SW4ru97FY2VitSqyCgURrM5OMk1W+Ww12b6S+VZN5ex50aNT296DfED/ByidlYaAoVihqjZuoB3HlQBBXydFpA== + dependencies: + "@hello-pangea/dnd" "^16.6.0" + "@types/lodash" "^4.14.202" + "@types/numeral" "^2.0.5" + "@types/react-window" "^1.8.8" + "@types/refractor" "^3.4.0" + chroma-js "^2.4.2" + classnames "^2.5.1" lodash "^4.17.21" - mdast-util-to-hast "^10.0.0" + mdast-util-to-hast "^10.2.0" numeral "^2.0.6" - prop-types "^15.6.0" - react-beautiful-dnd "^13.1.0" - react-dropzone "^11.5.3" - react-element-to-jsx-string "^14.3.4" - react-focus-on "^3.5.4" - react-input-autosize "^3.0.0" + prop-types "^15.8.1" + react-dropzone "^11.7.1" + react-element-to-jsx-string "^15.0.0" + react-focus-on "^3.9.1" react-is "^17.0.2" - react-virtualized-auto-sizer "^1.0.6" - react-window "^1.8.6" - refractor "^3.5.0" - rehype-raw "^5.0.0" - rehype-react "^6.0.0" + react-remove-scroll-bar "^2.3.4" + react-virtualized-auto-sizer "^1.0.24" + react-window "^1.8.10" + refractor "^3.6.0" + rehype-raw "^5.1.0" + rehype-react "^6.2.1" rehype-stringify "^8.0.0" remark-breaks "^2.0.2" remark-emoji "^2.1.0" - remark-parse "^8.0.3" - remark-rehype "^8.0.0" - tabbable "^5.2.1" + remark-parse-no-trim "^8.0.4" + remark-rehype "^8.1.0" + tabbable "^5.3.3" text-diff "^1.0.1" - unified "^9.2.0" + unified "^9.2.2" unist-util-visit "^2.0.3" url-parse "^1.5.10" uuid "^8.3.0" - vfile "^4.2.0" + vfile "^4.2.1" -"@emotion/cache@^11.7.1": - version "11.7.1" - resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.7.1.tgz#08d080e396a42e0037848214e8aa7bf879065539" - integrity sha512-r65Zy4Iljb8oyjtLeCuBH8Qjiy107dOYC6SJq7g7GV5UCQWMObY4SJDPGFjiiVpPrOJ2hmJOoBiYTC7hwx9E2A== +"@emotion/babel-plugin@^11.12.0": + version "11.12.0" + resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.12.0.tgz#7b43debb250c313101b3f885eba634f1d723fcc2" + integrity sha512-y2WQb+oP8Jqvvclh8Q55gLUyb7UFvgv7eJfsj7td5TToBrIUtPay2kMrZi4xjq9qw2vD0ZR5fSho0yqoFgX7Rw== dependencies: - "@emotion/memoize" "^0.7.4" - "@emotion/sheet" "^1.1.0" - "@emotion/utils" "^1.0.0" - "@emotion/weak-memoize" "^0.2.5" - stylis "4.0.13" - -"@emotion/hash@^0.8.0": - version "0.8.0" - resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" - integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== + "@babel/helper-module-imports" "^7.16.7" + "@babel/runtime" "^7.18.3" + "@emotion/hash" "^0.9.2" + "@emotion/memoize" "^0.9.0" + "@emotion/serialize" "^1.2.0" + babel-plugin-macros "^3.1.0" + convert-source-map "^1.5.0" + escape-string-regexp "^4.0.0" + find-root "^1.1.0" + source-map "^0.5.7" + stylis "4.2.0" + +"@emotion/cache@^11.13.0": + version "11.13.1" + resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.13.1.tgz#fecfc54d51810beebf05bf2a161271a1a91895d7" + integrity sha512-iqouYkuEblRcXmylXIwwOodiEK5Ifl7JcX7o6V4jI3iW4mLXX3dmt5xwBtIkJiQEXFAI+pC8X0i67yiPkH9Ucw== + dependencies: + "@emotion/memoize" "^0.9.0" + "@emotion/sheet" "^1.4.0" + "@emotion/utils" "^1.4.0" + "@emotion/weak-memoize" "^0.4.0" + stylis "4.2.0" + +"@emotion/css@^11.13.0": + version "11.13.0" + resolved "https://registry.yarnpkg.com/@emotion/css/-/css-11.13.0.tgz#3b44f008ce782dafa7cecff75b263af174d0c702" + integrity sha512-BUk99ylT+YHl+W/HN7nv1RCTkDYmKKqa1qbvM/qLSQEg61gipuBF5Hptk/2/ERmX2DCv0ccuFGhz9i0KSZOqPg== + dependencies: + "@emotion/babel-plugin" "^11.12.0" + "@emotion/cache" "^11.13.0" + "@emotion/serialize" "^1.3.0" + "@emotion/sheet" "^1.4.0" + "@emotion/utils" "^1.4.0" + +"@emotion/hash@^0.9.2": + version "0.9.2" + resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.2.tgz#ff9221b9f58b4dfe61e619a7788734bd63f6898b" + integrity sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g== "@emotion/is-prop-valid@^1.1.0": version "1.2.0" @@ -1336,64 +1365,75 @@ dependencies: "@emotion/memoize" "^0.8.0" -"@emotion/memoize@^0.7.4": - version "0.7.5" - resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.5.tgz#2c40f81449a4e554e9fc6396910ed4843ec2be50" - integrity sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ== - "@emotion/memoize@^0.8.0": version "0.8.0" resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.0.tgz#f580f9beb67176fa57aae70b08ed510e1b18980f" integrity sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA== -"@emotion/react@^11.7.1": - version "11.7.1" - resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.7.1.tgz#3f800ce9b20317c13e77b8489ac4a0b922b2fe07" - integrity sha512-DV2Xe3yhkF1yT4uAUoJcYL1AmrnO5SVsdfvu+fBuS7IbByDeTVx9+wFmvx9Idzv7/78+9Mgx2Hcmr7Fex3tIyw== - dependencies: - "@babel/runtime" "^7.13.10" - "@emotion/cache" "^11.7.1" - "@emotion/serialize" "^1.0.2" - "@emotion/sheet" "^1.1.0" - "@emotion/utils" "^1.0.0" - "@emotion/weak-memoize" "^0.2.5" +"@emotion/memoize@^0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.9.0.tgz#745969d649977776b43fc7648c556aaa462b4102" + integrity sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ== + +"@emotion/react@^11.13.3": + version "11.13.3" + resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.13.3.tgz#a69d0de2a23f5b48e0acf210416638010e4bd2e4" + integrity sha512-lIsdU6JNrmYfJ5EbUCf4xW1ovy5wKQ2CkPRM4xogziOxH1nXxBSjpC9YqbFAP7circxMfYp+6x676BqWcEiixg== + dependencies: + "@babel/runtime" "^7.18.3" + "@emotion/babel-plugin" "^11.12.0" + "@emotion/cache" "^11.13.0" + "@emotion/serialize" "^1.3.1" + "@emotion/use-insertion-effect-with-fallbacks" "^1.1.0" + "@emotion/utils" "^1.4.0" + "@emotion/weak-memoize" "^0.4.0" hoist-non-react-statics "^3.3.1" -"@emotion/serialize@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.0.2.tgz#77cb21a0571c9f68eb66087754a65fa97bfcd965" - integrity sha512-95MgNJ9+/ajxU7QIAruiOAdYNjxZX7G2mhgrtDWswA21VviYIRP1R5QilZ/bDY42xiKsaktP4egJb3QdYQZi1A== +"@emotion/serialize@^1.2.0", "@emotion/serialize@^1.3.0", "@emotion/serialize@^1.3.1": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.3.2.tgz#e1c1a2e90708d5d85d81ccaee2dfeb3cc0cccf7a" + integrity sha512-grVnMvVPK9yUVE6rkKfAJlYZgo0cu3l9iMC77V7DW6E1DUIrU68pSEXRmFZFOFB1QFo57TncmOcvcbMDWsL4yA== dependencies: - "@emotion/hash" "^0.8.0" - "@emotion/memoize" "^0.7.4" - "@emotion/unitless" "^0.7.5" - "@emotion/utils" "^1.0.0" + "@emotion/hash" "^0.9.2" + "@emotion/memoize" "^0.9.0" + "@emotion/unitless" "^0.10.0" + "@emotion/utils" "^1.4.1" csstype "^3.0.2" -"@emotion/sheet@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.1.0.tgz#56d99c41f0a1cda2726a05aa6a20afd4c63e58d2" - integrity sha512-u0AX4aSo25sMAygCuQTzS+HsImZFuS8llY8O7b9MDRzbJM0kVJlAz6KNDqcG7pOuQZJmj/8X/rAW+66kMnMW+g== +"@emotion/sheet@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.4.0.tgz#c9299c34d248bc26e82563735f78953d2efca83c" + integrity sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg== "@emotion/stylis@^0.8.4": version "0.8.5" resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.5.tgz#deacb389bd6ee77d1e7fcaccce9e16c5c7e78e04" integrity sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ== -"@emotion/unitless@^0.7.4", "@emotion/unitless@^0.7.5": +"@emotion/unitless@^0.10.0": + version "0.10.0" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.10.0.tgz#2af2f7c7e5150f497bdabd848ce7b218a27cf745" + integrity sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg== + +"@emotion/unitless@^0.7.4": version "0.7.5" resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== -"@emotion/utils@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.0.0.tgz#abe06a83160b10570816c913990245813a2fd6af" - integrity sha512-mQC2b3XLDs6QCW+pDQDiyO/EdGZYOygE8s5N5rrzjSI4M3IejPE/JPndCBwRT9z982aqQNi6beWs1UeayrQxxA== +"@emotion/use-insertion-effect-with-fallbacks@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz#1a818a0b2c481efba0cf34e5ab1e0cb2dcb9dfaf" + integrity sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw== -"@emotion/weak-memoize@^0.2.5": - version "0.2.5" - resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" - integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== +"@emotion/utils@^1.4.0", "@emotion/utils@^1.4.1": + version "1.4.1" + resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.4.1.tgz#b3adbb43de12ee2149541c4f1337d2eb7774f0ad" + integrity sha512-BymCXzCG3r72VKJxaYVwOXATqXIZ85cuvg0YOUDxMGNrKc1DJRZk8MgV5wyXRyEayIMd4FuXJIUgTBXvDNW5cA== + +"@emotion/weak-memoize@^0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz#5e13fac887f08c44f76b0ccaf3370eb00fec9bb6" + integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg== "@eslint/eslintrc@^1.0.5": version "1.0.5" @@ -1410,6 +1450,19 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" +"@hello-pangea/dnd@^16.6.0": + version "16.6.0" + resolved "https://registry.yarnpkg.com/@hello-pangea/dnd/-/dnd-16.6.0.tgz#7509639c7bd13f55e537b65a9dcfcd54e7c99ac7" + integrity sha512-vfZ4GydqbtUPXSLfAvKvXQ6xwRzIjUSjVU0Sx+70VOhc2xx6CdmJXJ8YhH70RpbTUGjxctslQTHul9sIOxCfFQ== + dependencies: + "@babel/runtime" "^7.24.1" + css-box-model "^1.2.1" + memoize-one "^6.0.0" + raf-schd "^4.0.3" + react-redux "^8.1.3" + redux "^4.2.1" + use-memo-one "^1.1.3" + "@humanwhocodes/config-array@^0.9.2": version "0.9.3" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.9.3.tgz#f2564c744b387775b436418491f15fce6601f63e" @@ -2167,11 +2220,6 @@ dependencies: "@types/node" "*" -"@types/chroma-js@^2.0.0": - version "2.1.3" - resolved "https://registry.yarnpkg.com/@types/chroma-js/-/chroma-js-2.1.3.tgz#0b03d737ff28fad10eb884e0c6cedd5ffdc4ba0a" - integrity sha512-1xGPhoSGY1CPmXLCBcjVZSQinFjL26vlR8ZqprsBWiFyED4JacJJ9zHhh5aaUXqbY9B37mKQ73nlydVAXmr1+g== - "@types/connect-history-api-fallback@^1.3.5": version "1.3.5" resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz#d1f7a8a09d0ed5a57aee5ae9c18ab9b803205dae" @@ -2478,10 +2526,10 @@ dependencies: "@types/unist" "*" -"@types/hoist-non-react-statics@^3.3.0": - version "3.3.1" - resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" - integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== +"@types/hoist-non-react-statics@^3.3.1": + version "3.3.5" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz#dab7867ef789d87e2b4b0003c9d65c49cc44a494" + integrity sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg== dependencies: "@types/react" "*" hoist-non-react-statics "^3.3.0" @@ -2553,10 +2601,10 @@ resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.2.tgz#fd2cd2edbaa7eaac7e7f3c1748b52a19143846c9" integrity sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA== -"@types/lodash@^4.14.160": - version "4.14.178" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.178.tgz#341f6d2247db528d4a13ddbb374bcdc80406f4f8" - integrity sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw== +"@types/lodash@^4.14.202": + version "4.17.9" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.9.tgz#0dc4902c229f6b8e2ac5456522104d7b1a230290" + integrity sha512-w9iWudx1XWOHW5lQRS9iKpK/XuRhnN+0T7HvdCCd802FYkT1AMTnxndJHGrNJwRoRHkslGr4S29tjm1cT7x/7w== "@types/markdown-it@^12.2.3": version "12.2.3" @@ -2598,10 +2646,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.21.tgz#474d7589a30afcf5291f59bd49cca9ad171ffde4" integrity sha512-Pf8M1XD9i1ksZEcCP8vuSNwooJ/bZapNmIzpmsMaL+jMI+8mEYU3PKvs+xDNuQcJWF/x24WzY4qxLtB0zNow9A== -"@types/numeral@^0.0.28": - version "0.0.28" - resolved "https://registry.yarnpkg.com/@types/numeral/-/numeral-0.0.28.tgz#e43928f0bda10b169b6f7ecf99e3ddf836b8ebe4" - integrity sha512-Sjsy10w6XFHDktJJdXzBJmoondAKW+LcGpRFH+9+zXEDj0cOH8BxJuZA9vUDSMAzU1YRJlsPKmZEEiTYDlICLw== +"@types/numeral@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@types/numeral/-/numeral-2.0.5.tgz#388e5c4ff4b0e1787f130753cbbe83d3ba770858" + integrity sha512-kH8I7OSSwQu9DS9JYdFWbuvhVzvFRoCPCkGxNwoGgaPeDfEPJlcxNvEOypZhQ3XXHsGbfIuYcxcJxKUfJHnRfw== "@types/parse-json@^4.0.0": version "4.0.0" @@ -2643,13 +2691,6 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== -"@types/react-beautiful-dnd@^13.1.2": - version "13.1.2" - resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.2.tgz#510405abb09f493afdfd898bf83995dc6385c130" - integrity sha512-+OvPkB8CdE/bGdXKyIhc/Lm2U7UAYCCJgsqmopFmh9gbAudmslkI8eOrPDjg4JhwSE6wytz4a3/wRjKtovHVJg== - dependencies: - "@types/react" "*" - "@types/react-dom@^17.0.9": version "17.0.11" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.11.tgz#e1eadc3c5e86bdb5f7684e00274ae228e7bcc466" @@ -2657,34 +2698,10 @@ dependencies: "@types/react" "*" -"@types/react-input-autosize@^2.2.1": - version "2.2.1" - resolved "https://registry.yarnpkg.com/@types/react-input-autosize/-/react-input-autosize-2.2.1.tgz#6a335212e7fce1e1a4da56ae2095c8c5c35fbfe6" - integrity sha512-RxzEjd4gbLAAdLQ92Q68/AC+TfsAKTc4evsArUH1aIShIMqQMIMjsxoSnwyjtbFTO/AGIW/RQI94XSdvOxCz/w== - dependencies: - "@types/react" "*" - -"@types/react-redux@^7.1.20": - version "7.1.22" - resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.22.tgz#0eab76a37ef477cc4b53665aeaf29cb60631b72a" - integrity sha512-GxIA1kM7ClU73I6wg9IRTVwSO9GS+SAKZKe0Enj+82HMU6aoESFU2HNAdNi3+J53IaOHPiUfT3kSG4L828joDQ== - dependencies: - "@types/hoist-non-react-statics" "^3.3.0" - "@types/react" "*" - hoist-non-react-statics "^3.3.0" - redux "^4.0.0" - -"@types/react-virtualized-auto-sizer@^1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@types/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.1.tgz#b3187dae1dfc4c15880c9cfc5b45f2719ea6ebd4" - integrity sha512-GH8sAnBEM5GV9LTeiz56r4ZhMOUSrP43tAQNSRVxNexDjcNKLCEtnxusAItg1owFUFE6k0NslV26gqVClVvong== - dependencies: - "@types/react" "*" - -"@types/react-window@^1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.5.tgz#285fcc5cea703eef78d90f499e1457e9b5c02fc1" - integrity sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw== +"@types/react-window@^1.8.8": + version "1.8.8" + resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.8.tgz#c20645414d142364fbe735818e1c1e0a145696e3" + integrity sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q== dependencies: "@types/react" "*" @@ -2697,18 +2714,13 @@ "@types/scheduler" "*" csstype "^3.0.2" -"@types/refractor@^3.0.0": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/refractor/-/refractor-3.0.2.tgz#2d42128d59f78f84d2c799ffc5ab5cadbcba2d82" - integrity sha512-2HMXuwGuOqzUG+KUTm9GDJCHl0LCBKsB5cg28ujEmVi/0qgTb6jOmkVSO5K48qXksyl2Fr3C0Q2VrgD4zbwyXg== +"@types/refractor@^3.4.0": + version "3.4.1" + resolved "https://registry.yarnpkg.com/@types/refractor/-/refractor-3.4.1.tgz#8b109804f77b3da8fad543d3f575fef1ece8835a" + integrity sha512-wYuorIiCTSuvRT9srwt+taF6mH/ww+SyN2psM0sjef2qW+sS8GmshgDGTEDgWB1sTVGgYVE6EK7dBA2MxQxibg== dependencies: "@types/prismjs" "*" -"@types/resize-observer-browser@^0.1.5": - version "0.1.6" - resolved "https://registry.yarnpkg.com/@types/resize-observer-browser/-/resize-observer-browser-0.1.6.tgz#d8e6c2f830e2650dc06fe74464472ff64b54a302" - integrity sha512-61IfTac0s9jvNtBCpyo86QeaN8qqpMGHdK0uGKCCIy2dt5/Yk84VduHIdWAcmkC5QvdkPL0p5eWYgUZtHKKUVg== - "@types/resolve@1.17.1": version "1.17.1" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" @@ -2784,12 +2796,10 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== -"@types/vfile-message@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@types/vfile-message/-/vfile-message-2.0.0.tgz#690e46af0fdfc1f9faae00cd049cc888957927d5" - integrity sha512-GpTIuDpb9u4zIO165fUy9+fXcULdD8HFRNli04GehoMVbeNq7D6OBnqSmg3lxZnC+UvgUhEWKxdKiwYUkGltIw== - dependencies: - vfile-message "*" +"@types/use-sync-external-store@^0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43" + integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA== "@types/ws@^8.2.2": version "8.2.2" @@ -3236,12 +3246,12 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -aria-hidden@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.1.3.tgz#bb48de18dc84787a3c6eee113709c473c64ec254" - integrity sha512-RhVWFtKH5BiGMycI72q2RAFMLQi8JP9bLuQXgR5a8Znp7P5KOIADSJeyfI8PCVxLEp067B2HbP5JIiI/PXIZeA== +aria-hidden@^1.2.2: + version "1.2.4" + resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.4.tgz#b78e383fdbc04d05762c78b4a25a501e736c4522" + integrity sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A== dependencies: - tslib "^1.0.0" + tslib "^2.0.0" aria-query@^4.2.2: version "4.2.2" @@ -3885,10 +3895,10 @@ chokidar@^3.4.2, chokidar@^3.5.2, chokidar@^3.5.3: optionalDependencies: fsevents "~2.3.2" -chroma-js@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/chroma-js/-/chroma-js-2.3.0.tgz#fdf68a18d8f97ce006422338044218ccbffb9ecb" - integrity sha512-dRgAp9FHHy+VfE7e3/I5HHU0+zZlUHBODcjvXUXinsR/NnHCO+kfv68ofzrAqFI80q2IWvDSAmHAqHh93TGgKg== +chroma-js@^2.4.2: + version "2.6.0" + resolved "https://registry.yarnpkg.com/chroma-js/-/chroma-js-2.6.0.tgz#578743dd359698a75067a19fa5571dec54d0b70b" + integrity sha512-BLHvCB9s8Z1EV4ethr6xnkl/P2YRFOGqfgvuMG/MyCbZPrTA+NeiByY6XvgF0zP4/2deU2CXnWyMa3zu1LqQ3A== chrome-trace-event@^1.0.2: version "1.0.3" @@ -3905,10 +3915,10 @@ cjs-module-lexer@^1.0.0: resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40" integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== -classnames@^2.2.6: - version "2.3.1" - resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" - integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA== +classnames@^2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" + integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== clean-css@^5.2.2: version "5.2.4" @@ -4126,6 +4136,11 @@ convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: dependencies: safe-buffer "~5.1.1" +convert-source-map@^1.5.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" + integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== + cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" @@ -4207,7 +4222,7 @@ css-blank-pseudo@^3.0.2: dependencies: postcss-selector-parser "^6.0.8" -css-box-model@^1.2.0: +css-box-model@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1" integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw== @@ -5704,6 +5719,11 @@ find-cache-dir@^3.3.1: make-dir "^3.0.2" pkg-dir "^4.1.0" +find-root@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" + integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== + find-up@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" @@ -5747,10 +5767,10 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3" integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg== -focus-lock@^0.11.2: - version "0.11.2" - resolved "https://registry.yarnpkg.com/focus-lock/-/focus-lock-0.11.2.tgz#aeef3caf1cea757797ac8afdebaec8fd9ab243ed" - integrity sha512-pZ2bO++NWLHhiKkgP1bEXHhR1/OjVcSvlCJ98aNJDFeb7H5OOQaO+SKOZle6041O9rv2tmbrO4JzClAvDUHf0g== +focus-lock@^1.3.5: + version "1.3.5" + resolved "https://registry.yarnpkg.com/focus-lock/-/focus-lock-1.3.5.tgz#aa644576e5ec47d227b57eb14e1efb2abf33914c" + integrity sha512-QFaHbhv9WPUeLYBDe/PAuLKJ4Dd9OPvKs9xZBr3yLXnUrDNaVXKu2baDBXe3naPY30hgHYSsf2JW4jzas2mDEQ== dependencies: tslib "^2.0.3" @@ -7743,7 +7763,7 @@ mdast-util-definitions@^4.0.0: dependencies: unist-util-visit "^2.0.0" -mdast-util-to-hast@^10.0.0, mdast-util-to-hast@^10.2.0: +mdast-util-to-hast@^10.2.0: version "10.2.0" resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-10.2.0.tgz#61875526a017d8857b71abc9333942700b2d3604" integrity sha512-JoPBfJ3gBnHZ18icCwHR50orC9kNH81tiR1gs01D8Q5YpV6adHNO9nKNuFBCJQ941/32PT1a63UF/DitmS3amQ== @@ -7796,11 +7816,16 @@ memfs@^3.4.3: dependencies: fs-monkey "^1.0.4" -"memoize-one@>=3.1.1 <6", memoize-one@^5.1.1: +"memoize-one@>=3.1.1 <6": version "5.2.1" resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== +memoize-one@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045" + integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw== + merge-descriptors@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" @@ -9068,10 +9093,10 @@ prismjs@~1.17.0: optionalDependencies: clipboard "^2.0.0" -prismjs@~1.25.0: - version "1.25.0" - resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.25.0.tgz#6f822df1bdad965734b310b315a23315cf999756" - integrity sha512-WCjJHl1KEWbnkQom1+SzftbtXMKQoezOCYs5rECqMN+jP+apI7ftoflyqigqzopSO3hMhTEb0mFClA8lkolgEg== +prismjs@~1.27.0: + version "1.27.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.27.0.tgz#bb6ee3138a0b438a3653dd4d6ce0cc6510a45057" + integrity sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA== process-nextick-args@~2.0.0: version "2.0.1" @@ -9093,7 +9118,7 @@ prompts@^2.0.1, prompts@^2.4.2: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -9198,7 +9223,7 @@ quick-lru@^5.1.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== -raf-schd@^4.0.2: +raf-schd@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a" integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ== @@ -9244,19 +9269,6 @@ react-app-polyfill@^3.0.0: regenerator-runtime "^0.13.9" whatwg-fetch "^3.6.2" -react-beautiful-dnd@^13.1.0: - version "13.1.0" - resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz#ec97c81093593526454b0de69852ae433783844d" - integrity sha512-aGvblPZTJowOWUNiwd6tNfEpgkX5OxmpqxHKNW/4VmvZTNTbeiq7bA3bn5T+QSF2uibXB0D1DmJsb1aC/+3cUA== - dependencies: - "@babel/runtime" "^7.9.2" - css-box-model "^1.2.0" - memoize-one "^5.1.1" - raf-schd "^4.0.2" - react-redux "^7.2.0" - redux "^4.0.4" - use-memo-one "^1.1.1" - react-clientside-effect@^1.2.6: version "1.2.6" resolved "https://registry.yarnpkg.com/react-clientside-effect/-/react-clientside-effect-1.2.6.tgz#29f9b14e944a376b03fb650eed2a754dd128ea3a" @@ -9313,7 +9325,7 @@ react-dom@^17.0.2: object-assign "^4.1.1" scheduler "^0.20.2" -react-dropzone@^11.5.3: +react-dropzone@^11.7.1: version "11.7.1" resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-11.7.1.tgz#3851bb75b26af0bf1b17ce1449fd980e643b9356" integrity sha512-zxCMwhfPy1olUEbw3FLNPLhAm/HnaYH5aELIEglRbqabizKAdHs0h+WuyOpmA+v1JXn0++fpQDdNfUagWt5hJQ== @@ -9322,62 +9334,64 @@ react-dropzone@^11.5.3: file-selector "^0.4.0" prop-types "^15.8.1" -react-element-to-jsx-string@^14.3.4: - version "14.3.4" - resolved "https://registry.yarnpkg.com/react-element-to-jsx-string/-/react-element-to-jsx-string-14.3.4.tgz#709125bc72f06800b68f9f4db485f2c7d31218a8" - integrity sha512-t4ZwvV6vwNxzujDQ+37bspnLwA4JlgUPWhLjBJWsNIDceAf6ZKUTCjdm08cN6WeZ5pTMKiCJkmAYnpmR4Bm+dg== +react-element-to-jsx-string@^15.0.0: + version "15.0.0" + resolved "https://registry.yarnpkg.com/react-element-to-jsx-string/-/react-element-to-jsx-string-15.0.0.tgz#1cafd5b6ad41946ffc8755e254da3fc752a01ac6" + integrity sha512-UDg4lXB6BzlobN60P8fHWVPX3Kyw8ORrTeBtClmIlGdkOOE+GYQSFvmEU5iLLpwp/6v42DINwNcwOhOLfQ//FQ== dependencies: "@base2/pretty-print-object" "1.0.1" is-plain-object "5.0.0" - react-is "17.0.2" + react-is "18.1.0" react-error-overlay@^6.0.10: version "6.0.10" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.10.tgz#0fe26db4fa85d9dbb8624729580e90e7159a59a6" integrity sha512-mKR90fX7Pm5seCOfz8q9F+66VCc1PGsWSBxKbITjfKVQHMNF2zudxHnMdJiB1fRCb+XsbQV9sO9DCkgsMQgBIA== -react-focus-lock@^2.9.0: - version "2.9.1" - resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.9.1.tgz#094cfc19b4f334122c73bb0bff65d77a0c92dd16" - integrity sha512-pSWOQrUmiKLkffPO6BpMXN7SNKXMsuOakl652IBuALAu1esk+IcpJyM+ALcYzPTTFz1rD0R54aB9A4HuP5t1Wg== +react-focus-lock@^2.11.3: + version "2.13.2" + resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.13.2.tgz#e1addac2f8b9550bc0581f3c416755ba0f81f5ef" + integrity sha512-T/7bsofxYqnod2xadvuwjGKHOoL5GH7/EIPI5UyEvaU/c2CcphvGI371opFtuY/SYdbMsNiuF4HsHQ50nA/TKQ== dependencies: "@babel/runtime" "^7.0.0" - focus-lock "^0.11.2" + focus-lock "^1.3.5" prop-types "^15.6.2" react-clientside-effect "^1.2.6" - use-callback-ref "^1.3.0" + use-callback-ref "^1.3.2" use-sidecar "^1.1.2" -react-focus-on@^3.5.4: - version "3.6.0" - resolved "https://registry.yarnpkg.com/react-focus-on/-/react-focus-on-3.6.0.tgz#159e13082dad4ea1f07abe11254f0e981d5a7b79" - integrity sha512-onIRjpd9trAUenXNdDcvjc8KJUSklty4X/Gr7hAm/MzM7ekSF2pg9D8KBKL7ipige22IAPxLRRf/EmJji9KD6Q== +react-focus-on@^3.9.1: + version "3.9.4" + resolved "https://registry.yarnpkg.com/react-focus-on/-/react-focus-on-3.9.4.tgz#0b6c13273d86243c330d1aa53af39290f543da7b" + integrity sha512-NFKmeH6++wu8e7LJcbwV8TTd4L5w/U5LMXTMOdUcXhCcZ7F5VOvgeTHd4XN1PD7TNmdvldDu/ENROOykUQ4yQg== dependencies: - aria-hidden "^1.1.3" - react-focus-lock "^2.9.0" - react-remove-scroll "^2.5.2" - react-style-singleton "^2.2.0" + aria-hidden "^1.2.2" + react-focus-lock "^2.11.3" + react-remove-scroll "^2.6.0" + react-style-singleton "^2.2.1" tslib "^2.3.1" - use-callback-ref "^1.3.0" use-sidecar "^1.1.2" -react-input-autosize@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-3.0.0.tgz#6b5898c790d4478d69420b55441fcc31d5c50a85" - integrity sha512-nL9uS7jEs/zu8sqwFE5MAPx6pPkNAriACQ2rGLlqmKr2sPGtN7TXTyDdQt4lbNXVx7Uzadb40x8qotIuru6Rhg== - dependencies: - prop-types "^15.5.8" - -react-is@17.0.2, react-is@^17.0.1, react-is@^17.0.2: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" - integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== +react-is@18.1.0: + version "18.1.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.1.0.tgz#61aaed3096d30eacf2a2127118b5b41387d32a67" + integrity sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg== react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-is@^17.0.1, react-is@^17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + +react-is@^18.0.0: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" + integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== + react-query@^3.34.12: version "3.34.12" resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.34.12.tgz#dcaaf7b629f0868aae8afef9fb7692f6ea7643bf" @@ -9387,39 +9401,39 @@ react-query@^3.34.12: broadcast-channel "^3.4.1" match-sorter "^6.0.2" -react-redux@^7.2.0: - version "7.2.6" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.6.tgz#49633a24fe552b5f9caf58feb8a138936ddfe9aa" - integrity sha512-10RPdsz0UUrRL1NZE0ejTkucnclYSgXp5q+tB5SWx2qeG2ZJQJyymgAhwKy73yiL/13btfB6fPr+rgbMAaZIAQ== +react-redux@^8.1.3: + version "8.1.3" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.1.3.tgz#4fdc0462d0acb59af29a13c27ffef6f49ab4df46" + integrity sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw== dependencies: - "@babel/runtime" "^7.15.4" - "@types/react-redux" "^7.1.20" + "@babel/runtime" "^7.12.1" + "@types/hoist-non-react-statics" "^3.3.1" + "@types/use-sync-external-store" "^0.0.3" hoist-non-react-statics "^3.3.2" - loose-envify "^1.4.0" - prop-types "^15.7.2" - react-is "^17.0.2" + react-is "^18.0.0" + use-sync-external-store "^1.0.0" react-refresh@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046" integrity sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A== -react-remove-scroll-bar@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.1.tgz#9f13b05b249eaa57c8d646c1ebb83006b3581f5f" - integrity sha512-IvGX3mJclEF7+hga8APZczve1UyGMkMG+tjS0o/U1iLgvZRpjFAQEUBJ4JETfvbNlfNnZnoDyWJCICkA15Mghg== +react-remove-scroll-bar@^2.3.4, react-remove-scroll-bar@^2.3.6: + version "2.3.6" + resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz#3e585e9d163be84a010180b18721e851ac81a29c" + integrity sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g== dependencies: - react-style-singleton "^2.2.0" + react-style-singleton "^2.2.1" tslib "^2.0.0" -react-remove-scroll@^2.5.2: - version "2.5.3" - resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.5.3.tgz#a152196e710e8e5811be39dc352fd8a90b05c961" - integrity sha512-NQ1bXrxKrnK5pFo/GhLkXeo3CrK5steI+5L+jynwwIemvZyfXqaL0L5BzwJd7CSwNCU723DZaccvjuyOdoy3Xw== +react-remove-scroll@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.6.0.tgz#fb03a0845d7768a4f1519a99fdb84983b793dc07" + integrity sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ== dependencies: - react-remove-scroll-bar "^2.3.1" - react-style-singleton "^2.2.0" - tslib "^2.0.0" + react-remove-scroll-bar "^2.3.6" + react-style-singleton "^2.2.1" + tslib "^2.1.0" use-callback-ref "^1.3.0" use-sidecar "^1.1.2" @@ -9493,10 +9507,10 @@ react-scripts@^5.0.0: optionalDependencies: fsevents "^2.3.2" -react-style-singleton@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.0.tgz#70f45f5fef97fdb9a52eed98d1839fa6b9032b22" - integrity sha512-nK7mN92DMYZEu3cQcAhfwE48NpzO5RpxjG4okbSqRRbfal9Pk+fG2RdQXTMp+f6all1hB9LIJSt+j7dCYrU11g== +react-style-singleton@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4" + integrity sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g== dependencies: get-nonce "^1.0.0" invariant "^2.2.4" @@ -9513,15 +9527,15 @@ react-syntax-highlighter@^12.2.1: prismjs "^1.8.4" refractor "^2.4.1" -react-virtualized-auto-sizer@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.6.tgz#66c5b1c9278064c5ef1699ed40a29c11518f97ca" - integrity sha512-7tQ0BmZqfVF6YYEWcIGuoR3OdYe8I/ZFbNclFlGOC3pMqunkYF/oL30NCjSGl9sMEb17AnzixDz98Kqc3N76HQ== +react-virtualized-auto-sizer@^1.0.24: + version "1.0.24" + resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.24.tgz#3ebdc92f4b05ad65693b3cc8e7d8dd54924c0227" + integrity sha512-3kCn7N9NEb3FlvJrSHWGQ4iVl+ydQObq2fHMn12i5wbtm74zHOPhz/i64OL3c1S1vi9i2GXtZqNqUJTQ+BnNfg== -react-window@^1.8.6: - version "1.8.7" - resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.7.tgz#5e9fd0d23f48f432d7022cdb327219353a15f0d4" - integrity sha512-JHEZbPXBpKMmoNO1bNhoXOOLg/ujhL/BU4IqVU9r8eQPcy5KQnGHIHDRkJ0ns9IM5+Aq5LNwt3j8t3tIrePQzA== +react-window@^1.8.10: + version "1.8.10" + resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.10.tgz#9e6b08548316814b443f7002b1cf8fd3a1bdde03" + integrity sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg== dependencies: "@babel/runtime" "^7.0.0" memoize-one ">=3.1.1 <6" @@ -9578,10 +9592,10 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" -redux@^4.0.0, redux@^4.0.4: - version "4.1.2" - resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.2.tgz#140f35426d99bb4729af760afcf79eaaac407104" - integrity sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw== +redux@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197" + integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w== dependencies: "@babel/runtime" "^7.9.2" @@ -9594,14 +9608,14 @@ refractor@^2.4.1: parse-entities "^1.1.2" prismjs "~1.17.0" -refractor@^3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/refractor/-/refractor-3.5.0.tgz#334586f352dda4beaf354099b48c2d18e0819aec" - integrity sha512-QwPJd3ferTZ4cSPPjdP5bsYHMytwWYnAN5EEnLtGvkqp/FCCnGsBgxrm9EuIDnjUC3Uc/kETtvVi7fSIVC74Dg== +refractor@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/refractor/-/refractor-3.6.0.tgz#ac318f5a0715ead790fcfb0c71f4dd83d977935a" + integrity sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA== dependencies: hastscript "^6.0.0" parse-entities "^2.0.0" - prismjs "~1.25.0" + prismjs "~1.27.0" regenerate-unicode-properties@^9.0.0: version "9.0.0" @@ -9625,6 +9639,11 @@ regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.9: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== +regenerator-runtime@^0.14.0: + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== + regenerator-transform@^0.14.2: version "0.14.5" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.5.tgz#c98da154683671c9c4dcb16ece736517e1b7feb4" @@ -9674,14 +9693,14 @@ regjsparser@^0.7.0: dependencies: jsesc "~0.5.0" -rehype-raw@^5.0.0: +rehype-raw@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/rehype-raw/-/rehype-raw-5.1.0.tgz#66d5e8d7188ada2d31bc137bc19a1000cf2c6b7e" integrity sha512-MDvHAb/5mUnif2R+0IPCYJU8WjHa9UzGtM/F4AVy5GixPlDZ1z3HacYy4xojDU+uBa+0X/3PIfyQI26/2ljJNA== dependencies: hast-util-raw "^6.1.0" -rehype-react@^6.0.0: +rehype-react@^6.2.1: version "6.2.1" resolved "https://registry.yarnpkg.com/rehype-react/-/rehype-react-6.2.1.tgz#9b9bf188451ad6f63796b784fe1f51165c67b73a" integrity sha512-f9KIrjktvLvmbGc7si25HepocOg4z0MuNOtweigKzBcDjiGSTGhyz6VSgaV5K421Cq1O+z4/oxRJ5G9owo0KVg== @@ -9717,10 +9736,10 @@ remark-emoji@^2.1.0: node-emoji "^1.10.0" unist-util-visit "^2.0.3" -remark-parse@^8.0.3: - version "8.0.3" - resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-8.0.3.tgz#9c62aa3b35b79a486454c690472906075f40c7e1" - integrity sha512-E1K9+QLGgggHxCQtLt++uXltxEprmWzNfg+MxpfHsZlrddKzZ/hZyWHDbK3/Ap8HJQqYJRXP+jHczdL6q6i85Q== +remark-parse-no-trim@^8.0.4: + version "8.0.4" + resolved "https://registry.yarnpkg.com/remark-parse-no-trim/-/remark-parse-no-trim-8.0.4.tgz#f5c9531644284071d4a57a49e19a42ad4e8040bd" + integrity sha512-WtqeHNTZ0LSdyemmY1/G6y9WoEFblTtgckfKF5/NUnri919/0/dEu8RCDfvXtJvu96soMvT+mLWWgYVUaiHoag== dependencies: ccount "^1.0.0" collapse-white-space "^1.0.2" @@ -9732,14 +9751,13 @@ remark-parse@^8.0.3: parse-entities "^2.0.0" repeat-string "^1.5.4" state-toggle "^1.0.0" - trim "0.0.1" trim-trailing-lines "^1.0.0" unherit "^1.0.4" unist-util-remove-position "^2.0.0" vfile-location "^3.0.0" xtend "^4.0.1" -remark-rehype@^8.0.0: +remark-rehype@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/remark-rehype/-/remark-rehype-8.1.0.tgz#610509a043484c1e697437fa5eb3fd992617c945" integrity sha512-EbCu9kHgAxKmW1yEYjx3QafMyGY3q8noUbNUI5xyKbaFP89wbhDrKxyIQNukNYthzjNHZu6J7hwFg7hRm1svYA== @@ -10266,7 +10284,7 @@ source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, sourc resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -source-map@^0.5.0, source-map@^0.5.3: +source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.7: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= @@ -10544,10 +10562,10 @@ stylehacks@^5.0.2: browserslist "^4.16.6" postcss-selector-parser "^6.0.4" -stylis@4.0.13: - version "4.0.13" - resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.13.tgz#f5db332e376d13cc84ecfe5dace9a2a51d954c91" - integrity sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag== +stylis@4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51" + integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw== supports-color@^5.3.0, supports-color@^5.5.0: version "5.5.0" @@ -10644,10 +10662,10 @@ symbol-tree@^3.2.4: resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== -tabbable@^5.2.1: - version "5.3.2" - resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-5.3.2.tgz#66d6119ee8a533634c3f17deb0caa1c379e36ac7" - integrity sha512-6G/8EWRFx8CiSe2++/xHhXkmCRq2rHtDtZbQFHx34cvDfZzIBfvwG9zGUNTWMXWLCYvDj3aQqOzdl3oCxKuBkQ== +tabbable@^5.3.3: + version "5.3.3" + resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-5.3.3.tgz#aac0ff88c73b22d6c3c5a50b1586310006b47fbf" + integrity sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA== taffydb@2.6.2: version "2.6.2" @@ -10874,11 +10892,6 @@ trim-trailing-lines@^1.0.0: resolved "https://registry.yarnpkg.com/trim-trailing-lines/-/trim-trailing-lines-1.1.4.tgz#bd4abbec7cc880462f10b2c8b5ce1d8d1ec7c2c0" integrity sha512-rjUWSqnfTNrjbB9NQWfPMH/xRK1deHeGsHoVfpxJ++XeYXE0d6B1En37AHfw3jtfTU7dzMzZL2jjpe8Qb5gLIQ== -trim@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/trim/-/trim-0.0.1.tgz#5858547f6b290757ee95cccc666fb50084c460dd" - integrity sha1-WFhUf2spB1fulczMZm+1AITEYN0= - trough@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406" @@ -10899,7 +10912,7 @@ tsconfig-paths@^3.12.0: minimist "^1.2.0" strip-bom "^3.0.0" -tslib@^1.0.0, tslib@^1.8.1, tslib@^1.9.3: +tslib@^1.8.1, tslib@^1.9.3: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== @@ -10975,10 +10988,10 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" -typescript@^4.4.2: - version "4.5.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3" - integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA== +typescript@^4.9.5: + version "4.9.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" + integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" @@ -11036,7 +11049,7 @@ unicode-property-aliases-ecmascript@^2.0.0: resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz#0a36cb9a585c4f6abd51ad1deddb285c165297c8" integrity sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ== -unified@^9.2.0: +unified@^9.2.2: version "9.2.2" resolved "https://registry.yarnpkg.com/unified/-/unified-9.2.2.tgz#67649a1abfc3ab85d2969502902775eb03146975" integrity sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ== @@ -11094,13 +11107,6 @@ unist-util-stringify-position@^2.0.0: dependencies: "@types/unist" "^2.0.2" -unist-util-stringify-position@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-3.0.0.tgz#d517d2883d74d0daa0b565adc3d10a02b4a8cde9" - integrity sha512-SdfAl8fsDclywZpfMDTVDxA2V7LjtRDTOFd44wUJamgl6OlVngsqWjxvermMYf60elWHbxhuRCZml7AnuXCaSA== - dependencies: - "@types/unist" "^2.0.0" - unist-util-visit-parents@^2.0.0: version "2.1.2" resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-2.1.2.tgz#25e43e55312166f3348cae6743588781d112c1e9" @@ -11200,10 +11206,17 @@ use-callback-ref@^1.3.0: dependencies: tslib "^2.0.0" -use-memo-one@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.2.tgz#0c8203a329f76e040047a35a1197defe342fab20" - integrity sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ== +use-callback-ref@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.2.tgz#6134c7f6ff76e2be0b56c809b17a650c942b1693" + integrity sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA== + dependencies: + tslib "^2.0.0" + +use-memo-one@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.3.tgz#2fd2e43a2169eabc7496960ace8c79efef975e99" + integrity sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ== use-query-params@^1.2.3: version "1.2.3" @@ -11220,6 +11233,11 @@ use-sidecar@^1.1.2: detect-node-es "^1.1.0" tslib "^2.0.0" +use-sync-external-store@^1.0.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9" + integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw== + util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -11274,14 +11292,6 @@ vfile-location@^3.0.0, vfile-location@^3.2.0: resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-3.2.0.tgz#d8e41fbcbd406063669ebf6c33d56ae8721d0f3c" integrity sha512-aLEIZKv/oxuCDZ8lkJGhuhztf/BW4M+iHdCwglA/eWc+vtuRFJj8EtgceYFX4LRjOhCAAiNHsKGssC6onJ+jbA== -vfile-message@*: - version "3.1.0" - resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-3.1.0.tgz#5437035aa43185ff4b9210d32fada6c640e59143" - integrity sha512-4QJbBk+DkPEhBXq3f260xSaWtjE4gPKOfulzfMFF8ZNwaPZieWsg3iVlcmF04+eebzpcpeXOOFMfrYzJHVYg+g== - dependencies: - "@types/unist" "^2.0.0" - unist-util-stringify-position "^3.0.0" - vfile-message@^2.0.0: version "2.0.4" resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-2.0.4.tgz#5b43b88171d409eae58477d13f23dd41d52c371a" @@ -11290,7 +11300,7 @@ vfile-message@^2.0.0: "@types/unist" "^2.0.0" unist-util-stringify-position "^2.0.0" -vfile@^4.0.0, vfile@^4.2.0: +vfile@^4.0.0, vfile@^4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/vfile/-/vfile-4.2.1.tgz#03f1dce28fc625c625bc6514350fbdb00fa9e624" integrity sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA== From d5d3095125d118c09dcdff574a26e18e37420874 Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Sun, 6 Oct 2024 21:56:31 -0400 Subject: [PATCH 125/185] chore: Update beta-on-demand-feature-view.md (#4595) * Update beta-on-demand-feature-view.md * updated docs Signed-off-by: Francisco Javier Arceo --------- Signed-off-by: Francisco Javier Arceo --- docs/reference/beta-on-demand-feature-view.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/reference/beta-on-demand-feature-view.md b/docs/reference/beta-on-demand-feature-view.md index 6b4c3c667a..6ccc648616 100644 --- a/docs/reference/beta-on-demand-feature-view.md +++ b/docs/reference/beta-on-demand-feature-view.md @@ -4,16 +4,16 @@ ## Overview -On demand feature views allows data scientists to use existing features and request time data (features only available at request time) to transform and create new features. Users define python transformation logic which is executed in both historical retrieval and online retrieval paths. - -Currently, these transformations are executed locally. This is fine for online serving, but does not scale well offline. +On demand feature views allows data scientists to use existing features and request time data (features only available +at request time) to transform and create new features at the time the data is read from the online store. Users define +python transformation logic which is executed in both historical retrieval and online retrieval paths. ### Why use on demand feature views? This enables data scientists to easily impact the online feature retrieval path. For example, a data scientist could 1. Call `get_historical_features` to generate a training dataframe -2. Iterate in notebook on feature engineering in Pandas +2. Iterate in notebook on feature engineering in Pandas/Python 3. Copy transformation logic into on demand feature views and commit to a dev branch of the feature repository 4. Verify with `get_historical_features` (on a small dataset) that the transformation gives expected output over historical data 5. Verify with `get_online_features` on dev branch that the transformation correctly outputs online features From cf56cbb930bad6d701fdfc8737fa7dc558798869 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 16:26:17 +0000 Subject: [PATCH 126/185] chore: Bump rollup from 2.72.1 to 2.79.2 in /sdk/python/feast/ui (#4590) Bumps [rollup](https://github.com/rollup/rollup) from 2.72.1 to 2.79.2. - [Release notes](https://github.com/rollup/rollup/releases) - [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md) - [Commits](https://github.com/rollup/rollup/compare/v2.72.1...v2.79.2) --- updated-dependencies: - dependency-name: rollup dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdk/python/feast/ui/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sdk/python/feast/ui/yarn.lock b/sdk/python/feast/ui/yarn.lock index d9e9ce03c8..7e83084445 100644 --- a/sdk/python/feast/ui/yarn.lock +++ b/sdk/python/feast/ui/yarn.lock @@ -9471,9 +9471,9 @@ rollup-plugin-terser@^7.0.0: terser "^5.0.0" rollup@^2.43.1: - version "2.72.1" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.72.1.tgz#861c94790537b10008f0ca0fbc60e631aabdd045" - integrity sha512-NTc5UGy/NWFGpSqF1lFY8z9Adri6uhyMLI6LvPAXdBKoPRFhIIiBUpt+Qg2awixqO3xvzSijjhnb4+QEZwJmxA== + version "2.79.2" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.2.tgz#f150e4a5db4b121a21a747d762f701e5e9f49090" + integrity sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ== optionalDependencies: fsevents "~2.3.2" From 279d6f7c3074f5895634c2a234ae2cd582e606ca Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Wed, 9 Oct 2024 10:07:17 -0400 Subject: [PATCH 127/185] chore: Update README to add star history (#4609) * chore: Update README to add star history * Clean up badges --- infra/templates/README.md.jinja2 | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/infra/templates/README.md.jinja2 b/infra/templates/README.md.jinja2 index 9c7df17da9..4b9ed71fed 100644 --- a/infra/templates/README.md.jinja2 +++ b/infra/templates/README.md.jinja2 @@ -5,12 +5,14 @@


-[![unit-tests](https://github.com/feast-dev/feast/actions/workflows/unit_tests.yml/badge.svg?branch=master&event=push)](https://github.com/feast-dev/feast/actions/workflows/unit_tests.yml) +[![PyPI - Downloads](https://img.shields.io/pypi/dm/feast)](https://pypi.org/project/feast/) +[![GitHub contributors](https://img.shields.io/github/contributors/feast-dev/feast)](https://github.com/feast-dev/feast/graphs/contributors) +[![unit-tests](https://github.com/feast-dev/feast/actions/workflows/unit_tests.yml/badge.svg?branch=master)](https://github.com/feast-dev/feast/actions/workflows/unit_tests.yml) [![integration-tests-and-build](https://github.com/feast-dev/feast/actions/workflows/master_only.yml/badge.svg?branch=master&event=push)](https://github.com/feast-dev/feast/actions/workflows/master_only.yml) [![java-integration-tests](https://github.com/feast-dev/feast/actions/workflows/java_master_only.yml/badge.svg?branch=master&event=push)](https://github.com/feast-dev/feast/actions/workflows/java_master_only.yml) [![linter](https://github.com/feast-dev/feast/actions/workflows/linter.yml/badge.svg?branch=master&event=push)](https://github.com/feast-dev/feast/actions/workflows/linter.yml) [![Docs Latest](https://img.shields.io/badge/docs-latest-blue.svg)](https://docs.feast.dev/) -[![Python API](https://img.shields.io/readthedocs/feast/master?label=Python%20API)](http://rtd.feast.dev/) +[![Python API](https://img.shields.io/badge/docs-latest-brightgreen.svg)](http://rtd.feast.dev/) [![License](https://img.shields.io/badge/License-Apache%202.0-blue)](https://github.com/feast-dev/feast/blob/master/LICENSE) [![GitHub Release](https://img.shields.io/github/v/release/feast-dev/feast.svg?style=flat&sort=semver&color=blue)](https://github.com/feast-dev/feast/releases) @@ -159,6 +161,18 @@ Feast is a community project and is still under active development. Please have - [Development Guide for Feast](https://docs.feast.dev/project/development-guide) - [Development Guide for the Main Feast Repository](./CONTRIBUTING.md) +## 🌟 GitHub Star History +
+ + ## ✨ Contributors Thanks goes to these incredible people: From 704fff6d07ecf8c0e738d41045c84692547a6801 Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Wed, 9 Oct 2024 10:14:10 -0400 Subject: [PATCH 128/185] chore: Update compiled README (#4612) Signed-off-by: Francisco Javier Arceo --- README.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6f17e7fa6c..e02fb978d7 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,14 @@


-[![unit-tests](https://github.com/feast-dev/feast/actions/workflows/unit_tests.yml/badge.svg?branch=master&event=push)](https://github.com/feast-dev/feast/actions/workflows/unit_tests.yml) +[![PyPI - Downloads](https://img.shields.io/pypi/dm/feast)](https://pypi.org/project/feast/) +[![GitHub contributors](https://img.shields.io/github/contributors/feast-dev/feast)](https://github.com/feast-dev/feast/graphs/contributors) +[![unit-tests](https://github.com/feast-dev/feast/actions/workflows/unit_tests.yml/badge.svg?branch=master)](https://github.com/feast-dev/feast/actions/workflows/unit_tests.yml) [![integration-tests-and-build](https://github.com/feast-dev/feast/actions/workflows/master_only.yml/badge.svg?branch=master&event=push)](https://github.com/feast-dev/feast/actions/workflows/master_only.yml) [![java-integration-tests](https://github.com/feast-dev/feast/actions/workflows/java_master_only.yml/badge.svg?branch=master&event=push)](https://github.com/feast-dev/feast/actions/workflows/java_master_only.yml) [![linter](https://github.com/feast-dev/feast/actions/workflows/linter.yml/badge.svg?branch=master&event=push)](https://github.com/feast-dev/feast/actions/workflows/linter.yml) [![Docs Latest](https://img.shields.io/badge/docs-latest-blue.svg)](https://docs.feast.dev/) -[![Python API](https://img.shields.io/readthedocs/feast/master?label=Python%20API)](http://rtd.feast.dev/) +[![Python API](https://img.shields.io/badge/docs-latest-brightgreen.svg)](http://rtd.feast.dev/) [![License](https://img.shields.io/badge/License-Apache%202.0-blue)](https://github.com/feast-dev/feast/blob/master/LICENSE) [![GitHub Release](https://img.shields.io/github/v/release/feast-dev/feast.svg?style=flat&sort=semver&color=blue)](https://github.com/feast-dev/feast/releases) @@ -228,6 +230,18 @@ Feast is a community project and is still under active development. Please have - [Development Guide for Feast](https://docs.feast.dev/project/development-guide) - [Development Guide for the Main Feast Repository](./CONTRIBUTING.md) +## 🌟 GitHub Star History +

+ + + + + Star History Chart + + +

+ + ## ✨ Contributors Thanks goes to these incredible people: From b5ab6c799d529aaea19a196cedca7bb2c93cbbe9 Mon Sep 17 00:00:00 2001 From: Tommy Hughes IV Date: Wed, 9 Oct 2024 11:27:47 -0500 Subject: [PATCH 129/185] feat: Create initial structure of Feast Go Operator (#4596) * init feast-operator Signed-off-by: Tommy Hughes * add featurestore api Signed-off-by: Tommy Hughes * add spec.feastProject field Signed-off-by: Tommy Hughes * add bundle files Signed-off-by: Tommy Hughes * modify release and versioning scripts Signed-off-by: Tommy Hughes * add validation to feastProject field Signed-off-by: Tommy Hughes * add operator to Makefile Signed-off-by: Tommy Hughes * fixes Signed-off-by: Tommy Hughes --------- Signed-off-by: Tommy Hughes --- .github/workflows/publish.yml | 2 +- .releaserc.js | 1 + Makefile | 12 + infra/feast-operator/.dockerignore | 3 + infra/feast-operator/.gitignore | 30 ++ infra/feast-operator/.golangci.yml | 40 ++ infra/feast-operator/Dockerfile | 33 ++ infra/feast-operator/Makefile | 322 +++++++++++++++ infra/feast-operator/PROJECT | 22 + infra/feast-operator/README.md | 133 ++++++ .../api/v1alpha1/featurestore_types.go | 62 +++ .../api/v1alpha1/groupversion_info.go | 36 ++ .../api/v1alpha1/zz_generated.deepcopy.go | 114 ++++++ infra/feast-operator/bundle.Dockerfile | 20 + ...er-manager-metrics-service_v1_service.yaml | 19 + ...c.authorization.k8s.io_v1_clusterrole.yaml | 27 ++ ...c.authorization.k8s.io_v1_clusterrole.yaml | 23 ++ ...c.authorization.k8s.io_v1_clusterrole.yaml | 13 + .../feast-operator.clusterserviceversion.yaml | 221 ++++++++++ .../manifests/feast.dev_featurestores.yaml | 64 +++ .../bundle/metadata/annotations.yaml | 14 + .../bundle/tests/scorecard/config.yaml | 70 ++++ infra/feast-operator/cmd/main.go | 148 +++++++ .../crd/bases/feast.dev_featurestores.yaml | 58 +++ .../config/crd/kustomization.yaml | 22 + .../config/crd/kustomizeconfig.yaml | 19 + .../config/default/kustomization.yaml | 142 +++++++ .../default/manager_auth_proxy_patch.yaml | 39 ++ .../config/default/manager_config_patch.yaml | 10 + .../config/manager/kustomization.yaml | 8 + .../config/manager/manager.yaml | 94 +++++ .../feast-operator.clusterserviceversion.yaml | 51 +++ .../config/manifests/kustomization.yaml | 28 ++ .../config/prometheus/kustomization.yaml | 2 + .../config/prometheus/monitor.yaml | 21 + .../rbac/auth_proxy_client_clusterrole.yaml | 12 + .../config/rbac/auth_proxy_role.yaml | 20 + .../config/rbac/auth_proxy_role_binding.yaml | 15 + .../config/rbac/auth_proxy_service.yaml | 17 + .../config/rbac/featurestore_editor_role.yaml | 27 ++ .../config/rbac/featurestore_viewer_role.yaml | 23 ++ .../config/rbac/kustomization.yaml | 24 ++ .../config/rbac/leader_election_role.yaml | 40 ++ .../rbac/leader_election_role_binding.yaml | 15 + infra/feast-operator/config/rbac/role.yaml | 32 ++ .../config/rbac/role_binding.yaml | 15 + .../config/rbac/service_account.yaml | 8 + .../config/samples/kustomization.yaml | 4 + .../config/samples/v1alpha1_featurestore.yaml | 9 + .../config/scorecard/bases/config.yaml | 7 + .../config/scorecard/kustomization.yaml | 16 + .../scorecard/patches/basic.config.yaml | 10 + .../config/scorecard/patches/olm.config.yaml | 50 +++ infra/feast-operator/dist/install.yaml | 381 ++++++++++++++++++ infra/feast-operator/go.mod | 73 ++++ infra/feast-operator/go.sum | 205 ++++++++++ infra/feast-operator/hack/boilerplate.go.txt | 15 + .../controller/featurestore_controller.go | 62 +++ .../featurestore_controller_test.go | 84 ++++ .../internal/controller/suite_test.go | 90 +++++ .../feast-operator/test/e2e/e2e_suite_test.go | 32 ++ infra/feast-operator/test/e2e/e2e_test.go | 122 ++++++ infra/feast-operator/test/utils/utils.go | 140 +++++++ infra/scripts/release/files_to_bump.txt | 2 + 64 files changed, 3472 insertions(+), 1 deletion(-) create mode 100644 infra/feast-operator/.dockerignore create mode 100644 infra/feast-operator/.gitignore create mode 100644 infra/feast-operator/.golangci.yml create mode 100644 infra/feast-operator/Dockerfile create mode 100644 infra/feast-operator/Makefile create mode 100644 infra/feast-operator/PROJECT create mode 100644 infra/feast-operator/README.md create mode 100644 infra/feast-operator/api/v1alpha1/featurestore_types.go create mode 100644 infra/feast-operator/api/v1alpha1/groupversion_info.go create mode 100644 infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go create mode 100644 infra/feast-operator/bundle.Dockerfile create mode 100644 infra/feast-operator/bundle/manifests/feast-operator-controller-manager-metrics-service_v1_service.yaml create mode 100644 infra/feast-operator/bundle/manifests/feast-operator-featurestore-editor-role_rbac.authorization.k8s.io_v1_clusterrole.yaml create mode 100644 infra/feast-operator/bundle/manifests/feast-operator-featurestore-viewer-role_rbac.authorization.k8s.io_v1_clusterrole.yaml create mode 100644 infra/feast-operator/bundle/manifests/feast-operator-metrics-reader_rbac.authorization.k8s.io_v1_clusterrole.yaml create mode 100644 infra/feast-operator/bundle/manifests/feast-operator.clusterserviceversion.yaml create mode 100644 infra/feast-operator/bundle/manifests/feast.dev_featurestores.yaml create mode 100644 infra/feast-operator/bundle/metadata/annotations.yaml create mode 100644 infra/feast-operator/bundle/tests/scorecard/config.yaml create mode 100644 infra/feast-operator/cmd/main.go create mode 100644 infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml create mode 100644 infra/feast-operator/config/crd/kustomization.yaml create mode 100644 infra/feast-operator/config/crd/kustomizeconfig.yaml create mode 100644 infra/feast-operator/config/default/kustomization.yaml create mode 100644 infra/feast-operator/config/default/manager_auth_proxy_patch.yaml create mode 100644 infra/feast-operator/config/default/manager_config_patch.yaml create mode 100644 infra/feast-operator/config/manager/kustomization.yaml create mode 100644 infra/feast-operator/config/manager/manager.yaml create mode 100644 infra/feast-operator/config/manifests/bases/feast-operator.clusterserviceversion.yaml create mode 100644 infra/feast-operator/config/manifests/kustomization.yaml create mode 100644 infra/feast-operator/config/prometheus/kustomization.yaml create mode 100644 infra/feast-operator/config/prometheus/monitor.yaml create mode 100644 infra/feast-operator/config/rbac/auth_proxy_client_clusterrole.yaml create mode 100644 infra/feast-operator/config/rbac/auth_proxy_role.yaml create mode 100644 infra/feast-operator/config/rbac/auth_proxy_role_binding.yaml create mode 100644 infra/feast-operator/config/rbac/auth_proxy_service.yaml create mode 100644 infra/feast-operator/config/rbac/featurestore_editor_role.yaml create mode 100644 infra/feast-operator/config/rbac/featurestore_viewer_role.yaml create mode 100644 infra/feast-operator/config/rbac/kustomization.yaml create mode 100644 infra/feast-operator/config/rbac/leader_election_role.yaml create mode 100644 infra/feast-operator/config/rbac/leader_election_role_binding.yaml create mode 100644 infra/feast-operator/config/rbac/role.yaml create mode 100644 infra/feast-operator/config/rbac/role_binding.yaml create mode 100644 infra/feast-operator/config/rbac/service_account.yaml create mode 100644 infra/feast-operator/config/samples/kustomization.yaml create mode 100644 infra/feast-operator/config/samples/v1alpha1_featurestore.yaml create mode 100644 infra/feast-operator/config/scorecard/bases/config.yaml create mode 100644 infra/feast-operator/config/scorecard/kustomization.yaml create mode 100644 infra/feast-operator/config/scorecard/patches/basic.config.yaml create mode 100644 infra/feast-operator/config/scorecard/patches/olm.config.yaml create mode 100644 infra/feast-operator/dist/install.yaml create mode 100644 infra/feast-operator/go.mod create mode 100644 infra/feast-operator/go.sum create mode 100644 infra/feast-operator/hack/boilerplate.go.txt create mode 100644 infra/feast-operator/internal/controller/featurestore_controller.go create mode 100644 infra/feast-operator/internal/controller/featurestore_controller_test.go create mode 100644 infra/feast-operator/internal/controller/suite_test.go create mode 100644 infra/feast-operator/test/e2e/e2e_suite_test.go create mode 100644 infra/feast-operator/test/e2e/e2e_test.go create mode 100644 infra/feast-operator/test/utils/utils.go diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c2a5bd21ee..7d5dca8e08 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -49,7 +49,7 @@ jobs: needs: [get-version, publish-python-sdk] strategy: matrix: - component: [feature-server, feature-server-java, feature-transformation-server, feast-helm-operator] + component: [feature-server, feature-server-java, feature-transformation-server, feast-helm-operator, feast-operator] env: MAVEN_CACHE: gs://feast-templocation-kf-feast/.m2.2020-08-19.tar REGISTRY: feastdev diff --git a/.releaserc.js b/.releaserc.js index 60084dc24f..f2be244005 100644 --- a/.releaserc.js +++ b/.releaserc.js @@ -67,6 +67,7 @@ module.exports = { "java/pom.xml", "infra/charts/**/*.*", "infra/feast-helm-operator/**/*", + "infra/feast-operator/**/*", "ui/package.json", "sdk/python/feast/ui/package.json", "sdk/python/feast/ui/yarn.lock" diff --git a/Makefile b/Makefile index 10187b8ebc..22d4d25f4e 100644 --- a/Makefile +++ b/Makefile @@ -442,6 +442,18 @@ build-feast-helm-operator-docker: VERSION=$(VERSION) \ $(MAKE) docker-build +push-feast-operator-docker: + cd infra/feast-operator && \ + IMAGE_TAG_BASE=$(REGISTRY)/feast-operator \ + VERSION=$(VERSION) \ + $(MAKE) docker-push + +build-feast-operator-docker: + cd infra/feast-operator && \ + IMAGE_TAG_BASE=$(REGISTRY)/feast-operator \ + VERSION=$(VERSION) \ + $(MAKE) docker-build + # Dev images build-feature-server-dev: diff --git a/infra/feast-operator/.dockerignore b/infra/feast-operator/.dockerignore new file mode 100644 index 0000000000..a3aab7af70 --- /dev/null +++ b/infra/feast-operator/.dockerignore @@ -0,0 +1,3 @@ +# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file +# Ignore build and test binaries. +bin/ diff --git a/infra/feast-operator/.gitignore b/infra/feast-operator/.gitignore new file mode 100644 index 0000000000..72df211c0c --- /dev/null +++ b/infra/feast-operator/.gitignore @@ -0,0 +1,30 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin/* +Dockerfile.cross + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Go workspace file +go.work + +# Kubernetes Generated files - skip generated files, except for vendored files +!vendor/**/zz_generated.* + +# editor and IDE paraphernalia +.idea +.vscode +*.swp +*.swo +*~ + +# Installer file generated by Kustomize - skip 'dist/' directories within the Feast project except this one. +!dist/ \ No newline at end of file diff --git a/infra/feast-operator/.golangci.yml b/infra/feast-operator/.golangci.yml new file mode 100644 index 0000000000..ca69a11f6f --- /dev/null +++ b/infra/feast-operator/.golangci.yml @@ -0,0 +1,40 @@ +run: + timeout: 5m + allow-parallel-runners: true + +issues: + # don't skip warning about doc comments + # don't exclude the default set of lint + exclude-use-default: false + # restore some of the defaults + # (fill in the rest as needed) + exclude-rules: + - path: "api/*" + linters: + - lll + - path: "internal/*" + linters: + - dupl + - lll +linters: + disable-all: true + enable: + - dupl + - errcheck + - exportloopref + - goconst + - gocyclo + - gofmt + - goimports + - gosimple + - govet + - ineffassign + - lll + - misspell + - nakedret + - prealloc + - staticcheck + - typecheck + - unconvert + - unparam + - unused diff --git a/infra/feast-operator/Dockerfile b/infra/feast-operator/Dockerfile new file mode 100644 index 0000000000..aca26f9229 --- /dev/null +++ b/infra/feast-operator/Dockerfile @@ -0,0 +1,33 @@ +# Build the manager binary +FROM golang:1.21 AS builder +ARG TARGETOS +ARG TARGETARCH + +WORKDIR /workspace +# Copy the Go Modules manifests +COPY go.mod go.mod +COPY go.sum go.sum +# cache deps before building and copying source so that we don't need to re-download as much +# and so that source changes don't invalidate our downloaded layer +RUN go mod download + +# Copy the go source +COPY cmd/main.go cmd/main.go +COPY api/ api/ +COPY internal/controller/ internal/controller/ + +# Build +# the GOARCH has not a default value to allow the binary be built according to the host where the command +# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO +# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, +# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. +RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go + +# Use distroless as minimal base image to package the manager binary +# Refer to https://github.com/GoogleContainerTools/distroless for more details +FROM gcr.io/distroless/static:nonroot +WORKDIR / +COPY --from=builder /workspace/manager . +USER 65532:65532 + +ENTRYPOINT ["/manager"] diff --git a/infra/feast-operator/Makefile b/infra/feast-operator/Makefile new file mode 100644 index 0000000000..a6b922b062 --- /dev/null +++ b/infra/feast-operator/Makefile @@ -0,0 +1,322 @@ +# VERSION defines the project version for the bundle. +# Update this value when you upgrade the version of your project. +# To re-generate a bundle for another specific version without changing the standard setup, you can: +# - use the VERSION as arg of the bundle target (e.g make bundle VERSION=0.0.2) +# - use environment variables to overwrite this value (e.g export VERSION=0.0.2) +VERSION ?= 0.40.0 + +# CHANNELS define the bundle channels used in the bundle. +# Add a new line here if you would like to change its default config. (E.g CHANNELS = "candidate,fast,stable") +# To re-generate a bundle for other specific channels without changing the standard setup, you can: +# - use the CHANNELS as arg of the bundle target (e.g make bundle CHANNELS=candidate,fast,stable) +# - use environment variables to overwrite this value (e.g export CHANNELS="candidate,fast,stable") +ifneq ($(origin CHANNELS), undefined) +BUNDLE_CHANNELS := --channels=$(CHANNELS) +endif + +# DEFAULT_CHANNEL defines the default channel used in the bundle. +# Add a new line here if you would like to change its default config. (E.g DEFAULT_CHANNEL = "stable") +# To re-generate a bundle for any other default channel without changing the default setup, you can: +# - use the DEFAULT_CHANNEL as arg of the bundle target (e.g make bundle DEFAULT_CHANNEL=stable) +# - use environment variables to overwrite this value (e.g export DEFAULT_CHANNEL="stable") +ifneq ($(origin DEFAULT_CHANNEL), undefined) +BUNDLE_DEFAULT_CHANNEL := --default-channel=$(DEFAULT_CHANNEL) +endif +BUNDLE_METADATA_OPTS ?= $(BUNDLE_CHANNELS) $(BUNDLE_DEFAULT_CHANNEL) + +# IMAGE_TAG_BASE defines the docker.io namespace and part of the image name for remote images. +# This variable is used to construct full image tags for bundle and catalog images. +# +# For example, running 'make bundle-build bundle-push catalog-build catalog-push' will build and push both +# feast.dev/feast-operator-bundle:$VERSION and feast.dev/feast-operator-catalog:$VERSION. +IMAGE_TAG_BASE ?= feastdev/feast-operator + +# BUNDLE_IMG defines the image:tag used for the bundle. +# You can use it as an arg. (E.g make bundle-build BUNDLE_IMG=/:) +BUNDLE_IMG ?= $(IMAGE_TAG_BASE)-bundle:v$(VERSION) + +# BUNDLE_GEN_FLAGS are the flags passed to the operator-sdk generate bundle command +BUNDLE_GEN_FLAGS ?= -q --overwrite --version $(VERSION) $(BUNDLE_METADATA_OPTS) + +# USE_IMAGE_DIGESTS defines if images are resolved via tags or digests +# You can enable this value if you would like to use SHA Based Digests +# To enable set flag to true +USE_IMAGE_DIGESTS ?= false +ifeq ($(USE_IMAGE_DIGESTS), true) + BUNDLE_GEN_FLAGS += --use-image-digests +endif + +# Set the Operator SDK version to use. By default, what is installed on the system is used. +# This is useful for CI or a project to utilize a specific version of the operator-sdk toolkit. +OPERATOR_SDK_VERSION ?= v1.37.0 +# Image URL to use all building/pushing image targets +IMG ?= $(IMAGE_TAG_BASE):$(VERSION) +# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. +ENVTEST_K8S_VERSION = 1.29.0 + +# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) +ifeq (,$(shell go env GOBIN)) +GOBIN=$(shell go env GOPATH)/bin +else +GOBIN=$(shell go env GOBIN) +endif + +# CONTAINER_TOOL defines the container tool to be used for building images. +# Be aware that the target commands are only tested with Docker which is +# scaffolded by default. However, you might want to replace it to use other +# tools. (i.e. podman) +CONTAINER_TOOL ?= docker + +# Setting SHELL to bash allows bash commands to be executed by recipes. +# Options are set to exit when a recipe line exits non-zero or a piped command fails. +SHELL = /usr/bin/env bash -o pipefail +.SHELLFLAGS = -ec + +.PHONY: all +all: build + +##@ General + +# The help target prints out all targets with their descriptions organized +# beneath their categories. The categories are represented by '##@' and the +# target descriptions by '##'. The awk command is responsible for reading the +# entire set of makefiles included in this invocation, looking for lines of the +# file as xyz: ## something, and then pretty-format the target and help. Then, +# if there's a line with ##@ something, that gets pretty-printed as a category. +# More info on the usage of ANSI control characters for terminal formatting: +# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters +# More info on the awk command: +# http://linuxcommand.org/lc3_adv_awk.php + +.PHONY: help +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +##@ Development + +.PHONY: manifests +manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. + $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + +.PHONY: generate +generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. + $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." + +.PHONY: fmt +fmt: ## Run go fmt against code. + go fmt ./... + +.PHONY: vet +vet: ## Run go vet against code. + go vet ./... + +.PHONY: test +test: build-installer fmt vet lint envtest ## Run tests. + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test $$(go list ./... | grep -v /e2e) -coverprofile cover.out + +# Utilize Kind or modify the e2e tests to load the image locally, enabling compatibility with other vendors. +.PHONY: test-e2e # Run the e2e tests against a Kind k8s instance that is spun up. +test-e2e: + go test ./test/e2e/ -v -ginkgo.v + +.PHONY: lint +lint: golangci-lint ## Run golangci-lint linter & yamllint + $(GOLANGCI_LINT) run + +.PHONY: lint-fix +lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes + $(GOLANGCI_LINT) run --fix + +##@ Build + +.PHONY: build +build: manifests generate fmt vet ## Build manager binary. + go build -o bin/manager cmd/main.go + +.PHONY: run +run: manifests generate fmt vet ## Run a controller from your host. + go run ./cmd/main.go + +# If you wish to build the manager image targeting other platforms you can use the --platform flag. +# (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it. +# More info: https://docs.docker.com/develop/develop-images/build_enhancements/ +.PHONY: docker-build +docker-build: ## Build docker image with the manager. + $(CONTAINER_TOOL) build -t ${IMG} . + +.PHONY: docker-push +docker-push: ## Push docker image with the manager. + $(CONTAINER_TOOL) push ${IMG} + +# PLATFORMS defines the target platforms for the manager image be built to provide support to multiple +# architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to: +# - be able to use docker buildx. More info: https://docs.docker.com/build/buildx/ +# - have enabled BuildKit. More info: https://docs.docker.com/develop/develop-images/build_enhancements/ +# - be able to push the image to your registry (i.e. if you do not set a valid value via IMG=> then the export will fail) +# To adequately provide solutions that are compatible with multiple platforms, you should consider using this option. +PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le +.PHONY: docker-buildx +docker-buildx: ## Build and push docker image for the manager for cross-platform support + # copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile + sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross + - $(CONTAINER_TOOL) buildx create --name project-v3-builder + $(CONTAINER_TOOL) buildx use project-v3-builder + - $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross . + - $(CONTAINER_TOOL) buildx rm project-v3-builder + rm Dockerfile.cross + +.PHONY: build-installer +build-installer: manifests generate kustomize ## Generate a consolidated YAML with CRDs and deployment. + mkdir -p dist + cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + $(KUSTOMIZE) build config/default > dist/install.yaml + +##@ Deployment + +ifndef ignore-not-found + ignore-not-found = false +endif + +.PHONY: install +install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. + $(KUSTOMIZE) build config/crd | $(KUBECTL) apply -f - + +.PHONY: uninstall +uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/crd | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - + +.PHONY: deploy +deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. + cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + $(KUSTOMIZE) build config/default | $(KUBECTL) apply -f - + +.PHONY: undeploy +undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. + $(KUSTOMIZE) build config/default | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - + +##@ Dependencies + +## Location to install dependencies to +LOCALBIN ?= $(shell pwd)/bin +$(LOCALBIN): + mkdir -p $(LOCALBIN) + +## Tool Binaries +KUBECTL ?= kubectl +KUSTOMIZE ?= $(LOCALBIN)/kustomize-$(KUSTOMIZE_VERSION) +CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen-$(CONTROLLER_TOOLS_VERSION) +ENVTEST ?= $(LOCALBIN)/setup-envtest-$(ENVTEST_VERSION) +GOLANGCI_LINT = $(LOCALBIN)/golangci-lint-$(GOLANGCI_LINT_VERSION) + +## Tool Versions +KUSTOMIZE_VERSION ?= v5.3.0 +CONTROLLER_TOOLS_VERSION ?= v0.14.0 +ENVTEST_VERSION ?= release-0.17 +GOLANGCI_LINT_VERSION ?= v1.57.2 + +.PHONY: kustomize +kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. +$(KUSTOMIZE): $(LOCALBIN) + $(call go-install-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v5,$(KUSTOMIZE_VERSION)) + +.PHONY: controller-gen +controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. +$(CONTROLLER_GEN): $(LOCALBIN) + $(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen,$(CONTROLLER_TOOLS_VERSION)) + +.PHONY: envtest +envtest: $(ENVTEST) ## Download setup-envtest locally if necessary. +$(ENVTEST): $(LOCALBIN) + $(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest,$(ENVTEST_VERSION)) + +.PHONY: golangci-lint +golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary. +$(GOLANGCI_LINT): $(LOCALBIN) + $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/cmd/golangci-lint,${GOLANGCI_LINT_VERSION}) + +# go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist +# $1 - target path with name of binary (ideally with version) +# $2 - package url which can be installed +# $3 - specific version of package +define go-install-tool +@[ -f $(1) ] || { \ +set -e; \ +package=$(2)@$(3) ;\ +echo "Downloading $${package}" ;\ +GOBIN=$(LOCALBIN) go install $${package} ;\ +mv "$$(echo "$(1)" | sed "s/-$(3)$$//")" $(1) ;\ +} +endef + +.PHONY: operator-sdk +OPERATOR_SDK ?= $(LOCALBIN)/operator-sdk +operator-sdk: ## Download operator-sdk locally if necessary. +ifeq (,$(wildcard $(OPERATOR_SDK))) +ifeq (, $(shell which operator-sdk 2>/dev/null)) + @{ \ + set -e ;\ + mkdir -p $(dir $(OPERATOR_SDK)) ;\ + OS=$(shell go env GOOS) && ARCH=$(shell go env GOARCH) && \ + curl -sSLo $(OPERATOR_SDK) https://github.com/operator-framework/operator-sdk/releases/download/$(OPERATOR_SDK_VERSION)/operator-sdk_$${OS}_$${ARCH} ;\ + chmod +x $(OPERATOR_SDK) ;\ + } +else +OPERATOR_SDK = $(shell which operator-sdk) +endif +endif + +.PHONY: bundle +bundle: manifests kustomize operator-sdk ## Generate bundle manifests and metadata, then validate generated files. + $(OPERATOR_SDK) generate kustomize manifests -q + cd config/manager && $(KUSTOMIZE) edit set image controller=$(IMG) + $(KUSTOMIZE) build config/manifests | $(OPERATOR_SDK) generate bundle $(BUNDLE_GEN_FLAGS) + $(OPERATOR_SDK) bundle validate ./bundle + +.PHONY: bundle-build +bundle-build: ## Build the bundle image. + docker build -f bundle.Dockerfile -t $(BUNDLE_IMG) . + +.PHONY: bundle-push +bundle-push: ## Push the bundle image. + $(MAKE) docker-push IMG=$(BUNDLE_IMG) + +.PHONY: opm +OPM = $(LOCALBIN)/opm +opm: ## Download opm locally if necessary. +ifeq (,$(wildcard $(OPM))) +ifeq (,$(shell which opm 2>/dev/null)) + @{ \ + set -e ;\ + mkdir -p $(dir $(OPM)) ;\ + OS=$(shell go env GOOS) && ARCH=$(shell go env GOARCH) && \ + curl -sSLo $(OPM) https://github.com/operator-framework/operator-registry/releases/download/v1.23.0/$${OS}-$${ARCH}-opm ;\ + chmod +x $(OPM) ;\ + } +else +OPM = $(shell which opm) +endif +endif + +# A comma-separated list of bundle images (e.g. make catalog-build BUNDLE_IMGS=example.com/operator-bundle:v0.1.0,example.com/operator-bundle:v0.2.0). +# These images MUST exist in a registry and be pull-able. +BUNDLE_IMGS ?= $(BUNDLE_IMG) + +# The image tag given to the resulting catalog image (e.g. make catalog-build CATALOG_IMG=example.com/operator-catalog:v0.2.0). +CATALOG_IMG ?= $(IMAGE_TAG_BASE)-catalog:v$(VERSION) + +# Set CATALOG_BASE_IMG to an existing catalog image tag to add $BUNDLE_IMGS to that image. +ifneq ($(origin CATALOG_BASE_IMG), undefined) +FROM_INDEX_OPT := --from-index $(CATALOG_BASE_IMG) +endif + +# Build a catalog image by adding bundle images to an empty catalog using the operator package manager tool, 'opm'. +# This recipe invokes 'opm' in 'semver' bundle add mode. For more information on add modes, see: +# https://github.com/operator-framework/community-operators/blob/7f1438c/docs/packaging-operator.md#updating-your-existing-operator +.PHONY: catalog-build +catalog-build: opm ## Build a catalog image. + $(OPM) index add --container-tool docker --mode semver --tag $(CATALOG_IMG) --bundles $(BUNDLE_IMGS) $(FROM_INDEX_OPT) + +# Push the catalog image. +.PHONY: catalog-push +catalog-push: ## Push a catalog image. + $(MAKE) docker-push IMG=$(CATALOG_IMG) diff --git a/infra/feast-operator/PROJECT b/infra/feast-operator/PROJECT new file mode 100644 index 0000000000..7716e6a3ac --- /dev/null +++ b/infra/feast-operator/PROJECT @@ -0,0 +1,22 @@ +# Code generated by tool. DO NOT EDIT. +# This file is used to track the info used to scaffold your project +# and allow the plugins properly work. +# More info: https://book.kubebuilder.io/reference/project-config.html +domain: feast.dev +layout: +- go.kubebuilder.io/v4 +plugins: + manifests.sdk.operatorframework.io/v2: {} + scorecard.sdk.operatorframework.io/v2: {} +projectName: feast-operator +repo: github.com/feast-dev/feast/infra/feast-operator +resources: +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: feast.dev + kind: FeatureStore + path: github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1 + version: v1alpha1 +version: "3" diff --git a/infra/feast-operator/README.md b/infra/feast-operator/README.md new file mode 100644 index 0000000000..32e2ef11b5 --- /dev/null +++ b/infra/feast-operator/README.md @@ -0,0 +1,133 @@ +# Feast Operator +This is a K8s Operator that can be used to deploy and manage **Feast**, an open source feature store for machine learning. + +## Getting Started + +### Prerequisites +- go version v1.21.0+ +- docker version 17.03+. +- kubectl version v1.11.3+. +- Access to a Kubernetes v1.11.3+ cluster. + +### To Deploy on the cluster +**Install the CRDs into the cluster:** + +```sh +make install +``` + +**Deploy the Manager to the cluster:** + +```sh +make deploy +``` + +> **NOTE**: If you encounter RBAC errors, you may need to grant yourself cluster-admin +privileges or be logged in as admin. + +**Create instances of your solution** +You can apply the samples (examples) from the config/sample: + +```sh +kubectl apply -k config/samples/ +``` + +>**NOTE**: Ensure that the samples has default values to test it out. + +### To Uninstall +**Delete the instances (CRs) from the cluster:** + +```sh +kubectl delete -k config/samples/ +``` + +**Delete the APIs(CRDs) from the cluster:** + +```sh +make uninstall +``` + +**UnDeploy the controller from the cluster:** + +```sh +make undeploy +``` + +## Project Distribution + +Following are the steps to build the installer and distribute this project to users. + +1. Build the installer for the image built and published in the registry: + +```sh +make build-installer +``` + +NOTE: The makefile target mentioned above generates an 'install.yaml' +file in the dist directory. This file contains all the resources built +with Kustomize, which are necessary to install this project without +its dependencies. + +2. Using the installer + +Users can just run kubectl apply -f to install the project, i.e.: + +```sh +kubectl apply -f https://raw.githubusercontent.com//feast-operator//dist/install.yaml +``` + +## Contributing +Additional Feast contrib information can be found on the project's [README](https://github.com/feast-dev/feast?tab=readme-ov-file#-contributing). + +**Before submitting a PR, the following command should run to a successful completion:** + +```sh +make test +``` + +**Build and push your image to the location specified by `IMG`:** + +```sh +make docker-build docker-push IMG=/feast-operator: +``` + +**NOTE:** This image ought to be published in the personal registry you specified. +And it is required to have access to pull the image from the working environment. +Make sure you have the proper permission to the registry if the above commands don’t work. + +**Install the CRDs into the cluster:** + +```sh +make install +``` + +**Deploy the Manager to the cluster with the image specified by `IMG`:** + +```sh +make deploy IMG=/feast-operator: +``` + +### Prerequisites +- go version v1.21 +- operator-sdk version v1.37.0 + +**NOTE:** Run `make help` for more information on all potential `make` targets + +More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html) + +## License + +Copyright 2024 Feast Community. + +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. + diff --git a/infra/feast-operator/api/v1alpha1/featurestore_types.go b/infra/feast-operator/api/v1alpha1/featurestore_types.go new file mode 100644 index 0000000000..030ff408b4 --- /dev/null +++ b/infra/feast-operator/api/v1alpha1/featurestore_types.go @@ -0,0 +1,62 @@ +/* +Copyright 2024 Feast Community. + +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 v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// FeatureStoreSpec defines the desired state of FeatureStore +type FeatureStoreSpec struct { + // +kubebuilder:validation:Pattern="^[A-Za-z0-9][A-Za-z0-9_]*$" + // FeastProject is the Feast project id. This can be any alphanumeric string with underscores, but it cannot start with an underscore. + FeastProject string `json:"feastProject"` +} + +// FeatureStoreStatus defines the observed state of FeatureStore +type FeatureStoreStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// FeatureStore is the Schema for the featurestores API +type FeatureStore struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec FeatureStoreSpec `json:"spec,omitempty"` + Status FeatureStoreStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// FeatureStoreList contains a list of FeatureStore +type FeatureStoreList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []FeatureStore `json:"items"` +} + +func init() { + SchemeBuilder.Register(&FeatureStore{}, &FeatureStoreList{}) +} diff --git a/infra/feast-operator/api/v1alpha1/groupversion_info.go b/infra/feast-operator/api/v1alpha1/groupversion_info.go new file mode 100644 index 0000000000..21f8720ed4 --- /dev/null +++ b/infra/feast-operator/api/v1alpha1/groupversion_info.go @@ -0,0 +1,36 @@ +/* +Copyright 2024 Feast Community. + +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 v1alpha1 contains API Schema definitions for the v1alpha1 API group +// +kubebuilder:object:generate=true +// +groupName=feast.dev +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "feast.dev", Version: "v1alpha1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go b/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 0000000000..3f664edded --- /dev/null +++ b/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,114 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2024 Feast Community. + +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. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FeatureStore) DeepCopyInto(out *FeatureStore) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureStore. +func (in *FeatureStore) DeepCopy() *FeatureStore { + if in == nil { + return nil + } + out := new(FeatureStore) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FeatureStore) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FeatureStoreList) DeepCopyInto(out *FeatureStoreList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]FeatureStore, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureStoreList. +func (in *FeatureStoreList) DeepCopy() *FeatureStoreList { + if in == nil { + return nil + } + out := new(FeatureStoreList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FeatureStoreList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FeatureStoreSpec) DeepCopyInto(out *FeatureStoreSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureStoreSpec. +func (in *FeatureStoreSpec) DeepCopy() *FeatureStoreSpec { + if in == nil { + return nil + } + out := new(FeatureStoreSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FeatureStoreStatus) DeepCopyInto(out *FeatureStoreStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureStoreStatus. +func (in *FeatureStoreStatus) DeepCopy() *FeatureStoreStatus { + if in == nil { + return nil + } + out := new(FeatureStoreStatus) + in.DeepCopyInto(out) + return out +} diff --git a/infra/feast-operator/bundle.Dockerfile b/infra/feast-operator/bundle.Dockerfile new file mode 100644 index 0000000000..ab3f14a9da --- /dev/null +++ b/infra/feast-operator/bundle.Dockerfile @@ -0,0 +1,20 @@ +FROM scratch + +# Core bundle labels. +LABEL operators.operatorframework.io.bundle.mediatype.v1=registry+v1 +LABEL operators.operatorframework.io.bundle.manifests.v1=manifests/ +LABEL operators.operatorframework.io.bundle.metadata.v1=metadata/ +LABEL operators.operatorframework.io.bundle.package.v1=feast-operator +LABEL operators.operatorframework.io.bundle.channels.v1=alpha +LABEL operators.operatorframework.io.metrics.builder=operator-sdk-v1.37.0 +LABEL operators.operatorframework.io.metrics.mediatype.v1=metrics+v1 +LABEL operators.operatorframework.io.metrics.project_layout=go.kubebuilder.io/v4 + +# Labels for testing. +LABEL operators.operatorframework.io.test.mediatype.v1=scorecard+v1 +LABEL operators.operatorframework.io.test.config.v1=tests/scorecard/ + +# Copy files to locations specified by labels. +COPY bundle/manifests /manifests/ +COPY bundle/metadata /metadata/ +COPY bundle/tests/scorecard /tests/scorecard/ diff --git a/infra/feast-operator/bundle/manifests/feast-operator-controller-manager-metrics-service_v1_service.yaml b/infra/feast-operator/bundle/manifests/feast-operator-controller-manager-metrics-service_v1_service.yaml new file mode 100644 index 0000000000..e0cd9dc254 --- /dev/null +++ b/infra/feast-operator/bundle/manifests/feast-operator-controller-manager-metrics-service_v1_service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + creationTimestamp: null + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: feast-operator + control-plane: controller-manager + name: feast-operator-controller-manager-metrics-service +spec: + ports: + - name: https + port: 8443 + protocol: TCP + targetPort: https + selector: + control-plane: controller-manager +status: + loadBalancer: {} diff --git a/infra/feast-operator/bundle/manifests/feast-operator-featurestore-editor-role_rbac.authorization.k8s.io_v1_clusterrole.yaml b/infra/feast-operator/bundle/manifests/feast-operator-featurestore-editor-role_rbac.authorization.k8s.io_v1_clusterrole.yaml new file mode 100644 index 0000000000..aff4d1f984 --- /dev/null +++ b/infra/feast-operator/bundle/manifests/feast-operator-featurestore-editor-role_rbac.authorization.k8s.io_v1_clusterrole.yaml @@ -0,0 +1,27 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: feast-operator + name: feast-operator-featurestore-editor-role +rules: +- apiGroups: + - feast.dev + resources: + - featurestores + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - feast.dev + resources: + - featurestores/status + verbs: + - get diff --git a/infra/feast-operator/bundle/manifests/feast-operator-featurestore-viewer-role_rbac.authorization.k8s.io_v1_clusterrole.yaml b/infra/feast-operator/bundle/manifests/feast-operator-featurestore-viewer-role_rbac.authorization.k8s.io_v1_clusterrole.yaml new file mode 100644 index 0000000000..bcf9699fc1 --- /dev/null +++ b/infra/feast-operator/bundle/manifests/feast-operator-featurestore-viewer-role_rbac.authorization.k8s.io_v1_clusterrole.yaml @@ -0,0 +1,23 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: feast-operator + name: feast-operator-featurestore-viewer-role +rules: +- apiGroups: + - feast.dev + resources: + - featurestores + verbs: + - get + - list + - watch +- apiGroups: + - feast.dev + resources: + - featurestores/status + verbs: + - get diff --git a/infra/feast-operator/bundle/manifests/feast-operator-metrics-reader_rbac.authorization.k8s.io_v1_clusterrole.yaml b/infra/feast-operator/bundle/manifests/feast-operator-metrics-reader_rbac.authorization.k8s.io_v1_clusterrole.yaml new file mode 100644 index 0000000000..eedd865263 --- /dev/null +++ b/infra/feast-operator/bundle/manifests/feast-operator-metrics-reader_rbac.authorization.k8s.io_v1_clusterrole.yaml @@ -0,0 +1,13 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: feast-operator + name: feast-operator-metrics-reader +rules: +- nonResourceURLs: + - /metrics + verbs: + - get diff --git a/infra/feast-operator/bundle/manifests/feast-operator.clusterserviceversion.yaml b/infra/feast-operator/bundle/manifests/feast-operator.clusterserviceversion.yaml new file mode 100644 index 0000000000..8c403f7888 --- /dev/null +++ b/infra/feast-operator/bundle/manifests/feast-operator.clusterserviceversion.yaml @@ -0,0 +1,221 @@ +apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + annotations: + alm-examples: |- + [ + { + "apiVersion": "feast.dev/v1alpha1", + "kind": "FeatureStore", + "metadata": { + "labels": { + "app.kubernetes.io/managed-by": "kustomize", + "app.kubernetes.io/name": "feast-operator" + }, + "name": "featurestore-sample" + }, + "spec": null + } + ] + capabilities: Basic Install + createdAt: "2024-10-09T16:16:53Z" + operators.operatorframework.io/builder: operator-sdk-v1.37.0 + operators.operatorframework.io/project_layout: go.kubebuilder.io/v4 + name: feast-operator.v0.40.0 + namespace: placeholder +spec: + apiservicedefinitions: {} + customresourcedefinitions: + owned: + - description: FeatureStore is the Schema for the featurestores API + displayName: Feature Store + kind: FeatureStore + name: featurestores.feast.dev + version: v1alpha1 + description: Feast (Feature Store) is an open source feature store for machine learning. + displayName: Feast Operator + icon: + - base64data: "" + mediatype: "" + install: + spec: + clusterPermissions: + - rules: + - apiGroups: + - feast.dev + resources: + - featurestores + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - feast.dev + resources: + - featurestores/finalizers + verbs: + - update + - apiGroups: + - feast.dev + resources: + - featurestores/status + verbs: + - get + - patch + - update + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create + - apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create + serviceAccountName: feast-operator-controller-manager + deployments: + - label: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: feast-operator + control-plane: controller-manager + name: feast-operator-controller-manager + spec: + replicas: 1 + selector: + matchLabels: + control-plane: controller-manager + strategy: {} + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + labels: + control-plane: controller-manager + spec: + containers: + - args: + - --secure-listen-address=0.0.0.0:8443 + - --upstream=http://127.0.0.1:8080/ + - --logtostderr=true + - --v=0 + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.16.0 + name: kube-rbac-proxy + ports: + - containerPort: 8443 + name: https + protocol: TCP + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 5m + memory: 64Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + - args: + - --health-probe-bind-address=:8081 + - --metrics-bind-address=127.0.0.1:8080 + - --leader-elect + command: + - /manager + image: feastdev/feast-operator:0.40.0 + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + name: manager + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + securityContext: + runAsNonRoot: true + serviceAccountName: feast-operator-controller-manager + terminationGracePeriodSeconds: 10 + permissions: + - rules: + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch + serviceAccountName: feast-operator-controller-manager + strategy: deployment + installModes: + - supported: false + type: OwnNamespace + - supported: false + type: SingleNamespace + - supported: false + type: MultiNamespace + - supported: true + type: AllNamespaces + keywords: + - feast + - feature + - store + - ai + links: + - name: Feast Operator + url: https://feast.dev/ + maintainers: + - email: feast-discuss@googlegroups.com + name: feast-discuss + maturity: alpha + provider: + name: Feast Community + url: https://lf-aidata.atlassian.net/wiki/spaces/FEAST/ + version: 0.40.0 diff --git a/infra/feast-operator/bundle/manifests/feast.dev_featurestores.yaml b/infra/feast-operator/bundle/manifests/feast.dev_featurestores.yaml new file mode 100644 index 0000000000..43df8e3f84 --- /dev/null +++ b/infra/feast-operator/bundle/manifests/feast.dev_featurestores.yaml @@ -0,0 +1,64 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + creationTimestamp: null + name: featurestores.feast.dev +spec: + group: feast.dev + names: + kind: FeatureStore + listKind: FeatureStoreList + plural: featurestores + singular: featurestore + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: FeatureStore is the Schema for the featurestores API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: FeatureStoreSpec defines the desired state of FeatureStore + properties: + feastProject: + description: FeastProject is the Feast project id. This can be any + alphanumeric string with underscores, but it cannot start with an + underscore. + pattern: ^[A-Za-z0-9][A-Za-z0-9_]*$ + type: string + required: + - feastProject + type: object + status: + description: FeatureStoreStatus defines the observed state of FeatureStore + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null diff --git a/infra/feast-operator/bundle/metadata/annotations.yaml b/infra/feast-operator/bundle/metadata/annotations.yaml new file mode 100644 index 0000000000..bf929b9755 --- /dev/null +++ b/infra/feast-operator/bundle/metadata/annotations.yaml @@ -0,0 +1,14 @@ +annotations: + # Core bundle annotations. + operators.operatorframework.io.bundle.mediatype.v1: registry+v1 + operators.operatorframework.io.bundle.manifests.v1: manifests/ + operators.operatorframework.io.bundle.metadata.v1: metadata/ + operators.operatorframework.io.bundle.package.v1: feast-operator + operators.operatorframework.io.bundle.channels.v1: alpha + operators.operatorframework.io.metrics.builder: operator-sdk-v1.37.0 + operators.operatorframework.io.metrics.mediatype.v1: metrics+v1 + operators.operatorframework.io.metrics.project_layout: go.kubebuilder.io/v4 + + # Annotations for testing. + operators.operatorframework.io.test.mediatype.v1: scorecard+v1 + operators.operatorframework.io.test.config.v1: tests/scorecard/ diff --git a/infra/feast-operator/bundle/tests/scorecard/config.yaml b/infra/feast-operator/bundle/tests/scorecard/config.yaml new file mode 100644 index 0000000000..aaf374d37d --- /dev/null +++ b/infra/feast-operator/bundle/tests/scorecard/config.yaml @@ -0,0 +1,70 @@ +apiVersion: scorecard.operatorframework.io/v1alpha3 +kind: Configuration +metadata: + name: config +stages: +- parallel: true + tests: + - entrypoint: + - scorecard-test + - basic-check-spec + image: quay.io/operator-framework/scorecard-test:v1.37.0 + labels: + suite: basic + test: basic-check-spec-test + storage: + spec: + mountPath: {} + - entrypoint: + - scorecard-test + - olm-bundle-validation + image: quay.io/operator-framework/scorecard-test:v1.37.0 + labels: + suite: olm + test: olm-bundle-validation-test + storage: + spec: + mountPath: {} + - entrypoint: + - scorecard-test + - olm-crds-have-validation + image: quay.io/operator-framework/scorecard-test:v1.37.0 + labels: + suite: olm + test: olm-crds-have-validation-test + storage: + spec: + mountPath: {} + - entrypoint: + - scorecard-test + - olm-crds-have-resources + image: quay.io/operator-framework/scorecard-test:v1.37.0 + labels: + suite: olm + test: olm-crds-have-resources-test + storage: + spec: + mountPath: {} + - entrypoint: + - scorecard-test + - olm-spec-descriptors + image: quay.io/operator-framework/scorecard-test:v1.37.0 + labels: + suite: olm + test: olm-spec-descriptors-test + storage: + spec: + mountPath: {} + - entrypoint: + - scorecard-test + - olm-status-descriptors + image: quay.io/operator-framework/scorecard-test:v1.37.0 + labels: + suite: olm + test: olm-status-descriptors-test + storage: + spec: + mountPath: {} +storage: + spec: + mountPath: {} diff --git a/infra/feast-operator/cmd/main.go b/infra/feast-operator/cmd/main.go new file mode 100644 index 0000000000..3ca6c89508 --- /dev/null +++ b/infra/feast-operator/cmd/main.go @@ -0,0 +1,148 @@ +/* +Copyright 2024 Feast Community. + +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 main + +import ( + "crypto/tls" + "flag" + "os" + + // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) + // to ensure that exec-entrypoint and run can make use of them. + _ "k8s.io/client-go/plugin/pkg/client/auth" + + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "sigs.k8s.io/controller-runtime/pkg/webhook" + + feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + "github.com/feast-dev/feast/infra/feast-operator/internal/controller" + //+kubebuilder:scaffold:imports +) + +var ( + scheme = runtime.NewScheme() + setupLog = ctrl.Log.WithName("setup") +) + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + + utilruntime.Must(feastdevv1alpha1.AddToScheme(scheme)) + //+kubebuilder:scaffold:scheme +} + +func main() { + var metricsAddr string + var enableLeaderElection bool + var probeAddr string + var secureMetrics bool + var enableHTTP2 bool + flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") + flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") + flag.BoolVar(&enableLeaderElection, "leader-elect", false, + "Enable leader election for controller manager. "+ + "Enabling this will ensure there is only one active controller manager.") + flag.BoolVar(&secureMetrics, "metrics-secure", false, + "If set the metrics endpoint is served securely") + flag.BoolVar(&enableHTTP2, "enable-http2", false, + "If set, HTTP/2 will be enabled for the metrics and webhook servers") + opts := zap.Options{ + Development: true, + } + opts.BindFlags(flag.CommandLine) + flag.Parse() + + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + + // if the enable-http2 flag is false (the default), http/2 should be disabled + // due to its vulnerabilities. More specifically, disabling http/2 will + // prevent from being vulnerable to the HTTP/2 Stream Cancellation and + // Rapid Reset CVEs. For more information see: + // - https://github.com/advisories/GHSA-qppj-fm5r-hxr3 + // - https://github.com/advisories/GHSA-4374-p667-p6c8 + disableHTTP2 := func(c *tls.Config) { + setupLog.Info("disabling http/2") + c.NextProtos = []string{"http/1.1"} + } + + tlsOpts := []func(*tls.Config){} + if !enableHTTP2 { + tlsOpts = append(tlsOpts, disableHTTP2) + } + + webhookServer := webhook.NewServer(webhook.Options{ + TLSOpts: tlsOpts, + }) + + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + Scheme: scheme, + Metrics: metricsserver.Options{ + BindAddress: metricsAddr, + SecureServing: secureMetrics, + TLSOpts: tlsOpts, + }, + WebhookServer: webhookServer, + HealthProbeBindAddress: probeAddr, + LeaderElection: enableLeaderElection, + LeaderElectionID: "66483dbd.feast.dev", + // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily + // when the Manager ends. This requires the binary to immediately end when the + // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly + // speeds up voluntary leader transitions as the new leader don't have to wait + // LeaseDuration time first. + // + // In the default scaffold provided, the program ends immediately after + // the manager stops, so would be fine to enable this option. However, + // if you are doing or is intended to do any operation such as perform cleanups + // after the manager stops then its usage might be unsafe. + // LeaderElectionReleaseOnCancel: true, + }) + if err != nil { + setupLog.Error(err, "unable to start manager") + os.Exit(1) + } + + if err = (&controller.FeatureStoreReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "FeatureStore") + os.Exit(1) + } + //+kubebuilder:scaffold:builder + + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up health check") + os.Exit(1) + } + if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up ready check") + os.Exit(1) + } + + setupLog.Info("starting manager") + if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + setupLog.Error(err, "problem running manager") + os.Exit(1) + } +} diff --git a/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml b/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml new file mode 100644 index 0000000000..d6bd053692 --- /dev/null +++ b/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml @@ -0,0 +1,58 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: featurestores.feast.dev +spec: + group: feast.dev + names: + kind: FeatureStore + listKind: FeatureStoreList + plural: featurestores + singular: featurestore + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: FeatureStore is the Schema for the featurestores API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: FeatureStoreSpec defines the desired state of FeatureStore + properties: + feastProject: + description: FeastProject is the Feast project id. This can be any + alphanumeric string with underscores, but it cannot start with an + underscore. + pattern: ^[A-Za-z0-9][A-Za-z0-9_]*$ + type: string + required: + - feastProject + type: object + status: + description: FeatureStoreStatus defines the observed state of FeatureStore + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/infra/feast-operator/config/crd/kustomization.yaml b/infra/feast-operator/config/crd/kustomization.yaml new file mode 100644 index 0000000000..fcdd0c61d9 --- /dev/null +++ b/infra/feast-operator/config/crd/kustomization.yaml @@ -0,0 +1,22 @@ +# This kustomization.yaml is not intended to be run by itself, +# since it depends on service name and namespace that are out of this kustomize package. +# It should be run by config/default +resources: +- bases/feast.dev_featurestores.yaml +#+kubebuilder:scaffold:crdkustomizeresource + +patches: +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. +# patches here are for enabling the conversion webhook for each CRD +#+kubebuilder:scaffold:crdkustomizewebhookpatch + +# [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. +# patches here are for enabling the CA injection for each CRD +#- path: patches/cainjection_in_featurestores.yaml +#+kubebuilder:scaffold:crdkustomizecainjectionpatch + +# [WEBHOOK] To enable webhook, uncomment the following section +# the following config is for teaching kustomize how to do kustomization for CRDs. + +#configurations: +#- kustomizeconfig.yaml diff --git a/infra/feast-operator/config/crd/kustomizeconfig.yaml b/infra/feast-operator/config/crd/kustomizeconfig.yaml new file mode 100644 index 0000000000..ec5c150a9d --- /dev/null +++ b/infra/feast-operator/config/crd/kustomizeconfig.yaml @@ -0,0 +1,19 @@ +# This file is for teaching kustomize how to substitute name and namespace reference in CRD +nameReference: +- kind: Service + version: v1 + fieldSpecs: + - kind: CustomResourceDefinition + version: v1 + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/name + +namespace: +- kind: CustomResourceDefinition + version: v1 + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/namespace + create: false + +varReference: +- path: metadata/annotations diff --git a/infra/feast-operator/config/default/kustomization.yaml b/infra/feast-operator/config/default/kustomization.yaml new file mode 100644 index 0000000000..957965b9b3 --- /dev/null +++ b/infra/feast-operator/config/default/kustomization.yaml @@ -0,0 +1,142 @@ +# Adds namespace to all resources. +namespace: feast-operator-system + +# Value of this field is prepended to the +# names of all resources, e.g. a deployment named +# "wordpress" becomes "alices-wordpress". +# Note that it should also match with the prefix (text before '-') of the namespace +# field above. +namePrefix: feast-operator- + +# Labels to add to all resources and selectors. +#labels: +#- includeSelectors: true +# pairs: +# someName: someValue + +resources: +- ../crd +- ../rbac +- ../manager +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in +# crd/kustomization.yaml +#- ../webhook +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. +#- ../certmanager +# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. +#- ../prometheus + +patches: +# Protect the /metrics endpoint by putting it behind auth. +# If you want your controller-manager to expose the /metrics +# endpoint w/o any authn/z, please comment the following line. +- path: manager_auth_proxy_patch.yaml + +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in +# crd/kustomization.yaml +#- path: manager_webhook_patch.yaml + +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. +# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. +# 'CERTMANAGER' needs to be enabled to use ca injection +#- path: webhookcainjection_patch.yaml + +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. +# Uncomment the following replacements to add the cert-manager CA injection annotations +#replacements: +# - source: # Add cert-manager annotation to ValidatingWebhookConfiguration, MutatingWebhookConfiguration and CRDs +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert # this name should match the one in certificate.yaml +# fieldPath: .metadata.namespace # namespace of the certificate CR +# targets: +# - select: +# kind: ValidatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 0 +# create: true +# - select: +# kind: MutatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 0 +# create: true +# - select: +# kind: CustomResourceDefinition +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 0 +# create: true +# - source: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert # this name should match the one in certificate.yaml +# fieldPath: .metadata.name +# targets: +# - select: +# kind: ValidatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true +# - select: +# kind: MutatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true +# - select: +# kind: CustomResourceDefinition +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true +# - source: # Add cert-manager annotation to the webhook Service +# kind: Service +# version: v1 +# name: webhook-service +# fieldPath: .metadata.name # namespace of the service +# targets: +# - select: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# fieldPaths: +# - .spec.dnsNames.0 +# - .spec.dnsNames.1 +# options: +# delimiter: '.' +# index: 0 +# create: true +# - source: +# kind: Service +# version: v1 +# name: webhook-service +# fieldPath: .metadata.namespace # namespace of the service +# targets: +# - select: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# fieldPaths: +# - .spec.dnsNames.0 +# - .spec.dnsNames.1 +# options: +# delimiter: '.' +# index: 1 +# create: true diff --git a/infra/feast-operator/config/default/manager_auth_proxy_patch.yaml b/infra/feast-operator/config/default/manager_auth_proxy_patch.yaml new file mode 100644 index 0000000000..4c3c27602f --- /dev/null +++ b/infra/feast-operator/config/default/manager_auth_proxy_patch.yaml @@ -0,0 +1,39 @@ +# This patch inject a sidecar container which is a HTTP proxy for the +# controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: kube-rbac-proxy + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.16.0 + args: + - "--secure-listen-address=0.0.0.0:8443" + - "--upstream=http://127.0.0.1:8080/" + - "--logtostderr=true" + - "--v=0" + ports: + - containerPort: 8443 + protocol: TCP + name: https + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 5m + memory: 64Mi + - name: manager + args: + - "--health-probe-bind-address=:8081" + - "--metrics-bind-address=127.0.0.1:8080" + - "--leader-elect" diff --git a/infra/feast-operator/config/default/manager_config_patch.yaml b/infra/feast-operator/config/default/manager_config_patch.yaml new file mode 100644 index 0000000000..f6f5891692 --- /dev/null +++ b/infra/feast-operator/config/default/manager_config_patch.yaml @@ -0,0 +1,10 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: manager diff --git a/infra/feast-operator/config/manager/kustomization.yaml b/infra/feast-operator/config/manager/kustomization.yaml new file mode 100644 index 0000000000..aba3224be6 --- /dev/null +++ b/infra/feast-operator/config/manager/kustomization.yaml @@ -0,0 +1,8 @@ +resources: +- manager.yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +images: +- name: controller + newName: feastdev/feast-operator + newTag: 0.40.0 diff --git a/infra/feast-operator/config/manager/manager.yaml b/infra/feast-operator/config/manager/manager.yaml new file mode 100644 index 0000000000..90ef7b4863 --- /dev/null +++ b/infra/feast-operator/config/manager/manager.yaml @@ -0,0 +1,94 @@ +apiVersion: v1 +kind: Namespace +metadata: + labels: + control-plane: controller-manager + app.kubernetes.io/name: feast-operator + app.kubernetes.io/managed-by: kustomize + name: system +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system + labels: + control-plane: controller-manager + app.kubernetes.io/name: feast-operator + app.kubernetes.io/managed-by: kustomize +spec: + selector: + matchLabels: + control-plane: controller-manager + replicas: 1 + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + labels: + control-plane: controller-manager + spec: + # TODO(user): Uncomment the following code to configure the nodeAffinity expression + # according to the platforms which are supported by your solution. + # It is considered best practice to support multiple architectures. You can + # build your manager image using the makefile target docker-buildx. + # affinity: + # nodeAffinity: + # requiredDuringSchedulingIgnoredDuringExecution: + # nodeSelectorTerms: + # - matchExpressions: + # - key: kubernetes.io/arch + # operator: In + # values: + # - amd64 + # - arm64 + # - ppc64le + # - s390x + # - key: kubernetes.io/os + # operator: In + # values: + # - linux + securityContext: + runAsNonRoot: true + # TODO(user): For common cases that do not require escalating privileges + # it is recommended to ensure that all your Pods/Containers are restrictive. + # More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted + # Please uncomment the following code if your project does NOT have to work on old Kubernetes + # versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ). + # seccompProfile: + # type: RuntimeDefault + containers: + - command: + - /manager + args: + - --leader-elect + image: controller:latest + name: manager + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + # TODO(user): Configure the resources accordingly based on the project requirements. + # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + serviceAccountName: controller-manager + terminationGracePeriodSeconds: 10 diff --git a/infra/feast-operator/config/manifests/bases/feast-operator.clusterserviceversion.yaml b/infra/feast-operator/config/manifests/bases/feast-operator.clusterserviceversion.yaml new file mode 100644 index 0000000000..b9915a296a --- /dev/null +++ b/infra/feast-operator/config/manifests/bases/feast-operator.clusterserviceversion.yaml @@ -0,0 +1,51 @@ +apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + annotations: + alm-examples: '[]' + capabilities: Basic Install + name: feast-operator.v0.0.0 + namespace: placeholder +spec: + apiservicedefinitions: {} + customresourcedefinitions: + owned: + - description: FeatureStore is the Schema for the featurestores API + displayName: Feature Store + kind: FeatureStore + name: featurestores.feast.dev + version: v1alpha1 + description: Feast (Feature Store) is an open source feature store for machine learning. + displayName: Feast Operator + icon: + - base64data: "" + mediatype: "" + install: + spec: + deployments: null + strategy: "" + installModes: + - supported: false + type: OwnNamespace + - supported: false + type: SingleNamespace + - supported: false + type: MultiNamespace + - supported: true + type: AllNamespaces + keywords: + - feast + - feature + - store + - ai + links: + - name: Feast Operator + url: https://feast.dev/ + maintainers: + - email: feast-discuss@googlegroups.com + name: feast-discuss + maturity: alpha + provider: + name: Feast Community + url: https://lf-aidata.atlassian.net/wiki/spaces/FEAST/ + version: 0.0.0 diff --git a/infra/feast-operator/config/manifests/kustomization.yaml b/infra/feast-operator/config/manifests/kustomization.yaml new file mode 100644 index 0000000000..caa74b9949 --- /dev/null +++ b/infra/feast-operator/config/manifests/kustomization.yaml @@ -0,0 +1,28 @@ +# These resources constitute the fully configured set of manifests +# used to generate the 'manifests/' directory in a bundle. +resources: +- bases/feast-operator.clusterserviceversion.yaml +- ../default +- ../samples +- ../scorecard + +# [WEBHOOK] To enable webhooks, uncomment all the sections with [WEBHOOK] prefix. +# Do NOT uncomment sections with prefix [CERTMANAGER], as OLM does not support cert-manager. +# These patches remove the unnecessary "cert" volume and its manager container volumeMount. +#patchesJson6902: +#- target: +# group: apps +# version: v1 +# kind: Deployment +# name: controller-manager +# namespace: system +# patch: |- +# # Remove the manager container's "cert" volumeMount, since OLM will create and mount a set of certs. +# # Update the indices in this path if adding or removing containers/volumeMounts in the manager's Deployment. +# - op: remove + +# path: /spec/template/spec/containers/0/volumeMounts/0 +# # Remove the "cert" volume, since OLM will create and mount a set of certs. +# # Update the indices in this path if adding or removing volumes in the manager's Deployment. +# - op: remove +# path: /spec/template/spec/volumes/0 diff --git a/infra/feast-operator/config/prometheus/kustomization.yaml b/infra/feast-operator/config/prometheus/kustomization.yaml new file mode 100644 index 0000000000..ed137168a1 --- /dev/null +++ b/infra/feast-operator/config/prometheus/kustomization.yaml @@ -0,0 +1,2 @@ +resources: +- monitor.yaml diff --git a/infra/feast-operator/config/prometheus/monitor.yaml b/infra/feast-operator/config/prometheus/monitor.yaml new file mode 100644 index 0000000000..5548407967 --- /dev/null +++ b/infra/feast-operator/config/prometheus/monitor.yaml @@ -0,0 +1,21 @@ +# Prometheus Monitor Service (Metrics) +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + labels: + control-plane: controller-manager + app.kubernetes.io/name: feast-operator + app.kubernetes.io/managed-by: kustomize + name: controller-manager-metrics-monitor + namespace: system +spec: + endpoints: + - path: /metrics + port: https + scheme: https + bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + tlsConfig: + insecureSkipVerify: true + selector: + matchLabels: + control-plane: controller-manager diff --git a/infra/feast-operator/config/rbac/auth_proxy_client_clusterrole.yaml b/infra/feast-operator/config/rbac/auth_proxy_client_clusterrole.yaml new file mode 100644 index 0000000000..a22f13c5e5 --- /dev/null +++ b/infra/feast-operator/config/rbac/auth_proxy_client_clusterrole.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: feast-operator + app.kubernetes.io/managed-by: kustomize + name: metrics-reader +rules: +- nonResourceURLs: + - "/metrics" + verbs: + - get diff --git a/infra/feast-operator/config/rbac/auth_proxy_role.yaml b/infra/feast-operator/config/rbac/auth_proxy_role.yaml new file mode 100644 index 0000000000..55f8791646 --- /dev/null +++ b/infra/feast-operator/config/rbac/auth_proxy_role.yaml @@ -0,0 +1,20 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: feast-operator + app.kubernetes.io/managed-by: kustomize + name: proxy-role +rules: +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create diff --git a/infra/feast-operator/config/rbac/auth_proxy_role_binding.yaml b/infra/feast-operator/config/rbac/auth_proxy_role_binding.yaml new file mode 100644 index 0000000000..ffa85c82af --- /dev/null +++ b/infra/feast-operator/config/rbac/auth_proxy_role_binding.yaml @@ -0,0 +1,15 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/name: feast-operator + app.kubernetes.io/managed-by: kustomize + name: proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: proxy-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/infra/feast-operator/config/rbac/auth_proxy_service.yaml b/infra/feast-operator/config/rbac/auth_proxy_service.yaml new file mode 100644 index 0000000000..c2bf4e3793 --- /dev/null +++ b/infra/feast-operator/config/rbac/auth_proxy_service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + control-plane: controller-manager + app.kubernetes.io/name: feast-operator + app.kubernetes.io/managed-by: kustomize + name: controller-manager-metrics-service + namespace: system +spec: + ports: + - name: https + port: 8443 + protocol: TCP + targetPort: https + selector: + control-plane: controller-manager diff --git a/infra/feast-operator/config/rbac/featurestore_editor_role.yaml b/infra/feast-operator/config/rbac/featurestore_editor_role.yaml new file mode 100644 index 0000000000..37c38e6f61 --- /dev/null +++ b/infra/feast-operator/config/rbac/featurestore_editor_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to edit featurestores. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: feast-operator + app.kubernetes.io/managed-by: kustomize + name: featurestore-editor-role +rules: +- apiGroups: + - feast.dev + resources: + - featurestores + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - feast.dev + resources: + - featurestores/status + verbs: + - get diff --git a/infra/feast-operator/config/rbac/featurestore_viewer_role.yaml b/infra/feast-operator/config/rbac/featurestore_viewer_role.yaml new file mode 100644 index 0000000000..b4444cbe60 --- /dev/null +++ b/infra/feast-operator/config/rbac/featurestore_viewer_role.yaml @@ -0,0 +1,23 @@ +# permissions for end users to view featurestores. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: feast-operator + app.kubernetes.io/managed-by: kustomize + name: featurestore-viewer-role +rules: +- apiGroups: + - feast.dev + resources: + - featurestores + verbs: + - get + - list + - watch +- apiGroups: + - feast.dev + resources: + - featurestores/status + verbs: + - get diff --git a/infra/feast-operator/config/rbac/kustomization.yaml b/infra/feast-operator/config/rbac/kustomization.yaml new file mode 100644 index 0000000000..5e4972b539 --- /dev/null +++ b/infra/feast-operator/config/rbac/kustomization.yaml @@ -0,0 +1,24 @@ +resources: +# All RBAC will be applied under this service account in +# the deployment namespace. You may comment out this resource +# if your manager will use a service account that exists at +# runtime. Be sure to update RoleBinding and ClusterRoleBinding +# subjects if changing service account names. +- service_account.yaml +- role.yaml +- role_binding.yaml +- leader_election_role.yaml +- leader_election_role_binding.yaml +# Comment the following 4 lines if you want to disable +# the auth proxy (https://github.com/brancz/kube-rbac-proxy) +# which protects your /metrics endpoint. +- auth_proxy_service.yaml +- auth_proxy_role.yaml +- auth_proxy_role_binding.yaml +- auth_proxy_client_clusterrole.yaml +# For each CRD, "Editor" and "Viewer" roles are scaffolded by +# default, aiding admins in cluster management. Those roles are +# not used by the Project itself. You can comment the following lines +# if you do not want those helpers be installed with your Project. +- featurestore_editor_role.yaml +- featurestore_viewer_role.yaml diff --git a/infra/feast-operator/config/rbac/leader_election_role.yaml b/infra/feast-operator/config/rbac/leader_election_role.yaml new file mode 100644 index 0000000000..4a70cf87fe --- /dev/null +++ b/infra/feast-operator/config/rbac/leader_election_role.yaml @@ -0,0 +1,40 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/name: feast-operator + app.kubernetes.io/managed-by: kustomize + name: leader-election-role +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch diff --git a/infra/feast-operator/config/rbac/leader_election_role_binding.yaml b/infra/feast-operator/config/rbac/leader_election_role_binding.yaml new file mode 100644 index 0000000000..f3233ca146 --- /dev/null +++ b/infra/feast-operator/config/rbac/leader_election_role_binding.yaml @@ -0,0 +1,15 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: feast-operator + app.kubernetes.io/managed-by: kustomize + name: leader-election-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: leader-election-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/infra/feast-operator/config/rbac/role.yaml b/infra/feast-operator/config/rbac/role.yaml new file mode 100644 index 0000000000..f0bb2016af --- /dev/null +++ b/infra/feast-operator/config/rbac/role.yaml @@ -0,0 +1,32 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: manager-role +rules: +- apiGroups: + - feast.dev + resources: + - featurestores + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - feast.dev + resources: + - featurestores/finalizers + verbs: + - update +- apiGroups: + - feast.dev + resources: + - featurestores/status + verbs: + - get + - patch + - update diff --git a/infra/feast-operator/config/rbac/role_binding.yaml b/infra/feast-operator/config/rbac/role_binding.yaml new file mode 100644 index 0000000000..04272719e7 --- /dev/null +++ b/infra/feast-operator/config/rbac/role_binding.yaml @@ -0,0 +1,15 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/name: feast-operator + app.kubernetes.io/managed-by: kustomize + name: manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: manager-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/infra/feast-operator/config/rbac/service_account.yaml b/infra/feast-operator/config/rbac/service_account.yaml new file mode 100644 index 0000000000..6e6896f879 --- /dev/null +++ b/infra/feast-operator/config/rbac/service_account.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/name: feast-operator + app.kubernetes.io/managed-by: kustomize + name: controller-manager + namespace: system diff --git a/infra/feast-operator/config/samples/kustomization.yaml b/infra/feast-operator/config/samples/kustomization.yaml new file mode 100644 index 0000000000..4869cc7b24 --- /dev/null +++ b/infra/feast-operator/config/samples/kustomization.yaml @@ -0,0 +1,4 @@ +## Append samples of your project ## +resources: +- v1alpha1_featurestore.yaml +#+kubebuilder:scaffold:manifestskustomizesamples diff --git a/infra/feast-operator/config/samples/v1alpha1_featurestore.yaml b/infra/feast-operator/config/samples/v1alpha1_featurestore.yaml new file mode 100644 index 0000000000..2800d87e35 --- /dev/null +++ b/infra/feast-operator/config/samples/v1alpha1_featurestore.yaml @@ -0,0 +1,9 @@ +apiVersion: feast.dev/v1alpha1 +kind: FeatureStore +metadata: + labels: + app.kubernetes.io/name: feast-operator + app.kubernetes.io/managed-by: kustomize + name: featurestore-sample +spec: + # TODO(user): Add fields here diff --git a/infra/feast-operator/config/scorecard/bases/config.yaml b/infra/feast-operator/config/scorecard/bases/config.yaml new file mode 100644 index 0000000000..c77047841e --- /dev/null +++ b/infra/feast-operator/config/scorecard/bases/config.yaml @@ -0,0 +1,7 @@ +apiVersion: scorecard.operatorframework.io/v1alpha3 +kind: Configuration +metadata: + name: config +stages: +- parallel: true + tests: [] diff --git a/infra/feast-operator/config/scorecard/kustomization.yaml b/infra/feast-operator/config/scorecard/kustomization.yaml new file mode 100644 index 0000000000..50cd2d084e --- /dev/null +++ b/infra/feast-operator/config/scorecard/kustomization.yaml @@ -0,0 +1,16 @@ +resources: +- bases/config.yaml +patchesJson6902: +- path: patches/basic.config.yaml + target: + group: scorecard.operatorframework.io + version: v1alpha3 + kind: Configuration + name: config +- path: patches/olm.config.yaml + target: + group: scorecard.operatorframework.io + version: v1alpha3 + kind: Configuration + name: config +#+kubebuilder:scaffold:patchesJson6902 diff --git a/infra/feast-operator/config/scorecard/patches/basic.config.yaml b/infra/feast-operator/config/scorecard/patches/basic.config.yaml new file mode 100644 index 0000000000..fd6200ae97 --- /dev/null +++ b/infra/feast-operator/config/scorecard/patches/basic.config.yaml @@ -0,0 +1,10 @@ +- op: add + path: /stages/0/tests/- + value: + entrypoint: + - scorecard-test + - basic-check-spec + image: quay.io/operator-framework/scorecard-test:v1.37.0 + labels: + suite: basic + test: basic-check-spec-test diff --git a/infra/feast-operator/config/scorecard/patches/olm.config.yaml b/infra/feast-operator/config/scorecard/patches/olm.config.yaml new file mode 100644 index 0000000000..a547ce213d --- /dev/null +++ b/infra/feast-operator/config/scorecard/patches/olm.config.yaml @@ -0,0 +1,50 @@ +- op: add + path: /stages/0/tests/- + value: + entrypoint: + - scorecard-test + - olm-bundle-validation + image: quay.io/operator-framework/scorecard-test:v1.37.0 + labels: + suite: olm + test: olm-bundle-validation-test +- op: add + path: /stages/0/tests/- + value: + entrypoint: + - scorecard-test + - olm-crds-have-validation + image: quay.io/operator-framework/scorecard-test:v1.37.0 + labels: + suite: olm + test: olm-crds-have-validation-test +- op: add + path: /stages/0/tests/- + value: + entrypoint: + - scorecard-test + - olm-crds-have-resources + image: quay.io/operator-framework/scorecard-test:v1.37.0 + labels: + suite: olm + test: olm-crds-have-resources-test +- op: add + path: /stages/0/tests/- + value: + entrypoint: + - scorecard-test + - olm-spec-descriptors + image: quay.io/operator-framework/scorecard-test:v1.37.0 + labels: + suite: olm + test: olm-spec-descriptors-test +- op: add + path: /stages/0/tests/- + value: + entrypoint: + - scorecard-test + - olm-status-descriptors + image: quay.io/operator-framework/scorecard-test:v1.37.0 + labels: + suite: olm + test: olm-status-descriptors-test diff --git a/infra/feast-operator/dist/install.yaml b/infra/feast-operator/dist/install.yaml new file mode 100644 index 0000000000..63b3a742b1 --- /dev/null +++ b/infra/feast-operator/dist/install.yaml @@ -0,0 +1,381 @@ +apiVersion: v1 +kind: Namespace +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: feast-operator + control-plane: controller-manager + name: feast-operator-system +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: featurestores.feast.dev +spec: + group: feast.dev + names: + kind: FeatureStore + listKind: FeatureStoreList + plural: featurestores + singular: featurestore + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: FeatureStore is the Schema for the featurestores API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: FeatureStoreSpec defines the desired state of FeatureStore + properties: + feastProject: + description: FeastProject is the Feast project id. This can be any + alphanumeric string with underscores, but it cannot start with an + underscore. + pattern: ^[A-Za-z0-9][A-Za-z0-9_]*$ + type: string + required: + - feastProject + type: object + status: + description: FeatureStoreStatus defines the observed state of FeatureStore + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: feast-operator + name: feast-operator-controller-manager + namespace: feast-operator-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: feast-operator + name: feast-operator-leader-election-role + namespace: feast-operator-system +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: feast-operator + name: feast-operator-featurestore-editor-role +rules: +- apiGroups: + - feast.dev + resources: + - featurestores + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - feast.dev + resources: + - featurestores/status + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: feast-operator + name: feast-operator-featurestore-viewer-role +rules: +- apiGroups: + - feast.dev + resources: + - featurestores + verbs: + - get + - list + - watch +- apiGroups: + - feast.dev + resources: + - featurestores/status + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: feast-operator-manager-role +rules: +- apiGroups: + - feast.dev + resources: + - featurestores + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - feast.dev + resources: + - featurestores/finalizers + verbs: + - update +- apiGroups: + - feast.dev + resources: + - featurestores/status + verbs: + - get + - patch + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: feast-operator + name: feast-operator-metrics-reader +rules: +- nonResourceURLs: + - /metrics + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: feast-operator + name: feast-operator-proxy-role +rules: +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: feast-operator + name: feast-operator-leader-election-rolebinding + namespace: feast-operator-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: feast-operator-leader-election-role +subjects: +- kind: ServiceAccount + name: feast-operator-controller-manager + namespace: feast-operator-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: feast-operator + name: feast-operator-manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: feast-operator-manager-role +subjects: +- kind: ServiceAccount + name: feast-operator-controller-manager + namespace: feast-operator-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: feast-operator + name: feast-operator-proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: feast-operator-proxy-role +subjects: +- kind: ServiceAccount + name: feast-operator-controller-manager + namespace: feast-operator-system +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: feast-operator + control-plane: controller-manager + name: feast-operator-controller-manager-metrics-service + namespace: feast-operator-system +spec: + ports: + - name: https + port: 8443 + protocol: TCP + targetPort: https + selector: + control-plane: controller-manager +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: feast-operator + control-plane: controller-manager + name: feast-operator-controller-manager + namespace: feast-operator-system +spec: + replicas: 1 + selector: + matchLabels: + control-plane: controller-manager + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + labels: + control-plane: controller-manager + spec: + containers: + - args: + - --secure-listen-address=0.0.0.0:8443 + - --upstream=http://127.0.0.1:8080/ + - --logtostderr=true + - --v=0 + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.16.0 + name: kube-rbac-proxy + ports: + - containerPort: 8443 + name: https + protocol: TCP + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 5m + memory: 64Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + - args: + - --health-probe-bind-address=:8081 + - --metrics-bind-address=127.0.0.1:8080 + - --leader-elect + command: + - /manager + image: feastdev/feast-operator:0.40.0 + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + name: manager + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + securityContext: + runAsNonRoot: true + serviceAccountName: feast-operator-controller-manager + terminationGracePeriodSeconds: 10 diff --git a/infra/feast-operator/go.mod b/infra/feast-operator/go.mod new file mode 100644 index 0000000000..d39211dea8 --- /dev/null +++ b/infra/feast-operator/go.mod @@ -0,0 +1,73 @@ +module github.com/feast-dev/feast/infra/feast-operator + +go 1.21 + +require ( + github.com/onsi/ginkgo/v2 v2.14.0 + github.com/onsi/gomega v1.30.0 + k8s.io/apimachinery v0.29.2 + k8s.io/client-go v0.29.2 + sigs.k8s.io/controller-runtime v0.17.3 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/evanphx/json-patch/v5 v5.8.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/zapr v1.3.0 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.22.3 // indirect + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/imdario/mergo v0.3.6 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.18.0 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.45.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.26.0 // indirect + golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/oauth2 v0.12.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/term v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/time v0.3.0 // indirect + golang.org/x/tools v0.16.1 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.29.2 // indirect + k8s.io/apiextensions-apiserver v0.29.2 // indirect + k8s.io/component-base v0.29.2 // indirect + k8s.io/klog/v2 v2.110.1 // indirect + k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect + k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/infra/feast-operator/go.sum b/infra/feast-operator/go.sum new file mode 100644 index 0000000000..9b3607f06f --- /dev/null +++ b/infra/feast-operator/go.sum @@ -0,0 +1,205 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= +github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.8.0 h1:lRj6N9Nci7MvzrXuX6HFzU8XjmhPiXPlsKEy1u0KQro= +github.com/evanphx/json-patch/v5 v5.8.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= +github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.14.0 h1:vSmGj2Z5YPb9JwCWT6z6ihcUvDhuXLc3sJiqd3jMKAY= +github.com/onsi/ginkgo/v2 v2.14.0/go.mod h1:JkUdW7JkN0V6rFvsHcJ478egV3XH9NxpD27Hal/PhZw= +github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= +github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= +github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= +github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4= +golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= +golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.29.2 h1:hBC7B9+MU+ptchxEqTNW2DkUosJpp1P+Wn6YncZ474A= +k8s.io/api v0.29.2/go.mod h1:sdIaaKuU7P44aoyyLlikSLayT6Vb7bvJNCX105xZXY0= +k8s.io/apiextensions-apiserver v0.29.2 h1:UK3xB5lOWSnhaCk0RFZ0LUacPZz9RY4wi/yt2Iu+btg= +k8s.io/apiextensions-apiserver v0.29.2/go.mod h1:aLfYjpA5p3OwtqNXQFkhJ56TB+spV8Gc4wfMhUA3/b8= +k8s.io/apimachinery v0.29.2 h1:EWGpfJ856oj11C52NRCHuU7rFDwxev48z+6DSlGNsV8= +k8s.io/apimachinery v0.29.2/go.mod h1:6HVkd1FwxIagpYrHSwJlQqZI3G9LfYWRPAkUvLnXTKU= +k8s.io/client-go v0.29.2 h1:FEg85el1TeZp+/vYJM7hkDlSTFZ+c5nnK44DJ4FyoRg= +k8s.io/client-go v0.29.2/go.mod h1:knlvFZE58VpqbQpJNbCbctTVXcd35mMyAAwBdpt4jrA= +k8s.io/component-base v0.29.2 h1:lpiLyuvPA9yV1aQwGLENYyK7n/8t6l3nn3zAtFTJYe8= +k8s.io/component-base v0.29.2/go.mod h1:BfB3SLrefbZXiBfbM+2H1dlat21Uewg/5qtKOl8degM= +k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= +k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.17.3 h1:65QmN7r3FWgTxDMz9fvGnO1kbf2nu+acg9p2R9oYYYk= +sigs.k8s.io/controller-runtime v0.17.3/go.mod h1:N0jpP5Lo7lMTF9aL56Z/B2oWBJjey6StQM0jRbKQXtY= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/infra/feast-operator/hack/boilerplate.go.txt b/infra/feast-operator/hack/boilerplate.go.txt new file mode 100644 index 0000000000..db95ef3b01 --- /dev/null +++ b/infra/feast-operator/hack/boilerplate.go.txt @@ -0,0 +1,15 @@ +/* +Copyright 2024 Feast Community. + +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. +*/ \ No newline at end of file diff --git a/infra/feast-operator/internal/controller/featurestore_controller.go b/infra/feast-operator/internal/controller/featurestore_controller.go new file mode 100644 index 0000000000..d56a7ff024 --- /dev/null +++ b/infra/feast-operator/internal/controller/featurestore_controller.go @@ -0,0 +1,62 @@ +/* +Copyright 2024 Feast Community. + +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 controller + +import ( + "context" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" +) + +// FeatureStoreReconciler reconciles a FeatureStore object +type FeatureStoreReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +//+kubebuilder:rbac:groups=feast.dev,resources=featurestores,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=feast.dev,resources=featurestores/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=feast.dev,resources=featurestores/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// TODO(user): Modify the Reconcile function to compare the state specified by +// the FeatureStore object against the actual cluster state, and then +// perform operations to make the cluster state reflect the state specified by +// the user. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.17.3/pkg/reconcile +func (r *FeatureStoreReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + _ = log.FromContext(ctx) + + // TODO(user): your logic here + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *FeatureStoreReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&feastdevv1alpha1.FeatureStore{}). + Complete(r) +} diff --git a/infra/feast-operator/internal/controller/featurestore_controller_test.go b/infra/feast-operator/internal/controller/featurestore_controller_test.go new file mode 100644 index 0000000000..d4caf25497 --- /dev/null +++ b/infra/feast-operator/internal/controller/featurestore_controller_test.go @@ -0,0 +1,84 @@ +/* +Copyright 2024 Feast Community. + +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 controller + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" +) + +var _ = Describe("FeatureStore Controller", func() { + Context("When reconciling a resource", func() { + const resourceName = "test-resource" + + ctx := context.Background() + + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", // TODO(user):Modify as needed + } + featurestore := &feastdevv1alpha1.FeatureStore{} + + BeforeEach(func() { + By("creating the custom resource for the Kind FeatureStore") + err := k8sClient.Get(ctx, typeNamespacedName, featurestore) + if err != nil && errors.IsNotFound(err) { + resource := &feastdevv1alpha1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + Spec: feastdevv1alpha1.FeatureStoreSpec{FeastProject: "my_project"}, + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + }) + + AfterEach(func() { + // TODO(user): Cleanup logic after each test, like removing the resource instance. + resource := &feastdevv1alpha1.FeatureStore{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance FeatureStore") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + }) + It("should successfully reconcile the resource", func() { + By("Reconciling the created resource") + controllerReconciler := &FeatureStoreReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. + // Example: If you expect a certain status condition after reconciliation, verify it here. + }) + }) +}) diff --git a/infra/feast-operator/internal/controller/suite_test.go b/infra/feast-operator/internal/controller/suite_test.go new file mode 100644 index 0000000000..57091df5c0 --- /dev/null +++ b/infra/feast-operator/internal/controller/suite_test.go @@ -0,0 +1,90 @@ +/* +Copyright 2024 Feast Community. + +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 controller + +import ( + "fmt" + "path/filepath" + "runtime" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + //+kubebuilder:scaffold:imports +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var cfg *rest.Config +var k8sClient client.Client +var testEnv *envtest.Environment + +func TestControllers(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Controller Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: true, + + // The BinaryAssetsDirectory is only required if you want to run the tests directly + // without call the makefile target test. If not informed it will look for the + // default path defined in controller-runtime which is /usr/local/kubebuilder/. + // Note that you must have the required binaries setup under the bin directory to perform + // the tests directly. When we run make test it will be setup and used automatically. + BinaryAssetsDirectory: filepath.Join("..", "..", "bin", "k8s", + fmt.Sprintf("1.29.0-%s-%s", runtime.GOOS, runtime.GOARCH)), + } + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + err = feastdevv1alpha1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + //+kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + +}) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) diff --git a/infra/feast-operator/test/e2e/e2e_suite_test.go b/infra/feast-operator/test/e2e/e2e_suite_test.go new file mode 100644 index 0000000000..8e46d8a506 --- /dev/null +++ b/infra/feast-operator/test/e2e/e2e_suite_test.go @@ -0,0 +1,32 @@ +/* +Copyright 2024 Feast Community. + +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 e2e + +import ( + "fmt" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +// Run e2e tests using the Ginkgo runner. +func TestE2E(t *testing.T) { + RegisterFailHandler(Fail) + fmt.Fprintf(GinkgoWriter, "Starting feast-operator suite\n") + RunSpecs(t, "e2e suite") +} diff --git a/infra/feast-operator/test/e2e/e2e_test.go b/infra/feast-operator/test/e2e/e2e_test.go new file mode 100644 index 0000000000..b46b3105d2 --- /dev/null +++ b/infra/feast-operator/test/e2e/e2e_test.go @@ -0,0 +1,122 @@ +/* +Copyright 2024 Feast Community. + +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 e2e + +import ( + "fmt" + "os/exec" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/feast-dev/feast/infra/feast-operator/test/utils" +) + +const namespace = "feast-operator-system" + +var _ = Describe("controller", Ordered, func() { + BeforeAll(func() { + By("installing prometheus operator") + Expect(utils.InstallPrometheusOperator()).To(Succeed()) + + By("installing the cert-manager") + Expect(utils.InstallCertManager()).To(Succeed()) + + By("creating manager namespace") + cmd := exec.Command("kubectl", "create", "ns", namespace) + _, _ = utils.Run(cmd) + }) + + AfterAll(func() { + By("uninstalling the Prometheus manager bundle") + utils.UninstallPrometheusOperator() + + By("uninstalling the cert-manager bundle") + utils.UninstallCertManager() + + By("removing manager namespace") + cmd := exec.Command("kubectl", "delete", "ns", namespace) + _, _ = utils.Run(cmd) + }) + + Context("Operator", func() { + It("should run successfully", func() { + var controllerPodName string + var err error + + // projectimage stores the name of the image used in the example + var projectimage = "example.com/feast-operator:v0.0.1" + + By("building the manager(Operator) image") + cmd := exec.Command("make", "docker-build", fmt.Sprintf("IMG=%s", projectimage)) + _, err = utils.Run(cmd) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("loading the the manager(Operator) image on Kind") + err = utils.LoadImageToKindClusterWithName(projectimage) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("installing CRDs") + cmd = exec.Command("make", "install") + _, err = utils.Run(cmd) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("deploying the controller-manager") + cmd = exec.Command("make", "deploy", fmt.Sprintf("IMG=%s", projectimage)) + _, err = utils.Run(cmd) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("validating that the controller-manager pod is running as expected") + verifyControllerUp := func() error { + // Get pod name + + cmd = exec.Command("kubectl", "get", + "pods", "-l", "control-plane=controller-manager", + "-o", "go-template={{ range .items }}"+ + "{{ if not .metadata.deletionTimestamp }}"+ + "{{ .metadata.name }}"+ + "{{ \"\\n\" }}{{ end }}{{ end }}", + "-n", namespace, + ) + + podOutput, err := utils.Run(cmd) + ExpectWithOffset(2, err).NotTo(HaveOccurred()) + podNames := utils.GetNonEmptyLines(string(podOutput)) + if len(podNames) != 1 { + return fmt.Errorf("expect 1 controller pods running, but got %d", len(podNames)) + } + controllerPodName = podNames[0] + ExpectWithOffset(2, controllerPodName).Should(ContainSubstring("controller-manager")) + + // Validate pod status + cmd = exec.Command("kubectl", "get", + "pods", controllerPodName, "-o", "jsonpath={.status.phase}", + "-n", namespace, + ) + status, err := utils.Run(cmd) + ExpectWithOffset(2, err).NotTo(HaveOccurred()) + if string(status) != "Running" { + return fmt.Errorf("controller pod in %s status", status) + } + return nil + } + EventuallyWithOffset(1, verifyControllerUp, time.Minute, time.Second).Should(Succeed()) + + }) + }) +}) diff --git a/infra/feast-operator/test/utils/utils.go b/infra/feast-operator/test/utils/utils.go new file mode 100644 index 0000000000..cfd9e59582 --- /dev/null +++ b/infra/feast-operator/test/utils/utils.go @@ -0,0 +1,140 @@ +/* +Copyright 2024 Feast Community. + +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 utils + +import ( + "fmt" + "os" + "os/exec" + "strings" + + . "github.com/onsi/ginkgo/v2" //nolint:golint,revive +) + +const ( + prometheusOperatorVersion = "v0.72.0" + prometheusOperatorURL = "https://github.com/prometheus-operator/prometheus-operator/" + + "releases/download/%s/bundle.yaml" + + certmanagerVersion = "v1.14.4" + certmanagerURLTmpl = "https://github.com/jetstack/cert-manager/releases/download/%s/cert-manager.yaml" +) + +func warnError(err error) { + fmt.Fprintf(GinkgoWriter, "warning: %v\n", err) +} + +// InstallPrometheusOperator installs the prometheus Operator to be used to export the enabled metrics. +func InstallPrometheusOperator() error { + url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion) + cmd := exec.Command("kubectl", "create", "-f", url) + _, err := Run(cmd) + return err +} + +// Run executes the provided command within this context +func Run(cmd *exec.Cmd) ([]byte, error) { + dir, _ := GetProjectDir() + cmd.Dir = dir + + if err := os.Chdir(cmd.Dir); err != nil { + fmt.Fprintf(GinkgoWriter, "chdir dir: %s\n", err) + } + + cmd.Env = append(os.Environ(), "GO111MODULE=on") + command := strings.Join(cmd.Args, " ") + fmt.Fprintf(GinkgoWriter, "running: %s\n", command) + output, err := cmd.CombinedOutput() + if err != nil { + return output, fmt.Errorf("%s failed with error: (%v) %s", command, err, string(output)) + } + + return output, nil +} + +// UninstallPrometheusOperator uninstalls the prometheus +func UninstallPrometheusOperator() { + url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion) + cmd := exec.Command("kubectl", "delete", "-f", url) + if _, err := Run(cmd); err != nil { + warnError(err) + } +} + +// UninstallCertManager uninstalls the cert manager +func UninstallCertManager() { + url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion) + cmd := exec.Command("kubectl", "delete", "-f", url) + if _, err := Run(cmd); err != nil { + warnError(err) + } +} + +// InstallCertManager installs the cert manager bundle. +func InstallCertManager() error { + url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion) + cmd := exec.Command("kubectl", "apply", "-f", url) + if _, err := Run(cmd); err != nil { + return err + } + // Wait for cert-manager-webhook to be ready, which can take time if cert-manager + // was re-installed after uninstalling on a cluster. + cmd = exec.Command("kubectl", "wait", "deployment.apps/cert-manager-webhook", + "--for", "condition=Available", + "--namespace", "cert-manager", + "--timeout", "5m", + ) + + _, err := Run(cmd) + return err +} + +// LoadImageToKindCluster loads a local docker image to the kind cluster +func LoadImageToKindClusterWithName(name string) error { + cluster := "kind" + if v, ok := os.LookupEnv("KIND_CLUSTER"); ok { + cluster = v + } + kindOptions := []string{"load", "docker-image", name, "--name", cluster} + cmd := exec.Command("kind", kindOptions...) + _, err := Run(cmd) + return err +} + +// GetNonEmptyLines converts given command output string into individual objects +// according to line breakers, and ignores the empty elements in it. +func GetNonEmptyLines(output string) []string { + var res []string + elements := strings.Split(output, "\n") + for _, element := range elements { + if element != "" { + res = append(res, element) + } + } + + return res +} + +// GetProjectDir will return the directory where the project is +func GetProjectDir() (string, error) { + wd, err := os.Getwd() + if err != nil { + return wd, err + } + wd = strings.Replace(wd, "/test/e2e", "", -1) + return wd, nil +} diff --git a/infra/scripts/release/files_to_bump.txt b/infra/scripts/release/files_to_bump.txt index 4b3967b23e..d71f8da37d 100644 --- a/infra/scripts/release/files_to_bump.txt +++ b/infra/scripts/release/files_to_bump.txt @@ -12,5 +12,7 @@ infra/charts/feast-feature-server/README.md 3 infra/charts/feast-feature-server/values.yaml 12 infra/feast-helm-operator/Makefile 6 infra/feast-helm-operator/config/manager/kustomization.yaml 8 +infra/feast-operator/Makefile 6 +infra/feast-operator/config/manager/kustomization.yaml 8 java/pom.xml 38 ui/package.json 3 From ef9e0bbdb2a80250786b87972a53c4cf5890bb76 Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Wed, 9 Oct 2024 23:04:14 -0400 Subject: [PATCH 130/185] feat: Adding write capability to online store to on demand feature views (#4585) * merged changes Signed-off-by: Francisco Javier Arceo * saving progress Signed-off-by: Francisco Javier Arceo * merged changes to odfv Signed-off-by: Francisco Javier Arceo * linted Signed-off-by: Francisco Javier Arceo * adding the test needed to show the expected behavior Signed-off-by: Francisco Javier Arceo * updated test case Signed-off-by: Francisco Javier Arceo * saving progress Signed-off-by: Francisco Javier Arceo * merging Signed-off-by: Francisco Javier Arceo * merged Signed-off-by: Francisco Javier Arceo * merged Signed-off-by: Francisco Javier Arceo * merging Signed-off-by: Francisco Javier Arceo * adding the entity keys for now to do retrieval Signed-off-by: Francisco Javier Arceo * adding entity to odfv Signed-off-by: Francisco Javier Arceo * checking in progress...getting closer Signed-off-by: Francisco Javier Arceo * may have to revert some of this...looks like the challenge is getting the entities correct when storing writes. just checking in progress Signed-off-by: Francisco Javier Arceo * moving things around to make it easier to debug Signed-off-by: Francisco Javier Arceo * debugging Signed-off-by: Francisco Javier Arceo * merged Signed-off-by: Francisco Javier Arceo * merging Signed-off-by: Francisco Javier Arceo * Rebasing and merging changes from other PR Signed-off-by: Francisco Javier Arceo * Merging changes continued Signed-off-by: Francisco Javier Arceo * update the _make_inference to include odfvs with writes in the update map Signed-off-by: Francisco Javier Arceo * have the table being written now...the create table happens in the SqliteOnlineStore.update() method Signed-off-by: Francisco Javier Arceo * checking in progress Signed-off-by: Francisco Javier Arceo * adding logs Signed-off-by: Francisco Javier Arceo * updating permissions Signed-off-by: Francisco Javier Arceo * going to error out on purpose Signed-off-by: Francisco Javier Arceo * adding unit test and merging changes Signed-off-by: Francisco Javier Arceo * almost got everything working and type validation behaving Signed-off-by: Francisco Javier Arceo * cleaned up and have tests behaving Signed-off-by: Francisco Javier Arceo * adding print Signed-off-by: Francisco Javier Arceo * removing print Signed-off-by: Francisco Javier Arceo * checking in progress Signed-off-by: Francisco Javier Arceo * updating test Signed-off-by: Francisco Javier Arceo * adding test Signed-off-by: Francisco Javier Arceo * linted and updated Signed-off-by: Francisco Javier Arceo * removed print Signed-off-by: Francisco Javier Arceo * updated tests to test actual behavior Signed-off-by: Francisco Javier Arceo * checking in progress Signed-off-by: Francisco Javier Arceo * changing typo Signed-off-by: Francisco Javier Arceo * updating test Signed-off-by: Francisco Javier Arceo * testing changes Signed-off-by: Francisco Javier Arceo * checking to see if thing still working Signed-off-by: Francisco Javier Arceo * removed print Signed-off-by: Francisco Javier Arceo * undo change for odfv file Signed-off-by: Francisco Javier Arceo * updated tests Signed-off-by: Francisco Javier Arceo * okay well have the unit test working Signed-off-by: Francisco Javier Arceo * type changes, hope i dont regret them Signed-off-by: Francisco Javier Arceo * updated stream feature view piece Signed-off-by: Francisco Javier Arceo * updated sfv ifelse Signed-off-by: Francisco Javier Arceo * removing print Signed-off-by: Francisco Javier Arceo * formatted and updated test Signed-off-by: Francisco Javier Arceo * resolving some linter errors Signed-off-by: Francisco Javier Arceo * fixed linter and formatting Signed-off-by: Francisco Javier Arceo * okay think it is working Signed-off-by: Francisco Javier Arceo * linter Signed-off-by: Francisco Javier Arceo * updated type map for integration tests Signed-off-by: Francisco Javier Arceo * updated local feature store test Signed-off-by: Francisco Javier Arceo * fixed local fs test Signed-off-by: Francisco Javier Arceo * chore: Updated snowflake test to be more explicit about post apply entity_columns return value (#4603) chore: updated snowflake test to be more explicit about post apply entity_column return value Signed-off-by: Francisco Javier Arceo * merging Signed-off-by: Francisco Javier Arceo * fixed test to entity_rows_to_read Signed-off-by: Francisco Javier Arceo * resolved inf conflicts Signed-off-by: Francisco Javier Arceo * lint Signed-off-by: Francisco Javier Arceo * Updated tests and lint, think I have everything working Signed-off-by: Francisco Javier Arceo --------- Signed-off-by: Francisco Javier Arceo --- sdk/python/feast/errors.py | 13 +- sdk/python/feast/feature_service.py | 4 +- sdk/python/feast/feature_store.py | 70 +++- sdk/python/feast/inference.py | 39 ++- .../aws_lambda/lambda_engine.py | 5 +- .../batch_materialization_engine.py | 3 +- .../spark/spark_materialization_engine.py | 5 +- .../kubernetes/k8s_materialization_engine.py | 5 +- .../infra/materialization/local_engine.py | 5 +- .../infra/materialization/snowflake_engine.py | 5 +- .../feast/infra/online_stores/online_store.py | 6 +- .../feast/infra/passthrough_provider.py | 60 +++- sdk/python/feast/infra/provider.py | 8 +- sdk/python/feast/type_map.py | 1 + sdk/python/feast/types.py | 43 ++- sdk/python/feast/utils.py | 122 ++++++- .../test_local_feature_store.py | 50 ++- .../unit/online_store/test_online_writes.py | 3 + .../tests/unit/test_on_demand_feature_view.py | 97 +++++- .../test_on_demand_python_transformation.py | 300 +++++++++++++++++- 20 files changed, 737 insertions(+), 107 deletions(-) diff --git a/sdk/python/feast/errors.py b/sdk/python/feast/errors.py index 11ce9ebc62..84a9f12ec4 100644 --- a/sdk/python/feast/errors.py +++ b/sdk/python/feast/errors.py @@ -467,10 +467,15 @@ def __init__(self): class DataFrameSerializationError(FeastError): - def __init__(self, input_dict: dict): - super().__init__( - f"Failed to serialize the provided dictionary into a pandas DataFrame: {input_dict.keys()}" - ) + def __init__(self, input: Any): + if isinstance(input, dict): + super().__init__( + f"Failed to serialize the provided dictionary into a pandas DataFrame: {input.keys()}" + ) + else: + super().__init__( + "Failed to serialize the provided input into a pandas DataFrame" + ) class PermissionNotFoundException(FeastError): diff --git a/sdk/python/feast/feature_service.py b/sdk/python/feast/feature_service.py index 8b8cbac8ea..5c062f15c6 100644 --- a/sdk/python/feast/feature_service.py +++ b/sdk/python/feast/feature_service.py @@ -84,7 +84,9 @@ def __init__( if isinstance(feature_grouping, BaseFeatureView): self.feature_view_projections.append(feature_grouping.projection) - def infer_features(self, fvs_to_update: Dict[str, FeatureView]): + def infer_features( + self, fvs_to_update: Dict[str, Union[FeatureView, BaseFeatureView]] + ): """ Infers the features for the projections of this feature service, and updates this feature service in place. diff --git a/sdk/python/feast/feature_store.py b/sdk/python/feast/feature_store.py index 52556eda15..6112279a02 100644 --- a/sdk/python/feast/feature_store.py +++ b/sdk/python/feast/feature_store.py @@ -608,7 +608,12 @@ def _make_inferences( update_feature_views_with_inferred_features_and_entities( sfvs_to_update, entities + entities_to_update, self.config ) - # TODO(kevjumba): Update schema inferrence + # We need to attach the time stamp fields to the underlying data sources + # and cascade the dependencies + update_feature_views_with_inferred_features_and_entities( + odfvs_to_update, entities + entities_to_update, self.config + ) + # TODO(kevjumba): Update schema inference for sfv in sfvs_to_update: if not sfv.schema: raise ValueError( @@ -618,8 +623,13 @@ def _make_inferences( for odfv in odfvs_to_update: odfv.infer_features() + odfvs_to_write = [ + odfv for odfv in odfvs_to_update if odfv.write_to_online_store + ] + # Update to include ODFVs with write to online store fvs_to_update_map = { - view.name: view for view in [*views_to_update, *sfvs_to_update] + view.name: view + for view in [*views_to_update, *sfvs_to_update, *odfvs_to_write] } for feature_service in feature_services_to_update: feature_service.infer_features(fvs_to_update=fvs_to_update_map) @@ -847,6 +857,11 @@ def apply( ] sfvs_to_update = [ob for ob in objects if isinstance(ob, StreamFeatureView)] odfvs_to_update = [ob for ob in objects if isinstance(ob, OnDemandFeatureView)] + odfvs_with_writes_to_update = [ + ob + for ob in objects + if isinstance(ob, OnDemandFeatureView) and ob.write_to_online_store + ] services_to_update = [ob for ob in objects if isinstance(ob, FeatureService)] data_sources_set_to_update = { ob for ob in objects if isinstance(ob, DataSource) @@ -868,10 +883,23 @@ def apply( for batch_source in batch_sources_to_add: data_sources_set_to_update.add(batch_source) - for fv in itertools.chain(views_to_update, sfvs_to_update): - data_sources_set_to_update.add(fv.batch_source) - if fv.stream_source: - data_sources_set_to_update.add(fv.stream_source) + for fv in itertools.chain( + views_to_update, sfvs_to_update, odfvs_with_writes_to_update + ): + if isinstance(fv, FeatureView): + data_sources_set_to_update.add(fv.batch_source) + if hasattr(fv, "stream_source"): + if fv.stream_source: + data_sources_set_to_update.add(fv.stream_source) + if isinstance(fv, OnDemandFeatureView): + for source_fvp in fv.source_feature_view_projections: + odfv_batch_source: Optional[DataSource] = ( + fv.source_feature_view_projections[source_fvp].batch_source + ) + if odfv_batch_source is not None: + data_sources_set_to_update.add(odfv_batch_source) + else: + pass for odfv in odfvs_to_update: for v in odfv.source_request_sources.values(): @@ -884,7 +912,9 @@ def apply( # Validate all feature views and make inferences. self._validate_all_feature_views( - views_to_update, odfvs_to_update, sfvs_to_update + views_to_update, + odfvs_to_update, + sfvs_to_update, ) self._make_inferences( data_sources_to_update, @@ -989,7 +1019,9 @@ def apply( tables_to_delete: List[FeatureView] = ( views_to_delete + sfvs_to_delete if not partial else [] # type: ignore ) - tables_to_keep: List[FeatureView] = views_to_update + sfvs_to_update # type: ignore + tables_to_keep: List[ + Union[FeatureView, StreamFeatureView, OnDemandFeatureView] + ] = views_to_update + sfvs_to_update + odfvs_with_writes_to_update # type: ignore self._get_provider().update_infra( project=self.project, @@ -1444,19 +1476,18 @@ def write_to_online_store( inputs: Optional the dictionary object to be written allow_registry_cache (optional): Whether to allow retrieving feature views from a cached registry. """ - # TODO: restrict this to work with online StreamFeatureViews and validate the FeatureView type + feature_view_dict = { + fv_proto.name: fv_proto + for fv_proto in self.list_all_feature_views(allow_registry_cache) + } try: - feature_view: FeatureView = self.get_stream_feature_view( - feature_view_name, allow_registry_cache=allow_registry_cache - ) + feature_view = feature_view_dict[feature_view_name] except FeatureViewNotFoundException: - feature_view = self.get_feature_view( - feature_view_name, allow_registry_cache=allow_registry_cache - ) + raise FeatureViewNotFoundException(feature_view_name, self.project) if df is not None and inputs is not None: raise ValueError("Both df and inputs cannot be provided at the same time.") if df is None and inputs is not None: - if isinstance(inputs, dict): + if isinstance(inputs, dict) or isinstance(inputs, List): try: df = pd.DataFrame(inputs) except Exception as _: @@ -1465,6 +1496,13 @@ def write_to_online_store( pass else: raise ValueError("inputs must be a dictionary or a pandas DataFrame.") + if df is not None and inputs is None: + if isinstance(df, dict) or isinstance(df, List): + try: + df = pd.DataFrame(df) + except Exception as _: + raise DataFrameSerializationError(df) + provider = self._get_provider() provider.ingest_df(feature_view, df) diff --git a/sdk/python/feast/inference.py b/sdk/python/feast/inference.py index b9fb9b694d..39782e1e31 100644 --- a/sdk/python/feast/inference.py +++ b/sdk/python/feast/inference.py @@ -183,10 +183,13 @@ def update_feature_views_with_inferred_features_and_entities( ) if not fv.features: - raise RegistryInferenceFailure( - "FeatureView", - f"Could not infer Features for the FeatureView named {fv.name}.", - ) + if isinstance(fv, OnDemandFeatureView): + return None + else: + raise RegistryInferenceFailure( + "FeatureView", + f"Could not infer Features for the FeatureView named {fv.name}.", + ) def _infer_features_and_entities( @@ -209,6 +212,7 @@ def _infer_features_and_entities( fv, join_keys, run_inference_for_features, config ) + entity_columns: List[Field] = fv.entity_columns if fv.entity_columns else [] columns_to_exclude = { fv.batch_source.timestamp_field, fv.batch_source.created_timestamp_column, @@ -235,7 +239,7 @@ def _infer_features_and_entities( if field.name not in [ entity_column.name for entity_column in fv.entity_columns ]: - fv.entity_columns.append(field) + entity_columns.append(field) elif not re.match( "^__|__$", col_name ): # double underscores often signal an internal-use column @@ -256,6 +260,8 @@ def _infer_features_and_entities( if field.name not in [feature.name for feature in fv.features]: fv.features.append(field) + fv.entity_columns = entity_columns + def _infer_on_demand_features_and_entities( fv: OnDemandFeatureView, @@ -282,18 +288,19 @@ def _infer_on_demand_features_and_entities( batch_source = getattr(source_feature_view, "batch_source") batch_field_mapping = getattr(batch_source or None, "field_mapping") - if batch_field_mapping: - for ( - original_col, - mapped_col, - ) in batch_field_mapping.items(): - if mapped_col in columns_to_exclude: - columns_to_exclude.remove(mapped_col) - columns_to_exclude.add(original_col) + for ( + original_col, + mapped_col, + ) in batch_field_mapping.items(): + if mapped_col in columns_to_exclude: + columns_to_exclude.remove(mapped_col) + columns_to_exclude.add(original_col) + + table_column_names_and_types = batch_source.get_table_column_names_and_types( + config + ) + batch_field_mapping = getattr(batch_source, "field_mapping", {}) - table_column_names_and_types = ( - batch_source.get_table_column_names_and_types(config) - ) for col_name, col_datatype in table_column_names_and_types: if col_name in columns_to_exclude: continue diff --git a/sdk/python/feast/infra/materialization/aws_lambda/lambda_engine.py b/sdk/python/feast/infra/materialization/aws_lambda/lambda_engine.py index 93e33d5949..2864012055 100644 --- a/sdk/python/feast/infra/materialization/aws_lambda/lambda_engine.py +++ b/sdk/python/feast/infra/materialization/aws_lambda/lambda_engine.py @@ -24,6 +24,7 @@ from feast.infra.offline_stores.offline_store import OfflineStore from feast.infra.online_stores.online_store import OnlineStore from feast.infra.registry.base_registry import BaseRegistry +from feast.on_demand_feature_view import OnDemandFeatureView from feast.repo_config import FeastConfigBaseModel, RepoConfig from feast.stream_feature_view import StreamFeatureView from feast.utils import _get_column_names @@ -80,10 +81,10 @@ def update( self, project: str, views_to_delete: Sequence[ - Union[BatchFeatureView, StreamFeatureView, FeatureView] + Union[BatchFeatureView, StreamFeatureView, FeatureView, OnDemandFeatureView] ], views_to_keep: Sequence[ - Union[BatchFeatureView, StreamFeatureView, FeatureView] + Union[BatchFeatureView, StreamFeatureView, FeatureView, OnDemandFeatureView] ], entities_to_delete: Sequence[Entity], entities_to_keep: Sequence[Entity], diff --git a/sdk/python/feast/infra/materialization/batch_materialization_engine.py b/sdk/python/feast/infra/materialization/batch_materialization_engine.py index 8e854a508d..af92b95d17 100644 --- a/sdk/python/feast/infra/materialization/batch_materialization_engine.py +++ b/sdk/python/feast/infra/materialization/batch_materialization_engine.py @@ -12,6 +12,7 @@ from feast.infra.offline_stores.offline_store import OfflineStore from feast.infra.online_stores.online_store import OnlineStore from feast.infra.registry.base_registry import BaseRegistry +from feast.on_demand_feature_view import OnDemandFeatureView from feast.repo_config import RepoConfig from feast.stream_feature_view import StreamFeatureView @@ -89,7 +90,7 @@ def update( Union[BatchFeatureView, StreamFeatureView, FeatureView] ], views_to_keep: Sequence[ - Union[BatchFeatureView, StreamFeatureView, FeatureView] + Union[BatchFeatureView, StreamFeatureView, FeatureView, OnDemandFeatureView] ], entities_to_delete: Sequence[Entity], entities_to_keep: Sequence[Entity], diff --git a/sdk/python/feast/infra/materialization/contrib/spark/spark_materialization_engine.py b/sdk/python/feast/infra/materialization/contrib/spark/spark_materialization_engine.py index 24608baebf..3abb6fffd6 100644 --- a/sdk/python/feast/infra/materialization/contrib/spark/spark_materialization_engine.py +++ b/sdk/python/feast/infra/materialization/contrib/spark/spark_materialization_engine.py @@ -23,6 +23,7 @@ from feast.infra.online_stores.online_store import OnlineStore from feast.infra.passthrough_provider import PassthroughProvider from feast.infra.registry.base_registry import BaseRegistry +from feast.on_demand_feature_view import OnDemandFeatureView from feast.protos.feast.core.FeatureView_pb2 import FeatureView as FeatureViewProto from feast.repo_config import FeastConfigBaseModel, RepoConfig from feast.stream_feature_view import StreamFeatureView @@ -77,10 +78,10 @@ def update( self, project: str, views_to_delete: Sequence[ - Union[BatchFeatureView, StreamFeatureView, FeatureView] + Union[BatchFeatureView, StreamFeatureView, FeatureView, OnDemandFeatureView] ], views_to_keep: Sequence[ - Union[BatchFeatureView, StreamFeatureView, FeatureView] + Union[BatchFeatureView, StreamFeatureView, FeatureView, OnDemandFeatureView] ], entities_to_delete: Sequence[Entity], entities_to_keep: Sequence[Entity], diff --git a/sdk/python/feast/infra/materialization/kubernetes/k8s_materialization_engine.py b/sdk/python/feast/infra/materialization/kubernetes/k8s_materialization_engine.py index 510b6b4e4c..a0ccbcd768 100644 --- a/sdk/python/feast/infra/materialization/kubernetes/k8s_materialization_engine.py +++ b/sdk/python/feast/infra/materialization/kubernetes/k8s_materialization_engine.py @@ -24,6 +24,7 @@ from feast.infra.offline_stores.offline_store import OfflineStore from feast.infra.online_stores.online_store import OnlineStore from feast.infra.registry.base_registry import BaseRegistry +from feast.on_demand_feature_view import OnDemandFeatureView from feast.repo_config import FeastConfigBaseModel from feast.stream_feature_view import StreamFeatureView from feast.utils import _get_column_names @@ -122,10 +123,10 @@ def update( self, project: str, views_to_delete: Sequence[ - Union[BatchFeatureView, StreamFeatureView, FeatureView] + Union[BatchFeatureView, StreamFeatureView, FeatureView, OnDemandFeatureView] ], views_to_keep: Sequence[ - Union[BatchFeatureView, StreamFeatureView, FeatureView] + Union[BatchFeatureView, StreamFeatureView, FeatureView, OnDemandFeatureView] ], entities_to_delete: Sequence[Entity], entities_to_keep: Sequence[Entity], diff --git a/sdk/python/feast/infra/materialization/local_engine.py b/sdk/python/feast/infra/materialization/local_engine.py index d818571453..fa60950f29 100644 --- a/sdk/python/feast/infra/materialization/local_engine.py +++ b/sdk/python/feast/infra/materialization/local_engine.py @@ -10,6 +10,7 @@ from feast.infra.offline_stores.offline_store import OfflineStore from feast.infra.online_stores.online_store import OnlineStore from feast.infra.registry.base_registry import BaseRegistry +from feast.on_demand_feature_view import OnDemandFeatureView from feast.repo_config import FeastConfigBaseModel, RepoConfig from feast.stream_feature_view import StreamFeatureView from feast.utils import ( @@ -69,10 +70,10 @@ def update( self, project: str, views_to_delete: Sequence[ - Union[BatchFeatureView, StreamFeatureView, FeatureView] + Union[BatchFeatureView, StreamFeatureView, FeatureView, OnDemandFeatureView] ], views_to_keep: Sequence[ - Union[BatchFeatureView, StreamFeatureView, FeatureView] + Union[BatchFeatureView, StreamFeatureView, FeatureView, OnDemandFeatureView] ], entities_to_delete: Sequence[Entity], entities_to_keep: Sequence[Entity], diff --git a/sdk/python/feast/infra/materialization/snowflake_engine.py b/sdk/python/feast/infra/materialization/snowflake_engine.py index 3b789a3e5d..f4be9dd7da 100644 --- a/sdk/python/feast/infra/materialization/snowflake_engine.py +++ b/sdk/python/feast/infra/materialization/snowflake_engine.py @@ -31,6 +31,7 @@ get_snowflake_online_store_path, package_snowpark_zip, ) +from feast.on_demand_feature_view import OnDemandFeatureView from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto from feast.protos.feast.types.Value_pb2 import Value as ValueProto from feast.repo_config import FeastConfigBaseModel, RepoConfig @@ -120,10 +121,10 @@ def update( self, project: str, views_to_delete: Sequence[ - Union[BatchFeatureView, StreamFeatureView, FeatureView] + Union[BatchFeatureView, StreamFeatureView, FeatureView, OnDemandFeatureView] ], views_to_keep: Sequence[ - Union[BatchFeatureView, StreamFeatureView, FeatureView] + Union[BatchFeatureView, StreamFeatureView, FeatureView, OnDemandFeatureView] ], entities_to_delete: Sequence[Entity], entities_to_keep: Sequence[Entity], diff --git a/sdk/python/feast/infra/online_stores/online_store.py b/sdk/python/feast/infra/online_stores/online_store.py index fdb5b055cf..ea86bd9175 100644 --- a/sdk/python/feast/infra/online_stores/online_store.py +++ b/sdk/python/feast/infra/online_stores/online_store.py @@ -17,6 +17,7 @@ from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Tuple, Union from feast import Entity, utils +from feast.batch_feature_view import BatchFeatureView from feast.feature_service import FeatureService from feast.feature_view import FeatureView from feast.infra.infra_object import InfraObject @@ -27,6 +28,7 @@ from feast.protos.feast.types.Value_pb2 import RepeatedValue from feast.protos.feast.types.Value_pb2 import Value as ValueProto from feast.repo_config import RepoConfig +from feast.stream_feature_view import StreamFeatureView class OnlineStore(ABC): @@ -288,7 +290,9 @@ def update( self, config: RepoConfig, tables_to_delete: Sequence[FeatureView], - tables_to_keep: Sequence[FeatureView], + tables_to_keep: Sequence[ + Union[BatchFeatureView, StreamFeatureView, FeatureView] + ], entities_to_delete: Sequence[Entity], entities_to_keep: Sequence[Entity], partial: bool, diff --git a/sdk/python/feast/infra/passthrough_provider.py b/sdk/python/feast/infra/passthrough_provider.py index c3c3048a89..11ccbb2450 100644 --- a/sdk/python/feast/infra/passthrough_provider.py +++ b/sdk/python/feast/infra/passthrough_provider.py @@ -5,7 +5,8 @@ import pyarrow as pa from tqdm import tqdm -from feast import importer +from feast import OnDemandFeatureView, importer +from feast.base_feature_view import BaseFeatureView from feast.batch_feature_view import BatchFeatureView from feast.data_source import DataSource from feast.entity import Entity @@ -122,7 +123,7 @@ def update_infra( self, project: str, tables_to_delete: Sequence[FeatureView], - tables_to_keep: Sequence[FeatureView], + tables_to_keep: Sequence[Union[FeatureView, OnDemandFeatureView]], entities_to_delete: Sequence[Entity], entities_to_keep: Sequence[Entity], partial: bool, @@ -160,7 +161,7 @@ def teardown_infra( def online_write_batch( self, config: RepoConfig, - table: FeatureView, + table: Union[FeatureView, BaseFeatureView, OnDemandFeatureView], data: List[ Tuple[EntityKeyProto, Dict[str, ValueProto], datetime, Optional[datetime]] ], @@ -274,25 +275,52 @@ def retrieve_online_documents( def ingest_df( self, - feature_view: FeatureView, + feature_view: Union[BaseFeatureView, FeatureView, OnDemandFeatureView], df: pd.DataFrame, + field_mapping: Optional[Dict] = None, ): table = pa.Table.from_pandas(df) - - if feature_view.batch_source.field_mapping is not None: - table = _run_pyarrow_field_mapping( - table, feature_view.batch_source.field_mapping + if isinstance(feature_view, OnDemandFeatureView): + if not field_mapping: + field_mapping = {} + table = _run_pyarrow_field_mapping(table, field_mapping) + join_keys = { + entity.name: entity.dtype.to_value_type() + for entity in feature_view.entity_columns + } + rows_to_write = _convert_arrow_to_proto(table, feature_view, join_keys) + + self.online_write_batch( + self.repo_config, feature_view, rows_to_write, progress=None ) + else: + if hasattr(feature_view, "entity_columns"): + join_keys = { + entity.name: entity.dtype.to_value_type() + for entity in feature_view.entity_columns + } + else: + join_keys = {} + + # Note: A dictionary mapping of column names in this data + # source to feature names in a feature table or view. Only used for feature + # columns, not entity or timestamp columns. + if hasattr(feature_view, "batch_source"): + if feature_view.batch_source.field_mapping is not None: + table = _run_pyarrow_field_mapping( + table, feature_view.batch_source.field_mapping + ) + else: + table = _run_pyarrow_field_mapping(table, {}) - join_keys = { - entity.name: entity.dtype.to_value_type() - for entity in feature_view.entity_columns - } - rows_to_write = _convert_arrow_to_proto(table, feature_view, join_keys) + if not isinstance(feature_view, BaseFeatureView): + for entity in feature_view.entity_columns: + join_keys[entity.name] = entity.dtype.to_value_type() + rows_to_write = _convert_arrow_to_proto(table, feature_view, join_keys) - self.online_write_batch( - self.repo_config, feature_view, rows_to_write, progress=None - ) + self.online_write_batch( + self.repo_config, feature_view, rows_to_write, progress=None + ) def ingest_df_to_offline_store(self, feature_view: FeatureView, table: pa.Table): if feature_view.batch_source.field_mapping is not None: diff --git a/sdk/python/feast/infra/provider.py b/sdk/python/feast/infra/provider.py index c0062dde02..22c2088861 100644 --- a/sdk/python/feast/infra/provider.py +++ b/sdk/python/feast/infra/provider.py @@ -8,6 +8,7 @@ from tqdm import tqdm from feast import FeatureService, errors +from feast.base_feature_view import BaseFeatureView from feast.data_source import DataSource from feast.entity import Entity from feast.feature_view import FeatureView @@ -15,6 +16,7 @@ from feast.infra.infra_object import Infra from feast.infra.offline_stores.offline_store import RetrievalJob from feast.infra.registry.base_registry import BaseRegistry +from feast.on_demand_feature_view import OnDemandFeatureView from feast.online_response import OnlineResponse from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto @@ -47,7 +49,7 @@ def update_infra( self, project: str, tables_to_delete: Sequence[FeatureView], - tables_to_keep: Sequence[FeatureView], + tables_to_keep: Sequence[Union[FeatureView, OnDemandFeatureView]], entities_to_delete: Sequence[Entity], entities_to_keep: Sequence[Entity], partial: bool, @@ -125,8 +127,9 @@ def online_write_batch( def ingest_df( self, - feature_view: FeatureView, + feature_view: Union[BaseFeatureView, FeatureView, OnDemandFeatureView], df: pd.DataFrame, + field_mapping: Optional[Dict] = None, ): """ Persists a dataframe to the online store. @@ -134,6 +137,7 @@ def ingest_df( Args: feature_view: The feature view to which the dataframe corresponds. df: The dataframe to be persisted. + field_mapping: A dictionary mapping dataframe column names to feature names. """ pass diff --git a/sdk/python/feast/type_map.py b/sdk/python/feast/type_map.py index e47e4fcbe4..315b73909e 100644 --- a/sdk/python/feast/type_map.py +++ b/sdk/python/feast/type_map.py @@ -559,6 +559,7 @@ def pa_to_feast_value_type(pa_type_as_str: str) -> ValueType: "binary": ValueType.BYTES, "bool": ValueType.BOOL, "null": ValueType.NULL, + "list": ValueType.DOUBLE_LIST, } value_type = type_map[pa_type_as_str] diff --git a/sdk/python/feast/types.py b/sdk/python/feast/types.py index 4b07c58d19..9fb3207e6d 100644 --- a/sdk/python/feast/types.py +++ b/sdk/python/feast/types.py @@ -12,9 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. from abc import ABC, abstractmethod +from datetime import datetime, timezone from enum import Enum from typing import Dict, Union +import pyarrow + from feast.value_type import ValueType PRIMITIVE_FEAST_TYPES_TO_VALUE_TYPES = { @@ -30,6 +33,10 @@ } +def _utc_now() -> datetime: + return datetime.now(tz=timezone.utc) + + class ComplexFeastType(ABC): """ A ComplexFeastType represents a structured type that is recognized by Feast. @@ -103,7 +110,6 @@ def __hash__(self): Float64 = PrimitiveFeastType.FLOAT64 UnixTimestamp = PrimitiveFeastType.UNIX_TIMESTAMP - SUPPORTED_BASE_TYPES = [ Invalid, String, @@ -159,7 +165,6 @@ def __str__(self): FeastType = Union[ComplexFeastType, PrimitiveFeastType] - VALUE_TYPES_TO_FEAST_TYPES: Dict["ValueType", FeastType] = { ValueType.UNKNOWN: Invalid, ValueType.BYTES: Bytes, @@ -180,6 +185,40 @@ def __str__(self): ValueType.UNIX_TIMESTAMP_LIST: Array(UnixTimestamp), } +FEAST_TYPES_TO_PYARROW_TYPES = { + String: pyarrow.string(), + Bool: pyarrow.bool_(), + Int32: pyarrow.int32(), + Int64: pyarrow.int64(), + Float32: pyarrow.float32(), + Float64: pyarrow.float64(), + # Note: datetime only supports microseconds https://github.com/python/cpython/blob/3.8/Lib/datetime.py#L1559 + UnixTimestamp: pyarrow.timestamp("us", tz=_utc_now().tzname()), +} + + +def from_feast_to_pyarrow_type(feast_type: FeastType) -> pyarrow.DataType: + """ + Converts a Feast type to a PyArrow type. + + Args: + feast_type: The Feast type to be converted. + + Raises: + ValueError: The conversion could not be performed. + """ + assert isinstance( + feast_type, (ComplexFeastType, PrimitiveFeastType) + ), f"Expected FeastType, got {type(feast_type)}" + if isinstance(feast_type, PrimitiveFeastType): + if feast_type in FEAST_TYPES_TO_PYARROW_TYPES: + return FEAST_TYPES_TO_PYARROW_TYPES[feast_type] + elif isinstance(feast_type, ComplexFeastType): + # Handle the case when feast_type is an instance of ComplexFeastType + pass + + raise ValueError(f"Could not convert Feast type {feast_type} to PyArrow type.") + def from_value_type( value_type: ValueType, diff --git a/sdk/python/feast/utils.py b/sdk/python/feast/utils.py index 8a9f1fadae..ec2da79782 100644 --- a/sdk/python/feast/utils.py +++ b/sdk/python/feast/utils.py @@ -28,7 +28,6 @@ from feast.constants import FEAST_FS_YAML_FILE_PATH_ENV_NAME from feast.entity import Entity from feast.errors import ( - EntityNotFoundException, FeatureNameCollisionError, FeatureViewNotFoundException, RequestDataNotFoundInEntityRowsException, @@ -43,10 +42,12 @@ from feast.protos.feast.types.Value_pb2 import RepeatedValue as RepeatedValueProto from feast.protos.feast.types.Value_pb2 import Value as ValueProto from feast.type_map import python_values_to_proto_values +from feast.types import from_feast_to_pyarrow_type from feast.value_type import ValueType from feast.version import get_version if typing.TYPE_CHECKING: + from feast.base_feature_view import BaseFeatureView from feast.feature_service import FeatureService from feast.feature_view import FeatureView from feast.infra.registry.base_registry import BaseRegistry @@ -227,6 +228,18 @@ def _coerce_datetime(ts): def _convert_arrow_to_proto( + table: Union[pyarrow.Table, pyarrow.RecordBatch], + feature_view: Union["FeatureView", "BaseFeatureView", "OnDemandFeatureView"], + join_keys: Dict[str, ValueType], +) -> List[Tuple[EntityKeyProto, Dict[str, ValueProto], datetime, Optional[datetime]]]: + # This is a workaround for isinstance(feature_view, OnDemandFeatureView), which triggers a circular import + if getattr(feature_view, "source_request_sources", None): + return _convert_arrow_odfv_to_proto(table, feature_view, join_keys) # type: ignore[arg-type] + else: + return _convert_arrow_fv_to_proto(table, feature_view, join_keys) # type: ignore[arg-type] + + +def _convert_arrow_fv_to_proto( table: Union[pyarrow.Table, pyarrow.RecordBatch], feature_view: "FeatureView", join_keys: Dict[str, ValueType], @@ -287,6 +300,76 @@ def _convert_arrow_to_proto( return list(zip(entity_keys, features, event_timestamps, created_timestamps)) +def _convert_arrow_odfv_to_proto( + table: Union[pyarrow.Table, pyarrow.RecordBatch], + feature_view: "OnDemandFeatureView", + join_keys: Dict[str, ValueType], +) -> List[Tuple[EntityKeyProto, Dict[str, ValueProto], datetime, Optional[datetime]]]: + # Avoid ChunkedArrays which guarantees `zero_copy_only` available. + if isinstance(table, pyarrow.Table): + table = table.to_batches()[0] + + columns = [ + (field.name, field.dtype.to_value_type()) for field in feature_view.features + ] + list(join_keys.items()) + + proto_values_by_column = { + column: python_values_to_proto_values( + table.column(column).to_numpy(zero_copy_only=False), value_type + ) + for column, value_type in columns + if column in table.column_names + } + # Adding On Demand Features + for feature in feature_view.features: + if ( + feature.name in [c[0] for c in columns] + and feature.name not in proto_values_by_column + ): + # initializing the column as null + null_column = pyarrow.array( + [None] * table.num_rows, + type=from_feast_to_pyarrow_type(feature.dtype), + ) + updated_table = pyarrow.RecordBatch.from_arrays( + table.columns + [null_column], + schema=table.schema.append( + pyarrow.field(feature.name, null_column.type) + ), + ) + proto_values_by_column[feature.name] = python_values_to_proto_values( + updated_table.column(feature.name).to_numpy(zero_copy_only=False), + feature.dtype.to_value_type(), + ) + + entity_keys = [ + EntityKeyProto( + join_keys=join_keys, + entity_values=[proto_values_by_column[k][idx] for k in join_keys], + ) + for idx in range(table.num_rows) + ] + + # Serialize the features per row + feature_dict = { + feature.name: proto_values_by_column[feature.name] + for feature in feature_view.features + } + features = [dict(zip(feature_dict, vars)) for vars in zip(*feature_dict.values())] + + # We need to artificially add event_timestamps and created_timestamps + event_timestamps = [] + timestamp_values = pd.to_datetime([_utc_now() for i in range(table.num_rows)]) + + for val in timestamp_values: + event_timestamps.append(_coerce_datetime(val)) + + # setting them equivalent + created_timestamps = event_timestamps + + return list(zip(entity_keys, features, event_timestamps, created_timestamps)) + + def _validate_entity_values(join_key_values: Dict[str, List[ValueProto]]): set_of_row_lengths = {len(v) for v in join_key_values.values()} if len(set_of_row_lengths) > 1: @@ -931,6 +1014,17 @@ def _prepare_entities_to_read_from_online_store( num_rows = _validate_entity_values(entity_proto_values) + odfv_entities: List[Entity] = [] + request_source_keys: List[str] = [] + for on_demand_feature_view in requested_on_demand_feature_views: + odfv_entities.append(*getattr(on_demand_feature_view, "entities", [])) + for source in on_demand_feature_view.source_request_sources: + source_schema = on_demand_feature_view.source_request_sources[source].schema + for column in source_schema: + request_source_keys.append(column.name) + + join_keys_set.update(set(odfv_entities)) + join_key_values: Dict[str, List[ValueProto]] = {} request_data_features: Dict[str, List[ValueProto]] = {} # Entity rows may be either entities or request data. @@ -938,22 +1032,20 @@ def _prepare_entities_to_read_from_online_store( # Found request data if join_key_or_entity_name in needed_request_data: request_data_features[join_key_or_entity_name] = values - else: - if join_key_or_entity_name in join_keys_set: - join_key = join_key_or_entity_name - else: - try: - join_key = entity_name_to_join_key_map[join_key_or_entity_name] - except KeyError: - raise EntityNotFoundException(join_key_or_entity_name, project) - else: - warnings.warn( - "Using entity name is deprecated. Use join_key instead." - ) - - # All join keys should be returned in the result. + elif join_key_or_entity_name in join_keys_set: + # It's a join key + join_key = join_key_or_entity_name requested_result_row_names.add(join_key) join_key_values[join_key] = values + elif join_key_or_entity_name in entity_name_to_join_key_map: + # It's an entity name (deprecated) + join_key = entity_name_to_join_key_map[join_key_or_entity_name] + warnings.warn("Using entity name is deprecated. Use join_key instead.") + requested_result_row_names.add(join_key) + join_key_values[join_key] = values + else: + # Key is not recognized (likely a feature value), so we skip it. + continue # Or handle accordingly ensure_request_data_values_exist(needed_request_data, request_data_features) diff --git a/sdk/python/tests/unit/local_feast_tests/test_local_feature_store.py b/sdk/python/tests/unit/local_feast_tests/test_local_feature_store.py index cc48295b20..1b9b48d8d0 100644 --- a/sdk/python/tests/unit/local_feast_tests/test_local_feature_store.py +++ b/sdk/python/tests/unit/local_feast_tests/test_local_feature_store.py @@ -11,7 +11,7 @@ from feast.entity import Entity from feast.feast_object import ALL_RESOURCE_TYPES from feast.feature_store import FeatureStore -from feast.feature_view import DUMMY_ENTITY_ID, FeatureView +from feast.feature_view import DUMMY_ENTITY_ID, DUMMY_ENTITY_NAME, FeatureView from feast.field import Field from feast.infra.offline_stores.file_source import FileSource from feast.infra.online_stores.sqlite import SqliteOnlineStoreConfig @@ -20,7 +20,7 @@ from feast.permissions.policy import RoleBasedPolicy from feast.repo_config import RepoConfig from feast.stream_feature_view import stream_feature_view -from feast.types import Array, Bytes, Float32, Int64, String +from feast.types import Array, Bytes, Float32, Int64, String, ValueType, from_value_type from tests.integration.feature_repos.universal.feature_views import TAGS from tests.utils.cli_repo_creator import CliRunner, get_example_repo from tests.utils.data_source_test_creator import prep_file_source @@ -347,7 +347,7 @@ def test_apply_entities_and_feature_views(test_feature_store): "test_feature_store", [lazy_fixture("feature_store_with_local_registry")], ) -def test_apply_dummuy_entity_and_feature_view_columns(test_feature_store): +def test_apply_dummy_entity_and_feature_view_columns(test_feature_store): assert isinstance(test_feature_store, FeatureStore) # Create Feature Views batch_source = FileSource( @@ -357,33 +357,51 @@ def test_apply_dummuy_entity_and_feature_view_columns(test_feature_store): created_timestamp_column="timestamp", ) - e1 = Entity(name="fs1_my_entity_1", description="something") + e1 = Entity( + name="fs1_my_entity_1", description="something", value_type=ValueType.INT64 + ) - fv = FeatureView( + fv_no_entity = FeatureView( name="my_feature_view_no_entity", schema=[ Field(name="fs1_my_feature_1", dtype=Int64), - Field(name="fs1_my_feature_2", dtype=String), - Field(name="fs1_my_feature_3", dtype=Array(String)), - Field(name="fs1_my_feature_4", dtype=Array(Bytes)), - Field(name="fs1_my_entity_2", dtype=Int64), + Field(name="fs1_my_entity_1", dtype=Int64), ], entities=[], tags={"team": "matchmaking"}, source=batch_source, ttl=timedelta(minutes=5), ) + fv_with_entity = FeatureView( + name="my_feature_view_with_entity", + schema=[ + Field(name="fs1_my_feature_1", dtype=Int64), + Field(name="fs1_my_entity_1", dtype=Int64), + ], + entities=[e1], + tags={"team": "matchmaking"}, + source=batch_source, + ttl=timedelta(minutes=5), + ) # Check that the entity_columns are empty before applying - assert fv.entity_columns == [] - + assert fv_no_entity.entities == [DUMMY_ENTITY_NAME] + assert fv_no_entity.entity_columns == [] + # Note that this test is a special case rooted in the entity being included in the schema + assert fv_with_entity.entity_columns == [ + Field(name=e1.join_key, dtype=from_value_type(e1.value_type)) + ] # Register Feature View - test_feature_store.apply([fv, e1]) - fv_actual = test_feature_store.get_feature_view("my_feature_view_no_entity") - + test_feature_store.apply([e1, fv_no_entity, fv_with_entity]) + fv_from_online_store = test_feature_store.get_feature_view( + "my_feature_view_no_entity" + ) # Note that after the apply() the feature_view serializes the Dummy Entity ID - assert fv.entity_columns[0].name == DUMMY_ENTITY_ID - assert fv_actual.entity_columns[0].name == DUMMY_ENTITY_ID + assert fv_no_entity.entity_columns[0].name == DUMMY_ENTITY_ID + assert fv_from_online_store.entity_columns[0].name == DUMMY_ENTITY_ID + assert fv_from_online_store.entities == [] + assert fv_no_entity.entities == [DUMMY_ENTITY_NAME] + assert fv_with_entity.entity_columns[0].name == e1.join_key test_feature_store.teardown() diff --git a/sdk/python/tests/unit/online_store/test_online_writes.py b/sdk/python/tests/unit/online_store/test_online_writes.py index 7927b564c6..803a93b48d 100644 --- a/sdk/python/tests/unit/online_store/test_online_writes.py +++ b/sdk/python/tests/unit/online_store/test_online_writes.py @@ -77,6 +77,7 @@ def setUp(self): ) # Before apply() join_keys is empty assert driver_stats_fv.join_keys == [] + assert driver_stats_fv.entity_columns == [] @on_demand_feature_view( sources=[driver_stats_fv[["conv_rate", "acc_rate"]]], @@ -104,6 +105,8 @@ def test_view(inputs: dict[str, Any]) -> dict[str, Any]: ) # after apply() join_keys is [driver] assert driver_stats_fv.join_keys == [driver.join_key] + assert driver_stats_fv.entity_columns[0].name == driver.join_key + self.store.write_to_online_store( feature_view_name="driver_hourly_stats", df=driver_df ) diff --git a/sdk/python/tests/unit/test_on_demand_feature_view.py b/sdk/python/tests/unit/test_on_demand_feature_view.py index 6073891aba..4b30bd6be9 100644 --- a/sdk/python/tests/unit/test_on_demand_feature_view.py +++ b/sdk/python/tests/unit/test_on_demand_feature_view.py @@ -11,7 +11,7 @@ # 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 datetime from typing import Any, Dict, List import pandas as pd @@ -50,6 +50,15 @@ def python_native_udf(features_dict: Dict[str, Any]) -> Dict[str, Any]: return output_dict +def python_writes_test_udf(features_dict: Dict[str, Any]) -> Dict[str, Any]: + output_dict: Dict[str, List[Any]] = { + "output1": features_dict["feature1"] + 100, + "output2": features_dict["feature2"] + 101, + "output3": datetime.datetime.now(), + } + return output_dict + + @pytest.mark.filterwarnings("ignore:udf and udf_string parameters are deprecated") def test_hash(): file_source = FileSource(name="my-file-source", path="test.parquet") @@ -261,3 +270,89 @@ def test_from_proto_backwards_compatible_udf(): reserialized_proto.feature_transformation.udf_string == on_demand_feature_view.feature_transformation.udf_string ) + + +def test_on_demand_feature_view_writes_protos(): + file_source = FileSource(name="my-file-source", path="test.parquet") + feature_view = FeatureView( + name="my-feature-view", + entities=[], + schema=[ + Field(name="feature1", dtype=Float32), + Field(name="feature2", dtype=Float32), + ], + source=file_source, + ) + sources = [feature_view] + on_demand_feature_view = OnDemandFeatureView( + name="my-on-demand-feature-view", + sources=sources, + schema=[ + Field(name="output1", dtype=Float32), + Field(name="output2", dtype=Float32), + ], + feature_transformation=PandasTransformation( + udf=udf1, udf_string="udf1 source code" + ), + write_to_online_store=True, + ) + + proto = on_demand_feature_view.to_proto() + reserialized_proto = OnDemandFeatureView.from_proto(proto) + + assert on_demand_feature_view.write_to_online_store + assert proto.spec.write_to_online_store + assert reserialized_proto.write_to_online_store + + proto.spec.write_to_online_store = False + reserialized_proto = OnDemandFeatureView.from_proto(proto) + assert not reserialized_proto.write_to_online_store + + +def test_on_demand_feature_view_stored_writes(): + file_source = FileSource(name="my-file-source", path="test.parquet") + feature_view = FeatureView( + name="my-feature-view", + entities=[], + schema=[ + Field(name="feature1", dtype=Float32), + Field(name="feature2", dtype=Float32), + ], + source=file_source, + ) + sources = [feature_view] + + on_demand_feature_view = OnDemandFeatureView( + name="my-on-demand-feature-view", + sources=sources, + schema=[ + Field(name="output1", dtype=Float32), + Field(name="output2", dtype=Float32), + ], + feature_transformation=PythonTransformation( + udf=python_writes_test_udf, udf_string="python native udf source code" + ), + description="testing on demand feature view stored writes", + mode="python", + write_to_online_store=True, + ) + + transformed_output = on_demand_feature_view.transform_dict( + { + "feature1": 0, + "feature2": 1, + } + ) + expected_output = {"feature1": 0, "feature2": 1, "output1": 100, "output2": 102} + keys_to_validate = [ + "feature1", + "feature2", + "output1", + "output2", + ] + for k in keys_to_validate: + assert transformed_output[k] == expected_output[k] + + assert transformed_output["output3"] is not None and isinstance( + transformed_output["output3"], datetime.datetime + ) diff --git a/sdk/python/tests/unit/test_on_demand_python_transformation.py b/sdk/python/tests/unit/test_on_demand_python_transformation.py index b994ea8042..2410ee03aa 100644 --- a/sdk/python/tests/unit/test_on_demand_python_transformation.py +++ b/sdk/python/tests/unit/test_on_demand_python_transformation.py @@ -28,7 +28,9 @@ Float64, Int64, String, + UnixTimestamp, ValueType, + _utc_now, from_value_type, ) @@ -71,6 +73,13 @@ def setUp(self): timestamp_field="event_timestamp", created_timestamp_column="created", ) + input_request_source = RequestSource( + name="counter_source", + schema=[ + Field(name="counter", dtype=Int64), + Field(name="input_datetime", dtype=UnixTimestamp), + ], + ) driver_stats_fv = FeatureView( name="driver_hourly_stats", @@ -165,6 +174,36 @@ def python_singleton_view(inputs: dict[str, Any]) -> dict[str, Any]: ) return output + @on_demand_feature_view( + sources=[ + driver_stats_fv[["conv_rate", "acc_rate"]], + input_request_source, + ], + schema=[ + Field(name="conv_rate_plus_acc", dtype=Float64), + Field(name="current_datetime", dtype=UnixTimestamp), + Field(name="counter", dtype=Int64), + Field(name="input_datetime", dtype=UnixTimestamp), + ], + mode="python", + write_to_online_store=True, + ) + def python_stored_writes_feature_view( + inputs: dict[str, Any], + ) -> dict[str, Any]: + output: dict[str, Any] = { + "conv_rate_plus_acc": [ + conv_rate + acc_rate + for conv_rate, acc_rate in zip( + inputs["conv_rate"], inputs["acc_rate"] + ) + ], + "current_datetime": [datetime.now() for _ in inputs["conv_rate"]], + "counter": [c + 1 for c in inputs["counter"]], + "input_datetime": [d for d in inputs["input_datetime"]], + } + return output + with pytest.raises(TypeError): # Note the singleton view will fail as the type is # expected to be a list which can be confirmed in _infer_features_dict @@ -189,6 +228,7 @@ def python_singleton_view(inputs: dict[str, Any]) -> dict[str, Any]: python_view, python_demo_view, driver_stats_entity_less_fv, + python_stored_writes_feature_view, ] ) self.store.write_to_online_store( @@ -199,15 +239,17 @@ def python_singleton_view(inputs: dict[str, Any]) -> dict[str, Any]: ] assert driver_stats_entity_less_fv.entity_columns == [DUMMY_ENTITY_FIELD] - assert len(self.store.list_all_feature_views()) == 5 + assert len(self.store.list_all_feature_views()) == 6 assert len(self.store.list_feature_views()) == 2 - assert len(self.store.list_on_demand_feature_views()) == 3 + assert len(self.store.list_on_demand_feature_views()) == 4 assert len(self.store.list_stream_feature_views()) == 0 def test_python_pandas_parity(self): entity_rows = [ { "driver_id": 1001, + "counter": 0, + "input_datetime": _utc_now(), } ] @@ -289,6 +331,40 @@ def test_python_docs_demo(self): == online_python_response["conv_rate_plus_val2_python"][0] ) + def test_stored_writes(self): + # Note that here we shouldn't have to pass the request source features for reading + # because they should have already been written to the online store + current_datetime = _utc_now() + entity_rows_to_read = [ + { + "driver_id": 1001, + "counter": 0, + "input_datetime": current_datetime, + } + ] + + online_python_response = self.store.get_online_features( + entity_rows=entity_rows_to_read, + features=[ + "python_stored_writes_feature_view:conv_rate_plus_acc", + "python_stored_writes_feature_view:current_datetime", + "python_stored_writes_feature_view:counter", + "python_stored_writes_feature_view:input_datetime", + ], + ).to_dict() + + assert sorted(list(online_python_response.keys())) == sorted( + [ + "driver_id", + "conv_rate_plus_acc", + "counter", + "current_datetime", + "input_datetime", + ] + ) + print(online_python_response) + # Now this is where we need to test the stored writes, this should return the same output as the previous + class TestOnDemandPythonTransformationAllDataTypes(unittest.TestCase): def setUp(self): @@ -403,15 +479,15 @@ def python_view(inputs: dict[str, Any]) -> dict[str, Any]: self.store.apply( [driver, driver_stats_source, driver_stats_fv, python_view] ) - self.store.write_to_online_store( - feature_view_name="driver_hourly_stats", df=driver_df - ) - fv_applied = self.store.get_feature_view("driver_hourly_stats") assert fv_applied.entities == [driver.name] # Note here that after apply() is called, the entity_columns are populated with the join_key assert fv_applied.entity_columns[0].name == driver.join_key + self.store.write_to_online_store( + feature_view_name="driver_hourly_stats", df=driver_df + ) + def test_python_transformation_returning_all_data_types(self): entity_rows = [ { @@ -526,3 +602,215 @@ def python_view(inputs: dict[str, Any]) -> dict[str, Any]: ), ): store.apply([request_source, python_view]) + + +class TestOnDemandTransformationsWithWrites(unittest.TestCase): + def test_stored_writes(self): + with tempfile.TemporaryDirectory() as data_dir: + self.store = FeatureStore( + config=RepoConfig( + project="test_on_demand_python_transformation", + registry=os.path.join(data_dir, "registry.db"), + provider="local", + entity_key_serialization_version=2, + online_store=SqliteOnlineStoreConfig( + path=os.path.join(data_dir, "online.db") + ), + ) + ) + + # Generate test data. + end_date = datetime.now().replace(microsecond=0, second=0, minute=0) + start_date = end_date - timedelta(days=15) + + driver_entities = [1001, 1002, 1003, 1004, 1005] + driver_df = create_driver_hourly_stats_df( + driver_entities, start_date, end_date + ) + driver_stats_path = os.path.join(data_dir, "driver_stats.parquet") + driver_df.to_parquet( + path=driver_stats_path, allow_truncated_timestamps=True + ) + + driver = Entity(name="driver", join_keys=["driver_id"]) + + driver_stats_source = FileSource( + name="driver_hourly_stats_source", + path=driver_stats_path, + timestamp_field="event_timestamp", + created_timestamp_column="created", + ) + input_request_source = RequestSource( + name="counter_source", + schema=[ + Field(name="counter", dtype=Int64), + Field(name="input_datetime", dtype=UnixTimestamp), + ], + ) + + driver_stats_fv = FeatureView( + name="driver_hourly_stats", + entities=[driver], + ttl=timedelta(days=0), + schema=[ + Field(name="conv_rate", dtype=Float32), + Field(name="acc_rate", dtype=Float32), + Field(name="avg_daily_trips", dtype=Int64), + ], + online=True, + source=driver_stats_source, + ) + assert driver_stats_fv.entities == [driver.name] + assert driver_stats_fv.entity_columns == [] + + @on_demand_feature_view( + entities=[driver], + sources=[ + driver_stats_fv[["conv_rate", "acc_rate"]], + input_request_source, + ], + schema=[ + Field(name="conv_rate_plus_acc", dtype=Float64), + Field(name="current_datetime", dtype=UnixTimestamp), + Field(name="counter", dtype=Int64), + Field(name="input_datetime", dtype=UnixTimestamp), + ], + mode="python", + write_to_online_store=True, + ) + def python_stored_writes_feature_view( + inputs: dict[str, Any], + ) -> dict[str, Any]: + output: dict[str, Any] = { + "conv_rate_plus_acc": [ + conv_rate + acc_rate + for conv_rate, acc_rate in zip( + inputs["conv_rate"], inputs["acc_rate"] + ) + ], + "current_datetime": [datetime.now() for _ in inputs["conv_rate"]], + "counter": [c + 1 for c in inputs["counter"]], + "input_datetime": [d for d in inputs["input_datetime"]], + } + return output + + assert python_stored_writes_feature_view.entities == [driver.name] + assert python_stored_writes_feature_view.entity_columns == [] + + self.store.apply( + [ + driver, + driver_stats_source, + driver_stats_fv, + python_stored_writes_feature_view, + ] + ) + fv_applied = self.store.get_feature_view("driver_hourly_stats") + odfv_applied = self.store.get_on_demand_feature_view( + "python_stored_writes_feature_view" + ) + + assert fv_applied.entities == [driver.name] + assert odfv_applied.entities == [driver.name] + + # Note here that after apply() is called, the entity_columns are populated with the join_key + # assert fv_applied.entity_columns[0].name == driver.join_key + assert fv_applied.entity_columns[0].name == driver.join_key + assert odfv_applied.entity_columns[0].name == driver.join_key + + assert len(self.store.list_all_feature_views()) == 2 + assert len(self.store.list_feature_views()) == 1 + assert len(self.store.list_on_demand_feature_views()) == 1 + assert len(self.store.list_stream_feature_views()) == 0 + assert ( + driver_stats_fv.entity_columns + == self.store.get_feature_view("driver_hourly_stats").entity_columns + ) + assert ( + python_stored_writes_feature_view.entity_columns + == self.store.get_on_demand_feature_view( + "python_stored_writes_feature_view" + ).entity_columns + ) + + current_datetime = _utc_now() + fv_entity_rows_to_write = [ + { + "driver_id": 1001, + "conv_rate": 0.25, + "acc_rate": 0.25, + "avg_daily_trips": 2, + "event_timestamp": current_datetime, + "created": current_datetime, + } + ] + odfv_entity_rows_to_write = [ + { + "driver_id": 1001, + "counter": 0, + "input_datetime": current_datetime, + } + ] + fv_entity_rows_to_read = [ + { + "driver_id": 1001, + } + ] + # Note that here we shouldn't have to pass the request source features for reading + # because they should have already been written to the online store + odfv_entity_rows_to_read = [ + { + "driver_id": 1001, + "conv_rate": 0.25, + "acc_rate": 0.50, + "counter": 0, + "input_datetime": current_datetime, + } + ] + print("storing fv features") + self.store.write_to_online_store( + feature_view_name="driver_hourly_stats", + df=fv_entity_rows_to_write, + ) + print("reading fv features") + online_python_response = self.store.get_online_features( + entity_rows=fv_entity_rows_to_read, + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "driver_hourly_stats:avg_daily_trips", + ], + ).to_dict() + + assert online_python_response == { + "driver_id": [1001], + "conv_rate": [0.25], + "avg_daily_trips": [2], + "acc_rate": [0.25], + } + + print("storing odfv features") + self.store.write_to_online_store( + feature_view_name="python_stored_writes_feature_view", + df=odfv_entity_rows_to_write, + ) + print("reading odfv features") + online_odfv_python_response = self.store.get_online_features( + entity_rows=odfv_entity_rows_to_read, + features=[ + "python_stored_writes_feature_view:conv_rate_plus_acc", + "python_stored_writes_feature_view:current_datetime", + "python_stored_writes_feature_view:counter", + "python_stored_writes_feature_view:input_datetime", + ], + ).to_dict() + print(online_odfv_python_response) + assert sorted(list(online_odfv_python_response.keys())) == sorted( + [ + "driver_id", + "conv_rate_plus_acc", + "counter", + "current_datetime", + "input_datetime", + ] + ) From 00eaf744f50f56f449c7ad8f6ceb92a09304ee0b Mon Sep 17 00:00:00 2001 From: Abhinav Singh Date: Thu, 10 Oct 2024 11:36:44 +0530 Subject: [PATCH 131/185] feat: Add boto3 session based auth for dynamodb online store for cross account access (#4606) * added session auth for dynamodb online store Signed-off-by: asingh9530 * lint Signed-off-by: asingh9530 * Update sdk/python/feast/infra/online_stores/dynamodb.py Co-authored-by: Francisco Arceo Signed-off-by: asingh9530 * Update sdk/python/feast/infra/online_stores/dynamodb.py Co-authored-by: Francisco Arceo Signed-off-by: asingh9530 * dummy commit Signed-off-by: asingh9530 * dummy commit Signed-off-by: asingh9530 --------- Signed-off-by: asingh9530 Co-authored-by: Francisco Arceo --- .../feast/infra/online_stores/dynamodb.py | 82 +++++++++++++++---- 1 file changed, 64 insertions(+), 18 deletions(-) diff --git a/sdk/python/feast/infra/online_stores/dynamodb.py b/sdk/python/feast/infra/online_stores/dynamodb.py index b2488543b0..c0494272b3 100644 --- a/sdk/python/feast/infra/online_stores/dynamodb.py +++ b/sdk/python/feast/infra/online_stores/dynamodb.py @@ -70,6 +70,9 @@ class DynamoDBOnlineStoreConfig(FeastConfigBaseModel): tags: Union[Dict[str, str], None] = None """AWS resource tags added to each table""" + session_based_auth: bool = False + """AWS session based client authentication""" + class DynamoDBOnlineStore(OnlineStore): """ @@ -104,10 +107,14 @@ def update( online_config = config.online_store assert isinstance(online_config, DynamoDBOnlineStoreConfig) dynamodb_client = self._get_dynamodb_client( - online_config.region, online_config.endpoint_url + online_config.region, + online_config.endpoint_url, + online_config.session_based_auth, ) dynamodb_resource = self._get_dynamodb_resource( - online_config.region, online_config.endpoint_url + online_config.region, + online_config.endpoint_url, + online_config.session_based_auth, ) # Add Tags attribute to creation request only if configured to prevent # TagResource permission issues, even with an empty Tags array. @@ -166,7 +173,9 @@ def teardown( online_config = config.online_store assert isinstance(online_config, DynamoDBOnlineStoreConfig) dynamodb_resource = self._get_dynamodb_resource( - online_config.region, online_config.endpoint_url + online_config.region, + online_config.endpoint_url, + online_config.session_based_auth, ) for table in tables: @@ -201,7 +210,9 @@ def online_write_batch( online_config = config.online_store assert isinstance(online_config, DynamoDBOnlineStoreConfig) dynamodb_resource = self._get_dynamodb_resource( - online_config.region, online_config.endpoint_url + online_config.region, + online_config.endpoint_url, + online_config.session_based_auth, ) table_instance = dynamodb_resource.Table( @@ -228,7 +239,9 @@ def online_read( assert isinstance(online_config, DynamoDBOnlineStoreConfig) dynamodb_resource = self._get_dynamodb_resource( - online_config.region, online_config.endpoint_url + online_config.region, + online_config.endpoint_url, + online_config.session_based_auth, ) table_instance = dynamodb_resource.Table( _get_table_name(online_config, config, table) @@ -323,15 +336,27 @@ def _get_aioboto_session(self): def _get_aiodynamodb_client(self, region: str): return self._get_aioboto_session().create_client("dynamodb", region_name=region) - def _get_dynamodb_client(self, region: str, endpoint_url: Optional[str] = None): + def _get_dynamodb_client( + self, + region: str, + endpoint_url: Optional[str] = None, + session_based_auth: Optional[bool] = False, + ): if self._dynamodb_client is None: - self._dynamodb_client = _initialize_dynamodb_client(region, endpoint_url) + self._dynamodb_client = _initialize_dynamodb_client( + region, endpoint_url, session_based_auth + ) return self._dynamodb_client - def _get_dynamodb_resource(self, region: str, endpoint_url: Optional[str] = None): + def _get_dynamodb_resource( + self, + region: str, + endpoint_url: Optional[str] = None, + session_based_auth: Optional[bool] = False, + ): if self._dynamodb_resource is None: self._dynamodb_resource = _initialize_dynamodb_resource( - region, endpoint_url + region, endpoint_url, session_based_auth ) return self._dynamodb_resource @@ -443,17 +468,38 @@ def _to_client_batch_get_payload(online_config, table_name, batch): } -def _initialize_dynamodb_client(region: str, endpoint_url: Optional[str] = None): - return boto3.client( - "dynamodb", - region_name=region, - endpoint_url=endpoint_url, - config=Config(user_agent=get_user_agent()), - ) +def _initialize_dynamodb_client( + region: str, + endpoint_url: Optional[str] = None, + session_based_auth: Optional[bool] = False, +): + if session_based_auth: + return boto3.Session().client( + "dynamodb", + region_name=region, + endpoint_url=endpoint_url, + config=Config(user_agent=get_user_agent()), + ) + else: + return boto3.client( + "dynamodb", + region_name=region, + endpoint_url=endpoint_url, + config=Config(user_agent=get_user_agent()), + ) -def _initialize_dynamodb_resource(region: str, endpoint_url: Optional[str] = None): - return boto3.resource("dynamodb", region_name=region, endpoint_url=endpoint_url) +def _initialize_dynamodb_resource( + region: str, + endpoint_url: Optional[str] = None, + session_based_auth: Optional[bool] = False, +): + if session_based_auth: + return boto3.Session().resource( + "dynamodb", region_name=region, endpoint_url=endpoint_url + ) + else: + return boto3.resource("dynamodb", region_name=region, endpoint_url=endpoint_url) # TODO(achals): This form of user-facing templating is experimental. From 8e0c1b51665357d44229239401a04b76396b9047 Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Thu, 10 Oct 2024 08:58:57 -0400 Subject: [PATCH 132/185] feat: Adding documentation for On Demand Feature Transformations with writes (#4607) * Update beta-on-demand-feature-view.md * updated docs Signed-off-by: Francisco Javier Arceo * feat: Adding documentation for On Demand Feature Transformations with writes Signed-off-by: Francisco Javier Arceo --------- Signed-off-by: Francisco Javier Arceo --- docs/reference/beta-on-demand-feature-view.md | 76 +++++++++++++++++-- 1 file changed, 69 insertions(+), 7 deletions(-) diff --git a/docs/reference/beta-on-demand-feature-view.md b/docs/reference/beta-on-demand-feature-view.md index 6ccc648616..55fe534446 100644 --- a/docs/reference/beta-on-demand-feature-view.md +++ b/docs/reference/beta-on-demand-feature-view.md @@ -4,9 +4,16 @@ ## Overview -On demand feature views allows data scientists to use existing features and request time data (features only available -at request time) to transform and create new features at the time the data is read from the online store. Users define -python transformation logic which is executed in both historical retrieval and online retrieval paths. +On Demand Feature Views (ODFVs) allow data scientists to use existing features and request-time data (features only +available at request time) to transform and create new features. Users define Python transformation logic which is +executed during both historical retrieval and online retrieval. Additionally, ODFVs provide flexibility in +applying transformations either during data ingestion (at write time) or during feature retrieval (at read time), +controlled via the `write_to_online_store` parameter. + +By setting `write_to_online_store=True`, transformations are applied during data ingestion, and the transformed +features are stored in the online store. This can improve online feature retrieval performance by reducing computation +during reads. Conversely, if `write_to_online_store=False` (the default if omitted), transformations are applied during +feature retrieval. ### Why use on demand feature views? @@ -14,10 +21,11 @@ This enables data scientists to easily impact the online feature retrieval path. 1. Call `get_historical_features` to generate a training dataframe 2. Iterate in notebook on feature engineering in Pandas/Python -3. Copy transformation logic into on demand feature views and commit to a dev branch of the feature repository +3. Copy transformation logic into ODFVs and commit to a development branch of the feature repository 4. Verify with `get_historical_features` (on a small dataset) that the transformation gives expected output over historical data -5. Verify with `get_online_features` on dev branch that the transformation correctly outputs online features -6. Submit a pull request to the staging / prod branches which impact production traffic +5. Decide whether to apply the transformation on writes or on reads by setting the `write_to_online_store` parameter accordingly. +6. Verify with `get_online_features` on dev branch that the transformation correctly outputs online features +7. Submit a pull request to the staging / prod branches which impact production traffic ## CLI @@ -32,10 +40,18 @@ See [https://github.com/feast-dev/on-demand-feature-views-demo](https://github.c ### **Registering transformations** -On Demand Transformations support transformations using Pandas and native Python. Note, Native Python is much faster but not yet tested for offline retrieval. +On Demand Transformations support transformations using Pandas and native Python. Note, Native Python is much faster +but not yet tested for offline retrieval. + +When defining an ODFV, you can control when the transformation is applied using the write_to_online_store parameter: + +- `write_to_online_store=True`: The transformation is applied during data ingestion (on write), and the transformed features are stored in the online store. +- `write_to_online_store=False` (default when omitted): The transformation is applied during feature retrieval (on read). We register `RequestSource` inputs and the transform in `on_demand_feature_view`: +## Example of an On Demand Transformation on Read + ```python from feast import Field, RequestSource from feast.types import Float64, Int64 @@ -100,6 +116,52 @@ def transformed_conv_rate_python(inputs: Dict[str, Any]) -> Dict[str, Any]: return output ``` +## Example of an On Demand Transformation on Write + +```python +from feast import Field, on_demand_feature_view +from feast.types import Float64 +import pandas as pd + +# Existing Feature View +driver_hourly_stats_view = ... + +# Define an ODFV without RequestSource +@on_demand_feature_view( + sources=[driver_hourly_stats_view], + schema=[ + Field(name='conv_rate_adjusted', dtype=Float64), + ], + mode="pandas", + write_to_online_store=True, # Apply transformation during write time +) +def transformed_conv_rate(features_df: pd.DataFrame) -> pd.DataFrame: + df = pd.DataFrame() + df['conv_rate_adjusted'] = features_df['conv_rate'] * 1.1 # Adjust conv_rate by 10% + return df +``` +Then to ingest the data with the new feature view make sure to include all of the input features required for the +transformations: + +```python +from feast import FeatureStore +import pandas as pd + +store = FeatureStore(repo_path=".") + +# Data to ingest +data = pd.DataFrame({ + "driver_id": [1001], + "event_timestamp": [pd.Timestamp.now()], + "conv_rate": [0.5], + "acc_rate": [0.8], + "avg_daily_trips": [10], +}) + +# Ingest data to the online store +store.push("driver_hourly_stats_view", data) +``` + ### **Feature retrieval** {% hint style="info" %} From cd87562df7f55dfe888bfbbd9a33b87670681040 Mon Sep 17 00:00:00 2001 From: lokeshrangineni <19699092+lokeshrangineni@users.noreply.github.com> Date: Thu, 10 Oct 2024 11:36:38 -0400 Subject: [PATCH 133/185] docs: Adding example of RHOAI feast quick start guide (#4564) * Adding example of RHOAI feast quick start guide Signed-off-by: Lokesh Rangineni <19699092+lokeshrangineni@users.noreply.github.com> * Incorporating code review comments. Adding the missed link in the index page of root examples directory. Signed-off-by: Lokesh Rangineni <19699092+lokeshrangineni@users.noreply.github.com> * Incorporating code review comments from the @Daniel Dowler Added more details about RHOAI platform and reference to the documentation. Incorporated other minor comments. Signed-off-by: Lokesh Rangineni <19699092+lokeshrangineni@users.noreply.github.com> * Incorporating code review comment from Francisco. --------- Signed-off-by: Lokesh Rangineni <19699092+lokeshrangineni@users.noreply.github.com> --- examples/README.md | 2 + examples/rhoai-quickstart/README.md | 45 + .../feast-demo-quickstart.ipynb | 1539 +++++++++++++++++ 3 files changed, 1586 insertions(+) create mode 100644 examples/rhoai-quickstart/README.md create mode 100644 examples/rhoai-quickstart/feast-demo-quickstart.ipynb diff --git a/examples/README.md b/examples/README.md index 91799864aa..e796b5000e 100644 --- a/examples/README.md +++ b/examples/README.md @@ -14,3 +14,5 @@ 7. **[Podman/Podman Compose_local](https://github.com/feast-dev/feast/tree/master/examples/podman_local)**: Demonstrates how to deploy Feast remote server components using Podman Compose locally. +8. **[RHOAI Feast Demo](https://github.com/feast-dev/feast/tree/master/examples/rhoai-quickstart)**: Showcases Feast's core functionality using a Jupyter notebook, including fetching online feature data from a remote server and retrieving metadata from a remote registry. + diff --git a/examples/rhoai-quickstart/README.md b/examples/rhoai-quickstart/README.md new file mode 100644 index 0000000000..a8ac587968 --- /dev/null +++ b/examples/rhoai-quickstart/README.md @@ -0,0 +1,45 @@ +# Quickstart: Running Feast example + +This quickstart guide will walk you through setting up and using [Feast](https://feast.dev) as a feature store on Red Hat OpenShift AI. By the end of this tutorial, you’ll have the environment configured, sample data loaded, and features retrieved using Feast objects. + +## Prerequisites +This example uses Jupyter Notebook to showcase Feast's capabilities. You'll need an environment where Jupyter Notebook can be executed. + +You have two options for setting up the runtime environment: +1. **Running Jupyter on your local machine**: If you're new to Jupyter, refer to the official documentation [here](https://docs.jupyter.org/en/latest/running.html). +2. **Running Jupyter on Red Hat OpenShift AI (RHOAI)**: If you'd prefer to run Jupyter on the RHOAI platform, follow the instructions below. + +## Using the Red Hat OpenShift AI (RHOAI) Platform +You can execute the Jupyter notebook directly on the RHOAI platform. If you don't have an existing RHOAI cluster, you can try this Feast example in the developer sandbox. + +To get started, visit the [Red Hat OpenShift AI sandbox](https://www.redhat.com/en/technologies/cloud-computing/openshift/openshift-ai) and launch your environment. + +### Getting Started with RHOAI +Before proceeding, it's helpful to familiarize yourself with Red Hat OpenShift AI. If you're new to the platform, check out this [short introductory video](https://youtu.be/75WtOSpn5qU?si=uT1xZfpuJBkVP7ha) for a quick overview of its features. + +For detailed documentation on RHOAI, including how to work on data science projects, refer to the official product documentation [here](https://docs.redhat.com/en/documentation/red_hat_openshift_ai_cloud_service/1/html/working_on_data_science_projects/using-data-science-projects_projects). + +### Steps to Set Up a Workbench on the RHOAI Sandbox +Follow these brief steps to create a workbench on the RHOAI sandbox: + +1. Navigate to your project (namespace) → **Dev**. +2. Go to the **Workbenches** tab. +3. Select **Create Workbench** and provide the necessary details. +4. Click **Create**. + +Once your workbench is set up, you can import and run the Feast example using one of these methods: +1. **Clone the GitHub repository**: Clone the repo into your RHOAI workbench and run the Jupyter notebook. +2. **Upload files**: Upload the necessary files to your existing workbench and execute the notebook. + +## Notebook Overview +The [feast-demo-quickstart.ipynb](feast-demo-quickstart.ipynb) notebook will guide you through: + +This notebook will use Driver entity (or model) to demonstrate the feast functionalities. +You should be able to execute the same jupyter notebook in a standalone environment as well. + +- **Setting up the Feast repository**: Load sample driver data, generate training datasets, run offline inference, and ingest batch features into the online store. You'll also learn how to fetch features for inference using Feast’s `FeatureView` and `FeatureService`. + +- **Configuring a Remote Online Topology**: Set up a remote online server and client, and retrieve features in real-time using the remote online client. + +- **Configuring a Remote Registry Topology**: Set up a remote registry server and client, and retrieve Feast metadata using the remote registry client. + diff --git a/examples/rhoai-quickstart/feast-demo-quickstart.ipynb b/examples/rhoai-quickstart/feast-demo-quickstart.ipynb new file mode 100644 index 0000000000..18874e462e --- /dev/null +++ b/examples/rhoai-quickstart/feast-demo-quickstart.ipynb @@ -0,0 +1,1539 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "8aea7cf6-81de-48af-8470-f5b9bf229113", + "metadata": { + "tags": [] + }, + "source": [ + "## Installing Feast\n", + "Feast is a python dependency so we have to install it using `pip`" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "ceb60bdb-0c90-4991-91df-dc7334ff3a93", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\u001B[1m[\u001B[0m\u001B[34;49mnotice\u001B[0m\u001B[1;39;49m]\u001B[0m\u001B[39;49m A new release of pip available: \u001B[0m\u001B[31;49m22.2.2\u001B[0m\u001B[39;49m -> \u001B[0m\u001B[32;49m24.2\u001B[0m\n", + "\u001B[1m[\u001B[0m\u001B[34;49mnotice\u001B[0m\u001B[1;39;49m]\u001B[0m\u001B[39;49m To update, run: \u001B[0m\u001B[32;49mpip install --upgrade pip\u001B[0m\n", + "Note: you may need to restart the kernel to use updated packages.\n", + "feast package is installed\n" + ] + } + ], + "source": [ + "# WE MUST ENSURE PYTHON CONSISTENCY BETWEEN NOTEBOOK AND FEAST SERVERS\n", + "# LAUNCH THIS NOTEBOOK FROM A CLEAN PYTHON ENVIRONMENT >3.9\n", + "%pip install -q feast==0.40.1\n", + "# grpcio is needed as a dependency in the later section of the example to run the feast registry server.\n", + "%pip install -q grpcio\n" + ] + }, + { + "cell_type": "markdown", + "id": "9b70c61f-1e94-4354-8dcb-3a57be140d13", + "metadata": {}, + "source": [ + "## Creating and initializing Feast project" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "0fcecd34-ff0b-4528-88a5-3125187241f6", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'/opt/app-root/src/feast/examples/rhoai-quickstart'" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Displaying the current directory. We will know where the feast files will be created so that we can review them using jupyter console or explorer\n", + "%pwd" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "1ad595e5-d98a-4f9c-bead-f2d0ad274855", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Creating a new Feast repository in \u001B[1m\u001B[32m/opt/app-root/src/feast/examples/rhoai-quickstart/my_feast_project\u001B[0m.\n", + "\n" + ] + } + ], + "source": [ + "# Creating the feast repository. If there is already existing repository then removing it first.\n", + "!rm -rf my_feast_project\n", + "!feast init my_feast_project" + ] + }, + { + "cell_type": "markdown", + "id": "3beb2f85-3e28-4afa-8227-30271dd00c7f", + "metadata": {}, + "source": [ + "Above output displays where the feast repo has been created. It may differ based on the environment configuration." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "308610d4-9174-4972-a785-237c0b0477d8", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "/opt/app-root/src/feast/examples/rhoai-quickstart/my_feast_project/feature_repo\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/opt/app-root/lib64/python3.9/site-packages/IPython/core/magics/osm.py:417: UserWarning: using dhist requires you to install the `pickleshare` library.\n", + " self.shell.db['dhist'] = compress_dhist(dhist)[-100:]\n" + ] + } + ], + "source": [ + "# Going to change the current directory to feature_repo so that we can execute feast CLI commands.\n", + "%cd my_feast_project/feature_repo" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "7f5212d8-cd46-4017-a762-91a4fb56e0d5", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + ".\n", + " +-- data\n", + " +-- |-- driver_stats.parquet\n", + " +-- __init__.py\n", + " +-- __pycache__\n", + " +-- |-- __init__.cpython-39.pyc\n", + " +-- |-- test_workflow.cpython-39.pyc\n", + " +-- |-- example_repo.cpython-39.pyc\n", + " +-- feature_store.yaml\n", + " +-- example_repo.py\n", + " +-- test_workflow.py\n" + ] + } + ], + "source": [ + "# Inspect the feast repo path files. Displaying folder strucuture as tree. Going to describe each file/folder purpose.\n", + "!find . | sed -e 's/[^-][^\\/]*\\// |-- /g' -e 's/|-- \\(.*\\)/+-- \\1/'" + ] + }, + { + "cell_type": "markdown", + "id": "2fa8d4c3-8f7d-4758-8048-945ebd0af53f", + "metadata": { + "tags": [] + }, + "source": [ + "Now the feast repo has been created for you. Running the `feast init` command populated the directory with an example feature store structure, complete with example data.\n", + "\n", + "We are defining an entity for the driver in the current example. You can think of an entity as a primary key used to fetch features. Rest of the example will work on the driver data. All the data is coming from the `data/driver_stats.parquet` file which will act as offline store in our example.\n", + "\n", + "Inspect the below files before going further in the current example.\n", + "\n", + "`data` contains the parquet file data used to demonstrate this example.\n", + "\n", + "`example_repo.py` file will have the code to create feast objects such as FeatureView, FeatureServices and OnDemandFeatureViews required to demonstrate this example.\n", + "[my_feast_project/feature_repo/example_repo.py](./my_feast_project/feature_repo/example_repo.py)\n", + "\n", + "`feature_store.yaml` file will have all the configurations related to feast.\n", + "[my_feast_project/feature_repo/feature_store.yaml](./my_feast_project/feature_repo/feature_store.yaml)\n", + "\n", + "`test_workflow.py` contains the python code to demonstrate run all key Feast commands, including defining, retrieving, and pushing features.\n", + "[my_feast_project/feature_repo/test_workflow.py](./my_feast_project/feature_repo/test_workflow.py)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "f0a63ea0-93fa-4d4e-b12d-6caef86478f9", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "project: my_feast_project\n", + "# By default, the registry is a file (but can be turned into a more scalable SQL-backed registry)\n", + "registry: data/registry.db\n", + "# The provider primarily specifies default offline / online stores & storing the registry in a given cloud\n", + "provider: local\n", + "online_store:\n", + " type: sqlite\n", + " path: data/online_store.db\n", + "entity_key_serialization_version: 2\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "!cat feature_store.yaml" + ] + }, + { + "cell_type": "raw", + "id": "7094b08a-24bb-4608-9182-b8d894f85e51", + "metadata": {}, + "source": [ + "The default feature store configuration uses the sqllite as registry and online store, dask as offline store. \n", + "You can find more information: https://docs.feast.dev/reference/providers/local." + ] + }, + { + "cell_type": "markdown", + "id": "cd23ce19-03fa-4353-86d3-261c7f514fd8", + "metadata": {}, + "source": [ + "File `data/driver_stats.parquet` is generated by the `feast init` command and it acts a historical information source to this example. We have defined this source in the [my_feast_project/feature_repo/example_repo.py](./my_feast_project/feature_repo/example_repo.py) file.\n", + "\n", + "```python\n", + "driver_stats_source = FileSource(\n", + " name=\"driver_hourly_stats_source\",\n", + " path=\"/opt/app-root/src/feast/examples/rhoai-quickstart/my_feast_project/feature_repo/data/driver_stats.parquet\",\n", + " timestamp_field=\"event_timestamp\",\n", + " created_timestamp_column=\"created\",\n", + ")\n", + "```\n" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "6dc4f196-d9ec-421f-9cce-16678b783ea7", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
event_timestampdriver_idconv_rateacc_rateavg_daily_tripscreated
02024-09-09 17:00:00+00:0010050.3737580.4754013542024-09-24 17:07:41.972
12024-09-09 18:00:00+00:0010050.0579710.3755695172024-09-24 17:07:41.972
22024-09-09 19:00:00+00:0010050.3838320.3232744842024-09-24 17:07:41.972
32024-09-09 20:00:00+00:0010050.4033900.5706646342024-09-24 17:07:41.972
42024-09-09 21:00:00+00:0010050.5367410.6451071282024-09-24 17:07:41.972
.....................
18022024-09-24 15:00:00+00:0010010.5340480.6216125112024-09-24 17:07:41.972
18032024-09-24 16:00:00+00:0010010.7762480.1203843112024-09-24 17:07:41.972
18042021-04-12 07:00:00+00:0010010.0588210.1097815812024-09-24 17:07:41.972
18052024-09-17 05:00:00+00:0010030.2978630.940503132024-09-24 17:07:41.972
18062024-09-17 05:00:00+00:0010030.2978630.940503132024-09-24 17:07:41.972
\n", + "

1807 rows × 6 columns

\n", + "
" + ], + "text/plain": [ + " event_timestamp driver_id conv_rate acc_rate \\\n", + "0 2024-09-09 17:00:00+00:00 1005 0.373758 0.475401 \n", + "1 2024-09-09 18:00:00+00:00 1005 0.057971 0.375569 \n", + "2 2024-09-09 19:00:00+00:00 1005 0.383832 0.323274 \n", + "3 2024-09-09 20:00:00+00:00 1005 0.403390 0.570664 \n", + "4 2024-09-09 21:00:00+00:00 1005 0.536741 0.645107 \n", + "... ... ... ... ... \n", + "1802 2024-09-24 15:00:00+00:00 1001 0.534048 0.621612 \n", + "1803 2024-09-24 16:00:00+00:00 1001 0.776248 0.120384 \n", + "1804 2021-04-12 07:00:00+00:00 1001 0.058821 0.109781 \n", + "1805 2024-09-17 05:00:00+00:00 1003 0.297863 0.940503 \n", + "1806 2024-09-17 05:00:00+00:00 1003 0.297863 0.940503 \n", + "\n", + " avg_daily_trips created \n", + "0 354 2024-09-24 17:07:41.972 \n", + "1 517 2024-09-24 17:07:41.972 \n", + "2 484 2024-09-24 17:07:41.972 \n", + "3 634 2024-09-24 17:07:41.972 \n", + "4 128 2024-09-24 17:07:41.972 \n", + "... ... ... \n", + "1802 511 2024-09-24 17:07:41.972 \n", + "1803 311 2024-09-24 17:07:41.972 \n", + "1804 581 2024-09-24 17:07:41.972 \n", + "1805 13 2024-09-24 17:07:41.972 \n", + "1806 13 2024-09-24 17:07:41.972 \n", + "\n", + "[1807 rows x 6 columns]" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import pandas as pd\n", + "pd.read_parquet(\"data/driver_stats.parquet\")" + ] + }, + { + "cell_type": "markdown", + "id": "92557e68-f044-444e-a68a-31385b95092e", + "metadata": {}, + "source": [ + "You have not created any feast objects to do that you have to execute command `feast apply` on the directory where `feature_store.yaml` exists. Lets go and do that now." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "8e0a05ec-f449-4371-9d55-f30176b51b38", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Below folder is creating interference with the feast apply command so deleting it in case if it exists.\n", + "!rm -rf .ipynb_checkpoints/" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "5c6baa83-9834-46ac-82a4-cc0a9ae33080", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "/opt/app-root/lib64/python3.9/site-packages/feast/feature_store.py:590: RuntimeWarning: On demand feature view is an experimental feature. This API is stable, but the functionality does not scale well for offline retrieval\n", + " warnings.warn(\n", + "09/24/2024 06:01:41 PM root WARNING: Cannot use sqlite_vec for vector search\n", + "09/24/2024 06:01:41 PM root WARNING: Cannot use sqlite_vec for vector search\n", + "09/24/2024 06:01:41 PM root WARNING: Cannot use sqlite_vec for vector search\n", + "09/24/2024 06:01:41 PM root WARNING: Cannot use sqlite_vec for vector search\n", + "Created entity \u001B[1m\u001B[32mdriver\u001B[0m\n", + "Created feature view \u001B[1m\u001B[32mdriver_hourly_stats\u001B[0m\n", + "Created feature view \u001B[1m\u001B[32mdriver_hourly_stats_fresh\u001B[0m\n", + "Created on demand feature view \u001B[1m\u001B[32mtransformed_conv_rate\u001B[0m\n", + "Created on demand feature view \u001B[1m\u001B[32mtransformed_conv_rate_fresh\u001B[0m\n", + "Created feature service \u001B[1m\u001B[32mdriver_activity_v2\u001B[0m\n", + "Created feature service \u001B[1m\u001B[32mdriver_activity_v1\u001B[0m\n", + "Created feature service \u001B[1m\u001B[32mdriver_activity_v3\u001B[0m\n", + "\n", + "09/24/2024 06:01:41 PM root WARNING: Cannot use sqlite_vec for vector search\n", + "09/24/2024 06:01:41 PM root WARNING: Cannot use sqlite_vec for vector search\n", + "Created sqlite table \u001B[1m\u001B[32mmy_feast_project_driver_hourly_stats_fresh\u001B[0m\n", + "Created sqlite table \u001B[1m\u001B[32mmy_feast_project_driver_hourly_stats\u001B[0m\n", + "\n" + ] + } + ], + "source": [ + "# this command will actual creates the feast objects mentioned in `example_repo.py`\n", + "!feast apply" + ] + }, + { + "cell_type": "markdown", + "id": "8ab04029-b33c-49d4-b107-59ed4c662fa5", + "metadata": {}, + "source": [ + "## Generating the training Data\n", + "\n", + "To train a model, we need features and labels. Often, this label data is stored separately (e.g. you have one table storing user survey results and another set of tables with feature values). Feast can help generate the features that map to these labels.\n", + "\n", + "\n", + "Feast needs a list of entities (e.g. driver ids) and timestamps. Feast will join relevant tables to create the relevant feature vectors. There are two ways to generate this list:\n", + "\n", + "* The user can query that table of labels with timestamps and pass that into Feast as an entity dataframe for training data generation.\n", + "\n", + "* The user can also query that table with a SQL query which pulls entities. See the [documentation](https://docs.feast.dev/getting-started/concepts/feature-retrieval) on feature retrieval for details\n", + "\n", + "Note: we include timestamps because we want the features for the same driver at various timestamps to be used in a model." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "7dd75fe4-4eed-4a2a-8714-e8079c4d5772", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "WARNING:root:_list_feature_views will make breaking changes. Please use _list_batch_feature_views instead. _list_feature_views will behave like _list_all_feature_views in the future.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "----- Feature schema -----\n", + "\n", + "\n", + "RangeIndex: 3 entries, 0 to 2\n", + "Data columns (total 10 columns):\n", + " # Column Non-Null Count Dtype \n", + "--- ------ -------------- ----- \n", + " 0 driver_id 3 non-null int64 \n", + " 1 event_timestamp 3 non-null datetime64[ns, UTC]\n", + " 2 label_driver_reported_satisfaction 3 non-null int64 \n", + " 3 val_to_add 3 non-null int64 \n", + " 4 val_to_add_2 3 non-null int64 \n", + " 5 conv_rate 3 non-null float32 \n", + " 6 acc_rate 3 non-null float32 \n", + " 7 avg_daily_trips 3 non-null int32 \n", + " 8 conv_rate_plus_val1 3 non-null float64 \n", + " 9 conv_rate_plus_val2 3 non-null float64 \n", + "dtypes: datetime64[ns, UTC](1), float32(2), float64(2), int32(1), int64(4)\n", + "memory usage: 332.0 bytes\n", + "None\n", + "\n", + "----- Example features -----\n", + "\n", + " driver_id event_timestamp label_driver_reported_satisfaction \\\n", + "0 1001 2021-04-12 10:59:42+00:00 1 \n", + "1 1002 2021-04-12 08:12:10+00:00 5 \n", + "2 1003 2021-04-12 16:40:26+00:00 3 \n", + "\n", + " val_to_add val_to_add_2 conv_rate acc_rate avg_daily_trips \\\n", + "0 1 10 0.058821 0.109781 581 \n", + "1 2 20 0.806576 0.493560 809 \n", + "2 3 30 0.122915 0.132378 636 \n", + "\n", + " conv_rate_plus_val1 conv_rate_plus_val2 \n", + "0 1.058821 10.058821 \n", + "1 2.806576 20.806576 \n", + "2 3.122915 30.122915 \n" + ] + } + ], + "source": [ + "from datetime import datetime\n", + "import pandas as pd\n", + "\n", + "from feast import FeatureStore\n", + "\n", + "# Note: see https://docs.feast.dev/getting-started/concepts/feature-retrieval for \n", + "# more details on how to retrieve for all entities in the offline store instead\n", + "entity_df = pd.DataFrame.from_dict(\n", + " {\n", + " # entity's join key -> entity values\n", + " \"driver_id\": [1001, 1002, 1003],\n", + " # \"event_timestamp\" (reserved key) -> timestamps\n", + " \"event_timestamp\": [\n", + " datetime(2021, 4, 12, 10, 59, 42),\n", + " datetime(2021, 4, 12, 8, 12, 10),\n", + " datetime(2021, 4, 12, 16, 40, 26),\n", + " ],\n", + " # (optional) label name -> label values. Feast does not process these\n", + " \"label_driver_reported_satisfaction\": [1, 5, 3],\n", + " # values we're using for an on-demand transformation\n", + " \"val_to_add\": [1, 2, 3],\n", + " \"val_to_add_2\": [10, 20, 30],\n", + " }\n", + ")\n", + "\n", + "store = FeatureStore(repo_path=\".\")\n", + "\n", + "training_df = store.get_historical_features(\n", + " entity_df=entity_df,\n", + " features=[\n", + " \"driver_hourly_stats:conv_rate\",\n", + " \"driver_hourly_stats:acc_rate\",\n", + " \"driver_hourly_stats:avg_daily_trips\",\n", + " \"transformed_conv_rate:conv_rate_plus_val1\",\n", + " \"transformed_conv_rate:conv_rate_plus_val2\",\n", + " ],\n", + ").to_df()\n", + "\n", + "print(\"----- Feature schema -----\\n\")\n", + "print(training_df.info())\n", + "\n", + "print()\n", + "print(\"----- Example features -----\\n\")\n", + "print(training_df.head())" + ] + }, + { + "cell_type": "markdown", + "id": "70322893-947f-46cf-a4a8-6a49d8c799b5", + "metadata": { + "tags": [] + }, + "source": [ + "## Run offline inference (batch scoring)\n", + "To power a batch model, we primarily need to pull features with the get_historical_features call, but using the current timestamp" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "c1fccc14-9d94-4d1a-a91a-d8b82f8d8044", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "WARNING:root:_list_feature_views will make breaking changes. Please use _list_batch_feature_views instead. _list_feature_views will behave like _list_all_feature_views in the future.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "----- Example features -----\n", + "\n", + " driver_id event_timestamp \\\n", + "0 1002 2024-09-24 18:01:58.027897+00:00 \n", + "1 1001 2024-09-24 18:01:58.027897+00:00 \n", + "2 1003 2024-09-24 18:01:58.027897+00:00 \n", + "\n", + " label_driver_reported_satisfaction val_to_add val_to_add_2 conv_rate \\\n", + "0 5 2 20 0.311688 \n", + "1 1 1 10 0.776248 \n", + "2 3 3 30 0.235401 \n", + "\n", + " acc_rate avg_daily_trips conv_rate_plus_val1 conv_rate_plus_val2 \n", + "0 0.991556 579 2.311688 20.311688 \n", + "1 0.120384 311 1.776248 10.776248 \n", + "2 0.644993 381 3.235401 30.235401 \n" + ] + } + ], + "source": [ + "entity_df[\"event_timestamp\"] = pd.to_datetime(\"now\", utc=True)\n", + "training_df = store.get_historical_features(\n", + " entity_df=entity_df,\n", + " features=[\n", + " \"driver_hourly_stats:conv_rate\",\n", + " \"driver_hourly_stats:acc_rate\",\n", + " \"driver_hourly_stats:avg_daily_trips\",\n", + " \"transformed_conv_rate:conv_rate_plus_val1\",\n", + " \"transformed_conv_rate:conv_rate_plus_val2\",\n", + " ],\n", + ").to_df()\n", + "\n", + "print(\"\\n----- Example features -----\\n\")\n", + "print(training_df.head())" + ] + }, + { + "cell_type": "markdown", + "id": "56bd764f-4455-4ade-b630-81fdbb2eb444", + "metadata": {}, + "source": [ + "## Ingest batch features into your online store" + ] + }, + { + "cell_type": "markdown", + "id": "05e5595e-589e-4639-b631-073183f8240d", + "metadata": {}, + "source": [ + "This command will generate the features from offline store and stores into online store. This command will call `get_historical_features` to get the data from offline store." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "b95b0b1b-d608-429d-91ac-ef96c698d9ca", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "09/24/2024 06:02:09 PM root WARNING: _list_feature_views will make breaking changes. Please use _list_batch_feature_views instead. _list_feature_views will behave like _list_all_feature_views in the future.\n", + "Materializing \u001B[1m\u001B[32m2\u001B[0m feature views to \u001B[1m\u001B[32m2024-09-24 18:02:06+00:00\u001B[0m into the \u001B[1m\u001B[32msqlite\u001B[0m online store.\n", + "\n", + "\u001B[1m\u001B[32mdriver_hourly_stats\u001B[0m from \u001B[1m\u001B[32m2024-09-23 18:02:09+00:00\u001B[0m to \u001B[1m\u001B[32m2024-09-24 18:02:06+00:00\u001B[0m:\n", + " 0%| | 0/5 [00:00), datetime.datetime(2024, 9, 24, 18, 2, 6, tzinfo=))])>,\n", + " ), datetime.datetime(2024, 9, 24, 18, 2, 6, tzinfo=))])>,\n", + " }, feature_transformation = )>,\n", + " }, feature_transformation = )>]" + ] + }, + "execution_count": 50, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Listing all feature views using remote registry client\n", + "registry_feature_store_client.list_all_feature_views(allow_cache=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "id": "31889388-5efc-423c-b64e-e8e5c0b67a68", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[,\n", + " )>,\n", + " ,\n", + " ]" + ] + }, + "execution_count": 51, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Listing all feature services using remote registry client\n", + "registry_feature_store_client.list_feature_services()" + ] + }, + { + "cell_type": "markdown", + "id": "a074f5bc-1d1b-4ad1-9b08-b3aea58761e8", + "metadata": { + "tags": [] + }, + "source": [ + "## Stopping the online, registry server" + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "id": "d8f353e0-3874-49f7-af82-bd79396eb6d8", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "/opt/app-root/src/feast/examples/rhoai-quickstart/my_feast_project/feature_repo\n", + "1001130+ 17522 16173 0 18:03 ? 00:00:04 /opt/app-root/bin/python3.9 /opt/app-root/bin/feast serve\n", + "1001130+ 17570 17522 0 18:03 ? 00:00:00 /opt/app-root/bin/python3.9 /opt/app-root/bin/feast serve\n", + "1001130+ 17902 16173 0 18:07 ? 00:00:04 /opt/app-root/bin/python3.9 /opt/app-root/bin/feast serve_registry\n", + "1001130+ 18481 18438 0 18:15 ? 00:00:00 grep feast serve\n" + ] + } + ], + "source": [ + "%%sh\n", + "# checking if the registry server and online server process is already running.\n", + "pwd\n", + "ps -ef | grep 'feast serve'" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "id": "8c84142d-b835-440e-9177-9b8bfe00768f", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "remote online and registry server has been stopped.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[2024-09-24 18:16:00 +0000] [17522] [INFO] Handling signal: term\n", + "[2024-09-24 18:16:00 +0000] [17570] [INFO] Shutting down\n", + "[2024-09-24 18:16:00 +0000] [17570] [INFO] Error while closing socket [Errno 9] Bad file descriptor\n", + "[2024-09-24 18:16:00 +0000] [17570] [INFO] Waiting for application shutdown.\n", + "[2024-09-24 18:16:00 +0000] [17570] [INFO] Application shutdown complete.\n", + "[2024-09-24 18:16:00 +0000] [17570] [INFO] Finished server process [17570]\n", + "[2024-09-24 18:16:00 +0000] [17522] [ERROR] Worker (pid:17570) was sent SIGTERM!\n", + "[2024-09-24 18:16:00 +0000] [17522] [INFO] Shutting down: Master\n" + ] + } + ], + "source": [ + "feast_online_server_process.terminate() # Stop the remote Feast online server\n", + "feast_remote_registry_server_process.terminate() # stops the remote registry server\n", + "print(\"remote online and registry server has been stopped.\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "id": "93bf01b6-5bf1-4345-9bef-2e83edc489b7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "/opt/app-root/src/feast/examples/rhoai-quickstart/my_feast_project/feature_repo\n", + "1001130+ 18542 18499 0 18:16 ? 00:00:00 grep feast serve\n" + ] + } + ], + "source": [ + "%%sh\n", + "# checking if the registry server and online server process stopped. wait for some time until it kills.\n", + "pwd\n", + "ps -ef | grep 'feast serve'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cbe06b35-7586-4b26-a63b-29bcd7ea3037", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.9", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.16" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file From 7f00b164f4812de2801e14e39735f4937c7a2a15 Mon Sep 17 00:00:00 2001 From: Harri Lehtola <1781172+peruukki@users.noreply.github.com> Date: Sat, 12 Oct 2024 13:49:08 +0300 Subject: [PATCH 134/185] chore: Remove unused `expectationsData` in Feast UI (#4621) Gets rid of this when running `yarn start`: ```sh WARNING in src/pages/saved-data-sets/DatasetExpectationsTab.tsx Line 22:7: 'expectationsData' is defined but never used @typescript-eslint/no-unused-vars ``` Signed-off-by: Harri Lehtola --- ui/src/pages/saved-data-sets/DatasetExpectationsTab.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/ui/src/pages/saved-data-sets/DatasetExpectationsTab.tsx b/ui/src/pages/saved-data-sets/DatasetExpectationsTab.tsx index dc49355c28..8122c56559 100644 --- a/ui/src/pages/saved-data-sets/DatasetExpectationsTab.tsx +++ b/ui/src/pages/saved-data-sets/DatasetExpectationsTab.tsx @@ -19,8 +19,6 @@ const DatasetExpectationsTab = () => { ); } - let expectationsData; - return isSuccess ? (
{JSON.stringify(data.spec, null, 2)}
From 043eff1a87bdf775a437503395acda87cbecf875 Mon Sep 17 00:00:00 2001 From: Rob Howley Date: Sat, 12 Oct 2024 08:21:51 -0400 Subject: [PATCH 135/185] perf: Parallelize read calls by table and batch (#4619) --- .../feast/infra/online_stores/dynamodb.py | 52 ++++++++++++------- .../feast/infra/online_stores/online_store.py | 16 +++++- 2 files changed, 48 insertions(+), 20 deletions(-) diff --git a/sdk/python/feast/infra/online_stores/dynamodb.py b/sdk/python/feast/infra/online_stores/dynamodb.py index c0494272b3..a6cbfb41d2 100644 --- a/sdk/python/feast/infra/online_stores/dynamodb.py +++ b/sdk/python/feast/infra/online_stores/dynamodb.py @@ -11,6 +11,7 @@ # 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 asyncio import itertools import logging from datetime import datetime @@ -297,7 +298,6 @@ async def online_read_async( batch_size = online_config.batch_size entity_ids = self._to_entity_ids(config, entity_keys) entity_ids_iter = iter(entity_ids) - result: List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]] = [] table_name = _get_table_name(online_config, config, table) deserialize = TypeDeserializer().deserialize @@ -309,24 +309,40 @@ def to_tbl_resp(raw_client_response): "values": deserialize(raw_client_response["values"]), } + batches = [] + entity_id_batches = [] + while True: + batch = list(itertools.islice(entity_ids_iter, batch_size)) + if not batch: + break + entity_id_batch = self._to_client_batch_get_payload( + online_config, table_name, batch + ) + batches.append(batch) + entity_id_batches.append(entity_id_batch) + async with self._get_aiodynamodb_client(online_config.region) as client: - while True: - batch = list(itertools.islice(entity_ids_iter, batch_size)) - - # No more items to insert - if len(batch) == 0: - break - batch_entity_ids = self._to_client_batch_get_payload( - online_config, table_name, batch - ) - response = await client.batch_get_item( - RequestItems=batch_entity_ids, - ) - batch_result = self._process_batch_get_response( - table_name, response, entity_ids, batch, to_tbl_response=to_tbl_resp - ) - result.extend(batch_result) - return result + response_batches = await asyncio.gather( + *[ + client.batch_get_item( + RequestItems=entity_id_batch, + ) + for entity_id_batch in entity_id_batches + ] + ) + + result_batches = [] + for batch, response in zip(batches, response_batches): + result_batch = self._process_batch_get_response( + table_name, + response, + entity_ids, + batch, + to_tbl_response=to_tbl_resp, + ) + result_batches.append(result_batch) + + return list(itertools.chain(*result_batches)) def _get_aioboto_session(self): if self._aioboto_session is None: diff --git a/sdk/python/feast/infra/online_stores/online_store.py b/sdk/python/feast/infra/online_stores/online_store.py index ea86bd9175..cf2d68eb74 100644 --- a/sdk/python/feast/infra/online_stores/online_store.py +++ b/sdk/python/feast/infra/online_stores/online_store.py @@ -11,7 +11,7 @@ # 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 asyncio from abc import ABC, abstractmethod from datetime import datetime from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Tuple, Union @@ -240,7 +240,7 @@ async def get_online_features_async( native_entity_values=True, ) - for table, requested_features in grouped_refs: + async def query_table(table, requested_features): # Get the correct set of entity values with the correct join keys. table_entity_values, idxs = utils._get_unique_entities( table, @@ -258,6 +258,18 @@ async def get_online_features_async( requested_features=requested_features, ) + return idxs, read_rows + + all_responses = await asyncio.gather( + *[ + query_table(table, requested_features) + for table, requested_features in grouped_refs + ] + ) + + for (idxs, read_rows), (table, requested_features) in zip( + all_responses, grouped_refs + ): feature_data = utils._convert_rows_to_protobuf( requested_features, read_rows ) From 945b0faadd40c8dc76d104bce14ee902bd513127 Mon Sep 17 00:00:00 2001 From: lokeshrangineni <19699092+lokeshrangineni@users.noreply.github.com> Date: Mon, 14 Oct 2024 16:12:04 -0400 Subject: [PATCH 136/185] fix: Updating the documentation and adding tests for project length (#4628) Updated the documentation to make it clear after spending some time. Addressing the issue - https://github.com/feast-dev/feast/issues/4626 Signed-off-by: lrangine <19699092+lokeshrangineni@users.noreply.github.com> --- sdk/python/feast/repo_config.py | 4 +-- ..._operations_validate_feast_project_name.py | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 sdk/python/tests/unit/sdk/python/feast/test_repo_operations_validate_feast_project_name.py diff --git a/sdk/python/feast/repo_config.py b/sdk/python/feast/repo_config.py index bf0bde6fcb..1b991d058b 100644 --- a/sdk/python/feast/repo_config.py +++ b/sdk/python/feast/repo_config.py @@ -158,9 +158,9 @@ class RepoConfig(FeastBaseModel): """Repo config. Typically loaded from `feature_store.yaml`""" project: StrictStr - """ str: Feast project id. This can be any alphanumeric string up to 16 characters. + """ str: This acts as a Feast unique project identifier. This can be any alphanumeric string and can have '_' but can not start with '_'. You can have multiple independent feature repositories deployed to the same cloud - provider account, as long as they have different project ids. + provider account, as long as they have different project identifier. """ provider: StrictStr = "local" diff --git a/sdk/python/tests/unit/sdk/python/feast/test_repo_operations_validate_feast_project_name.py b/sdk/python/tests/unit/sdk/python/feast/test_repo_operations_validate_feast_project_name.py new file mode 100644 index 0000000000..ba4a60ddc0 --- /dev/null +++ b/sdk/python/tests/unit/sdk/python/feast/test_repo_operations_validate_feast_project_name.py @@ -0,0 +1,26 @@ +from sdk.python.feast.repo_operations import is_valid_name + + +def test_is_valid_name(): + test_cases = [ + # Valid project name cases + ("valid_name1", True), + ("username_1234", True), + ("valid123", True), + ("1234567890123456", True), + ("invalid_name_", True), + ("12345678901234567", True), + ("too_long_name_123", True), + # Invalid project name cases + ("_invalidName", False), + ("invalid-Name", False), + ("invalid name", False), + ("invalid@name", False), + ("invalid$name", False), + ("__", False), + ] + + for name, expected in test_cases: + assert ( + is_valid_name(name) == expected + ), f"Failed for project invalid name: {name}" From ba05893ba6db2d8d1e7bcc8cf8162f4fb72c9563 Mon Sep 17 00:00:00 2001 From: Rob Howley Date: Mon, 14 Oct 2024 16:49:10 -0400 Subject: [PATCH 137/185] fix: Avoid the python 3.9+ threadpool cleanup bug (#4627) fix: dont allow for new refresh threadpool tasks to start during shutdown Signed-off-by: Rob Howley --- sdk/python/feast/feature_server.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/sdk/python/feast/feature_server.py b/sdk/python/feast/feature_server.py index 9757e95143..c188bd0d7b 100644 --- a/sdk/python/feast/feature_server.py +++ b/sdk/python/feast/feature_server.py @@ -85,11 +85,12 @@ def stop_refresh(): active_timer.cancel() def async_refresh(): + if shutting_down: + return + store.refresh_registry() nonlocal registry_proto registry_proto = store.registry.proto() - if shutting_down: - return if registry_ttl_sec: nonlocal active_timer @@ -222,8 +223,12 @@ def write_to_online_store(body=Depends(get_body)): ) @app.get("/health") - def health(): - return Response(status_code=status.HTTP_200_OK) + async def health(): + return ( + Response(status_code=status.HTTP_200_OK) + if registry_proto + else Response(status_code=status.HTTP_503_SERVICE_UNAVAILABLE) + ) @app.post("/materialize", dependencies=[Depends(inject_user_details)]) def materialize(body=Depends(get_body)): From ac62a323c86fba8096eefde85775dd7a857e9e25 Mon Sep 17 00:00:00 2001 From: Daniele Martinoli <86618610+dmartinol@users.noreply.github.com> Date: Tue, 15 Oct 2024 17:38:17 +0200 Subject: [PATCH 138/185] fix: Remote apply using offline store (#4559) * remote apply using offline store Signed-off-by: Daniele Martinoli * passing data source proto to the offline server Signed-off-by: Daniele Martinoli * fixed linting, added permission asserts Signed-off-by: Daniele Martinoli --------- Signed-off-by: Daniele Martinoli --- sdk/python/feast/feature_store.py | 19 ++- sdk/python/feast/inference.py | 10 +- .../infra/offline_stores/offline_store.py | 27 ++++- .../feast/infra/offline_stores/remote.py | 53 ++++++++- .../feast/infra/passthrough_provider.py | 20 +++- sdk/python/feast/infra/provider.py | 26 +++- sdk/python/feast/infra/registry/remote.py | 6 +- sdk/python/feast/offline_server.py | 46 ++++++++ .../client/grpc_client_auth_interceptor.py | 10 +- sdk/python/tests/foo_provider.py | 18 ++- .../unit/infra/test_inference_unit_tests.py | 111 +++++++++++------- 11 files changed, 282 insertions(+), 64 deletions(-) diff --git a/sdk/python/feast/feature_store.py b/sdk/python/feast/feature_store.py index 6112279a02..9a652fd060 100644 --- a/sdk/python/feast/feature_store.py +++ b/sdk/python/feast/feature_store.py @@ -602,16 +602,23 @@ def _make_inferences( # New feature views may reference previously applied entities. entities = self._list_entities() + provider = self._get_provider() update_feature_views_with_inferred_features_and_entities( - views_to_update, entities + entities_to_update, self.config + provider, + views_to_update, + entities + entities_to_update, + self.config, ) update_feature_views_with_inferred_features_and_entities( - sfvs_to_update, entities + entities_to_update, self.config + provider, + sfvs_to_update, + entities + entities_to_update, + self.config, ) # We need to attach the time stamp fields to the underlying data sources # and cascade the dependencies update_feature_views_with_inferred_features_and_entities( - odfvs_to_update, entities + entities_to_update, self.config + provider, odfvs_to_update, entities + entities_to_update, self.config ) # TODO(kevjumba): Update schema inference for sfv in sfvs_to_update: @@ -1529,9 +1536,12 @@ def write_to_offline_store( feature_view_name, allow_registry_cache=allow_registry_cache ) + provider = self._get_provider() # Get columns of the batch source and the input dataframe. column_names_and_types = ( - feature_view.batch_source.get_table_column_names_and_types(self.config) + provider.get_table_column_names_and_types_from_data_source( + self.config, feature_view.batch_source + ) ) source_columns = [column for column, _ in column_names_and_types] input_columns = df.columns.values.tolist() @@ -1545,7 +1555,6 @@ def write_to_offline_store( df = df.reindex(columns=source_columns) table = pa.Table.from_pandas(df) - provider = self._get_provider() provider.ingest_df_to_offline_store(feature_view, table) def get_online_features( diff --git a/sdk/python/feast/inference.py b/sdk/python/feast/inference.py index 39782e1e31..f2a2ee637f 100644 --- a/sdk/python/feast/inference.py +++ b/sdk/python/feast/inference.py @@ -13,6 +13,7 @@ from feast.infra.offline_stores.file_source import FileSource from feast.infra.offline_stores.redshift_source import RedshiftSource from feast.infra.offline_stores.snowflake_source import SnowflakeSource +from feast.infra.provider import Provider from feast.on_demand_feature_view import OnDemandFeatureView from feast.repo_config import RepoConfig from feast.stream_feature_view import StreamFeatureView @@ -95,6 +96,7 @@ def update_data_sources_with_inferred_event_timestamp_col( def update_feature_views_with_inferred_features_and_entities( + provider: Provider, fvs: Union[List[FeatureView], List[StreamFeatureView], List[OnDemandFeatureView]], entities: List[Entity], config: RepoConfig, @@ -176,6 +178,7 @@ def update_feature_views_with_inferred_features_and_entities( if run_inference_for_entities or run_inference_for_features: _infer_features_and_entities( + provider, fv, join_keys, run_inference_for_features, @@ -193,6 +196,7 @@ def update_feature_views_with_inferred_features_and_entities( def _infer_features_and_entities( + provider: Provider, fv: Union[FeatureView, OnDemandFeatureView], join_keys: Set[Optional[str]], run_inference_for_features, @@ -222,8 +226,10 @@ def _infer_features_and_entities( columns_to_exclude.remove(mapped_col) columns_to_exclude.add(original_col) - table_column_names_and_types = fv.batch_source.get_table_column_names_and_types( - config + table_column_names_and_types = ( + provider.get_table_column_names_and_types_from_data_source( + config, fv.batch_source + ) ) for col_name, col_datatype in table_column_names_and_types: diff --git a/sdk/python/feast/infra/offline_stores/offline_store.py b/sdk/python/feast/infra/offline_stores/offline_store.py index d973844531..69d6bb278b 100644 --- a/sdk/python/feast/infra/offline_stores/offline_store.py +++ b/sdk/python/feast/infra/offline_stores/offline_store.py @@ -15,7 +15,16 @@ from abc import ABC from datetime import datetime from pathlib import Path -from typing import TYPE_CHECKING, Any, Callable, List, Optional, Union +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Iterable, + List, + Optional, + Tuple, + Union, +) import pandas as pd import pyarrow @@ -352,8 +361,8 @@ def offline_write_batch( """ raise NotImplementedError - @staticmethod def validate_data_source( + self, config: RepoConfig, data_source: DataSource, ): @@ -365,3 +374,17 @@ def validate_data_source( data_source: DataSource object that needs to be validated """ data_source.validate(config=config) + + def get_table_column_names_and_types_from_data_source( + self, + config: RepoConfig, + data_source: DataSource, + ) -> Iterable[Tuple[str, str]]: + """ + Returns the list of column names and raw column types for a DataSource. + + Args: + config: Configuration object used to configure a feature store. + data_source: DataSource object + """ + return data_source.get_table_column_names_and_types(config=config) diff --git a/sdk/python/feast/infra/offline_stores/remote.py b/sdk/python/feast/infra/offline_stores/remote.py index 8154f75f87..7ee018ac6d 100644 --- a/sdk/python/feast/infra/offline_stores/remote.py +++ b/sdk/python/feast/infra/offline_stores/remote.py @@ -3,7 +3,7 @@ import uuid from datetime import datetime from pathlib import Path -from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, Union +from typing import Any, Callable, Dict, Iterable, List, Literal, Optional, Tuple, Union import numpy as np import pandas as pd @@ -328,6 +328,57 @@ def offline_write_batch( entity_df=None, ) + def validate_data_source( + self, + config: RepoConfig, + data_source: DataSource, + ): + assert isinstance(config.offline_store, RemoteOfflineStoreConfig) + + client = build_arrow_flight_client( + config.offline_store.host, config.offline_store.port, config.auth_config + ) + + api_parameters = { + "data_source_proto": str(data_source), + } + logger.debug(f"validating DataSource {data_source.name}") + _call_put( + api=OfflineStore.validate_data_source.__name__, + api_parameters=api_parameters, + client=client, + table=None, + entity_df=None, + ) + + def get_table_column_names_and_types_from_data_source( + self, config: RepoConfig, data_source: DataSource + ) -> Iterable[Tuple[str, str]]: + assert isinstance(config.offline_store, RemoteOfflineStoreConfig) + + client = build_arrow_flight_client( + config.offline_store.host, config.offline_store.port, config.auth_config + ) + + api_parameters = { + "data_source_proto": str(data_source), + } + logger.debug( + f"Calling {OfflineStore.get_table_column_names_and_types_from_data_source.__name__} with {api_parameters}" + ) + table = _send_retrieve_remote( + api=OfflineStore.get_table_column_names_and_types_from_data_source.__name__, + api_parameters=api_parameters, + client=client, + table=None, + entity_df=None, + ) + + logger.debug( + f"get_table_column_names_and_types_from_data_source for {data_source.name}: {table}" + ) + return zip(table.column("name").to_pylist(), table.column("type").to_pylist()) + def _create_retrieval_metadata(feature_refs: List[str], entity_df: pd.DataFrame): entity_schema = _get_entity_schema( diff --git a/sdk/python/feast/infra/passthrough_provider.py b/sdk/python/feast/infra/passthrough_provider.py index 11ccbb2450..5acfc0d6f3 100644 --- a/sdk/python/feast/infra/passthrough_provider.py +++ b/sdk/python/feast/infra/passthrough_provider.py @@ -1,5 +1,16 @@ from datetime import datetime, timedelta -from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Tuple, Union +from typing import ( + Any, + Callable, + Dict, + Iterable, + List, + Mapping, + Optional, + Sequence, + Tuple, + Union, +) import pandas as pd import pyarrow as pa @@ -455,3 +466,10 @@ def validate_data_source( data_source: DataSource, ): self.offline_store.validate_data_source(config=config, data_source=data_source) + + def get_table_column_names_and_types_from_data_source( + self, config: RepoConfig, data_source: DataSource + ) -> Iterable[Tuple[str, str]]: + return self.offline_store.get_table_column_names_and_types_from_data_source( + config=config, data_source=data_source + ) diff --git a/sdk/python/feast/infra/provider.py b/sdk/python/feast/infra/provider.py index 22c2088861..0723e0513f 100644 --- a/sdk/python/feast/infra/provider.py +++ b/sdk/python/feast/infra/provider.py @@ -1,7 +1,18 @@ from abc import ABC, abstractmethod from datetime import datetime from pathlib import Path -from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Tuple, Union +from typing import ( + Any, + Callable, + Dict, + Iterable, + List, + Mapping, + Optional, + Sequence, + Tuple, + Union, +) import pandas as pd import pyarrow @@ -405,6 +416,19 @@ def validate_data_source( """ pass + @abstractmethod + def get_table_column_names_and_types_from_data_source( + self, config: RepoConfig, data_source: DataSource + ) -> Iterable[Tuple[str, str]]: + """ + Returns the list of column names and raw column types for a DataSource. + + Args: + config: Configuration object used to configure a feature store. + data_source: DataSource object + """ + pass + def get_provider(config: RepoConfig) -> Provider: if "." not in config.provider: diff --git a/sdk/python/feast/infra/registry/remote.py b/sdk/python/feast/infra/registry/remote.py index cdb45f0363..424c59c57d 100644 --- a/sdk/python/feast/infra/registry/remote.py +++ b/sdk/python/feast/infra/registry/remote.py @@ -15,7 +15,6 @@ from feast.infra.infra_object import Infra from feast.infra.registry.base_registry import BaseRegistry from feast.on_demand_feature_view import OnDemandFeatureView -from feast.permissions.auth.auth_type import AuthType from feast.permissions.auth_model import AuthConfig, NoAuthConfig from feast.permissions.client.grpc_client_auth_interceptor import ( GrpcClientAuthHeaderInterceptor, @@ -67,9 +66,8 @@ def __init__( ): self.auth_config = auth_config self.channel = grpc.insecure_channel(registry_config.path) - if self.auth_config.type != AuthType.NONE.value: - auth_header_interceptor = GrpcClientAuthHeaderInterceptor(auth_config) - self.channel = grpc.intercept_channel(self.channel, auth_header_interceptor) + auth_header_interceptor = GrpcClientAuthHeaderInterceptor(auth_config) + self.channel = grpc.intercept_channel(self.channel, auth_header_interceptor) self.stub = RegistryServer_pb2_grpc.RegistryServerStub(self.channel) def close(self): diff --git a/sdk/python/feast/offline_server.py b/sdk/python/feast/offline_server.py index ff3db579d0..0cb40ad934 100644 --- a/sdk/python/feast/offline_server.py +++ b/sdk/python/feast/offline_server.py @@ -7,9 +7,11 @@ import pyarrow as pa import pyarrow.flight as fl +from google.protobuf.json_format import Parse from feast import FeatureStore, FeatureView, utils from feast.arrow_error_handler import arrow_server_error_handling_decorator +from feast.data_source import DataSource from feast.feature_logging import FeatureServiceLoggingSource from feast.feature_view import DUMMY_ENTITY_NAME from feast.infra.offline_stores.offline_utils import get_offline_store_from_config @@ -26,6 +28,7 @@ init_security_manager, str_to_auth_manager_type, ) +from feast.protos.feast.core.DataSource_pb2 import DataSource as DataSourceProto from feast.saved_dataset import SavedDatasetStorage logger = logging.getLogger(__name__) @@ -138,6 +141,9 @@ def _call_api(self, api: str, command: dict, key: str): elif api == OfflineServer.persist.__name__: self.persist(command, key) remove_data = True + elif api == OfflineServer.validate_data_source.__name__: + self.validate_data_source(command) + remove_data = True except Exception as e: remove_data = True logger.exception(e) @@ -224,6 +230,11 @@ def do_get(self, context: fl.ServerCallContext, ticket: fl.Ticket): table = self.pull_all_from_table_or_query(command).to_arrow() elif api == OfflineServer.pull_latest_from_table_or_query.__name__: table = self.pull_latest_from_table_or_query(command).to_arrow() + elif ( + api + == OfflineServer.get_table_column_names_and_types_from_data_source.__name__ + ): + table = self.get_table_column_names_and_types_from_data_source(command) else: raise NotImplementedError except Exception as e: @@ -457,6 +468,41 @@ def persist(self, command: dict, key: str): traceback.print_exc() raise e + @staticmethod + def _extract_data_source_from_command(command) -> DataSource: + data_source_proto_str = command["data_source_proto"] + logger.debug(f"Extracted data_source_proto {data_source_proto_str}") + data_source_proto = DataSourceProto() + Parse(data_source_proto_str, data_source_proto) + data_source = DataSource.from_proto(data_source_proto) + logger.debug(f"Converted to DataSource {data_source}") + return data_source + + def validate_data_source(self, command: dict): + data_source = OfflineServer._extract_data_source_from_command(command) + logger.debug(f"Validating data source {data_source.name}") + assert_permissions(data_source, actions=[AuthzedAction.READ_OFFLINE]) + + self.offline_store.validate_data_source( + config=self.store.config, + data_source=data_source, + ) + + def get_table_column_names_and_types_from_data_source(self, command: dict): + data_source = OfflineServer._extract_data_source_from_command(command) + logger.debug(f"Fetching table columns metadata from {data_source.name}") + assert_permissions(data_source, actions=[AuthzedAction.READ_OFFLINE]) + + column_names_and_types = data_source.get_table_column_names_and_types( + self.store.config + ) + + column_names, types = zip(*column_names_and_types) + logger.debug( + f"DataSource {data_source.name} has columns {column_names} with types {types}" + ) + return pa.table({"name": column_names, "type": types}) + def remove_dummies(fv: FeatureView) -> FeatureView: """ diff --git a/sdk/python/feast/permissions/client/grpc_client_auth_interceptor.py b/sdk/python/feast/permissions/client/grpc_client_auth_interceptor.py index 121735e351..9a6bef2c07 100644 --- a/sdk/python/feast/permissions/client/grpc_client_auth_interceptor.py +++ b/sdk/python/feast/permissions/client/grpc_client_auth_interceptor.py @@ -3,6 +3,7 @@ import grpc from feast.errors import FeastError +from feast.permissions.auth.auth_type import AuthType from feast.permissions.auth_model import AuthConfig from feast.permissions.client.client_auth_token import get_auth_token @@ -15,8 +16,8 @@ class GrpcClientAuthHeaderInterceptor( grpc.StreamUnaryClientInterceptor, grpc.StreamStreamClientInterceptor, ): - def __init__(self, auth_type: AuthConfig): - self._auth_type = auth_type + def __init__(self, auth_config: AuthConfig): + self._auth_config = auth_config def intercept_unary_unary( self, continuation, client_call_details, request_iterator @@ -39,7 +40,8 @@ def intercept_stream_stream( return self._handle_call(continuation, client_call_details, request_iterator) def _handle_call(self, continuation, client_call_details, request_iterator): - client_call_details = self._append_auth_header_metadata(client_call_details) + if self._auth_config.type != AuthType.NONE.value: + client_call_details = self._append_auth_header_metadata(client_call_details) result = continuation(client_call_details, request_iterator) if result.exception() is not None: mapped_error = FeastError.from_error_detail(result.exception().details()) @@ -52,7 +54,7 @@ def _append_auth_header_metadata(self, client_call_details): "Intercepted the grpc api method call to inject Authorization header " ) metadata = client_call_details.metadata or [] - access_token = get_auth_token(self._auth_type) + access_token = get_auth_token(self._auth_config) metadata.append((b"authorization", b"Bearer " + access_token.encode("utf-8"))) client_call_details = client_call_details._replace(metadata=metadata) return client_call_details diff --git a/sdk/python/tests/foo_provider.py b/sdk/python/tests/foo_provider.py index 8e8f54db24..6fe6d15150 100644 --- a/sdk/python/tests/foo_provider.py +++ b/sdk/python/tests/foo_provider.py @@ -1,6 +1,17 @@ from datetime import datetime from pathlib import Path -from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Tuple, Union +from typing import ( + Any, + Callable, + Dict, + Iterable, + List, + Mapping, + Optional, + Sequence, + Tuple, + Union, +) import pandas import pyarrow @@ -141,6 +152,11 @@ def validate_data_source( ): pass + def get_table_column_names_and_types_from_data_source( + self, config: RepoConfig, data_source: DataSource + ) -> Iterable[Tuple[str, str]]: + return [] + def get_online_features( self, config: RepoConfig, diff --git a/sdk/python/tests/unit/infra/test_inference_unit_tests.py b/sdk/python/tests/unit/infra/test_inference_unit_tests.py index 3d8fe8c967..54488d4321 100644 --- a/sdk/python/tests/unit/infra/test_inference_unit_tests.py +++ b/sdk/python/tests/unit/infra/test_inference_unit_tests.py @@ -6,7 +6,10 @@ from feast import BigQuerySource, FileSource, RedshiftSource, SnowflakeSource from feast.data_source import RequestSource from feast.entity import Entity -from feast.errors import DataSourceNoNameException, SpecifiedFeaturesNotPresentError +from feast.errors import ( + DataSourceNoNameException, + SpecifiedFeaturesNotPresentError, +) from feast.feature_service import FeatureService from feast.feature_view import FeatureView from feast.field import Field @@ -14,6 +17,7 @@ from feast.infra.offline_stores.contrib.spark_offline_store.spark_source import ( SparkSource, ) +from feast.infra.provider import get_provider from feast.on_demand_feature_view import on_demand_feature_view from feast.repo_config import RepoConfig from feast.types import Float32, Float64, Int64, String, UnixTimestamp @@ -253,15 +257,18 @@ def test_feature_view_inference_respects_basic_inference(): assert len(feature_view_1.features) == 1 assert len(feature_view_1.entity_columns) == 1 + config = RepoConfig( + provider="local", + project="test", + entity_key_serialization_version=2, + registry="dummy_registry.pb", + ) + provider = get_provider(config) update_feature_views_with_inferred_features_and_entities( + provider, [feature_view_1], [entity1], - RepoConfig( - provider="local", - project="test", - entity_key_serialization_version=2, - registry="dummy_registry.pb", - ), + config, ) assert len(feature_view_1.schema) == 2 assert len(feature_view_1.features) == 1 @@ -271,15 +278,18 @@ def test_feature_view_inference_respects_basic_inference(): assert len(feature_view_2.features) == 1 assert len(feature_view_2.entity_columns) == 2 + config = RepoConfig( + provider="local", + project="test", + entity_key_serialization_version=2, + registry="dummy_registry.pb", + ) + provider = get_provider(config) update_feature_views_with_inferred_features_and_entities( + provider, [feature_view_2], [entity1, entity2], - RepoConfig( - provider="local", - project="test", - entity_key_serialization_version=2, - registry="dummy_registry.pb", - ), + config, ) assert len(feature_view_2.schema) == 3 assert len(feature_view_2.features) == 1 @@ -305,15 +315,18 @@ def test_feature_view_inference_on_entity_value_types(): assert len(feature_view_1.features) == 1 assert len(feature_view_1.entity_columns) == 0 + config = RepoConfig( + provider="local", + project="test", + entity_key_serialization_version=2, + registry="dummy_registry.pb", + ) + provider = get_provider(config) update_feature_views_with_inferred_features_and_entities( + provider, [feature_view_1], [entity1], - RepoConfig( - provider="local", - project="test", - entity_key_serialization_version=2, - registry="dummy_registry.pb", - ), + config, ) # The schema must be entity and features @@ -378,15 +391,18 @@ def test_feature_view_inference_on_entity_columns(simple_dataset_1): assert len(feature_view_1.features) == 1 assert len(feature_view_1.entity_columns) == 0 + config = RepoConfig( + provider="local", + project="test", + entity_key_serialization_version=2, + registry="dummy_registry.pb", + ) + provider = get_provider(config) update_feature_views_with_inferred_features_and_entities( + provider, [feature_view_1], [entity1], - RepoConfig( - provider="local", - project="test", - entity_key_serialization_version=2, - registry="dummy_registry.pb", - ), + config, ) # Since there is already a feature specified, additional features are not inferred. @@ -416,15 +432,18 @@ def test_feature_view_inference_on_feature_columns(simple_dataset_1): assert len(feature_view_1.features) == 0 assert len(feature_view_1.entity_columns) == 1 + config = RepoConfig( + provider="local", + project="test", + entity_key_serialization_version=2, + registry="dummy_registry.pb", + ) + provider = get_provider(config) update_feature_views_with_inferred_features_and_entities( + provider, [feature_view_1], [entity1], - RepoConfig( - provider="local", - project="test", - entity_key_serialization_version=2, - registry="dummy_registry.pb", - ), + config, ) # The schema is a property concatenating features and entity_columns @@ -471,15 +490,18 @@ def test_update_feature_services_with_inferred_features(simple_dataset_1): assert len(feature_service.feature_view_projections[1].features) == 0 assert len(feature_service.feature_view_projections[1].desired_features) == 0 + config = RepoConfig( + provider="local", + project="test", + entity_key_serialization_version=2, + registry="dummy_registry.pb", + ) + provider = get_provider(config) update_feature_views_with_inferred_features_and_entities( + provider, [feature_view_1, feature_view_2], [entity1], - RepoConfig( - provider="local", - project="test", - entity_key_serialization_version=2, - registry="dummy_registry.pb", - ), + config, ) feature_service.infer_features( fvs_to_update={ @@ -531,15 +553,18 @@ def test_update_feature_services_with_specified_features(simple_dataset_1): assert len(feature_service.feature_view_projections[1].features) == 1 assert len(feature_service.feature_view_projections[1].desired_features) == 0 + config = RepoConfig( + provider="local", + project="test", + entity_key_serialization_version=2, + registry="dummy_registry.pb", + ) + provider = get_provider(config) update_feature_views_with_inferred_features_and_entities( + provider, [feature_view_1, feature_view_2], [entity1], - RepoConfig( - provider="local", - project="test", - entity_key_serialization_version=2, - registry="dummy_registry.pb", - ), + config, ) assert len(feature_view_1.features) == 1 assert len(feature_view_2.features) == 1 From d6f3cb81a4c2a51bae2d29a185753cf6b1d2b16d Mon Sep 17 00:00:00 2001 From: Harri Lehtola <1781172+peruukki@users.noreply.github.com> Date: Tue, 15 Oct 2024 18:41:37 +0300 Subject: [PATCH 139/185] feat: Upgrade React from 17.0.2 to 18.3.1 in Feast UI (#4620) Also update related packages to better work with React 18. userEvent usage is updated to call `setup()` first and await all interactions so that the DOM updates before continuing with the test, according to e.g. https://testing-library.com/docs/user-event/intro#writing-tests-with-userevent. Signed-off-by: Harri Lehtola --- ui/package.json | 21 +- ui/src/FeastUISansProviders.test.tsx | 17 +- ui/src/components/ProjectSelector.test.tsx | 4 +- ui/src/index.tsx | 8 +- ui/src/test-utils.tsx | 6 +- ui/yarn.lock | 259 +++++++++++---------- 6 files changed, 169 insertions(+), 146 deletions(-) diff --git a/ui/package.json b/ui/package.json index 5e427b8905..a79b039a87 100644 --- a/ui/package.json +++ b/ui/package.json @@ -11,8 +11,8 @@ "peerDependencies": { "@elastic/eui": "^95.12.0", "@emotion/react": "^11.13.3", - "react": "^17.0.2", - "react-dom": "^17.0.2" + "react": "^17.0.2 || ^18.0.0", + "react-dom": "^17.0.2 || ^18.0.0" }, "peerDependenciesMeta": { "@elastic/eui": { @@ -33,7 +33,7 @@ "protobufjs": "^7.1.1", "query-string": "^7.1.1", "react-code-blocks": "^0.0.9-0", - "react-query": "^3.34.12", + "react-query": "^3.39.3", "react-router-dom": "<6.4.0", "react-scripts": "^5.0.0", "tslib": "^2.3.1", @@ -82,18 +82,19 @@ "@rollup/plugin-json": "^4.1.0", "@rollup/plugin-node-resolve": "^13.1.3", "@rollup/plugin-typescript": "^8.3.1", - "@testing-library/jest-dom": "^5.14.1", - "@testing-library/react": "^12.0.0", - "@testing-library/user-event": "^13.2.1", + "@testing-library/dom": "^10.4.0", + "@testing-library/jest-dom": "^6.5.0", + "@testing-library/react": "^16.0.1", + "@testing-library/user-event": "^14.5.2", "@types/d3": "^7.1.0", "@types/jest": "^27.0.1", "@types/node": "^16.7.13", - "@types/react": "^17.0.20", - "@types/react-dom": "^17.0.9", + "@types/react": "^18.3.11", + "@types/react-dom": "^18.3.0", "msw": "^0.36.8", "protobufjs-cli": "^1.0.2", - "react": "^17.0.2", - "react-dom": "^17.0.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", "rimraf": "^3.0.2", "rollup": "^2.68.0", "rollup-plugin-copy": "^3.4.0", diff --git a/ui/src/FeastUISansProviders.test.tsx b/ui/src/FeastUISansProviders.test.tsx index 4670232809..4af9490e10 100644 --- a/ui/src/FeastUISansProviders.test.tsx +++ b/ui/src/FeastUISansProviders.test.tsx @@ -65,9 +65,9 @@ test("full app rendering", async () => { }); }); -const leftClick = { button: 0 }; - test("routes are reachable", async () => { + const user = userEvent.setup(); + render(); // Wait for content to load @@ -89,10 +89,7 @@ test("routes are reachable", async () => { const routeRegExp = new RegExp(routeName, "i"); - userEvent.click( - screen.getByRole("button", { name: routeRegExp }), - leftClick - ); + await user.click(screen.getByRole("button", { name: routeRegExp })); // Should land on a page with the heading screen.getByRole("heading", { @@ -107,13 +104,15 @@ const featureViewName = spec.name!; const featureName = spec.features![0]!.name!; test("features are reachable", async () => { + const user = userEvent.setup(); + render(); // Wait for content to load await screen.findByText(/Explore this Project/i); const routeRegExp = new RegExp("Feature Views", "i"); - userEvent.click(screen.getByRole("button", { name: routeRegExp }), leftClick); + await user.click(screen.getByRole("button", { name: routeRegExp })); screen.getByRole("heading", { name: "Feature Views", @@ -122,12 +121,12 @@ test("features are reachable", async () => { await screen.findAllByText(/Feature Views/i); const fvRegExp = new RegExp(featureViewName, "i"); - userEvent.click(screen.getByRole("link", { name: fvRegExp }), leftClick); + await user.click(screen.getByRole("link", { name: fvRegExp })); await screen.findByText(featureName); const fRegExp = new RegExp(featureName, "i"); - userEvent.click(screen.getByRole("link", { name: fRegExp }), leftClick); + await user.click(screen.getByRole("link", { name: fRegExp })); // Should land on a page with the heading // await screen.findByText("Feature: " + featureName); screen.getByRole("heading", { diff --git a/ui/src/components/ProjectSelector.test.tsx b/ui/src/components/ProjectSelector.test.tsx index 0032b88839..fc5b3c6840 100644 --- a/ui/src/components/ProjectSelector.test.tsx +++ b/ui/src/components/ProjectSelector.test.tsx @@ -25,6 +25,8 @@ afterEach(() => server.resetHandlers()); afterAll(() => server.close()); test("in a full App render, it shows the right initial project", async () => { + const user = userEvent.setup(); + render(); const select = await screen.findByRole("combobox", { @@ -54,7 +56,7 @@ test("in a full App render, it shows the right initial project", async () => { // Do the select option user event // https://stackoverflow.com/a/69478957 - userEvent.selectOptions( + await user.selectOptions( // Find the select element within(topLevelNavigation).getByRole("combobox"), // Find and select the Ireland option diff --git a/ui/src/index.tsx b/ui/src/index.tsx index 7559d02ebf..04eda8a1ba 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -1,5 +1,5 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { createRoot } from "react-dom/client"; import { QueryClient } from "react-query"; import FeastUI from "./FeastUI"; @@ -91,7 +91,8 @@ const tabsRegistry = { ], }; -ReactDOM.render( +const root = createRoot(document.getElementById("root")!); +root.render( - , - document.getElementById("root") + ); diff --git a/ui/src/test-utils.tsx b/ui/src/test-utils.tsx index c62770b928..c180b01872 100644 --- a/ui/src/test-utils.tsx +++ b/ui/src/test-utils.tsx @@ -1,12 +1,12 @@ import React from "react"; -import { render } from "@testing-library/react"; +import { render, RenderOptions } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "react-query"; import { QueryParamProvider } from "use-query-params"; import { MemoryRouter as Router } from "react-router-dom"; import RouteAdapter from "./hacks/RouteAdapter"; interface ProvidersProps { - children: React.ReactElement; + children: React.ReactNode; } const queryClient = new QueryClient(); @@ -27,7 +27,7 @@ const AllTheProviders = ({ children }: ProvidersProps) => { const customRender = ( ui: React.ReactElement, - options?: Record + options?: Omit ) => render(ui, { wrapper: AllTheProviders, ...options }); // re-export everything diff --git a/ui/yarn.lock b/ui/yarn.lock index c79ca558a9..27cc4076a3 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +"@adobe/css-tools@^4.4.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.0.tgz#728c484f4e10df03d5a3acd0d8adcbbebff8ad63" + integrity sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ== + "@ampproject/remapping@^2.1.0": version "2.1.2" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.1.2.tgz#4edca94973ded9630d20101cd8559cedb8d8bd34" @@ -1164,7 +1169,7 @@ core-js-pure "^3.20.2" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.16.3", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.7.tgz#03ff99f64106588c9c403c6ecb8c3bafbbdff1fa" integrity sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ== @@ -1185,6 +1190,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.12.5", "@babel/runtime@^7.23.8", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.7.tgz#7ffb53c37a8f247c8c4d335e89cdf16a2e0d0fb6" + integrity sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.16.7", "@babel/template@^7.3.3": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155" @@ -2113,49 +2125,44 @@ "@svgr/plugin-svgo" "^5.5.0" loader-utils "^2.0.0" -"@testing-library/dom@^8.0.0": - version "8.11.3" - resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.11.3.tgz#38fd63cbfe14557021e88982d931e33fb7c1a808" - integrity sha512-9LId28I+lx70wUiZjLvi1DB/WT2zGOxUh46glrSNMaWVx849kKAluezVzZrXJfTKKoQTmEOutLes/bHg4Bj3aA== +"@testing-library/dom@^10.4.0": + version "10.4.0" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-10.4.0.tgz#82a9d9462f11d240ecadbf406607c6ceeeff43a8" + integrity sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ== dependencies: "@babel/code-frame" "^7.10.4" "@babel/runtime" "^7.12.5" - "@types/aria-query" "^4.2.0" - aria-query "^5.0.0" + "@types/aria-query" "^5.0.1" + aria-query "5.3.0" chalk "^4.1.0" dom-accessibility-api "^0.5.9" - lz-string "^1.4.4" + lz-string "^1.5.0" pretty-format "^27.0.2" -"@testing-library/jest-dom@^5.14.1": - version "5.16.1" - resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.16.1.tgz#3db7df5ae97596264a7da9696fe14695ba02e51f" - integrity sha512-ajUJdfDIuTCadB79ukO+0l8O+QwN0LiSxDaYUTI4LndbbUsGi6rWU1SCexXzBA2NSjlVB9/vbkasQIL3tmPBjw== +"@testing-library/jest-dom@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.5.0.tgz#50484da3f80fb222a853479f618a9ce5c47bfe54" + integrity sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA== dependencies: - "@babel/runtime" "^7.9.2" - "@types/testing-library__jest-dom" "^5.9.1" + "@adobe/css-tools" "^4.4.0" aria-query "^5.0.0" chalk "^3.0.0" - css "^3.0.0" css.escape "^1.5.1" - dom-accessibility-api "^0.5.6" - lodash "^4.17.15" + dom-accessibility-api "^0.6.3" + lodash "^4.17.21" redent "^3.0.0" -"@testing-library/react@^12.0.0": - version "12.1.2" - resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-12.1.2.tgz#f1bc9a45943461fa2a598bb4597df1ae044cfc76" - integrity sha512-ihQiEOklNyHIpo2Y8FREkyD1QAea054U0MVbwH1m8N9TxeFz+KoJ9LkqoKqJlzx2JDm56DVwaJ1r36JYxZM05g== +"@testing-library/react@^16.0.1": + version "16.0.1" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-16.0.1.tgz#29c0ee878d672703f5e7579f239005e4e0faa875" + integrity sha512-dSmwJVtJXmku+iocRhWOUFbrERC76TX2Mnf0ATODz8brzAZrMBbzLwQixlBSanZxR6LddK3eiwpSFZgDET1URg== dependencies: "@babel/runtime" "^7.12.5" - "@testing-library/dom" "^8.0.0" -"@testing-library/user-event@^13.2.1": - version "13.5.0" - resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-13.5.0.tgz#69d77007f1e124d55314a2b73fd204b333b13295" - integrity sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg== - dependencies: - "@babel/runtime" "^7.12.5" +"@testing-library/user-event@^14.5.2": + version "14.5.2" + resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.5.2.tgz#db7257d727c891905947bd1c1a99da20e03c2ebd" + integrity sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ== "@tootallnate/once@1": version "1.1.2" @@ -2167,10 +2174,10 @@ resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== -"@types/aria-query@^4.2.0": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.2.tgz#ed4e0ad92306a704f9fb132a0cfcf77486dbe2bc" - integrity sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig== +"@types/aria-query@^5.0.1": + version "5.0.4" + resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.4.tgz#1a31c3d378850d2778dabb6374d036dcba4ba708" + integrity sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw== "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14": version "7.1.18" @@ -2573,7 +2580,7 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@*", "@types/jest@^27.0.1": +"@types/jest@^27.0.1": version "27.4.0" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.4.0.tgz#037ab8b872067cae842a320841693080f9cb84ed" integrity sha512-gHl8XuC1RZ8H2j5sHv/JqsaxXkDDM9iDOgu0Wp8sjs4u/snb2PVehyWXJPr+ORA0RPpgw231mnutWI1+0hgjIQ== @@ -2691,10 +2698,10 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== -"@types/react-dom@^17.0.9": - version "17.0.11" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.11.tgz#e1eadc3c5e86bdb5f7684e00274ae228e7bcc466" - integrity sha512-f96K3k+24RaLGVu/Y2Ng3e1EbZ8/cVJvypZWd7cy0ofCBaf2lcM46xNhycMZ2xGwbBjRql7hOlZ+e2WlJ5MH3Q== +"@types/react-dom@^18.3.0": + version "18.3.0" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.0.tgz#0cbc818755d87066ab6ca74fbedb2547d74a82b0" + integrity sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg== dependencies: "@types/react" "*" @@ -2705,7 +2712,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^17.0.20": +"@types/react@*": version "17.0.38" resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.38.tgz#f24249fefd89357d5fa71f739a686b8d7c7202bd" integrity sha512-SI92X1IA+FMnP3qM5m4QReluXzhcmovhZnLNm3pyeQlooi02qI7sLiepEYqT678uNiyc25XfCqxREFpy3W7YhQ== @@ -2714,6 +2721,14 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/react@^18.3.11": + version "18.3.11" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.11.tgz#9d530601ff843ee0d7030d4227ea4360236bd537" + integrity sha512-r6QZ069rFTjrEYgFdOck1gK7FLVsgJE7tTz0pQBczlBNUhBNk0MQH4UbnFSwjpQLMkLzgqvBBa+qGpLje16eTQ== + dependencies: + "@types/prop-types" "*" + csstype "^3.0.2" + "@types/refractor@^3.4.0": version "3.4.1" resolved "https://registry.yarnpkg.com/@types/refractor/-/refractor-3.4.1.tgz#8b109804f77b3da8fad543d3f575fef1ece8835a" @@ -2772,13 +2787,6 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== -"@types/testing-library__jest-dom@^5.9.1": - version "5.14.2" - resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.2.tgz#564fb2b2dc827147e937a75b639a05d17ce18b44" - integrity sha512-vehbtyHUShPxIa9SioxDwCvgxukDMH//icJG90sXQBUm5lJOHLT5kNeU9tnivhnA/TkOFMzGIXN2cTc4hY8/kg== - dependencies: - "@types/jest" "*" - "@types/through@*": version "0.0.30" resolved "https://registry.yarnpkg.com/@types/through/-/through-0.0.30.tgz#e0e42ce77e897bd6aead6f6ea62aeb135b8a3895" @@ -3253,6 +3261,13 @@ aria-hidden@^1.2.2: dependencies: tslib "^2.0.0" +aria-query@5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e" + integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== + dependencies: + dequal "^2.0.3" + aria-query@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b" @@ -3342,11 +3357,6 @@ at-least-node@^1.0.0: resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== -atob@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" - integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== - attr-accept@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b" @@ -3564,9 +3574,9 @@ bfj@^7.0.2: tryer "^1.0.1" big-integer@^1.6.16: - version "1.6.51" - resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686" - integrity sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg== + version "1.6.52" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.52.tgz#60a887f3047614a8e1bffe5d7173490a97dc8c85" + integrity sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg== big.js@^5.2.2: version "5.2.2" @@ -4100,7 +4110,7 @@ compression@^1.7.4: concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== confusing-browser-globals@^1.0.11: version "1.0.11" @@ -4361,15 +4371,6 @@ css.escape@^1.5.1: resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" integrity sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s= -css@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/css/-/css-3.0.0.tgz#4447a4d58fdd03367c516ca9f64ae365cee4aa5d" - integrity sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ== - dependencies: - inherits "^2.0.4" - source-map "^0.6.1" - source-map-resolve "^0.6.0" - cssdb@^5.0.0: version "5.1.0" resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-5.1.0.tgz#ec728d5f5c0811debd0820cbebda505d43003400" @@ -4853,6 +4854,11 @@ depd@~1.1.2: resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= +dequal@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" + integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== + destroy@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" @@ -4946,10 +4952,15 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" -dom-accessibility-api@^0.5.6, dom-accessibility-api@^0.5.9: - version "0.5.11" - resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.11.tgz#79d5846c4f90eba3e617d9031e921de9324f84ed" - integrity sha512-7X6GvzjYf4yTdRKuCVScV+aA9Fvh5r8WzWrXBH9w82ZWB/eYDMGCnazoC/YAqAzUJWHzLOnZqr46K3iEyUhUvw== +dom-accessibility-api@^0.5.9: + version "0.5.16" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" + integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== + +dom-accessibility-api@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz#993e925cc1d73f2c662e7d75dd5a5445259a8fd8" + integrity sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w== dom-converter@^0.2.0: version "0.2.0" @@ -5868,7 +5879,7 @@ fs-monkey@^1.0.4: fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== fsevents@^2.3.2, fsevents@~2.3.2: version "2.3.2" @@ -5960,7 +5971,7 @@ glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: +glob@^7.1.1, glob@^7.1.2, glob@^7.1.4, glob@^7.1.6: version "7.2.0" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== @@ -5972,6 +5983,18 @@ glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^7.1.3: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + glob@^8.0.0: version "8.0.3" resolved "https://registry.yarnpkg.com/glob/-/glob-8.0.3.tgz#415c6eb2deed9e502c68fa44a272e6da6eeca42e" @@ -6478,7 +6501,7 @@ indent-string@^4.0.0: inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== dependencies: once "^1.3.0" wrappy "1" @@ -7696,10 +7719,10 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -lz-string@^1.4.4: - version "1.4.4" - resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" - integrity sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY= +lz-string@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" + integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ== magic-string@^0.25.0, magic-string@^0.25.7: version "0.25.7" @@ -7749,12 +7772,12 @@ marked@^4.0.10: integrity sha512-+Z6KDjSPa6/723PQYyc1axYZpYYpDnECDaU6hkaf5gqBieBkMKYReL5hteF2QizhlMbgbo8umXl/clZ67+GlsA== match-sorter@^6.0.2: - version "6.3.1" - resolved "https://registry.yarnpkg.com/match-sorter/-/match-sorter-6.3.1.tgz#98cc37fda756093424ddf3cbc62bfe9c75b92bda" - integrity sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw== + version "6.3.4" + resolved "https://registry.yarnpkg.com/match-sorter/-/match-sorter-6.3.4.tgz#afa779d8e922c81971fbcb4781c7003ace781be7" + integrity sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg== dependencies: - "@babel/runtime" "^7.12.5" - remove-accents "0.4.2" + "@babel/runtime" "^7.23.8" + remove-accents "0.5.0" mdast-util-definitions@^4.0.0: version "4.0.0" @@ -7917,7 +7940,7 @@ minimatch@3.0.4: dependencies: brace-expansion "^1.1.7" -minimatch@^3.0.2, minimatch@^3.0.4: +minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.1.1: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -8015,7 +8038,7 @@ mute-stream@0.0.8: nano-time@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/nano-time/-/nano-time-1.0.0.tgz#b0554f69ad89e22d0907f7a12b0993a5d96137ef" - integrity sha1-sFVPaa2J4i0JB/ehKwmTpdlhN+8= + integrity sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA== dependencies: big-integer "^1.6.16" @@ -8239,7 +8262,7 @@ on-headers@~1.0.2: once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== dependencies: wrappy "1" @@ -8462,7 +8485,7 @@ path-exists@^4.0.0: path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" @@ -9072,7 +9095,7 @@ pretty-error@^4.0.0: lodash "^4.17.20" renderkid "^3.0.0" -pretty-format@^27.0.0, pretty-format@^27.0.2, pretty-format@^27.4.6: +pretty-format@^27.0.0, pretty-format@^27.4.6: version "27.4.6" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.4.6.tgz#1b784d2f53c68db31797b2348fa39b49e31846b7" integrity sha512-NblstegA1y/RJW2VyML+3LlpFjzx62cUrtBIKIWDXEDkjNeleA7Od7nrzcs/VLQvAeV4CgSYhrN39DRN88Qi/g== @@ -9081,6 +9104,15 @@ pretty-format@^27.0.0, pretty-format@^27.0.2, pretty-format@^27.4.6: ansi-styles "^5.0.0" react-is "^17.0.1" +pretty-format@^27.0.2: + version "27.5.1" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" + integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== + dependencies: + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" + react-is "^17.0.1" + prismjs@^1.8.4: version "1.29.0" resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.29.0.tgz#f113555a8fa9b57c35e637bba27509dcf802dd12" @@ -9316,14 +9348,13 @@ react-dev-utils@^12.0.0: strip-ansi "^6.0.1" text-table "^0.2.0" -react-dom@^17.0.2: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" - integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== +react-dom@^18.3.1: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" + integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== dependencies: loose-envify "^1.1.0" - object-assign "^4.1.1" - scheduler "^0.20.2" + scheduler "^0.23.2" react-dropzone@^11.7.1: version "11.7.1" @@ -9392,10 +9423,10 @@ react-is@^18.0.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== -react-query@^3.34.12: - version "3.34.12" - resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.34.12.tgz#dcaaf7b629f0868aae8afef9fb7692f6ea7643bf" - integrity sha512-flDdudQVH4CqE+kNYYYyo4g2Yjek3H/36G3b9bK5oe26jD5gFnx+PPwnq0gayq5z2dcSfr2z4+drvuyeZ3A5QQ== +react-query@^3.39.3: + version "3.39.3" + resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.39.3.tgz#4cea7127c6c26bdea2de5fb63e51044330b03f35" + integrity sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g== dependencies: "@babel/runtime" "^7.5.5" broadcast-channel "^3.4.1" @@ -9540,13 +9571,12 @@ react-window@^1.8.10: "@babel/runtime" "^7.0.0" memoize-one ">=3.1.1 <6" -react@^17.0.2: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" - integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== +react@^18.3.1: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" + integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== dependencies: loose-envify "^1.1.0" - object-assign "^4.1.1" readable-stream@^2.0.1: version "2.3.7" @@ -9629,12 +9659,12 @@ regenerate@^1.4.2: resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== -regenerator-runtime@^0.13.11: +regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.4: version "0.13.11" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== -regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.9: +regenerator-runtime@^0.13.9: version "0.13.9" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== @@ -9764,10 +9794,10 @@ remark-rehype@^8.1.0: dependencies: mdast-util-to-hast "^10.2.0" -remove-accents@0.4.2: - version "0.4.2" - resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.4.2.tgz#0a43d3aaae1e80db919e07ae254b285d9e1c7bb5" - integrity sha1-CkPTqq4egNuRngeuJUsoXZ4ce7U= +remove-accents@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.5.0.tgz#77991f37ba212afba162e375b627631315bed687" + integrity sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A== renderkid@^3.0.0: version "3.0.0" @@ -10008,13 +10038,12 @@ saxes@^5.0.1: dependencies: xmlchars "^2.2.0" -scheduler@^0.20.2: - version "0.20.2" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" - integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== +scheduler@^0.23.2: + version "0.23.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" + integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== dependencies: loose-envify "^1.1.0" - object-assign "^4.1.1" schema-utils@2.7.0: version "2.7.0" @@ -10258,14 +10287,6 @@ source-map-loader@^3.0.0: iconv-lite "^0.6.3" source-map-js "^1.0.1" -source-map-resolve@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.6.0.tgz#3d9df87e236b53f16d01e58150fc7711138e5ed2" - integrity sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w== - dependencies: - atob "^2.1.2" - decode-uri-component "^0.2.0" - source-map-support@^0.5.6, source-map-support@~0.5.20: version "0.5.21" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" @@ -11752,7 +11773,7 @@ wrap-ansi@^7.0.0: wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== write-file-atomic@^3.0.0: version "3.0.3" From 63dca1f217f491c1b275ad4a2a72c47925a46906 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Oct 2024 15:49:09 +0000 Subject: [PATCH 140/185] chore: Bump golang.org/x/net from 0.19.0 to 0.23.0 in /infra/feast-operator (#4614) chore: Bump golang.org/x/net in /infra/feast-operator Bumps [golang.org/x/net](https://github.com/golang/net) from 0.19.0 to 0.23.0. - [Commits](https://github.com/golang/net/compare/v0.19.0...v0.23.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- infra/feast-operator/go.mod | 6 +++--- infra/feast-operator/go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/infra/feast-operator/go.mod b/infra/feast-operator/go.mod index d39211dea8..65d2aaac50 100644 --- a/infra/feast-operator/go.mod +++ b/infra/feast-operator/go.mod @@ -48,10 +48,10 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect - golang.org/x/net v0.19.0 // indirect + golang.org/x/net v0.23.0 // indirect golang.org/x/oauth2 v0.12.0 // indirect - golang.org/x/sys v0.16.0 // indirect - golang.org/x/term v0.15.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/term v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.3.0 // indirect golang.org/x/tools v0.16.1 // indirect diff --git a/infra/feast-operator/go.sum b/infra/feast-operator/go.sum index 9b3607f06f..be475e1101 100644 --- a/infra/feast-operator/go.sum +++ b/infra/feast-operator/go.sum @@ -128,8 +128,8 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4= golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -139,10 +139,10 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= From c0a10269914c2ca01fe1cf6b24b120bfa58d04e7 Mon Sep 17 00:00:00 2001 From: Tom Bourton Date: Tue, 15 Oct 2024 16:49:26 +0100 Subject: [PATCH 141/185] fix: Correctly handle list values in _python_value_to_proto_value (#4608) --- sdk/python/feast/type_map.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/sdk/python/feast/type_map.py b/sdk/python/feast/type_map.py index 315b73909e..8a88c24ffc 100644 --- a/sdk/python/feast/type_map.py +++ b/sdk/python/feast/type_map.py @@ -372,11 +372,18 @@ def _python_value_to_proto_value( if feast_value_type == ValueType.BYTES_LIST: raise _type_err(sample, ValueType.BYTES_LIST) - json_value = json.loads(sample) - if isinstance(json_value, list): + json_sample = json.loads(sample) + if isinstance(json_sample, list): + json_values = [json.loads(value) for value in values] if feast_value_type == ValueType.BOOL_LIST: - json_value = [bool(item) for item in json_value] - return [ProtoValue(**{field_name: proto_type(val=json_value)})] # type: ignore + json_values = [ + [bool(item) for item in list_item] + for list_item in json_values + ] + return [ + ProtoValue(**{field_name: proto_type(val=v)}) # type: ignore + for v in json_values + ] raise _type_err(sample, valid_types[0]) if sample is not None and not all( @@ -506,7 +513,14 @@ def python_values_to_proto_values( if value_type == ValueType.UNKNOWN: raise TypeError("Couldn't infer value type from empty value") - return _python_value_to_proto_value(value_type, values) + proto_values = _python_value_to_proto_value(value_type, values) + + if len(proto_values) != len(values): + raise ValueError( + f"Number of proto values {len(proto_values)} does not match number of values {len(values)}" + ) + + return proto_values def _proto_value_to_value_type(proto_value: ProtoValue) -> ValueType: From ca9fb9bc8c3f3b06b4ba5fce362e26633144715c Mon Sep 17 00:00:00 2001 From: Daniele Martinoli <86618610+dmartinol@users.noreply.github.com> Date: Tue, 15 Oct 2024 17:52:58 +0200 Subject: [PATCH 142/185] fix: Replaced ClusterRoles with local RoleBindings (#4625) * replaced fetching ClusterRoles with local RoleBindings Signed-off-by: Daniele Martinoli * added missing namespace configurations Signed-off-by: Daniele Martinoli --------- Signed-off-by: Daniele Martinoli --- .../components/authz_manager.md | 12 ++++- .../server/k8s/server_resources.yaml | 16 +++--- .../auth/kubernetes_token_parser.py | 50 +++++++++++-------- .../tests/unit/permissions/auth/conftest.py | 34 +++---------- .../permissions/auth/server/mock_utils.py | 14 +++--- .../permissions/auth/test_token_parser.py | 47 ++++++++--------- 6 files changed, 85 insertions(+), 88 deletions(-) diff --git a/docs/getting-started/components/authz_manager.md b/docs/getting-started/components/authz_manager.md index 20fcdca107..4014c697ef 100644 --- a/docs/getting-started/components/authz_manager.md +++ b/docs/getting-started/components/authz_manager.md @@ -114,4 +114,14 @@ auth: In case the client cannot run on the same cluster as the servers, the client token can be injected using the `LOCAL_K8S_TOKEN` environment variable on the client side. The value must refer to the token of a service account created on the servers cluster -and linked to the desired RBAC roles. \ No newline at end of file +and linked to the desired RBAC roles. + +#### Setting Up Kubernetes RBAC for Feast + +To ensure the Kubernetes RBAC environment aligns with Feast's RBAC configuration, follow these guidelines: +* The roles defined in Feast `Permission` instances must have corresponding Kubernetes RBAC `Role` names. +* The Kubernetes RBAC `Role` must reside in the same namespace as the Feast service. +* The client application can run in a different namespace, using its own dedicated `ServiceAccount`. +* Finally, the `RoleBinding` that links the client `ServiceAccount` to the RBAC `Role` must be defined in the namespace of the Feast service. + +If the above rules are satisfied, the Feast service must be granted permissions to fetch `RoleBinding` instances from the local namespace. \ No newline at end of file diff --git a/examples/rbac-remote/server/k8s/server_resources.yaml b/examples/rbac-remote/server/k8s/server_resources.yaml index 03e35495d6..21ffcaa68a 100644 --- a/examples/rbac-remote/server/k8s/server_resources.yaml +++ b/examples/rbac-remote/server/k8s/server_resources.yaml @@ -5,23 +5,25 @@ metadata: namespace: feast-dev --- apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole +kind: Role metadata: - name: feast-cluster-role + name: feast-role + namespace: feast-dev rules: - apiGroups: ["rbac.authorization.k8s.io"] - resources: ["roles", "rolebindings", "clusterrolebindings"] + resources: ["roles", "rolebindings"] verbs: ["get", "list", "watch"] --- apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding +kind: RoleBinding metadata: - name: feast-cluster-rolebinding + name: feast-rolebinding + namespace: feast-dev subjects: - kind: ServiceAccount name: feast-sa namespace: feast-dev roleRef: apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: feast-cluster-role + kind: Role + name: feast-role diff --git a/sdk/python/feast/permissions/auth/kubernetes_token_parser.py b/sdk/python/feast/permissions/auth/kubernetes_token_parser.py index c34ebf386d..8108703119 100644 --- a/sdk/python/feast/permissions/auth/kubernetes_token_parser.py +++ b/sdk/python/feast/permissions/auth/kubernetes_token_parser.py @@ -11,6 +11,7 @@ from feast.permissions.user import User logger = logging.getLogger(__name__) +_namespace_file_path = "/var/run/secrets/kubernetes.io/serviceaccount/namespace" class KubernetesTokenParser(TokenParser): @@ -48,24 +49,42 @@ async def user_details_from_access_token(self, access_token: str) -> User: if sa_name is not None and sa_name == intra_communication_base64: return User(username=sa_name, roles=[]) else: - roles = self.get_roles(sa_namespace, sa_name) - logger.info(f"Roles for ServiceAccount {sa_name}: {roles}") + current_namespace = self._read_namespace_from_file() + logger.info( + f"Looking for ServiceAccount roles of {sa_namespace}:{sa_name} in {current_namespace}" + ) + roles = self.get_roles( + current_namespace=current_namespace, + service_account_namespace=sa_namespace, + service_account_name=sa_name, + ) + logger.info(f"Roles: {roles}") return User(username=current_user, roles=roles) - def get_roles(self, namespace: str, service_account_name: str) -> list[str]: + def _read_namespace_from_file(self): + try: + with open(_namespace_file_path, "r") as file: + namespace = file.read().strip() + return namespace + except Exception as e: + raise e + + def get_roles( + self, + current_namespace: str, + service_account_namespace: str, + service_account_name: str, + ) -> list[str]: """ - Fetches the Kubernetes `Role`s associated to the given `ServiceAccount` in the given `namespace`. + Fetches the Kubernetes `Role`s associated to the given `ServiceAccount` in `current_namespace` namespace. - The research also includes the `ClusterRole`s, so the running deployment must be granted enough permissions to query - for such instances in all the namespaces. + The running deployment must be granted enough permissions to query for such instances in this namespace. Returns: - list[str]: Name of the `Role`s and `ClusterRole`s associated to the service account. No string manipulation is performed on the role name. + list[str]: Name of the `Role`s associated to the service account. No string manipulation is performed on the role name. """ - role_bindings = self.rbac_v1.list_namespaced_role_binding(namespace) - cluster_role_bindings = self.rbac_v1.list_cluster_role_binding() - + role_bindings = self.rbac_v1.list_namespaced_role_binding(current_namespace) roles: set[str] = set() for binding in role_bindings.items: @@ -74,16 +93,7 @@ def get_roles(self, namespace: str, service_account_name: str) -> list[str]: if ( subject.kind == "ServiceAccount" and subject.name == service_account_name - ): - roles.add(binding.role_ref.name) - - for binding in cluster_role_bindings.items: - if binding.subjects is not None: - for subject in binding.subjects: - if ( - subject.kind == "ServiceAccount" - and subject.name == service_account_name - and subject.namespace == namespace + and subject.namespace == service_account_namespace ): roles.add(binding.role_ref.name) diff --git a/sdk/python/tests/unit/permissions/auth/conftest.py b/sdk/python/tests/unit/permissions/auth/conftest.py index 5a29f8ec78..246a8c12f2 100644 --- a/sdk/python/tests/unit/permissions/auth/conftest.py +++ b/sdk/python/tests/unit/permissions/auth/conftest.py @@ -20,46 +20,28 @@ def sa_name(): @pytest.fixture -def namespace(): +def my_namespace(): return "my-ns" @pytest.fixture -def rolebindings(sa_name, namespace) -> dict: - roles = ["reader", "writer"] - items = [] - for r in roles: - items.append( - client.V1RoleBinding( - metadata=client.V1ObjectMeta(name=r, namespace=namespace), - subjects=[ - client.V1Subject( - kind="ServiceAccount", - name=sa_name, - api_group="rbac.authorization.k8s.io", - ) - ], - role_ref=client.V1RoleRef( - kind="Role", name=r, api_group="rbac.authorization.k8s.io" - ), - ) - ) - return {"items": client.V1RoleBindingList(items=items), "roles": roles} +def sa_namespace(): + return "sa-ns" @pytest.fixture -def clusterrolebindings(sa_name, namespace) -> dict: - roles = ["updater"] +def rolebindings(my_namespace, sa_name, sa_namespace) -> dict: + roles = ["reader", "writer"] items = [] for r in roles: items.append( - client.V1ClusterRoleBinding( - metadata=client.V1ObjectMeta(name=r, namespace=namespace), + client.V1RoleBinding( + metadata=client.V1ObjectMeta(name=r, namespace=my_namespace), subjects=[ client.V1Subject( kind="ServiceAccount", name=sa_name, - namespace=namespace, + namespace=sa_namespace, api_group="rbac.authorization.k8s.io", ) ], diff --git a/sdk/python/tests/unit/permissions/auth/server/mock_utils.py b/sdk/python/tests/unit/permissions/auth/server/mock_utils.py index 12f7785b05..5bde4d4ecb 100644 --- a/sdk/python/tests/unit/permissions/auth/server/mock_utils.py +++ b/sdk/python/tests/unit/permissions/auth/server/mock_utils.py @@ -53,11 +53,11 @@ async def mock_oath2(self, request): def mock_kubernetes(request, monkeypatch): + my_namespace = request.getfixturevalue("my_namespace") sa_name = request.getfixturevalue("sa_name") - namespace = request.getfixturevalue("namespace") - subject = f"system:serviceaccount:{namespace}:{sa_name}" + sa_namespace = request.getfixturevalue("sa_namespace") + subject = f"system:serviceaccount:{sa_namespace}:{sa_name}" rolebindings = request.getfixturevalue("rolebindings") - clusterrolebindings = request.getfixturevalue("clusterrolebindings") monkeypatch.setattr( "feast.permissions.auth.kubernetes_token_parser.config.load_incluster_config", @@ -71,11 +71,11 @@ def mock_kubernetes(request, monkeypatch): "feast.permissions.auth.kubernetes_token_parser.client.RbacAuthorizationV1Api.list_namespaced_role_binding", lambda *args, **kwargs: rolebindings["items"], ) - monkeypatch.setattr( - "feast.permissions.auth.kubernetes_token_parser.client.RbacAuthorizationV1Api.list_cluster_role_binding", - lambda *args, **kwargs: clusterrolebindings["items"], - ) monkeypatch.setattr( "feast.permissions.client.kubernetes_auth_client_manager.KubernetesAuthClientManager.get_token", lambda self: "my-token", ) + monkeypatch.setattr( + "feast.permissions.auth.kubernetes_token_parser.KubernetesTokenParser._read_namespace_from_file", + lambda self: my_namespace, + ) diff --git a/sdk/python/tests/unit/permissions/auth/test_token_parser.py b/sdk/python/tests/unit/permissions/auth/test_token_parser.py index bac2103b4f..8ac0f2b6d5 100644 --- a/sdk/python/tests/unit/permissions/auth/test_token_parser.py +++ b/sdk/python/tests/unit/permissions/auth/test_token_parser.py @@ -145,27 +145,26 @@ async def mock_oath2(self, request): @patch( "feast.permissions.auth.kubernetes_token_parser.client.RbacAuthorizationV1Api.list_namespaced_role_binding" ) -@patch( - "feast.permissions.auth.kubernetes_token_parser.client.RbacAuthorizationV1Api.list_cluster_role_binding" -) def test_k8s_token_validation_success( - mock_crb, mock_rb, mock_jwt, mock_config, rolebindings, - clusterrolebindings, + monkeypatch, + my_namespace, + sa_name, + sa_namespace, ): - sa_name = "my-name" - namespace = "my-ns" - subject = f"system:serviceaccount:{namespace}:{sa_name}" + monkeypatch.setattr( + "feast.permissions.auth.kubernetes_token_parser.KubernetesTokenParser._read_namespace_from_file", + lambda self: my_namespace, + ) + subject = f"system:serviceaccount:{sa_namespace}:{sa_name}" mock_jwt.return_value = {"sub": subject} mock_rb.return_value = rolebindings["items"] - mock_crb.return_value = clusterrolebindings["items"] roles = rolebindings["roles"] - croles = clusterrolebindings["roles"] access_token = "aaa-bbb-ccc" token_parser = KubernetesTokenParser() @@ -175,12 +174,10 @@ def test_k8s_token_validation_success( assertpy.assert_that(user).is_type_of(User) if isinstance(user, User): - assertpy.assert_that(user.username).is_equal_to(f"{namespace}:{sa_name}") - assertpy.assert_that(user.roles.sort()).is_equal_to((roles + croles).sort()) + assertpy.assert_that(user.username).is_equal_to(f"{sa_namespace}:{sa_name}") + assertpy.assert_that(user.roles.sort()).is_equal_to(roles.sort()) for r in roles: assertpy.assert_that(user.has_matching_role([r])).is_true() - for cr in croles: - assertpy.assert_that(user.has_matching_role([cr])).is_true() assertpy.assert_that(user.has_matching_role(["foo"])).is_false() @@ -212,30 +209,29 @@ def test_k8s_inter_server_comm( oidc_config, request, rolebindings, - clusterrolebindings, monkeypatch, ): if is_intra_server: subject = f":::{intra_communication_val}" else: sa_name = request.getfixturevalue("sa_name") - namespace = request.getfixturevalue("namespace") - subject = f"system:serviceaccount:{namespace}:{sa_name}" + sa_namespace = request.getfixturevalue("sa_namespace") + my_namespace = request.getfixturevalue("my_namespace") + subject = f"system:serviceaccount:{sa_namespace}:{sa_name}" rolebindings = request.getfixturevalue("rolebindings") - clusterrolebindings = request.getfixturevalue("clusterrolebindings") monkeypatch.setattr( "feast.permissions.auth.kubernetes_token_parser.client.RbacAuthorizationV1Api.list_namespaced_role_binding", lambda *args, **kwargs: rolebindings["items"], ) - monkeypatch.setattr( - "feast.permissions.auth.kubernetes_token_parser.client.RbacAuthorizationV1Api.list_cluster_role_binding", - lambda *args, **kwargs: clusterrolebindings["items"], - ) monkeypatch.setattr( "feast.permissions.client.kubernetes_auth_client_manager.KubernetesAuthClientManager.get_token", lambda self: "my-token", ) + monkeypatch.setattr( + "feast.permissions.auth.kubernetes_token_parser.KubernetesTokenParser._read_namespace_from_file", + lambda self: my_namespace, + ) monkeypatch.setattr( "feast.permissions.auth.kubernetes_token_parser.config.load_incluster_config", @@ -248,7 +244,6 @@ def test_k8s_inter_server_comm( ) roles = rolebindings["roles"] - croles = clusterrolebindings["roles"] access_token = "aaa-bbb-ccc" token_parser = KubernetesTokenParser() @@ -263,10 +258,8 @@ def test_k8s_inter_server_comm( else: assertpy.assert_that(user).is_type_of(User) if isinstance(user, User): - assertpy.assert_that(user.username).is_equal_to(f"{namespace}:{sa_name}") - assertpy.assert_that(user.roles.sort()).is_equal_to((roles + croles).sort()) + assertpy.assert_that(user.username).is_equal_to(f"{sa_namespace}:{sa_name}") + assertpy.assert_that(user.roles.sort()).is_equal_to(roles.sort()) for r in roles: assertpy.assert_that(user.has_matching_role([r])).is_true() - for cr in croles: - assertpy.assert_that(user.has_matching_role([cr])).is_true() assertpy.assert_that(user.has_matching_role(["foo"])).is_false() From 9e1363671e7857182530d924ff24f1fcc53ac4d5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Oct 2024 16:04:56 +0000 Subject: [PATCH 143/185] chore: Bump org.apache.avro:avro from 1.11.3 to 1.11.4 in /java/serving (#4610) Bumps org.apache.avro:avro from 1.11.3 to 1.11.4. --- updated-dependencies: - dependency-name: org.apache.avro:avro dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- java/serving/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/serving/pom.xml b/java/serving/pom.xml index cc3be9d166..ca7f8a73b5 100644 --- a/java/serving/pom.xml +++ b/java/serving/pom.xml @@ -287,7 +287,7 @@ org.apache.avro avro - 1.11.3 + 1.11.4 From 95fe8c27cdbf8ef5e40d46a512d36d730efd4167 Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Tue, 15 Oct 2024 12:35:36 -0400 Subject: [PATCH 144/185] chore: Updated tests to confirm behavior of inconsistent offline/online return values (#4615) --- .../test_on_demand_python_transformation.py | 153 +++++++++++++++++- 1 file changed, 152 insertions(+), 1 deletion(-) diff --git a/sdk/python/tests/unit/test_on_demand_python_transformation.py b/sdk/python/tests/unit/test_on_demand_python_transformation.py index 2410ee03aa..3a2ea7637b 100644 --- a/sdk/python/tests/unit/test_on_demand_python_transformation.py +++ b/sdk/python/tests/unit/test_on_demand_python_transformation.py @@ -425,6 +425,13 @@ def setUp(self): Field(name="avg_daily_trip_rank_names", dtype=Array(String)), ], ) + input_request = RequestSource( + name="vals_to_add", + schema=[ + Field(name="val_to_add", dtype=Int64), + Field(name="val_to_add_2", dtype=Int64), + ], + ) @on_demand_feature_view( sources=[request_source, driver_stats_fv], @@ -476,8 +483,37 @@ def python_view(inputs: dict[str, Any]) -> dict[str, Any]: output["achieved_ranks"] = ranks return output + @on_demand_feature_view( + sources=[ + driver_stats_fv, + input_request, + ], + schema=[ + Field(name="conv_rate_plus_val1", dtype=Float64), + Field(name="conv_rate_plus_val2", dtype=Float64), + ], + mode="pandas", + ) + def pandas_view(features_df: pd.DataFrame) -> pd.DataFrame: + df = pd.DataFrame() + df["conv_rate_plus_val1"] = ( + features_df["conv_rate"] + features_df["val_to_add"] + ) + df["conv_rate_plus_val2"] = ( + features_df["conv_rate"] + features_df["val_to_add_2"] + ) + return df + self.store.apply( - [driver, driver_stats_source, driver_stats_fv, python_view] + [ + driver, + driver_stats_source, + driver_stats_fv, + python_view, + pandas_view, + input_request, + request_source, + ] ) fv_applied = self.store.get_feature_view("driver_hourly_stats") assert fv_applied.entities == [driver.name] @@ -488,6 +524,121 @@ def python_view(inputs: dict[str, Any]) -> dict[str, Any]: feature_view_name="driver_hourly_stats", df=driver_df ) + batch_sample = pd.DataFrame(driver_entities, columns=["driver_id"]) + batch_sample["val_to_add"] = 0 + batch_sample["val_to_add_2"] = 1 + batch_sample["event_timestamp"] = start_date + batch_sample["created"] = start_date + fv_only_cols = ["driver_id", "event_timestamp", "created"] + + resp_base_fv = self.store.get_historical_features( + entity_df=batch_sample[fv_only_cols], + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "driver_hourly_stats:avg_daily_trips", + ], + ).to_df() + assert resp_base_fv is not None + assert sorted(resp_base_fv.columns) == [ + "acc_rate", + "avg_daily_trips", + "conv_rate", + "created__", + "driver_id", + "event_timestamp", + ] + resp = self.store.get_historical_features( + entity_df=batch_sample, + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "driver_hourly_stats:avg_daily_trips", + "pandas_view:conv_rate_plus_val1", + "pandas_view:conv_rate_plus_val2", + ], + ).to_df() + assert resp is not None + assert resp["conv_rate_plus_val1"].isnull().sum() == 0 + + # Now testing feature retrieval for driver ids not in the dataset + missing_batch_sample = pd.DataFrame([1234567890], columns=["driver_id"]) + missing_batch_sample["val_to_add"] = 0 + missing_batch_sample["val_to_add_2"] = 1 + missing_batch_sample["event_timestamp"] = start_date + missing_batch_sample["created"] = start_date + resp_offline = self.store.get_historical_features( + entity_df=missing_batch_sample, + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "driver_hourly_stats:avg_daily_trips", + "pandas_view:conv_rate_plus_val1", + "pandas_view:conv_rate_plus_val2", + ], + ).to_df() + assert resp_offline is not None + assert resp_offline["conv_rate_plus_val1"].isnull().sum() == 1 + assert sorted(resp_offline.columns) == [ + "acc_rate", + "avg_daily_trips", + "conv_rate", + "conv_rate_plus_val1", + "conv_rate_plus_val2", + "created__", + "driver_id", + "event_timestamp", + "val_to_add", + "val_to_add_2", + ] + with pytest.raises(TypeError): + _ = self.store.get_online_features( + entity_rows=[ + {"driver_id": 1234567890, "val_to_add": 0, "val_to_add_2": 1} + ], + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "driver_hourly_stats:avg_daily_trips", + "pandas_view:conv_rate_plus_val1", + "pandas_view:conv_rate_plus_val2", + ], + ) + resp_online = self.store.get_online_features( + entity_rows=[{"driver_id": 1001, "val_to_add": 0, "val_to_add_2": 1}], + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "driver_hourly_stats:avg_daily_trips", + "pandas_view:conv_rate_plus_val1", + "pandas_view:conv_rate_plus_val2", + ], + ).to_df() + assert resp_online is not None + assert sorted(resp_online.columns) == [ + "acc_rate", + "avg_daily_trips", + "conv_rate", + "conv_rate_plus_val1", + "conv_rate_plus_val2", + "driver_id", + # It does not have the items below + # "created__", + # "event_timestamp", + # "val_to_add", + # "val_to_add_2", + ] + # Note online and offline columns will not match because: + # you want to be space efficient online when considering the impact of network latency so you want to send + # and receive the minimally required set of data, which means after transformation you only need to send the + # output in the response. + # Offline, you will probably prioritize reproducibility and being able to iterate, which means you will want + # the underlying inputs into your transformation, so the extra data is tolerable. + assert sorted(resp_online.columns) != sorted(resp_offline.columns) + + def test_setup(self): + pass + def test_python_transformation_returning_all_data_types(self): entity_rows = [ { From f05e92861da64d1c5e9cfe3c6307b3422d0d83b8 Mon Sep 17 00:00:00 2001 From: Daniele Martinoli <86618610+dmartinol@users.noreply.github.com> Date: Wed, 16 Oct 2024 13:04:14 +0200 Subject: [PATCH 145/185] fix: Added support for multiple name patterns to Permissions (#4633) * added support for multiple name patterns to Permissions Signed-off-by: Daniele Martinoli * fixed lint issues Signed-off-by: Daniele Martinoli --------- Signed-off-by: Daniele Martinoli --- docs/getting-started/concepts/permission.md | 4 +- docs/reference/feast-cli-commands.md | 3 +- protos/feast/core/Permission.proto | 2 +- sdk/python/feast/cli.py | 2 +- sdk/python/feast/cli_utils.py | 2 +- sdk/python/feast/permissions/matcher.py | 54 ++++++++++++------ sdk/python/feast/permissions/permission.py | 40 ++++++++----- .../feast/protos/feast/core/Permission_pb2.py | 24 ++++---- .../protos/feast/core/Permission_pb2.pyi | 9 +-- .../registration/test_universal_registry.py | 10 ++-- sdk/python/tests/unit/permissions/conftest.py | 2 +- .../tests/unit/permissions/test_permission.py | 56 +++++++++++++++++-- 12 files changed, 143 insertions(+), 65 deletions(-) diff --git a/docs/getting-started/concepts/permission.md b/docs/getting-started/concepts/permission.md index 5bca1bd568..a635357968 100644 --- a/docs/getting-started/concepts/permission.md +++ b/docs/getting-started/concepts/permission.md @@ -36,7 +36,7 @@ The permission model is based on the following components: The `Permission` class identifies a single permission configured on the feature store and is identified by these attributes: - `name`: The permission name. - `types`: The list of protected resource types. Defaults to all managed types, e.g. the `ALL_RESOURCE_TYPES` alias. All sub-classes are included in the resource match. -- `name_pattern`: A regex to match the resource name. Defaults to `None`, meaning that no name filtering is applied +- `name_patterns`: A list of regex patterns to match resource names. If any regex matches, the `Permission` policy is applied. Defaults to `[]`, meaning no name filtering is applied. - `required_tags`: Dictionary of key-value pairs that must match the resource tags. Defaults to `None`, meaning that no tags filtering is applied. - `actions`: The actions authorized by this permission. Defaults to `ALL_VALUES`, an alias defined in the `action` module. - `policy`: The policy to be applied to validate a client request. @@ -95,7 +95,7 @@ The following permission grants authorization to read the offline store of all t Permission( name="reader", types=[FeatureView], - name_pattern=".*risky.*", + name_patterns=".*risky.*", # Accepts both `str` or `list[str]` types policy=RoleBasedPolicy(roles=["trusted"]), actions=[AuthzedAction.READ_OFFLINE], ) diff --git a/docs/reference/feast-cli-commands.md b/docs/reference/feast-cli-commands.md index b32db3215a..8f1a7c302e 100644 --- a/docs/reference/feast-cli-commands.md +++ b/docs/reference/feast-cli-commands.md @@ -172,9 +172,10 @@ Options: ```text +-----------------------+-------------+-----------------------+-----------+----------------+-------------------------+ -| NAME | TYPES | NAME_PATTERN | ACTIONS | ROLES | REQUIRED_TAGS | +| NAME | TYPES | NAME_PATTERNS | ACTIONS | ROLES | REQUIRED_TAGS | +=======================+=============+=======================+===========+================+================+========+ | reader_permission1234 | FeatureView | transformed_conv_rate | DESCRIBE | reader | - | +| | | driver_hourly_stats | DESCRIBE | reader | - | +-----------------------+-------------+-----------------------+-----------+----------------+-------------------------+ | writer_permission1234 | FeatureView | transformed_conv_rate | CREATE | writer | - | +-----------------------+-------------+-----------------------+-----------+----------------+-------------------------+ diff --git a/protos/feast/core/Permission.proto b/protos/feast/core/Permission.proto index 400f70a11b..8a876a0dc7 100644 --- a/protos/feast/core/Permission.proto +++ b/protos/feast/core/Permission.proto @@ -50,7 +50,7 @@ message PermissionSpec { repeated Type types = 3; - string name_pattern = 4; + repeated string name_patterns = 4; map required_tags = 5; diff --git a/sdk/python/feast/cli.py b/sdk/python/feast/cli.py index 499788101e..010493f01c 100644 --- a/sdk/python/feast/cli.py +++ b/sdk/python/feast/cli.py @@ -1211,7 +1211,7 @@ def feast_permissions_list_command(ctx: click.Context, verbose: bool, tags: list headers=[ "NAME", "TYPES", - "NAME_PATTERN", + "NAME_PATTERNS", "ACTIONS", "ROLES", "REQUIRED_TAGS", diff --git a/sdk/python/feast/cli_utils.py b/sdk/python/feast/cli_utils.py index 4152eb219b..38ebf91570 100644 --- a/sdk/python/feast/cli_utils.py +++ b/sdk/python/feast/cli_utils.py @@ -196,7 +196,7 @@ def handle_not_verbose_permissions_command( [ p.name, _to_multi_line([t.__name__ for t in p.types]), # type: ignore[union-attr, attr-defined] - p.name_pattern, + _to_multi_line(p.name_patterns), _to_multi_line([a.value.upper() for a in p.actions]), _to_multi_line(sorted(roles)), _dict_to_multi_line(p.required_tags), diff --git a/sdk/python/feast/permissions/matcher.py b/sdk/python/feast/permissions/matcher.py index 337bfd5c57..5cb0de85e3 100644 --- a/sdk/python/feast/permissions/matcher.py +++ b/sdk/python/feast/permissions/matcher.py @@ -44,7 +44,7 @@ def _get_type(resource: "FeastObject") -> Any: def resource_match_config( resource: "FeastObject", expected_types: list["FeastObject"], - name_pattern: Optional[str] = None, + name_patterns: list[str], required_tags: Optional[dict[str, str]] = None, ) -> bool: """ @@ -53,7 +53,7 @@ def resource_match_config( Args: resource: A FeastObject instance to match agains the permission. expected_types: The list of object types configured in the permission. Type match also includes all the sub-classes. - name_pattern: The optional name pattern filter configured in the permission. + name_patterns: The possibly empty list of name pattern filters configured in the permission. required_tags: The optional dictionary of required tags configured in the permission. Returns: @@ -75,21 +75,8 @@ def resource_match_config( ) return False - if name_pattern is not None: - if hasattr(resource, "name"): - if isinstance(resource.name, str): - match = bool(re.fullmatch(name_pattern, resource.name)) - if not match: - logger.info( - f"Resource name {resource.name} does not match pattern {name_pattern}" - ) - return False - else: - logger.warning( - f"Resource {resource} has no `name` attribute of unexpected type {type(resource.name)}" - ) - else: - logger.warning(f"Resource {resource} has no `name` attribute") + if not _resource_name_matches_name_patterns(resource, name_patterns): + return False if required_tags: if hasattr(resource, "required_tags"): @@ -112,6 +99,39 @@ def resource_match_config( return True +def _resource_name_matches_name_patterns( + resource: "FeastObject", + name_patterns: list[str], +) -> bool: + if not hasattr(resource, "name"): + logger.warning(f"Resource {resource} has no `name` attribute") + return True + + if not name_patterns: + return True + + if resource.name is None: + return True + + if not isinstance(resource.name, str): + logger.warning( + f"Resource {resource} has `name` attribute of unexpected type {type(resource.name)}" + ) + return True + + for name_pattern in name_patterns: + match = bool(re.fullmatch(name_pattern, resource.name)) + if not match: + logger.info( + f"Resource name {resource.name} does not match pattern {name_pattern}" + ) + else: + logger.info(f"Resource name {resource.name} matched pattern {name_pattern}") + return True + + return False + + def actions_match_config( requested_actions: list[AuthzedAction], allowed_actions: list[AuthzedAction], diff --git a/sdk/python/feast/permissions/permission.py b/sdk/python/feast/permissions/permission.py index 9046abbfa9..964ca743e7 100644 --- a/sdk/python/feast/permissions/permission.py +++ b/sdk/python/feast/permissions/permission.py @@ -33,7 +33,7 @@ class Permission(ABC): name: The permission name (can be duplicated, used for logging troubleshooting). types: The list of protected resource types as defined by the `FeastObject` type. The match includes all the sub-classes of the given types. Defaults to all managed types (e.g. the `ALL_RESOURCE_TYPES` constant) - name_pattern: A regex to match the resource name. Defaults to None, meaning that no name filtering is applied + name_patterns: A possibly empty list of regex patterns to match the resource name. Defaults to empty list, e.g. no name filtering is applied be present in a resource tags with the given value. Defaults to None, meaning that no tags filtering is applied. actions: The actions authorized by this permission. Defaults to `ALL_ACTIONS`. policy: The policy to be applied to validate a client request. @@ -43,7 +43,7 @@ class Permission(ABC): _name: str _types: list["FeastObject"] - _name_pattern: Optional[str] + _name_patterns: list[str] _actions: list[AuthzedAction] _policy: Policy _tags: Dict[str, str] @@ -54,8 +54,8 @@ class Permission(ABC): def __init__( self, name: str, - types: Optional[Union[list["FeastObject"], "FeastObject"]] = None, - name_pattern: Optional[str] = None, + types: Optional[Union[list["FeastObject"], "FeastObject"]] = [], + name_patterns: Optional[Union[str, list[str]]] = [], actions: Union[list[AuthzedAction], AuthzedAction] = ALL_ACTIONS, policy: Policy = AllowAll, tags: Optional[dict[str, str]] = None, @@ -74,7 +74,7 @@ def __init__( raise ValueError("The list 'policy' must be non-empty.") self._name = name self._types = types if isinstance(types, list) else [types] - self._name_pattern = _normalize_name_pattern(name_pattern) + self._name_patterns = _normalize_name_patterns(name_patterns) self._actions = actions if isinstance(actions, list) else [actions] self._policy = policy self._tags = _normalize_tags(tags) @@ -88,7 +88,7 @@ def __eq__(self, other): if ( self.name != other.name - or self.name_pattern != other.name_pattern + or self.name_patterns != other.name_patterns or self.tags != other.tags or self.policy != other.policy or self.actions != other.actions @@ -116,8 +116,8 @@ def types(self) -> list["FeastObject"]: return self._types @property - def name_pattern(self) -> Optional[str]: - return self._name_pattern + def name_patterns(self) -> list[str]: + return self._name_patterns @property def actions(self) -> list[AuthzedAction]: @@ -143,7 +143,7 @@ def match_resource(self, resource: "FeastObject") -> bool: return resource_match_config( resource=resource, expected_types=self.types, - name_pattern=self.name_pattern, + name_patterns=self.name_patterns, required_tags=self.required_tags, ) @@ -175,6 +175,9 @@ def from_proto(permission_proto: PermissionProto) -> Any: ) for t in permission_proto.spec.types ] + name_patterns = [ + name_pattern for name_pattern in permission_proto.spec.name_patterns + ] actions = [ AuthzedAction[PermissionSpecProto.AuthzedAction.Name(action)] for action in permission_proto.spec.actions @@ -183,7 +186,7 @@ def from_proto(permission_proto: PermissionProto) -> Any: permission = Permission( permission_proto.spec.name, types, - permission_proto.spec.name_pattern or None, + name_patterns, actions, Policy.from_proto(permission_proto.spec.policy), dict(permission_proto.spec.tags) or None, @@ -220,7 +223,7 @@ def to_proto(self) -> PermissionProto: permission_spec = PermissionSpecProto( name=self.name, types=types, - name_pattern=self.name_pattern if self.name_pattern is not None else "", + name_patterns=self.name_patterns, actions=actions, policy=self.policy.to_proto(), tags=self.tags, @@ -236,10 +239,17 @@ def to_proto(self) -> PermissionProto: return PermissionProto(spec=permission_spec, meta=meta) -def _normalize_name_pattern(name_pattern: Optional[str]): - if name_pattern is not None: - return name_pattern.strip() - return None +def _normalize_name_patterns( + name_patterns: Optional[Union[str, list[str]]], +) -> list[str]: + if name_patterns is None: + return [] + if isinstance(name_patterns, str): + return _normalize_name_patterns([name_patterns]) + normalized_name_patterns = [] + for name_pattern in name_patterns: + normalized_name_patterns.append(name_pattern.strip()) + return normalized_name_patterns def _normalize_tags(tags: Optional[dict[str, str]]): diff --git a/sdk/python/feast/protos/feast/core/Permission_pb2.py b/sdk/python/feast/protos/feast/core/Permission_pb2.py index 822ad0261b..706fd2eec4 100644 --- a/sdk/python/feast/protos/feast/core/Permission_pb2.py +++ b/sdk/python/feast/protos/feast/core/Permission_pb2.py @@ -16,7 +16,7 @@ from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1b\x66\x65\x61st/core/Permission.proto\x12\nfeast.core\x1a\x17\x66\x65\x61st/core/Policy.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"`\n\nPermission\x12(\n\x04spec\x18\x01 \x01(\x0b\x32\x1a.feast.core.PermissionSpec\x12(\n\x04meta\x18\x02 \x01(\x0b\x32\x1a.feast.core.PermissionMeta\"\x9f\x06\n\x0ePermissionSpec\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12.\n\x05types\x18\x03 \x03(\x0e\x32\x1f.feast.core.PermissionSpec.Type\x12\x14\n\x0cname_pattern\x18\x04 \x01(\t\x12\x43\n\rrequired_tags\x18\x05 \x03(\x0b\x32,.feast.core.PermissionSpec.RequiredTagsEntry\x12\x39\n\x07\x61\x63tions\x18\x06 \x03(\x0e\x32(.feast.core.PermissionSpec.AuthzedAction\x12\"\n\x06policy\x18\x07 \x01(\x0b\x32\x12.feast.core.Policy\x12\x32\n\x04tags\x18\x08 \x03(\x0b\x32$.feast.core.PermissionSpec.TagsEntry\x1a\x33\n\x11RequiredTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x89\x01\n\rAuthzedAction\x12\n\n\x06\x43REATE\x10\x00\x12\x0c\n\x08\x44\x45SCRIBE\x10\x01\x12\n\n\x06UPDATE\x10\x02\x12\n\n\x06\x44\x45LETE\x10\x03\x12\x0f\n\x0bREAD_ONLINE\x10\x04\x12\x10\n\x0cREAD_OFFLINE\x10\x05\x12\x10\n\x0cWRITE_ONLINE\x10\x06\x12\x11\n\rWRITE_OFFLINE\x10\x07\"\xe1\x01\n\x04Type\x12\x10\n\x0c\x46\x45\x41TURE_VIEW\x10\x00\x12\x1a\n\x16ON_DEMAND_FEATURE_VIEW\x10\x01\x12\x16\n\x12\x42\x41TCH_FEATURE_VIEW\x10\x02\x12\x17\n\x13STREAM_FEATURE_VIEW\x10\x03\x12\n\n\x06\x45NTITY\x10\x04\x12\x13\n\x0f\x46\x45\x41TURE_SERVICE\x10\x05\x12\x0f\n\x0b\x44\x41TA_SOURCE\x10\x06\x12\x18\n\x14VALIDATION_REFERENCE\x10\x07\x12\x11\n\rSAVED_DATASET\x10\x08\x12\x0e\n\nPERMISSION\x10\t\x12\x0b\n\x07PROJECT\x10\n\"\x83\x01\n\x0ePermissionMeta\x12\x35\n\x11\x63reated_timestamp\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12:\n\x16last_updated_timestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.TimestampBT\n\x10\x66\x65\x61st.proto.coreB\x0fPermissionProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1b\x66\x65\x61st/core/Permission.proto\x12\nfeast.core\x1a\x17\x66\x65\x61st/core/Policy.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"`\n\nPermission\x12(\n\x04spec\x18\x01 \x01(\x0b\x32\x1a.feast.core.PermissionSpec\x12(\n\x04meta\x18\x02 \x01(\x0b\x32\x1a.feast.core.PermissionMeta\"\xa0\x06\n\x0ePermissionSpec\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12.\n\x05types\x18\x03 \x03(\x0e\x32\x1f.feast.core.PermissionSpec.Type\x12\x15\n\rname_patterns\x18\x04 \x03(\t\x12\x43\n\rrequired_tags\x18\x05 \x03(\x0b\x32,.feast.core.PermissionSpec.RequiredTagsEntry\x12\x39\n\x07\x61\x63tions\x18\x06 \x03(\x0e\x32(.feast.core.PermissionSpec.AuthzedAction\x12\"\n\x06policy\x18\x07 \x01(\x0b\x32\x12.feast.core.Policy\x12\x32\n\x04tags\x18\x08 \x03(\x0b\x32$.feast.core.PermissionSpec.TagsEntry\x1a\x33\n\x11RequiredTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x89\x01\n\rAuthzedAction\x12\n\n\x06\x43REATE\x10\x00\x12\x0c\n\x08\x44\x45SCRIBE\x10\x01\x12\n\n\x06UPDATE\x10\x02\x12\n\n\x06\x44\x45LETE\x10\x03\x12\x0f\n\x0bREAD_ONLINE\x10\x04\x12\x10\n\x0cREAD_OFFLINE\x10\x05\x12\x10\n\x0cWRITE_ONLINE\x10\x06\x12\x11\n\rWRITE_OFFLINE\x10\x07\"\xe1\x01\n\x04Type\x12\x10\n\x0c\x46\x45\x41TURE_VIEW\x10\x00\x12\x1a\n\x16ON_DEMAND_FEATURE_VIEW\x10\x01\x12\x16\n\x12\x42\x41TCH_FEATURE_VIEW\x10\x02\x12\x17\n\x13STREAM_FEATURE_VIEW\x10\x03\x12\n\n\x06\x45NTITY\x10\x04\x12\x13\n\x0f\x46\x45\x41TURE_SERVICE\x10\x05\x12\x0f\n\x0b\x44\x41TA_SOURCE\x10\x06\x12\x18\n\x14VALIDATION_REFERENCE\x10\x07\x12\x11\n\rSAVED_DATASET\x10\x08\x12\x0e\n\nPERMISSION\x10\t\x12\x0b\n\x07PROJECT\x10\n\"\x83\x01\n\x0ePermissionMeta\x12\x35\n\x11\x63reated_timestamp\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12:\n\x16last_updated_timestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.TimestampBT\n\x10\x66\x65\x61st.proto.coreB\x0fPermissionProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -31,15 +31,15 @@ _globals['_PERMISSION']._serialized_start=101 _globals['_PERMISSION']._serialized_end=197 _globals['_PERMISSIONSPEC']._serialized_start=200 - _globals['_PERMISSIONSPEC']._serialized_end=999 - _globals['_PERMISSIONSPEC_REQUIREDTAGSENTRY']._serialized_start=535 - _globals['_PERMISSIONSPEC_REQUIREDTAGSENTRY']._serialized_end=586 - _globals['_PERMISSIONSPEC_TAGSENTRY']._serialized_start=588 - _globals['_PERMISSIONSPEC_TAGSENTRY']._serialized_end=631 - _globals['_PERMISSIONSPEC_AUTHZEDACTION']._serialized_start=634 - _globals['_PERMISSIONSPEC_AUTHZEDACTION']._serialized_end=771 - _globals['_PERMISSIONSPEC_TYPE']._serialized_start=774 - _globals['_PERMISSIONSPEC_TYPE']._serialized_end=999 - _globals['_PERMISSIONMETA']._serialized_start=1002 - _globals['_PERMISSIONMETA']._serialized_end=1133 + _globals['_PERMISSIONSPEC']._serialized_end=1000 + _globals['_PERMISSIONSPEC_REQUIREDTAGSENTRY']._serialized_start=536 + _globals['_PERMISSIONSPEC_REQUIREDTAGSENTRY']._serialized_end=587 + _globals['_PERMISSIONSPEC_TAGSENTRY']._serialized_start=589 + _globals['_PERMISSIONSPEC_TAGSENTRY']._serialized_end=632 + _globals['_PERMISSIONSPEC_AUTHZEDACTION']._serialized_start=635 + _globals['_PERMISSIONSPEC_AUTHZEDACTION']._serialized_end=772 + _globals['_PERMISSIONSPEC_TYPE']._serialized_start=775 + _globals['_PERMISSIONSPEC_TYPE']._serialized_end=1000 + _globals['_PERMISSIONMETA']._serialized_start=1003 + _globals['_PERMISSIONMETA']._serialized_end=1134 # @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/Permission_pb2.pyi b/sdk/python/feast/protos/feast/core/Permission_pb2.pyi index 1155c13188..b2387d2946 100644 --- a/sdk/python/feast/protos/feast/core/Permission_pb2.pyi +++ b/sdk/python/feast/protos/feast/core/Permission_pb2.pyi @@ -134,7 +134,7 @@ class PermissionSpec(google.protobuf.message.Message): NAME_FIELD_NUMBER: builtins.int PROJECT_FIELD_NUMBER: builtins.int TYPES_FIELD_NUMBER: builtins.int - NAME_PATTERN_FIELD_NUMBER: builtins.int + NAME_PATTERNS_FIELD_NUMBER: builtins.int REQUIRED_TAGS_FIELD_NUMBER: builtins.int ACTIONS_FIELD_NUMBER: builtins.int POLICY_FIELD_NUMBER: builtins.int @@ -145,7 +145,8 @@ class PermissionSpec(google.protobuf.message.Message): """Name of Feast project.""" @property def types(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[global___PermissionSpec.Type.ValueType]: ... - name_pattern: builtins.str + @property + def name_patterns(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: ... @property def required_tags(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: ... @property @@ -163,14 +164,14 @@ class PermissionSpec(google.protobuf.message.Message): name: builtins.str = ..., project: builtins.str = ..., types: collections.abc.Iterable[global___PermissionSpec.Type.ValueType] | None = ..., - name_pattern: builtins.str = ..., + name_patterns: collections.abc.Iterable[builtins.str] | None = ..., required_tags: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., actions: collections.abc.Iterable[global___PermissionSpec.AuthzedAction.ValueType] | None = ..., policy: feast.core.Policy_pb2.Policy | None = ..., tags: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["policy", b"policy"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["actions", b"actions", "name", b"name", "name_pattern", b"name_pattern", "policy", b"policy", "project", b"project", "required_tags", b"required_tags", "tags", b"tags", "types", b"types"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["actions", b"actions", "name", b"name", "name_patterns", b"name_patterns", "policy", b"policy", "project", b"project", "required_tags", b"required_tags", "tags", b"tags", "types", b"types"]) -> None: ... global___PermissionSpec = PermissionSpec diff --git a/sdk/python/tests/integration/registration/test_universal_registry.py b/sdk/python/tests/integration/registration/test_universal_registry.py index 0bed89ca16..a194b8ae26 100644 --- a/sdk/python/tests/integration/registration/test_universal_registry.py +++ b/sdk/python/tests/integration/registration/test_universal_registry.py @@ -1463,7 +1463,7 @@ def test_apply_permission_success(test_registry): and isinstance(permission.policy, RoleBasedPolicy) and len(permission.policy.roles) == 1 and permission.policy.roles[0] == "reader" - and permission.name_pattern is None + and permission.name_patterns == [] and permission.tags is None and permission.required_tags is None ) @@ -1481,7 +1481,7 @@ def test_apply_permission_success(test_registry): and isinstance(permission.policy, RoleBasedPolicy) and len(permission.policy.roles) == 1 and permission.policy.roles[0] == "reader" - and permission.name_pattern is None + and permission.name_patterns == [] and permission.tags is None and permission.required_tags is None ) @@ -1511,7 +1511,7 @@ def test_apply_permission_success(test_registry): and len(updated_permission.policy.roles) == 2 and "reader" in updated_permission.policy.roles and "writer" in updated_permission.policy.roles - and updated_permission.name_pattern is None + and updated_permission.name_patterns == [] and updated_permission.tags is None and updated_permission.required_tags is None ) @@ -1527,7 +1527,7 @@ def test_apply_permission_success(test_registry): actions=[AuthzedAction.DESCRIBE, AuthzedAction.WRITE_ONLINE], policy=RoleBasedPolicy(roles=["reader", "writer"]), types=FeatureView, - name_pattern="aaa", + name_patterns="aaa", tags={"team": "matchmaking"}, required_tags={"tag1": "tag1-value"}, ) @@ -1549,7 +1549,7 @@ def test_apply_permission_success(test_registry): and len(updated_permission.policy.roles) == 2 and "reader" in updated_permission.policy.roles and "writer" in updated_permission.policy.roles - and updated_permission.name_pattern == "aaa" + and updated_permission.name_patterns == ["aaa"] and "team" in updated_permission.tags and updated_permission.tags["team"] == "matchmaking" and updated_permission.required_tags["tag1"] == "tag1-value" diff --git a/sdk/python/tests/unit/permissions/conftest.py b/sdk/python/tests/unit/permissions/conftest.py index 6adbc6ec54..ba277d13b4 100644 --- a/sdk/python/tests/unit/permissions/conftest.py +++ b/sdk/python/tests/unit/permissions/conftest.py @@ -79,7 +79,7 @@ def security_manager() -> SecurityManager: Permission( name="special", types=FeatureView, - name_pattern="special.*", + name_patterns="special.*", policy=RoleBasedPolicy(roles=["special-reader"]), actions=[AuthzedAction.DESCRIBE, AuthzedAction.UPDATE], ) diff --git a/sdk/python/tests/unit/permissions/test_permission.py b/sdk/python/tests/unit/permissions/test_permission.py index 606d750d81..8f1f2c46ba 100644 --- a/sdk/python/tests/unit/permissions/test_permission.py +++ b/sdk/python/tests/unit/permissions/test_permission.py @@ -11,9 +11,8 @@ from feast.feature_view import FeatureView from feast.on_demand_feature_view import OnDemandFeatureView from feast.permissions.action import ALL_ACTIONS, AuthzedAction -from feast.permissions.permission import ( - Permission, -) +from feast.permissions.matcher import _resource_name_matches_name_patterns +from feast.permissions.permission import Permission, _normalize_name_patterns from feast.permissions.policy import AllowAll, Policy from feast.saved_dataset import ValidationReference from feast.stream_feature_view import StreamFeatureView @@ -23,7 +22,7 @@ def test_defaults(): p = Permission(name="test") assertpy.assert_that(type(p.types)).is_equal_to(list) assertpy.assert_that(p.types).is_equal_to(ALL_RESOURCE_TYPES) - assertpy.assert_that(p.name_pattern).is_none() + assertpy.assert_that(p.name_patterns).is_equal_to([]) assertpy.assert_that(p.tags).is_none() assertpy.assert_that(type(p.actions)).is_equal_to(list) assertpy.assert_that(p.actions).is_equal_to(ALL_ACTIONS) @@ -66,6 +65,53 @@ def test_normalized_args(): assertpy.assert_that(type(p.actions)).is_equal_to(list) assertpy.assert_that(p.actions).is_equal_to([AuthzedAction.CREATE]) + p = Permission(name="test", name_patterns=None) + assertpy.assert_that(type(p.name_patterns)).is_equal_to(list) + assertpy.assert_that(p.name_patterns).is_equal_to([]) + + p = Permission(name="test", name_patterns="a_pattern") + assertpy.assert_that(type(p.name_patterns)).is_equal_to(list) + assertpy.assert_that(p.name_patterns).is_equal_to(["a_pattern"]) + + p = Permission(name="test", name_patterns=["pattern1", "pattern2"]) + assertpy.assert_that(type(p.name_patterns)).is_equal_to(list) + assertpy.assert_that(p.name_patterns).is_equal_to(["pattern1", "pattern2"]) + + p = Permission( + name="test", name_patterns=[" pattern1 ", " pattern2", "pattern3 "] + ) + assertpy.assert_that(type(p.name_patterns)).is_equal_to(list) + assertpy.assert_that(p.name_patterns).is_equal_to( + ["pattern1", "pattern2", "pattern3"] + ) + + +@pytest.mark.parametrize( + "name, patterns, result", + [ + (None, None, True), + (None, "", True), + (None, [], True), + (None, [""], True), + ("name", "name", True), + ("name", "another", False), + ("name", ".*me", True), + ("name", "^na.*", True), + ("123_must_start_by_number", r"^[\d].*", True), + ("name", ["invalid", "another_invalid"], False), + ("name", ["invalid", "name"], True), + ("name", ["name", "invalid"], True), + ("name", ["invalid", "another_invalid", "name"], True), + ("name", ["invalid", "name", "name"], True), + ], +) +def test_match_name_patterns(name, patterns, result): + assertpy.assert_that( + _resource_name_matches_name_patterns( + Permission(name=name), _normalize_name_patterns(patterns) + ) + ).is_equal_to(result) + @pytest.mark.parametrize( "resource, types, result", @@ -152,7 +198,7 @@ def test_match_resource_with_subclasses(resource, types, result): ], ) def test_resource_match_with_name_filter(pattern, name, match): - p = Permission(name="test", name_pattern=pattern) + p = Permission(name="test", name_patterns=pattern) for t in ALL_RESOURCE_TYPES: resource = Mock(spec=t) resource.name = name From 3e313b15efc7fc72d35d70315fc8b7c172fc7993 Mon Sep 17 00:00:00 2001 From: Theodor Mihalache <84387487+tmihalac@users.noreply.github.com> Date: Thu, 17 Oct 2024 11:05:38 -0400 Subject: [PATCH 146/185] fix: Changes template file path to relative path (#4624) * fix: changes following issue 4593 Signed-off-by: Theodor Mihalache * fix: changes following issue 4593 - Fixed file path in templates to be relative path Signed-off-by: Theodor Mihalache * fix: Fixes to relative path in FileSource Signed-off-by: Theodor Mihalache --------- Signed-off-by: Theodor Mihalache --- sdk/python/feast/feature_store.py | 4 +- sdk/python/feast/infra/offline_stores/dask.py | 40 +++++++++++++++---- .../feast/infra/offline_stores/duckdb.py | 17 ++++++-- .../feast/infra/offline_stores/file_source.py | 11 ++++- sdk/python/feast/infra/offline_stores/ibis.py | 37 +++++++++++------ sdk/python/feast/repo_config.py | 1 + .../feast/templates/cassandra/bootstrap.py | 4 +- .../feast/templates/hazelcast/bootstrap.py | 4 +- sdk/python/feast/templates/hbase/bootstrap.py | 4 +- sdk/python/feast/templates/local/bootstrap.py | 8 +++- sdk/python/tests/doctest/test_all.py | 2 +- .../offline_stores/test_offline_store.py | 2 +- sdk/python/tests/unit/test_offline_server.py | 1 + .../tests/utils/auth_permissions_util.py | 1 + 14 files changed, 104 insertions(+), 32 deletions(-) diff --git a/sdk/python/feast/feature_store.py b/sdk/python/feast/feature_store.py index 9a652fd060..9b1a35303e 100644 --- a/sdk/python/feast/feature_store.py +++ b/sdk/python/feast/feature_store.py @@ -713,7 +713,7 @@ def plan( >>> fs = FeatureStore(repo_path="project/feature_repo") >>> driver = Entity(name="driver_id", description="driver id") >>> driver_hourly_stats = FileSource( - ... path="project/feature_repo/data/driver_stats.parquet", + ... path="data/driver_stats.parquet", ... timestamp_field="event_timestamp", ... created_timestamp_column="created", ... ) @@ -827,7 +827,7 @@ def apply( >>> fs = FeatureStore(repo_path="project/feature_repo") >>> driver = Entity(name="driver_id", description="driver id") >>> driver_hourly_stats = FileSource( - ... path="project/feature_repo/data/driver_stats.parquet", + ... path="data/driver_stats.parquet", ... timestamp_field="event_timestamp", ... created_timestamp_column="created", ... ) diff --git a/sdk/python/feast/infra/offline_stores/dask.py b/sdk/python/feast/infra/offline_stores/dask.py index 52ad88d299..d26e8609ba 100644 --- a/sdk/python/feast/infra/offline_stores/dask.py +++ b/sdk/python/feast/infra/offline_stores/dask.py @@ -57,6 +57,7 @@ def __init__( self, evaluation_function: Callable, full_feature_names: bool, + repo_path: str, on_demand_feature_views: Optional[List[OnDemandFeatureView]] = None, metadata: Optional[RetrievalMetadata] = None, ): @@ -67,6 +68,7 @@ def __init__( self._full_feature_names = full_feature_names self._on_demand_feature_views = on_demand_feature_views or [] self._metadata = metadata + self.repo_path = repo_path @property def full_feature_names(self) -> bool: @@ -99,8 +101,13 @@ def persist( if not allow_overwrite and os.path.exists(storage.file_options.uri): raise SavedDatasetLocationAlreadyExists(location=storage.file_options.uri) + if not Path(storage.file_options.uri).is_absolute(): + absolute_path = Path(self.repo_path) / storage.file_options.uri + else: + absolute_path = Path(storage.file_options.uri) + filesystem, path = FileSource.create_filesystem_and_path( - storage.file_options.uri, + str(absolute_path), storage.file_options.s3_endpoint_override, ) @@ -243,7 +250,9 @@ def evaluate_historical_retrieval(): all_join_keys = list(set(all_join_keys + join_keys)) - df_to_join = _read_datasource(feature_view.batch_source) + df_to_join = _read_datasource( + feature_view.batch_source, config.repo_path + ) df_to_join, timestamp_field = _field_mapping( df_to_join, @@ -297,6 +306,7 @@ def evaluate_historical_retrieval(): min_event_timestamp=entity_df_event_timestamp_range[0], max_event_timestamp=entity_df_event_timestamp_range[1], ), + repo_path=str(config.repo_path), ) return job @@ -316,7 +326,7 @@ def pull_latest_from_table_or_query( # Create lazy function that is only called from the RetrievalJob object def evaluate_offline_job(): - source_df = _read_datasource(data_source) + source_df = _read_datasource(data_source, config.repo_path) source_df = _normalize_timestamp( source_df, timestamp_field, created_timestamp_column @@ -377,6 +387,7 @@ def evaluate_offline_job(): return DaskRetrievalJob( evaluation_function=evaluate_offline_job, full_feature_names=False, + repo_path=str(config.repo_path), ) @staticmethod @@ -420,8 +431,13 @@ def write_logged_features( # Since this code will be mostly used from Go-created thread, it's better to avoid producing new threads data = pyarrow.parquet.read_table(data, use_threads=False, pre_buffer=False) + if config.repo_path is not None and not Path(destination.path).is_absolute(): + absolute_path = config.repo_path / destination.path + else: + absolute_path = Path(destination.path) + filesystem, path = FileSource.create_filesystem_and_path( - destination.path, + str(absolute_path), destination.s3_endpoint_override, ) @@ -456,8 +472,14 @@ def offline_write_batch( ) file_options = feature_view.batch_source.file_options + + if config.repo_path is not None and not Path(file_options.uri).is_absolute(): + absolute_path = config.repo_path / file_options.uri + else: + absolute_path = Path(file_options.uri) + filesystem, path = FileSource.create_filesystem_and_path( - file_options.uri, file_options.s3_endpoint_override + str(absolute_path), file_options.s3_endpoint_override ) prev_table = pyarrow.parquet.read_table( path, filesystem=filesystem, memory_map=True @@ -493,7 +515,7 @@ def _get_entity_df_event_timestamp_range( ) -def _read_datasource(data_source) -> dd.DataFrame: +def _read_datasource(data_source, repo_path) -> dd.DataFrame: storage_options = ( { "client_kwargs": { @@ -504,8 +526,12 @@ def _read_datasource(data_source) -> dd.DataFrame: else None ) + if not Path(data_source.path).is_absolute(): + path = repo_path / data_source.path + else: + path = data_source.path return dd.read_parquet( - data_source.path, + path, storage_options=storage_options, ) diff --git a/sdk/python/feast/infra/offline_stores/duckdb.py b/sdk/python/feast/infra/offline_stores/duckdb.py index a639d54add..e64da029a6 100644 --- a/sdk/python/feast/infra/offline_stores/duckdb.py +++ b/sdk/python/feast/infra/offline_stores/duckdb.py @@ -27,7 +27,7 @@ from feast.repo_config import FeastConfigBaseModel, RepoConfig -def _read_data_source(data_source: DataSource) -> Table: +def _read_data_source(data_source: DataSource, repo_path: str) -> Table: assert isinstance(data_source, FileSource) if isinstance(data_source.file_format, ParquetFormat): @@ -43,6 +43,7 @@ def _read_data_source(data_source: DataSource) -> Table: def _write_data_source( table: Table, data_source: DataSource, + repo_path: str, mode: str = "append", allow_overwrite: bool = False, ): @@ -50,14 +51,24 @@ def _write_data_source( file_options = data_source.file_options - if mode == "overwrite" and not allow_overwrite and os.path.exists(file_options.uri): + if not Path(file_options.uri).is_absolute(): + absolute_path = Path(repo_path) / file_options.uri + else: + absolute_path = Path(file_options.uri) + + if ( + mode == "overwrite" + and not allow_overwrite + and os.path.exists(str(absolute_path)) + ): raise SavedDatasetLocationAlreadyExists(location=file_options.uri) if isinstance(data_source.file_format, ParquetFormat): if mode == "overwrite": table = table.to_pyarrow() + filesystem, path = FileSource.create_filesystem_and_path( - file_options.uri, + str(absolute_path), file_options.s3_endpoint_override, ) diff --git a/sdk/python/feast/infra/offline_stores/file_source.py b/sdk/python/feast/infra/offline_stores/file_source.py index 3fdc6cba31..9557b8077d 100644 --- a/sdk/python/feast/infra/offline_stores/file_source.py +++ b/sdk/python/feast/infra/offline_stores/file_source.py @@ -1,3 +1,4 @@ +from pathlib import Path from typing import Callable, Dict, Iterable, List, Optional, Tuple import pyarrow @@ -154,8 +155,16 @@ def source_datatype_to_feast_value_type() -> Callable[[str], ValueType]: def get_table_column_names_and_types( self, config: RepoConfig ) -> Iterable[Tuple[str, str]]: + if ( + config.repo_path is not None + and not Path(self.file_options.uri).is_absolute() + ): + absolute_path = config.repo_path / self.file_options.uri + else: + absolute_path = Path(self.file_options.uri) + filesystem, path = FileSource.create_filesystem_and_path( - self.path, self.file_options.s3_endpoint_override + str(absolute_path), self.file_options.s3_endpoint_override ) # TODO why None check necessary diff --git a/sdk/python/feast/infra/offline_stores/ibis.py b/sdk/python/feast/infra/offline_stores/ibis.py index 61c477baec..66d00ca629 100644 --- a/sdk/python/feast/infra/offline_stores/ibis.py +++ b/sdk/python/feast/infra/offline_stores/ibis.py @@ -46,8 +46,8 @@ def pull_latest_from_table_or_query_ibis( created_timestamp_column: Optional[str], start_date: datetime, end_date: datetime, - data_source_reader: Callable[[DataSource], Table], - data_source_writer: Callable[[pyarrow.Table, DataSource], None], + data_source_reader: Callable[[DataSource, str], Table], + data_source_writer: Callable[[pyarrow.Table, DataSource, str], None], staging_location: Optional[str] = None, staging_location_endpoint_override: Optional[str] = None, ) -> RetrievalJob: @@ -57,7 +57,7 @@ def pull_latest_from_table_or_query_ibis( start_date = start_date.astimezone(tz=timezone.utc) end_date = end_date.astimezone(tz=timezone.utc) - table = data_source_reader(data_source) + table = data_source_reader(data_source, str(config.repo_path)) table = table.select(*fields) @@ -87,6 +87,7 @@ def pull_latest_from_table_or_query_ibis( data_source_writer=data_source_writer, staging_location=staging_location, staging_location_endpoint_override=staging_location_endpoint_override, + repo_path=str(config.repo_path), ) @@ -147,8 +148,8 @@ def get_historical_features_ibis( entity_df: Union[pd.DataFrame, str], registry: BaseRegistry, project: str, - data_source_reader: Callable[[DataSource], Table], - data_source_writer: Callable[[pyarrow.Table, DataSource], None], + data_source_reader: Callable[[DataSource, str], Table], + data_source_writer: Callable[[pyarrow.Table, DataSource, str], None], full_feature_names: bool = False, staging_location: Optional[str] = None, staging_location_endpoint_override: Optional[str] = None, @@ -174,7 +175,9 @@ def get_historical_features_ibis( def read_fv( feature_view: FeatureView, feature_refs: List[str], full_feature_names: bool ) -> Tuple: - fv_table: Table = data_source_reader(feature_view.batch_source) + fv_table: Table = data_source_reader( + feature_view.batch_source, str(config.repo_path) + ) for old_name, new_name in feature_view.batch_source.field_mapping.items(): if old_name in fv_table.columns: @@ -247,6 +250,7 @@ def read_fv( data_source_writer=data_source_writer, staging_location=staging_location, staging_location_endpoint_override=staging_location_endpoint_override, + repo_path=str(config.repo_path), ) @@ -258,8 +262,8 @@ def pull_all_from_table_or_query_ibis( timestamp_field: str, start_date: datetime, end_date: datetime, - data_source_reader: Callable[[DataSource], Table], - data_source_writer: Callable[[pyarrow.Table, DataSource], None], + data_source_reader: Callable[[DataSource, str], Table], + data_source_writer: Callable[[pyarrow.Table, DataSource, str], None], staging_location: Optional[str] = None, staging_location_endpoint_override: Optional[str] = None, ) -> RetrievalJob: @@ -267,7 +271,7 @@ def pull_all_from_table_or_query_ibis( start_date = start_date.astimezone(tz=timezone.utc) end_date = end_date.astimezone(tz=timezone.utc) - table = data_source_reader(data_source) + table = data_source_reader(data_source, str(config.repo_path)) table = table.select(*fields) @@ -290,6 +294,7 @@ def pull_all_from_table_or_query_ibis( data_source_writer=data_source_writer, staging_location=staging_location, staging_location_endpoint_override=staging_location_endpoint_override, + repo_path=str(config.repo_path), ) @@ -319,7 +324,7 @@ def offline_write_batch_ibis( feature_view: FeatureView, table: pyarrow.Table, progress: Optional[Callable[[int], Any]], - data_source_writer: Callable[[pyarrow.Table, DataSource], None], + data_source_writer: Callable[[pyarrow.Table, DataSource, str], None], ): pa_schema, column_names = get_pyarrow_schema_from_batch_source( config, feature_view.batch_source @@ -330,7 +335,9 @@ def offline_write_batch_ibis( f"The schema is expected to be {pa_schema} with the columns (in this exact order) to be {column_names}." ) - data_source_writer(ibis.memtable(table), feature_view.batch_source) + data_source_writer( + ibis.memtable(table), feature_view.batch_source, str(config.repo_path) + ) def deduplicate( @@ -469,6 +476,7 @@ def __init__( data_source_writer, staging_location, staging_location_endpoint_override, + repo_path, ) -> None: super().__init__() self.table = table @@ -480,6 +488,7 @@ def __init__( self.data_source_writer = data_source_writer self.staging_location = staging_location self.staging_location_endpoint_override = staging_location_endpoint_override + self.repo_path = repo_path def _to_df_internal(self, timeout: Optional[int] = None) -> pd.DataFrame: return self.table.execute() @@ -502,7 +511,11 @@ def persist( timeout: Optional[int] = None, ): self.data_source_writer( - self.table, storage.to_data_source(), "overwrite", allow_overwrite + self.table, + storage.to_data_source(), + self.repo_path, + "overwrite", + allow_overwrite, ) @property diff --git a/sdk/python/feast/repo_config.py b/sdk/python/feast/repo_config.py index 1b991d058b..0a5b484e8c 100644 --- a/sdk/python/feast/repo_config.py +++ b/sdk/python/feast/repo_config.py @@ -193,6 +193,7 @@ class RepoConfig(FeastBaseModel): """ Flags (deprecated field): Feature flags for experimental features """ repo_path: Optional[Path] = None + """When using relative path in FileSource path, this parameter is mandatory""" entity_key_serialization_version: StrictInt = 1 """ Entity key serialization version: This version is used to control what serialization scheme is diff --git a/sdk/python/feast/templates/cassandra/bootstrap.py b/sdk/python/feast/templates/cassandra/bootstrap.py index fa70917914..3338514114 100644 --- a/sdk/python/feast/templates/cassandra/bootstrap.py +++ b/sdk/python/feast/templates/cassandra/bootstrap.py @@ -275,7 +275,9 @@ def bootstrap(): # example_repo.py example_py_file = repo_path / "example_repo.py" - replace_str_in_file(example_py_file, "%PARQUET_PATH%", str(driver_stats_path)) + replace_str_in_file( + example_py_file, "%PARQUET_PATH%", str(driver_stats_path.relative_to(repo_path)) + ) # store config yaml, interact with user and then customize file: settings = collect_cassandra_store_settings() diff --git a/sdk/python/feast/templates/hazelcast/bootstrap.py b/sdk/python/feast/templates/hazelcast/bootstrap.py index e5018e4fe0..7a2b49d249 100644 --- a/sdk/python/feast/templates/hazelcast/bootstrap.py +++ b/sdk/python/feast/templates/hazelcast/bootstrap.py @@ -165,7 +165,9 @@ def bootstrap(): # example_repo.py example_py_file = repo_path / "example_repo.py" - replace_str_in_file(example_py_file, "%PARQUET_PATH%", str(driver_stats_path)) + replace_str_in_file( + example_py_file, "%PARQUET_PATH%", str(driver_stats_path.relative_to(repo_path)) + ) # store config yaml, interact with user and then customize file: settings = collect_hazelcast_online_store_settings() diff --git a/sdk/python/feast/templates/hbase/bootstrap.py b/sdk/python/feast/templates/hbase/bootstrap.py index 125eb7c2e7..94be8e441d 100644 --- a/sdk/python/feast/templates/hbase/bootstrap.py +++ b/sdk/python/feast/templates/hbase/bootstrap.py @@ -23,7 +23,9 @@ def bootstrap(): driver_df.to_parquet(path=str(driver_stats_path), allow_truncated_timestamps=True) example_py_file = repo_path / "example_repo.py" - replace_str_in_file(example_py_file, "%PARQUET_PATH%", str(driver_stats_path)) + replace_str_in_file( + example_py_file, "%PARQUET_PATH%", str(driver_stats_path.relative_to(repo_path)) + ) if __name__ == "__main__": diff --git a/sdk/python/feast/templates/local/bootstrap.py b/sdk/python/feast/templates/local/bootstrap.py index e2c1efdbc4..9f6a5a6c96 100644 --- a/sdk/python/feast/templates/local/bootstrap.py +++ b/sdk/python/feast/templates/local/bootstrap.py @@ -25,8 +25,12 @@ def bootstrap(): example_py_file = repo_path / "example_repo.py" replace_str_in_file(example_py_file, "%PROJECT_NAME%", str(project_name)) - replace_str_in_file(example_py_file, "%PARQUET_PATH%", str(driver_stats_path)) - replace_str_in_file(example_py_file, "%LOGGING_PATH%", str(data_path)) + replace_str_in_file( + example_py_file, "%PARQUET_PATH%", str(driver_stats_path.relative_to(repo_path)) + ) + replace_str_in_file( + example_py_file, "%LOGGING_PATH%", str(data_path.relative_to(repo_path)) + ) if __name__ == "__main__": diff --git a/sdk/python/tests/doctest/test_all.py b/sdk/python/tests/doctest/test_all.py index 52348e7da4..d1b2161252 100644 --- a/sdk/python/tests/doctest/test_all.py +++ b/sdk/python/tests/doctest/test_all.py @@ -26,7 +26,7 @@ def setup_feature_store(): description="driver id", ) driver_hourly_stats = FileSource( - path="project/feature_repo/data/driver_stats.parquet", + path="data/driver_stats.parquet", timestamp_field="event_timestamp", created_timestamp_column="created", ) diff --git a/sdk/python/tests/unit/infra/offline_stores/test_offline_store.py b/sdk/python/tests/unit/infra/offline_stores/test_offline_store.py index 6d5eeb90c7..afc0e4e5c8 100644 --- a/sdk/python/tests/unit/infra/offline_stores/test_offline_store.py +++ b/sdk/python/tests/unit/infra/offline_stores/test_offline_store.py @@ -109,7 +109,7 @@ def metadata(self) -> Optional[RetrievalMetadata]: ) def retrieval_job(request, environment): if request.param is DaskRetrievalJob: - return DaskRetrievalJob(lambda: 1, full_feature_names=False) + return DaskRetrievalJob(lambda: 1, full_feature_names=False, repo_path="") elif request.param is RedshiftRetrievalJob: offline_store_config = RedshiftOfflineStoreConfig( cluster_id="feast-int-bucket", diff --git a/sdk/python/tests/unit/test_offline_server.py b/sdk/python/tests/unit/test_offline_server.py index 7c38d9bfca..e82e2fa687 100644 --- a/sdk/python/tests/unit/test_offline_server.py +++ b/sdk/python/tests/unit/test_offline_server.py @@ -95,6 +95,7 @@ def remote_feature_store(offline_server): provider="local", offline_store=offline_config, entity_key_serialization_version=2, + # repo_config = ) ) return store diff --git a/sdk/python/tests/utils/auth_permissions_util.py b/sdk/python/tests/utils/auth_permissions_util.py index 3b5e589812..b8ca7355e9 100644 --- a/sdk/python/tests/utils/auth_permissions_util.py +++ b/sdk/python/tests/utils/auth_permissions_util.py @@ -119,6 +119,7 @@ def get_remote_registry_store(server_port, feature_store): registry=registry_config, provider="local", entity_key_serialization_version=2, + repo_path=feature_store.repo_path, ) ) return store From 9d7202dface24831034d437403edfbd6b114c71d Mon Sep 17 00:00:00 2001 From: Harri Lehtola <1781172+peruukki@users.noreply.github.com> Date: Thu, 17 Oct 2024 18:07:50 +0300 Subject: [PATCH 147/185] chore: Remove unused d3 dependency from Feast UI (#4641) d3 is no longer used by Feast UI nor by any of its dependencies. Signed-off-by: Harri Lehtola --- ui/package.json | 5 +- ui/yarn.lock | 493 +----------------------------------------------- 2 files changed, 7 insertions(+), 491 deletions(-) diff --git a/ui/package.json b/ui/package.json index a79b039a87..09c16f2571 100644 --- a/ui/package.json +++ b/ui/package.json @@ -27,7 +27,6 @@ "@elastic/eui": "^95.12.0", "@emotion/css": "^11.13.0", "@emotion/react": "^11.13.3", - "d3": "^7.3.0", "inter-ui": "^3.19.3", "moment": "^2.29.1", "protobufjs": "^7.1.1", @@ -57,8 +56,7 @@ }, "jest": { "moduleNameMapper": { - "chroma-js": "/node_modules/chroma-js/dist/chroma.min.cjs", - "d3": "/node_modules/d3/dist/d3.min.js" + "chroma-js": "/node_modules/chroma-js/dist/chroma.min.cjs" } }, "browserslist": { @@ -86,7 +84,6 @@ "@testing-library/jest-dom": "^6.5.0", "@testing-library/react": "^16.0.1", "@testing-library/user-event": "^14.5.2", - "@types/d3": "^7.1.0", "@types/jest": "^27.0.1", "@types/node": "^16.7.13", "@types/react": "^18.3.11", diff --git a/ui/yarn.lock b/ui/yarn.lock index 27cc4076a3..894398942c 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -2247,216 +2247,6 @@ resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== -"@types/d3-array@*": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.0.2.tgz#71c35bca8366a40d1b8fce9279afa4a77fb0065d" - integrity sha512-5mjGjz6XOXKOCdTajXTZ/pMsg236RdiwKPrRPWAEf/2S/+PzwY+LLYShUpeysWaMvsdS7LArh6GdUefoxpchsQ== - -"@types/d3-axis@*": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/d3-axis/-/d3-axis-3.0.1.tgz#6afc20744fa5cc0cbc3e2bd367b140a79ed3e7a8" - integrity sha512-zji/iIbdd49g9WN0aIsGcwcTBUkgLsCSwB+uH+LPVDAiKWENMtI3cJEWt+7/YYwelMoZmbBfzA3qCdrZ2XFNnw== - dependencies: - "@types/d3-selection" "*" - -"@types/d3-brush@*": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/d3-brush/-/d3-brush-3.0.1.tgz#ae5f17ce391935ca88b29000e60ee20452c6357c" - integrity sha512-B532DozsiTuQMHu2YChdZU0qsFJSio3Q6jmBYGYNp3gMDzBmuFFgPt9qKA4VYuLZMp4qc6eX7IUFUEsvHiXZAw== - dependencies: - "@types/d3-selection" "*" - -"@types/d3-chord@*": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/d3-chord/-/d3-chord-3.0.1.tgz#54c8856c19c8e4ab36a53f73ba737de4768ad248" - integrity sha512-eQfcxIHrg7V++W8Qxn6QkqBNBokyhdWSAS73AbkbMzvLQmVVBviknoz2SRS/ZJdIOmhcmmdCRE/NFOm28Z1AMw== - -"@types/d3-color@*": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.0.2.tgz#53f2d6325f66ee79afd707c05ac849e8ae0edbb0" - integrity sha512-WVx6zBiz4sWlboCy7TCgjeyHpNjMsoF36yaagny1uXfbadc9f+5BeBf7U+lRmQqY3EHbGQpP8UdW8AC+cywSwQ== - -"@types/d3-contour@*": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/d3-contour/-/d3-contour-3.0.1.tgz#9ff4e2fd2a3910de9c5097270a7da8a6ef240017" - integrity sha512-C3zfBrhHZvrpAAK3YXqLWVAGo87A4SvJ83Q/zVJ8rFWJdKejUnDYaWZPkA8K84kb2vDA/g90LTQAz7etXcgoQQ== - dependencies: - "@types/d3-array" "*" - "@types/geojson" "*" - -"@types/d3-delaunay@*": - version "6.0.0" - resolved "https://registry.yarnpkg.com/@types/d3-delaunay/-/d3-delaunay-6.0.0.tgz#c09953ac7e5460997f693d2d7bf3522e0d4a88e6" - integrity sha512-iGm7ZaGLq11RK3e69VeMM6Oqj2SjKUB9Qhcyd1zIcqn2uE8w9GFB445yCY46NOQO3ByaNyktX1DK+Etz7ZaX+w== - -"@types/d3-dispatch@*": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/d3-dispatch/-/d3-dispatch-3.0.1.tgz#a1b18ae5fa055a6734cb3bd3cbc6260ef19676e3" - integrity sha512-NhxMn3bAkqhjoxabVJWKryhnZXXYYVQxaBnbANu0O94+O/nX9qSjrA1P1jbAQJxJf+VC72TxDX/YJcKue5bRqw== - -"@types/d3-drag@*": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/d3-drag/-/d3-drag-3.0.1.tgz#fb1e3d5cceeee4d913caa59dedf55c94cb66e80f" - integrity sha512-o1Va7bLwwk6h03+nSM8dpaGEYnoIG19P0lKqlic8Un36ymh9NSkNFX1yiXMKNMx8rJ0Kfnn2eovuFaL6Jvj0zA== - dependencies: - "@types/d3-selection" "*" - -"@types/d3-dsv@*": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/d3-dsv/-/d3-dsv-3.0.0.tgz#f3c61fb117bd493ec0e814856feb804a14cfc311" - integrity sha512-o0/7RlMl9p5n6FQDptuJVMxDf/7EDEv2SYEO/CwdG2tr1hTfUVi0Iavkk2ax+VpaQ/1jVhpnj5rq1nj8vwhn2A== - -"@types/d3-ease@*": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.0.tgz#c29926f8b596f9dadaeca062a32a45365681eae0" - integrity sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA== - -"@types/d3-fetch@*": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/d3-fetch/-/d3-fetch-3.0.1.tgz#f9fa88b81aa2eea5814f11aec82ecfddbd0b8fe0" - integrity sha512-toZJNOwrOIqz7Oh6Q7l2zkaNfXkfR7mFSJvGvlD/Ciq/+SQ39d5gynHJZ/0fjt83ec3WL7+u3ssqIijQtBISsw== - dependencies: - "@types/d3-dsv" "*" - -"@types/d3-force@*": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/d3-force/-/d3-force-3.0.3.tgz#76cb20d04ae798afede1ea6e41750763ff5a9c82" - integrity sha512-z8GteGVfkWJMKsx6hwC3SiTSLspL98VNpmvLpEFJQpZPq6xpA1I8HNBDNSpukfK0Vb0l64zGFhzunLgEAcBWSA== - -"@types/d3-format@*": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-3.0.1.tgz#194f1317a499edd7e58766f96735bdc0216bb89d" - integrity sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg== - -"@types/d3-geo@*": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/d3-geo/-/d3-geo-3.0.2.tgz#e7ec5f484c159b2c404c42d260e6d99d99f45d9a" - integrity sha512-DbqK7MLYA8LpyHQfv6Klz0426bQEf7bRTvhMy44sNGVyZoWn//B0c+Qbeg8Osi2Obdc9BLLXYAKpyWege2/7LQ== - dependencies: - "@types/geojson" "*" - -"@types/d3-hierarchy@*": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-3.0.2.tgz#ca63f2f4da15b8f129c5b7dffd71d904cba6aca2" - integrity sha512-+krnrWOZ+aQB6v+E+jEkmkAx9HvsNAD+1LCD0vlBY3t+HwjKnsBFbpVLx6WWzDzCIuiTWdAxXMEnGnVXpB09qQ== - -"@types/d3-interpolate@*": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz#e7d17fa4a5830ad56fe22ce3b4fac8541a9572dc" - integrity sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw== - dependencies: - "@types/d3-color" "*" - -"@types/d3-path@*": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.0.0.tgz#939e3a784ae4f80b1fde8098b91af1776ff1312b" - integrity sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg== - -"@types/d3-polygon@*": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/d3-polygon/-/d3-polygon-3.0.0.tgz#5200a3fa793d7736fa104285fa19b0dbc2424b93" - integrity sha512-D49z4DyzTKXM0sGKVqiTDTYr+DHg/uxsiWDAkNrwXYuiZVd9o9wXZIo+YsHkifOiyBkmSWlEngHCQme54/hnHw== - -"@types/d3-quadtree@*": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/d3-quadtree/-/d3-quadtree-3.0.2.tgz#433112a178eb7df123aab2ce11c67f51cafe8ff5" - integrity sha512-QNcK8Jguvc8lU+4OfeNx+qnVy7c0VrDJ+CCVFS9srBo2GL9Y18CnIxBdTF3v38flrGy5s1YggcoAiu6s4fLQIw== - -"@types/d3-random@*": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/d3-random/-/d3-random-3.0.1.tgz#5c8d42b36cd4c80b92e5626a252f994ca6bfc953" - integrity sha512-IIE6YTekGczpLYo/HehAy3JGF1ty7+usI97LqraNa8IiDur+L44d0VOjAvFQWJVdZOJHukUJw+ZdZBlgeUsHOQ== - -"@types/d3-scale-chromatic@*": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz#103124777e8cdec85b20b51fd3397c682ee1e954" - integrity sha512-dsoJGEIShosKVRBZB0Vo3C8nqSDqVGujJU6tPznsBJxNJNwMF8utmS83nvCBKQYPpjCzaaHcrf66iTRpZosLPw== - -"@types/d3-scale@*": - version "4.0.2" - resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.2.tgz#41be241126af4630524ead9cb1008ab2f0f26e69" - integrity sha512-Yk4htunhPAwN0XGlIwArRomOjdoBFXC3+kCxK2Ubg7I9shQlVSJy/pG/Ht5ASN+gdMIalpk8TJ5xV74jFsetLA== - dependencies: - "@types/d3-time" "*" - -"@types/d3-selection@*": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-3.0.2.tgz#23e48a285b24063630bbe312cc0cfe2276de4a59" - integrity sha512-d29EDd0iUBrRoKhPndhDY6U/PYxOWqgIZwKTooy2UkBfU7TNZNpRho0yLWPxlatQrFWk2mnTu71IZQ4+LRgKlQ== - -"@types/d3-shape@*": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.0.2.tgz#4b1ca4ddaac294e76b712429726d40365cd1e8ca" - integrity sha512-5+ButCmIfNX8id5seZ7jKj3igdcxx+S9IDBiT35fQGTLZUfkFgTv+oBH34xgeoWDKpWcMITSzBILWQtBoN5Piw== - dependencies: - "@types/d3-path" "*" - -"@types/d3-time-format@*": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-4.0.0.tgz#ee7b6e798f8deb2d9640675f8811d0253aaa1946" - integrity sha512-yjfBUe6DJBsDin2BMIulhSHmr5qNR5Pxs17+oW4DoVPyVIXZ+m6bs7j1UVKP08Emv6jRmYrYqxYzO63mQxy1rw== - -"@types/d3-time@*": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.0.tgz#e1ac0f3e9e195135361fa1a1d62f795d87e6e819" - integrity sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg== - -"@types/d3-timer@*": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.0.tgz#e2505f1c21ec08bda8915238e397fb71d2fc54ce" - integrity sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g== - -"@types/d3-transition@*": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-3.0.1.tgz#c9a96125567173d6163a6985b874f79154f4cc3d" - integrity sha512-Sv4qEI9uq3bnZwlOANvYK853zvpdKEm1yz9rcc8ZTsxvRklcs9Fx4YFuGA3gXoQN/c/1T6QkVNjhaRO/cWj94g== - dependencies: - "@types/d3-selection" "*" - -"@types/d3-zoom@*": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/d3-zoom/-/d3-zoom-3.0.1.tgz#4bfc7e29625c4f79df38e2c36de52ec3e9faf826" - integrity sha512-7s5L9TjfqIYQmQQEUcpMAcBOahem7TRoSO/+Gkz02GbMVuULiZzjF2BOdw291dbO2aNon4m2OdFsRGaCq2caLQ== - dependencies: - "@types/d3-interpolate" "*" - "@types/d3-selection" "*" - -"@types/d3@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@types/d3/-/d3-7.1.0.tgz#8f32a7e7f434d8f920c8b1ebdfed55e18c033720" - integrity sha512-gYWvgeGjEl+zmF8c+U1RNIKqe7sfQwIXeLXO5Os72TjDjCEtgpvGBvZ8dXlAuSS1m6B90Y1Uo6Bm36OGR/OtCA== - dependencies: - "@types/d3-array" "*" - "@types/d3-axis" "*" - "@types/d3-brush" "*" - "@types/d3-chord" "*" - "@types/d3-color" "*" - "@types/d3-contour" "*" - "@types/d3-delaunay" "*" - "@types/d3-dispatch" "*" - "@types/d3-drag" "*" - "@types/d3-dsv" "*" - "@types/d3-ease" "*" - "@types/d3-fetch" "*" - "@types/d3-force" "*" - "@types/d3-format" "*" - "@types/d3-geo" "*" - "@types/d3-hierarchy" "*" - "@types/d3-interpolate" "*" - "@types/d3-path" "*" - "@types/d3-polygon" "*" - "@types/d3-quadtree" "*" - "@types/d3-random" "*" - "@types/d3-scale" "*" - "@types/d3-scale-chromatic" "*" - "@types/d3-selection" "*" - "@types/d3-shape" "*" - "@types/d3-time" "*" - "@types/d3-time-format" "*" - "@types/d3-timer" "*" - "@types/d3-transition" "*" - "@types/d3-zoom" "*" - "@types/eslint@^7.28.2": version "7.29.0" resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.29.0.tgz#e56ddc8e542815272720bb0b4ccc2aff9c3e1c78" @@ -2506,11 +2296,6 @@ dependencies: "@types/node" "*" -"@types/geojson@*": - version "7946.0.8" - resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.8.tgz#30744afdb385e2945e22f3b033f897f76b1f12ca" - integrity sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA== - "@types/glob@^7.1.1": version "7.2.0" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb" @@ -4057,16 +3842,16 @@ comma-separated-tokens@^1.0.0: resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea" integrity sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw== -commander@7, commander@^7.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" - integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== - commander@^2.20.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== +commander@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + commander@^8.3.0: version "8.3.0" resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" @@ -4466,250 +4251,6 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.10.tgz#2ad3a7bed70f35b965707c092e5f30b327c290e5" integrity sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA== -"d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@3: - version "3.1.1" - resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.1.1.tgz#7797eb53ead6b9083c75a45a681e93fc41bc468c" - integrity sha512-33qQ+ZoZlli19IFiQx4QEpf2CBEayMRzhlisJHSCsSUbDXv6ZishqS1x7uFVClKG4Wr7rZVHvaAttoLow6GqdQ== - dependencies: - internmap "1 - 2" - -d3-axis@3: - version "3.0.0" - resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-3.0.0.tgz#c42a4a13e8131d637b745fc2973824cfeaf93322" - integrity sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw== - -d3-brush@3: - version "3.0.0" - resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-3.0.0.tgz#6f767c4ed8dcb79de7ede3e1c0f89e63ef64d31c" - integrity sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ== - dependencies: - d3-dispatch "1 - 3" - d3-drag "2 - 3" - d3-interpolate "1 - 3" - d3-selection "3" - d3-transition "3" - -d3-chord@3: - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-chord/-/d3-chord-3.0.1.tgz#d156d61f485fce8327e6abf339cb41d8cbba6966" - integrity sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g== - dependencies: - d3-path "1 - 3" - -"d3-color@1 - 3", d3-color@3: - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.0.1.tgz#03316e595955d1fcd39d9f3610ad41bb90194d0a" - integrity sha512-6/SlHkDOBLyQSJ1j1Ghs82OIUXpKWlR0hCsw0XrLSQhuUPuCSmLQ1QPH98vpnQxMUQM2/gfAkUEWsupVpd9JGw== - -d3-contour@3: - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-contour/-/d3-contour-3.0.1.tgz#2c64255d43059599cd0dba8fe4cc3d51ccdd9bbd" - integrity sha512-0Oc4D0KyhwhM7ZL0RMnfGycLN7hxHB8CMmwZ3+H26PWAG0ozNuYG5hXSDNgmP1SgJkQMrlG6cP20HoaSbvcJTQ== - dependencies: - d3-array "2 - 3" - -d3-delaunay@6: - version "6.0.2" - resolved "https://registry.yarnpkg.com/d3-delaunay/-/d3-delaunay-6.0.2.tgz#7fd3717ad0eade2fc9939f4260acfb503f984e92" - integrity sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ== - dependencies: - delaunator "5" - -"d3-dispatch@1 - 3", d3-dispatch@3: - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e" - integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg== - -"d3-drag@2 - 3", d3-drag@3: - version "3.0.0" - resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba" - integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg== - dependencies: - d3-dispatch "1 - 3" - d3-selection "3" - -"d3-dsv@1 - 3", d3-dsv@3: - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-3.0.1.tgz#c63af978f4d6a0d084a52a673922be2160789b73" - integrity sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q== - dependencies: - commander "7" - iconv-lite "0.6" - rw "1" - -"d3-ease@1 - 3", d3-ease@3: - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4" - integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w== - -d3-fetch@3: - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-fetch/-/d3-fetch-3.0.1.tgz#83141bff9856a0edb5e38de89cdcfe63d0a60a22" - integrity sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw== - dependencies: - d3-dsv "1 - 3" - -d3-force@3: - version "3.0.0" - resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-3.0.0.tgz#3e2ba1a61e70888fe3d9194e30d6d14eece155c4" - integrity sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg== - dependencies: - d3-dispatch "1 - 3" - d3-quadtree "1 - 3" - d3-timer "1 - 3" - -"d3-format@1 - 3", d3-format@3: - version "3.1.0" - resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641" - integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA== - -d3-geo@3: - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-3.0.1.tgz#4f92362fd8685d93e3b1fae0fd97dc8980b1ed7e" - integrity sha512-Wt23xBych5tSy9IYAM1FR2rWIBFWa52B/oF/GYe5zbdHrg08FU8+BuI6X4PvTwPDdqdAdq04fuWJpELtsaEjeA== - dependencies: - d3-array "2.5.0 - 3" - -d3-hierarchy@3: - version "3.1.1" - resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-3.1.1.tgz#9cbb0ffd2375137a351e6cfeed344a06d4ff4597" - integrity sha512-LtAIu54UctRmhGKllleflmHalttH3zkfSi4NlKrTAoFKjC+AFBJohsCAdgCBYQwH0F8hIOGY89X1pPqAchlMkA== - -"d3-interpolate@1 - 3", "d3-interpolate@1.2.0 - 3", d3-interpolate@3: - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" - integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== - dependencies: - d3-color "1 - 3" - -"d3-path@1 - 3", d3-path@3: - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.0.1.tgz#f09dec0aaffd770b7995f1a399152bf93052321e" - integrity sha512-gq6gZom9AFZby0YLduxT1qmrp4xpBA1YZr19OI717WIdKE2OM5ETq5qrHLb301IgxhLwcuxvGZVLeeWc/k1I6w== - -d3-polygon@3: - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-3.0.1.tgz#0b45d3dd1c48a29c8e057e6135693ec80bf16398" - integrity sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg== - -"d3-quadtree@1 - 3", d3-quadtree@3: - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz#6dca3e8be2b393c9a9d514dabbd80a92deef1a4f" - integrity sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw== - -d3-random@3: - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-random/-/d3-random-3.0.1.tgz#d4926378d333d9c0bfd1e6fa0194d30aebaa20f4" - integrity sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ== - -d3-scale-chromatic@3: - version "3.0.0" - resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz#15b4ceb8ca2bb0dcb6d1a641ee03d59c3b62376a" - integrity sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g== - dependencies: - d3-color "1 - 3" - d3-interpolate "1 - 3" - -d3-scale@4: - version "4.0.2" - resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396" - integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ== - dependencies: - d3-array "2.10.0 - 3" - d3-format "1 - 3" - d3-interpolate "1.2.0 - 3" - d3-time "2.1.1 - 3" - d3-time-format "2 - 4" - -"d3-selection@2 - 3", d3-selection@3: - version "3.0.0" - resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31" - integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ== - -d3-shape@3: - version "3.1.0" - resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.1.0.tgz#c8a495652d83ea6f524e482fca57aa3f8bc32556" - integrity sha512-tGDh1Muf8kWjEDT/LswZJ8WF85yDZLvVJpYU9Nq+8+yW1Z5enxrmXOhTArlkaElU+CTn0OTVNli+/i+HP45QEQ== - dependencies: - d3-path "1 - 3" - -"d3-time-format@2 - 4", d3-time-format@4: - version "4.1.0" - resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a" - integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg== - dependencies: - d3-time "1 - 3" - -"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@3: - version "3.0.0" - resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.0.0.tgz#65972cb98ae2d4954ef5c932e8704061335d4975" - integrity sha512-zmV3lRnlaLI08y9IMRXSDshQb5Nj77smnfpnd2LrBa/2K281Jijactokeak14QacHs/kKq0AQ121nidNYlarbQ== - dependencies: - d3-array "2 - 3" - -"d3-timer@1 - 3", d3-timer@3: - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0" - integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== - -"d3-transition@2 - 3", d3-transition@3: - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f" - integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w== - dependencies: - d3-color "1 - 3" - d3-dispatch "1 - 3" - d3-ease "1 - 3" - d3-interpolate "1 - 3" - d3-timer "1 - 3" - -d3-zoom@3: - version "3.0.0" - resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-3.0.0.tgz#d13f4165c73217ffeaa54295cd6969b3e7aee8f3" - integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw== - dependencies: - d3-dispatch "1 - 3" - d3-drag "2 - 3" - d3-interpolate "1 - 3" - d3-selection "2 - 3" - d3-transition "2 - 3" - -d3@^7.3.0: - version "7.3.0" - resolved "https://registry.yarnpkg.com/d3/-/d3-7.3.0.tgz#f3d5a22c1f658952a6491cf50132f5267ed7a40a" - integrity sha512-MDRLJCMK232OJQRqGljQ/gCxtB8k3/sLKFjftMjzPB3nKVUODpdW9Rb3vcq7U8Ka5YKoZkAmp++Ur6I+6iNWIw== - dependencies: - d3-array "3" - d3-axis "3" - d3-brush "3" - d3-chord "3" - d3-color "3" - d3-contour "3" - d3-delaunay "6" - d3-dispatch "3" - d3-drag "3" - d3-dsv "3" - d3-ease "3" - d3-fetch "3" - d3-force "3" - d3-format "3" - d3-geo "3" - d3-hierarchy "3" - d3-interpolate "3" - d3-path "3" - d3-polygon "3" - d3-quadtree "3" - d3-random "3" - d3-scale "4" - d3-scale-chromatic "3" - d3-selection "3" - d3-shape "3" - d3-time "3" - d3-time-format "4" - d3-timer "3" - d3-transition "3" - d3-zoom "3" - damerau-levenshtein@^1.0.7: version "1.0.8" resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" @@ -4827,13 +4368,6 @@ del@^6.0.0: rimraf "^3.0.2" slash "^3.0.0" -delaunator@5: - version "5.0.0" - resolved "https://registry.yarnpkg.com/delaunator/-/delaunator-5.0.0.tgz#60f052b28bd91c9b4566850ebf7756efe821d81b" - integrity sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw== - dependencies: - robust-predicates "^3.0.0" - delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -6428,7 +5962,7 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" -iconv-lite@0.6, iconv-lite@^0.6.3: +iconv-lite@^0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== @@ -6560,11 +6094,6 @@ internal-slot@^1.0.3: has "^1.0.3" side-channel "^1.0.4" -"internmap@1 - 2": - version "2.0.3" - resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009" - integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== - invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" @@ -9912,11 +9441,6 @@ rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2: dependencies: glob "^7.1.3" -robust-predicates@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.1.tgz#ecde075044f7f30118682bd9fb3f123109577f9a" - integrity sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g== - rollup-plugin-copy@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/rollup-plugin-copy/-/rollup-plugin-copy-3.4.0.tgz#f1228a3ffb66ffad8606e2f3fb7ff23141ed3286" @@ -9986,11 +9510,6 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -rw@1: - version "1.3.3" - resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4" - integrity sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q= - rxjs@^7.2.0: version "7.5.4" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.4.tgz#3d6bd407e6b7ce9a123e76b1e770dc5761aa368d" From 14c1000554c590cb89ecb5ef44c57aa1b5dd1387 Mon Sep 17 00:00:00 2001 From: Daniele Martinoli <86618610+dmartinol@users.noreply.github.com> Date: Thu, 17 Oct 2024 17:22:07 +0200 Subject: [PATCH 148/185] feat: An action to test operator at PR time (#4635) --- .github/workflows/operator_pr.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/workflows/operator_pr.yml diff --git a/.github/workflows/operator_pr.yml b/.github/workflows/operator_pr.yml new file mode 100644 index 0000000000..e4d371b945 --- /dev/null +++ b/.github/workflows/operator_pr.yml @@ -0,0 +1,18 @@ +name: operator-pr + +on: [pull_request] +jobs: + operator-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Go + uses: actions/setup-go@v2 + with: + go-version: 1.21.x + - name: Operator tests + run: | + cd infra/feast-operator/ + make test + - name: After code formatting, check for uncommitted differences + run: git diff --exit-code infra/feast-operator From 8e3327733a2985725c60527caf95fd9b4eaa94a2 Mon Sep 17 00:00:00 2001 From: Rob Howley Date: Thu, 17 Oct 2024 14:21:17 -0400 Subject: [PATCH 149/185] chore: Update Adopters to include myself (#4643) * Update ADOPTERS.md Signed-off-by: Rob Howley * trailing pipe for consistency Signed-off-by: Rob Howley --------- Signed-off-by: Rob Howley --- community/ADOPTERS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/community/ADOPTERS.md b/community/ADOPTERS.md index a16fbef379..5ef285b41b 100644 --- a/community/ADOPTERS.md +++ b/community/ADOPTERS.md @@ -11,5 +11,6 @@ alphabetical order. | Get Ground | Zhiling Chen | zhilingc | | Gojek | Pradithya Aria Pura | pradithya | | Twitter | David Liu | mavysavydav| +| SeatGeek | Rob Howley | robhowley | | Shopify | Matt Delacour | MattDelac | | Snowflake | Miles Adkins | sfc-gh-madkins | From 0a2bb47fdf6de8adc8e5321248cd8b07b8f64627 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Oct 2024 15:33:08 -0400 Subject: [PATCH 150/185] chore: Bump starlette from 0.38.5 to 0.40.0 in /sdk/python/requirements (#4629) Bumps [starlette](https://github.com/encode/starlette) from 0.38.5 to 0.40.0. - [Release notes](https://github.com/encode/starlette/releases) - [Changelog](https://github.com/encode/starlette/blob/master/docs/release-notes.md) - [Commits](https://github.com/encode/starlette/compare/0.38.5...0.40.0) --- updated-dependencies: - dependency-name: starlette dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdk/python/requirements/py3.10-ci-requirements.txt | 2 +- sdk/python/requirements/py3.10-requirements.txt | 2 +- sdk/python/requirements/py3.11-ci-requirements.txt | 2 +- sdk/python/requirements/py3.11-requirements.txt | 2 +- sdk/python/requirements/py3.9-ci-requirements.txt | 2 +- sdk/python/requirements/py3.9-requirements.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/sdk/python/requirements/py3.10-ci-requirements.txt b/sdk/python/requirements/py3.10-ci-requirements.txt index 6268de6ae1..be13d71b82 100644 --- a/sdk/python/requirements/py3.10-ci-requirements.txt +++ b/sdk/python/requirements/py3.10-ci-requirements.txt @@ -767,7 +767,7 @@ sqlparams==6.1.0 # via singlestoredb stack-data==0.6.3 # via ipython -starlette==0.38.5 +starlette==0.40.0 # via fastapi substrait==0.23.0 # via ibis-substrait diff --git a/sdk/python/requirements/py3.10-requirements.txt b/sdk/python/requirements/py3.10-requirements.txt index 9e5eb0be72..8d34dcdcf3 100644 --- a/sdk/python/requirements/py3.10-requirements.txt +++ b/sdk/python/requirements/py3.10-requirements.txt @@ -108,7 +108,7 @@ six==1.16.0 sniffio==1.3.1 # via anyio sqlalchemy[mypy]==2.0.35 -starlette==0.38.5 +starlette==0.40.0 # via fastapi tabulate==0.9.0 tenacity==8.5.0 diff --git a/sdk/python/requirements/py3.11-ci-requirements.txt b/sdk/python/requirements/py3.11-ci-requirements.txt index 946d4e0519..1c0d09139a 100644 --- a/sdk/python/requirements/py3.11-ci-requirements.txt +++ b/sdk/python/requirements/py3.11-ci-requirements.txt @@ -758,7 +758,7 @@ sqlparams==6.1.0 # via singlestoredb stack-data==0.6.3 # via ipython -starlette==0.38.5 +starlette==0.40.0 # via fastapi substrait==0.23.0 # via ibis-substrait diff --git a/sdk/python/requirements/py3.11-requirements.txt b/sdk/python/requirements/py3.11-requirements.txt index 1ce25e7d5b..649b08f492 100644 --- a/sdk/python/requirements/py3.11-requirements.txt +++ b/sdk/python/requirements/py3.11-requirements.txt @@ -106,7 +106,7 @@ six==1.16.0 sniffio==1.3.1 # via anyio sqlalchemy[mypy]==2.0.35 -starlette==0.38.5 +starlette==0.40.0 # via fastapi tabulate==0.9.0 tenacity==8.5.0 diff --git a/sdk/python/requirements/py3.9-ci-requirements.txt b/sdk/python/requirements/py3.9-ci-requirements.txt index 5ea2c58819..3dba480af6 100644 --- a/sdk/python/requirements/py3.9-ci-requirements.txt +++ b/sdk/python/requirements/py3.9-ci-requirements.txt @@ -775,7 +775,7 @@ sqlparams==6.1.0 # via singlestoredb stack-data==0.6.3 # via ipython -starlette==0.38.5 +starlette==0.40.0 # via fastapi substrait==0.23.0 # via ibis-substrait diff --git a/sdk/python/requirements/py3.9-requirements.txt b/sdk/python/requirements/py3.9-requirements.txt index 857d7d72bf..ba30a4ecf5 100644 --- a/sdk/python/requirements/py3.9-requirements.txt +++ b/sdk/python/requirements/py3.9-requirements.txt @@ -110,7 +110,7 @@ six==1.16.0 sniffio==1.3.1 # via anyio sqlalchemy[mypy]==2.0.35 -starlette==0.38.5 +starlette==0.40.0 # via fastapi tabulate==0.9.0 tenacity==8.5.0 From c1f19127ab5c22dfe67869990480e7e9d8183ab1 Mon Sep 17 00:00:00 2001 From: Rob Howley Date: Fri, 18 Oct 2024 11:28:01 -0400 Subject: [PATCH 151/185] perf: Default to async endpoints, use threadpool for sync (#4647) --- sdk/python/feast/feature_server.py | 16 ++++++++++++---- sdk/python/feast/infra/online_stores/dynamodb.py | 5 +++++ .../feast/infra/online_stores/online_store.py | 5 +++++ sdk/python/feast/infra/passthrough_provider.py | 7 +++++++ sdk/python/feast/infra/provider.py | 5 +++++ .../feast/infra/supported_async_methods.py | 10 ++++++++++ 6 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 sdk/python/feast/infra/supported_async_methods.py diff --git a/sdk/python/feast/feature_server.py b/sdk/python/feast/feature_server.py index c188bd0d7b..ab88eca89f 100644 --- a/sdk/python/feast/feature_server.py +++ b/sdk/python/feast/feature_server.py @@ -10,6 +10,7 @@ import psutil from dateutil import parser from fastapi import Depends, FastAPI, Request, Response, status +from fastapi.concurrency import run_in_threadpool from fastapi.logger import logger from fastapi.responses import JSONResponse from google.protobuf.json_format import MessageToDict @@ -112,7 +113,7 @@ async def get_body(request: Request): "/get-online-features", dependencies=[Depends(inject_user_details)], ) - def get_online_features(body=Depends(get_body)): + async def get_online_features(body=Depends(get_body)): body = json.loads(body) full_feature_names = body.get("full_feature_names", False) entity_rows = body["entities"] @@ -145,15 +146,22 @@ def get_online_features(body=Depends(get_body)): resource=od_feature_view, actions=[AuthzedAction.READ_ONLINE] ) - response_proto = store.get_online_features( + read_params = dict( features=features, entity_rows=entity_rows, full_feature_names=full_feature_names, - ).proto + ) + + if store._get_provider().async_supported.online.read: + response = await store.get_online_features_async(**read_params) + else: + response = await run_in_threadpool( + lambda: store.get_online_features(**read_params) + ) # Convert the Protobuf object to JSON and return it return MessageToDict( - response_proto, preserving_proto_field_name=True, float_precision=18 + response.proto, preserving_proto_field_name=True, float_precision=18 ) @app.post("/push", dependencies=[Depends(inject_user_details)]) diff --git a/sdk/python/feast/infra/online_stores/dynamodb.py b/sdk/python/feast/infra/online_stores/dynamodb.py index a6cbfb41d2..a915d2ee34 100644 --- a/sdk/python/feast/infra/online_stores/dynamodb.py +++ b/sdk/python/feast/infra/online_stores/dynamodb.py @@ -23,6 +23,7 @@ from feast.infra.infra_object import DYNAMODB_INFRA_OBJECT_CLASS_TYPE, InfraObject from feast.infra.online_stores.helpers import compute_entity_id from feast.infra.online_stores.online_store import OnlineStore +from feast.infra.supported_async_methods import SupportedAsyncMethods from feast.protos.feast.core.DynamoDBTable_pb2 import ( DynamoDBTable as DynamoDBTableProto, ) @@ -88,6 +89,10 @@ class DynamoDBOnlineStore(OnlineStore): _dynamodb_resource = None _aioboto_session = None + @property + def async_supported(self) -> SupportedAsyncMethods: + return SupportedAsyncMethods(read=True) + def update( self, config: RepoConfig, diff --git a/sdk/python/feast/infra/online_stores/online_store.py b/sdk/python/feast/infra/online_stores/online_store.py index cf2d68eb74..be2065040b 100644 --- a/sdk/python/feast/infra/online_stores/online_store.py +++ b/sdk/python/feast/infra/online_stores/online_store.py @@ -22,6 +22,7 @@ from feast.feature_view import FeatureView from feast.infra.infra_object import InfraObject from feast.infra.registry.base_registry import BaseRegistry +from feast.infra.supported_async_methods import SupportedAsyncMethods from feast.online_response import OnlineResponse from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto @@ -36,6 +37,10 @@ class OnlineStore(ABC): The interface that Feast uses to interact with the storage system that handles online features. """ + @property + def async_supported(self) -> SupportedAsyncMethods: + return SupportedAsyncMethods() + @abstractmethod def online_write_batch( self, diff --git a/sdk/python/feast/infra/passthrough_provider.py b/sdk/python/feast/infra/passthrough_provider.py index 5acfc0d6f3..ea75cf5ff2 100644 --- a/sdk/python/feast/infra/passthrough_provider.py +++ b/sdk/python/feast/infra/passthrough_provider.py @@ -35,6 +35,7 @@ from feast.infra.online_stores.helpers import get_online_store_from_config from feast.infra.provider import Provider from feast.infra.registry.base_registry import BaseRegistry +from feast.infra.supported_async_methods import ProviderAsyncMethods from feast.online_response import OnlineResponse from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto @@ -79,6 +80,12 @@ def offline_store(self): ) return self._offline_store + @property + def async_supported(self) -> ProviderAsyncMethods: + return ProviderAsyncMethods( + online=self.online_store.async_supported, + ) + @property def batch_engine(self) -> BatchMaterializationEngine: if self._batch_engine: diff --git a/sdk/python/feast/infra/provider.py b/sdk/python/feast/infra/provider.py index 0723e0513f..fb483d194e 100644 --- a/sdk/python/feast/infra/provider.py +++ b/sdk/python/feast/infra/provider.py @@ -27,6 +27,7 @@ from feast.infra.infra_object import Infra from feast.infra.offline_stores.offline_store import RetrievalJob from feast.infra.registry.base_registry import BaseRegistry +from feast.infra.supported_async_methods import ProviderAsyncMethods from feast.on_demand_feature_view import OnDemandFeatureView from feast.online_response import OnlineResponse from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto @@ -55,6 +56,10 @@ class Provider(ABC): def __init__(self, config: RepoConfig): pass + @property + def async_supported(self) -> ProviderAsyncMethods: + return ProviderAsyncMethods() + @abstractmethod def update_infra( self, diff --git a/sdk/python/feast/infra/supported_async_methods.py b/sdk/python/feast/infra/supported_async_methods.py new file mode 100644 index 0000000000..b675aa7040 --- /dev/null +++ b/sdk/python/feast/infra/supported_async_methods.py @@ -0,0 +1,10 @@ +from pydantic import BaseModel, Field + + +class SupportedAsyncMethods(BaseModel): + read: bool = Field(default=False) + write: bool = Field(default=False) + + +class ProviderAsyncMethods(BaseModel): + online: SupportedAsyncMethods = Field(default_factory=SupportedAsyncMethods) From 18d0eaabdb36b50eac8b2cbc1252894550d137aa Mon Sep 17 00:00:00 2001 From: Matt Green Date: Fri, 18 Oct 2024 10:46:39 -0700 Subject: [PATCH 152/185] chore: Change Field.__repr__ print format (#4636) change Field.__repr__ print format Signed-off-by: Matt Green --- sdk/python/feast/field.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/python/feast/field.py b/sdk/python/feast/field.py index b07bddfeac..7c9f14decf 100644 --- a/sdk/python/feast/field.py +++ b/sdk/python/feast/field.py @@ -81,7 +81,7 @@ def __lt__(self, other): return self.name < other.name def __repr__(self): - return f"{self.name}-{self.dtype}" + return f"Field(name='{self.name}', dtype={self.dtype}, description='{self.description}' tags={self.tags})" def __str__(self): return f"Field(name={self.name}, dtype={self.dtype}, tags={self.tags})" From 15e480816cc8f502ed3f7c337ae87f5d1962776f Mon Sep 17 00:00:00 2001 From: Matt Green Date: Fri, 18 Oct 2024 16:42:16 -0700 Subject: [PATCH 153/185] chore: Update 'What is Feast' documentation (#4651) update 'What is Feast' documentation Signed-off-by: Matt Green --- docs/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/README.md b/docs/README.md index 6652eaddc8..5e36e1ce40 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,6 +13,14 @@ for serving features at low-latency in production systems and applications. Feast is a configurable operational data system that re-uses existing infrastructure to manage and serve machine learning features to realtime models. For more details please review our [architecture](getting-started/architecture/overview.md). +Concretely, Feast provides: + +* A python SDK for programtically defining features, entities, sources, and (optionally) transformations +* A python SDK for for reading and writing features to configured offline and online data stores +* An [optional feature server](reference/feature-servers/README.md) for reading and writing features (useful for non-python languages) +* A [UI](reference/alpha-web-ui.md) for viewing and exploring information about features defined in the project +* A [CLI tool](reference/feast-cli-commands.md) for viewing and updating feature information + Feast allows ML platform teams to: * **Make features consistently available for training and low-latency serving** by managing an _offline store_ (to process historical data for scale-out batch scoring or model training), a low-latency _online store_ (to power real-time prediction)_,_ and a battle-tested _feature server_ (to serve pre-computed features online). From c40d539b85fe537077dd26904d78624da3d33951 Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Sat, 19 Oct 2024 15:36:20 -0400 Subject: [PATCH 154/185] feat: Adding mode='python' for get_historical_features on ODFVs (#4653) --- .../transformation/python_transformation.py | 8 ++++---- .../test_on_demand_python_transformation.py | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/sdk/python/feast/transformation/python_transformation.py b/sdk/python/feast/transformation/python_transformation.py index d828890b1e..7e7c6e8bc3 100644 --- a/sdk/python/feast/transformation/python_transformation.py +++ b/sdk/python/feast/transformation/python_transformation.py @@ -26,11 +26,11 @@ def __init__(self, udf: FunctionType, udf_string: str = ""): self.udf_string = udf_string def transform_arrow( - self, pa_table: pyarrow.Table, features: list[Field] + self, + pa_table: pyarrow.Table, + features: list[Field], ) -> pyarrow.Table: - raise Exception( - 'OnDemandFeatureView with mode "python" does not support offline processing.' - ) + return pyarrow.Table.from_pydict(self.udf(pa_table.to_pydict())) def transform(self, input_dict: dict) -> dict: # Ensuring that the inputs are included as well diff --git a/sdk/python/tests/unit/test_on_demand_python_transformation.py b/sdk/python/tests/unit/test_on_demand_python_transformation.py index 3a2ea7637b..b7ddfb9e75 100644 --- a/sdk/python/tests/unit/test_on_demand_python_transformation.py +++ b/sdk/python/tests/unit/test_on_demand_python_transformation.py @@ -561,6 +561,24 @@ def pandas_view(features_df: pd.DataFrame) -> pd.DataFrame: assert resp is not None assert resp["conv_rate_plus_val1"].isnull().sum() == 0 + batch_sample["avg_daily_trip_rank_thresholds"] = [ + [100, 250, 500, 1000] + ] * batch_sample.shape[0] + batch_sample["avg_daily_trip_rank_names"] = [ + ["Bronze", "Silver", "Gold", "Platinum"] + ] * batch_sample.shape[0] + resp_python = self.store.get_historical_features( + entity_df=batch_sample, + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "driver_hourly_stats:avg_daily_trips", + "python_view:conv_rate_plus_acc", + ], + ).to_df() + assert resp_python is not None + assert resp_python["conv_rate_plus_acc"].isnull().sum() == 0 + # Now testing feature retrieval for driver ids not in the dataset missing_batch_sample = pd.DataFrame([1234567890], columns=["driver_id"]) missing_batch_sample["val_to_add"] = 0 From 7292f85abc3280471f04a017f1733517450b561e Mon Sep 17 00:00:00 2001 From: Harri Lehtola <1781172+peruukki@users.noreply.github.com> Date: Sun, 20 Oct 2024 11:43:22 +0300 Subject: [PATCH 155/185] chore: Upgrade react-code-blocks to 0.1.6 in Feast UI (#4645) This is the only clear runtime dependency that had vulnerabilities listed in `yarn audit` output. Upgrading it to the latest version removes 4 high and 5 moderate level vulnerabilities. Signed-off-by: Harri Lehtola --- ui/package.json | 2 +- .../BatchSourcePropertiesView.tsx | 4 +- ui/yarn.lock | 294 +++++++----------- 3 files changed, 113 insertions(+), 187 deletions(-) diff --git a/ui/package.json b/ui/package.json index 09c16f2571..169aa11bf0 100644 --- a/ui/package.json +++ b/ui/package.json @@ -31,7 +31,7 @@ "moment": "^2.29.1", "protobufjs": "^7.1.1", "query-string": "^7.1.1", - "react-code-blocks": "^0.0.9-0", + "react-code-blocks": "^0.1.6", "react-query": "^3.39.3", "react-router-dom": "<6.4.0", "react-scripts": "^5.0.0", diff --git a/ui/src/pages/data-sources/BatchSourcePropertiesView.tsx b/ui/src/pages/data-sources/BatchSourcePropertiesView.tsx index b7cd3c90fc..ad86b4b5cb 100644 --- a/ui/src/pages/data-sources/BatchSourcePropertiesView.tsx +++ b/ui/src/pages/data-sources/BatchSourcePropertiesView.tsx @@ -70,11 +70,11 @@ const BatchSourcePropertiesView = (props: BatchSourcePropertiesViewProps) => { {batchSource.bigqueryOptions.table} ) : } diff --git a/ui/yarn.lock b/ui/yarn.lock index 894398942c..af2f2101cf 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -122,13 +122,6 @@ "@jridgewell/trace-mapping" "^0.3.17" jsesc "^2.5.1" -"@babel/helper-annotate-as-pure@^7.16.0": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb" - integrity sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA== - dependencies: - "@babel/types" "^7.18.6" - "@babel/helper-annotate-as-pure@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz#bb2339a7534a9c128e3102024c60760a3a7f3862" @@ -253,13 +246,6 @@ dependencies: "@babel/types" "^7.16.7" -"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.16.0": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.21.4.tgz#ac88b2f76093637489e718a90cec6cf8a9b029af" - integrity sha512-orajc5T2PsRYUN3ZryCEFeMDYwyw09c/pZeaQEZPH0MpKzSvn3e0uXsDBu3k03VI+9DBiRo+l22BfKTpKwa/Wg== - dependencies: - "@babel/types" "^7.21.4" - "@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz#25612a8091a999704461c8a222d0efec5d091437" @@ -341,11 +327,6 @@ dependencies: "@babel/types" "^7.22.5" -"@babel/helper-string-parser@^7.19.4": - version "7.19.4" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63" - integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw== - "@babel/helper-string-parser@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" @@ -356,11 +337,6 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad" integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw== -"@babel/helper-validator-identifier@^7.19.1": - version "7.19.1" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" - integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== - "@babel/helper-validator-identifier@^7.22.20": version "7.22.20" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" @@ -1215,7 +1191,7 @@ "@babel/parser" "^7.22.15" "@babel/types" "^7.22.15" -"@babel/traverse@^7.13.0", "@babel/traverse@^7.16.10", "@babel/traverse@^7.16.7", "@babel/traverse@^7.16.8", "@babel/traverse@^7.17.0", "@babel/traverse@^7.17.3", "@babel/traverse@^7.4.5", "@babel/traverse@^7.7.2": +"@babel/traverse@^7.13.0", "@babel/traverse@^7.16.10", "@babel/traverse@^7.16.7", "@babel/traverse@^7.16.8", "@babel/traverse@^7.17.0", "@babel/traverse@^7.17.3", "@babel/traverse@^7.7.2": version "7.23.2" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8" integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw== @@ -1247,15 +1223,6 @@ "@babel/helper-validator-identifier" "^7.16.7" to-fast-properties "^2.0.0" -"@babel/types@^7.18.6", "@babel/types@^7.21.4": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.21.4.tgz#2d5d6bb7908699b3b416409ffd3b5daa25b030d4" - integrity sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA== - dependencies: - "@babel/helper-string-parser" "^7.19.4" - "@babel/helper-validator-identifier" "^7.19.1" - to-fast-properties "^2.0.0" - "@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0": version "7.23.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb" @@ -1370,17 +1337,17 @@ resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.2.tgz#ff9221b9f58b4dfe61e619a7788734bd63f6898b" integrity sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g== -"@emotion/is-prop-valid@^1.1.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz#7f2d35c97891669f7e276eb71c83376a5dc44c83" - integrity sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg== +"@emotion/is-prop-valid@1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz#d4175076679c6a26faa92b03bb786f9e52612337" + integrity sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw== dependencies: - "@emotion/memoize" "^0.8.0" + "@emotion/memoize" "^0.8.1" -"@emotion/memoize@^0.8.0": - version "0.8.0" - resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.0.tgz#f580f9beb67176fa57aae70b08ed510e1b18980f" - integrity sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA== +"@emotion/memoize@^0.8.1": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.1.tgz#c1ddb040429c6d21d38cc945fe75c818cfb68e17" + integrity sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA== "@emotion/memoize@^0.9.0": version "0.9.0" @@ -1417,21 +1384,16 @@ resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.4.0.tgz#c9299c34d248bc26e82563735f78953d2efca83c" integrity sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg== -"@emotion/stylis@^0.8.4": - version "0.8.5" - resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.5.tgz#deacb389bd6ee77d1e7fcaccce9e16c5c7e78e04" - integrity sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ== +"@emotion/unitless@0.8.1": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.8.1.tgz#182b5a4704ef8ad91bde93f7a860a88fd92c79a3" + integrity sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ== "@emotion/unitless@^0.10.0": version "0.10.0" resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.10.0.tgz#2af2f7c7e5150f497bdabd848ce7b218a27cf745" integrity sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg== -"@emotion/unitless@^0.7.4": - version "0.7.5" - resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" - integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== - "@emotion/use-insertion-effect-with-fallbacks@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz#1a818a0b2c481efba0cf34e5ab1e0cb2dcb9dfaf" @@ -2572,6 +2534,11 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== +"@types/stylis@4.2.5": + version "4.2.5" + resolved "https://registry.yarnpkg.com/@types/stylis/-/stylis-4.2.5.tgz#1daa6456f40959d06157698a653a9ab0a70281df" + integrity sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw== + "@types/through@*": version "0.0.30" resolved "https://registry.yarnpkg.com/@types/through/-/through-0.0.30.tgz#e0e42ce77e897bd6aead6f6ea62aeb135b8a3895" @@ -3259,22 +3226,6 @@ babel-plugin-polyfill-regenerator@^0.3.0: dependencies: "@babel/helper-define-polyfill-provider" "^0.3.1" -"babel-plugin-styled-components@>= 1.12.0": - version "2.1.1" - resolved "https://registry.yarnpkg.com/babel-plugin-styled-components/-/babel-plugin-styled-components-2.1.1.tgz#cd977cc0ff8410d5cbfdd142e42576e9c8794b87" - integrity sha512-c8lJlszObVQPguHkI+akXv8+Jgb9Ccujx0EetL7oIvwU100LxO6XAGe45qry37wUL40a5U9f23SYrivro2XKhA== - dependencies: - "@babel/helper-annotate-as-pure" "^7.16.0" - "@babel/helper-module-imports" "^7.16.0" - babel-plugin-syntax-jsx "^6.18.0" - lodash "^4.17.21" - picomatch "^2.3.0" - -babel-plugin-syntax-jsx@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946" - integrity sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw== - babel-plugin-transform-react-remove-prop-types@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz#f2edaf9b4c6a5fbe5c1d678bfb531078c1555f3a" @@ -3744,15 +3695,6 @@ cli-width@^3.0.0: resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== -clipboard@^2.0.0: - version "2.0.11" - resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.11.tgz#62180360b97dd668b6b3a84ec226975762a70be5" - integrity sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw== - dependencies: - good-listener "^1.2.2" - select "^1.1.2" - tiny-emitter "^2.0.0" - cliui@^7.0.2: version "7.0.4" resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" @@ -4100,7 +4042,7 @@ css-select@^4.1.3: domutils "^2.8.0" nth-check "^2.0.1" -css-to-react-native@^3.0.0: +css-to-react-native@3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/css-to-react-native/-/css-to-react-native-3.2.0.tgz#cdd8099f71024e149e4f6fe17a7d46ecd55f1e32" integrity sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ== @@ -4246,6 +4188,11 @@ cssstyle@^2.3.0: dependencies: cssom "~0.3.6" +csstype@3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== + csstype@^3.0.2: version "3.0.10" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.10.tgz#2ad3a7bed70f35b965707c092e5f30b327c290e5" @@ -4373,11 +4320,6 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= -delegate@^3.1.2: - version "3.2.0" - resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166" - integrity sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw== - depd@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" @@ -5168,7 +5110,7 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" -fault@^1.0.2: +fault@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/fault/-/fault-1.0.4.tgz#eafcfc0a6d214fc94601e170df29954a4f842f13" integrity sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA== @@ -5594,13 +5536,6 @@ globby@^11.0.1, globby@^11.0.4: merge2 "^1.4.1" slash "^3.0.0" -good-listener@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50" - integrity sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw== - dependencies: - delegate "^3.1.2" - graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6: version "4.2.9" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" @@ -5761,16 +5696,6 @@ hast-util-whitespace@^1.0.0: resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-1.0.4.tgz#e4fe77c4a9ae1cb2e6c25e02df0043d0164f6e41" integrity sha512-I5GTdSfhYfAPNztx2xJRQpG8cuDSNt599/7YUn7Gx/WxNMsG+a835k97TDkFgk123cwjfwINaZknkKkphx/f2A== -hastscript@^5.0.0: - version "5.1.2" - resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-5.1.2.tgz#bde2c2e56d04c62dd24e8c5df288d050a355fb8a" - integrity sha512-WlztFuK+Lrvi3EggsqOkQ52rKbxkXL3RwB6t5lwoa8QLMemoWfBuL43eDrwOamJyR7uKQKdmKYaBH1NZBiIRrQ== - dependencies: - comma-separated-tokens "^1.0.0" - hast-util-parse-selector "^2.0.0" - property-information "^5.0.0" - space-separated-tokens "^1.0.0" - hastscript@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-6.0.0.tgz#e8768d7eac56c3fdeac8a92830d58e811e5bf640" @@ -5792,10 +5717,15 @@ headers-utils@^3.0.2: resolved "https://registry.yarnpkg.com/headers-utils/-/headers-utils-3.0.2.tgz#dfc65feae4b0e34357308aefbcafa99c895e59ef" integrity sha512-xAxZkM1dRyGV2Ou5bzMxBPNLoRCjcX+ya7KSWybQD2KwLphxsapUVK6x/02o7f4VU6GPSXch9vNY2+gkU8tYWQ== -highlight.js@~9.15.0, highlight.js@~9.15.1: - version "9.15.10" - resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.15.10.tgz#7b18ed75c90348c045eef9ed08ca1319a2219ad2" - integrity sha512-RoV7OkQm0T3os3Dd2VHLNMoaoDVx77Wygln3n9l5YV172XonWG6rgQD3XnF/BuFFZw9A0TJgmMSO8FEWQgvcXw== +highlight.js@^10.4.1, highlight.js@~10.7.0: + version "10.7.3" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531" + integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A== + +highlightjs-vue@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz#fdfe97fbea6354e70ee44e3a955875e114db086d" + integrity sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA== history@^5.2.0: version "5.2.0" @@ -5804,7 +5734,7 @@ history@^5.2.0: dependencies: "@babel/runtime" "^7.7.6" -hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: +hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -7233,13 +7163,13 @@ lower-case@^2.0.2: dependencies: tslib "^2.0.3" -lowlight@1.12.1: - version "1.12.1" - resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.12.1.tgz#014acf8dd73a370e02ff1cc61debcde3bb1681eb" - integrity sha512-OqaVxMGIESnawn+TU/QMV5BJLbUghUfjDWPAtFqDYDmDtr4FnB+op8xM+pR7nKlauHNUHXGt0VgWatFB8voS5w== +lowlight@^1.17.0: + version "1.20.0" + resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.20.0.tgz#ddb197d33462ad0d93bf19d17b6c301aa3941888" + integrity sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw== dependencies: - fault "^1.0.2" - highlight.js "~9.15.0" + fault "^1.0.0" + highlight.js "~10.7.0" lru-cache@^6.0.0: version "6.0.0" @@ -7576,6 +7506,11 @@ nanoid@^3.1.30: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c" integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA== +nanoid@^3.3.7: + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -7949,18 +7884,6 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse-entities@^1.1.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-1.2.2.tgz#c31bf0f653b6661354f8973559cb86dd1d5edf50" - integrity sha512-NzfpbxW/NPrzZ/yYSoQxyqUZMZXIdCfE0OIN4ESsnptHJECoUk3FZktxNuzQf4tjt5UEopnxpYJbvYuxIFDdsg== - dependencies: - character-entities "^1.0.0" - character-entities-legacy "^1.0.0" - character-reference-invalid "^1.0.0" - is-alphanumerical "^1.0.0" - is-decimal "^1.0.0" - is-hexadecimal "^1.0.0" - parse-entities@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-2.0.0.tgz#53c6eb5b9314a1f4ec99fa0fdf7ce01ecda0cbe8" @@ -8061,7 +7984,7 @@ picocolors@^1.0.1: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.0.tgz#5358b76a78cde483ba5cef6a9dc9671440b27d59" integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw== -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3, picomatch@^2.3.0: +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== @@ -8584,6 +8507,15 @@ postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0, postcss-value-parser@^ resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== +postcss@8.4.38: + version "8.4.38" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e" + integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== + dependencies: + nanoid "^3.3.7" + picocolors "^1.0.0" + source-map-js "^1.2.0" + postcss@^7.0.35: version "7.0.39" resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.39.tgz#9624375d965630e2e1f2c02a935c82a59cb48309" @@ -8642,18 +8574,11 @@ pretty-format@^27.0.2: ansi-styles "^5.0.0" react-is "^17.0.1" -prismjs@^1.8.4: +prismjs@^1.27.0: version "1.29.0" resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.29.0.tgz#f113555a8fa9b57c35e637bba27509dcf802dd12" integrity sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q== -prismjs@~1.17.0: - version "1.17.1" - resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.17.1.tgz#e669fcbd4cdd873c35102881c33b14d0d68519be" - integrity sha512-PrEDJAFdUGbOP6xK/UsfkC5ghJsPJviKgnQOoxaDbBjwc8op68Quupwt1DeAFoG8GImPhiKXAvvsH7wDSLsu1Q== - optionalDependencies: - clipboard "^2.0.0" - prismjs@~1.27.0: version "1.27.0" resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.27.0.tgz#bb6ee3138a0b438a3653dd4d6ce0cc6510a45057" @@ -8837,15 +8762,15 @@ react-clientside-effect@^1.2.6: dependencies: "@babel/runtime" "^7.12.13" -react-code-blocks@^0.0.9-0: - version "0.0.9-0" - resolved "https://registry.yarnpkg.com/react-code-blocks/-/react-code-blocks-0.0.9-0.tgz#0c6d04d8a40b74cffe95f24f1a8e62a0fda8c014" - integrity sha512-jdYJVZwGtsr6WIUaqILy5fkF1acf57YV5s0V3+w5o9v3omYnqBeO6EuZi1Vf2x1hahkYGEedsp46+ofdkYlqyw== +react-code-blocks@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/react-code-blocks/-/react-code-blocks-0.1.6.tgz#ec64e7899223d3e910eb916465a66d95ce1ae1b2" + integrity sha512-ENNuxG07yO+OuX1ChRje3ieefPRz6yrIpHmebQlaFQgzcAHbUfVeTINpOpoI9bSRSObeYo/OdHsporeToZ7fcg== dependencies: "@babel/runtime" "^7.10.4" - react-syntax-highlighter "^12.2.1" - styled-components "^5.1.1" - tslib "^2.0.0" + react-syntax-highlighter "^15.5.0" + styled-components "^6.1.0" + tslib "^2.6.0" react-dev-utils@^12.0.0: version "12.0.0" @@ -9076,16 +9001,17 @@ react-style-singleton@^2.2.1: invariant "^2.2.4" tslib "^2.0.0" -react-syntax-highlighter@^12.2.1: - version "12.2.1" - resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-12.2.1.tgz#14d78352da1c1c3f93c6698b70ec7c706b83493e" - integrity sha512-CTsp0ZWijwKRYFg9xhkWD4DSpQqE4vb2NKVMdPAkomnILSmsNBHE0n5GuI5zB+PU3ySVvXvdt9jo+ViD9XibCA== +react-syntax-highlighter@^15.5.0: + version "15.6.1" + resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz#fa567cb0a9f96be7bbccf2c13a3c4b5657d9543e" + integrity sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg== dependencies: "@babel/runtime" "^7.3.1" - highlight.js "~9.15.1" - lowlight "1.12.1" - prismjs "^1.8.4" - refractor "^2.4.1" + highlight.js "^10.4.1" + highlightjs-vue "^1.0.0" + lowlight "^1.17.0" + prismjs "^1.27.0" + refractor "^3.6.0" react-virtualized-auto-sizer@^1.0.24: version "1.0.24" @@ -9158,15 +9084,6 @@ redux@^4.2.1: dependencies: "@babel/runtime" "^7.9.2" -refractor@^2.4.1: - version "2.10.1" - resolved "https://registry.yarnpkg.com/refractor/-/refractor-2.10.1.tgz#166c32f114ed16fd96190ad21d5193d3afc7d34e" - integrity sha512-Xh9o7hQiQlDbxo5/XkOX6H+x/q8rmlmZKr97Ie1Q8ZM32IRRd3B/UxuA/yXDW79DBSXGWxm2yRTbcTVmAciJRw== - dependencies: - hastscript "^5.0.0" - parse-entities "^1.1.2" - prismjs "~1.17.0" - refractor@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/refractor/-/refractor-3.6.0.tgz#ac318f5a0715ead790fcfb0c71f4dd83d977935a" @@ -9615,11 +9532,6 @@ select-hose@^2.0.0: resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= -select@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" - integrity sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA== - selfsigned@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.0.0.tgz#e927cd5377cbb0a1075302cff8df1042cc2bce5b" @@ -9727,7 +9639,7 @@ setprototypeof@1.2.0: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== -shallowequal@^1.1.0: +shallowequal@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== @@ -9797,6 +9709,11 @@ source-map-js@^1.0.1: resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== +source-map-js@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + source-map-loader@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-3.0.1.tgz#9ae5edc7c2d42570934be4c95d1ccc6352eba52d" @@ -10078,21 +9995,20 @@ style-to-object@^0.3.0: dependencies: inline-style-parser "0.1.1" -styled-components@^5.1.1: - version "5.3.9" - resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-5.3.9.tgz#641af2a8bb89904de708c71b439caa9633e8f0ba" - integrity sha512-Aj3kb13B75DQBo2oRwRa/APdB5rSmwUfN5exyarpX+x/tlM/rwZA2vVk2vQgVSP6WKaZJHWwiFrzgHt+CLtB4A== - dependencies: - "@babel/helper-module-imports" "^7.0.0" - "@babel/traverse" "^7.4.5" - "@emotion/is-prop-valid" "^1.1.0" - "@emotion/stylis" "^0.8.4" - "@emotion/unitless" "^0.7.4" - babel-plugin-styled-components ">= 1.12.0" - css-to-react-native "^3.0.0" - hoist-non-react-statics "^3.0.0" - shallowequal "^1.1.0" - supports-color "^5.5.0" +styled-components@^6.1.0: + version "6.1.13" + resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-6.1.13.tgz#2d777750b773b31469bd79df754a32479e9f475e" + integrity sha512-M0+N2xSnAtwcVAQeFEsGWFFxXDftHUD7XrKla06QbpUMmbmtFBMMTcKWvFXtWxuD5qQkB8iU5gk6QASlx2ZRMw== + dependencies: + "@emotion/is-prop-valid" "1.2.2" + "@emotion/unitless" "0.8.1" + "@types/stylis" "4.2.5" + css-to-react-native "3.2.0" + csstype "3.1.3" + postcss "8.4.38" + shallowequal "1.1.0" + stylis "4.3.2" + tslib "2.6.2" stylehacks@^5.0.2: version "5.0.2" @@ -10107,7 +10023,12 @@ stylis@4.2.0: resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51" integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw== -supports-color@^5.3.0, supports-color@^5.5.0: +stylis@4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.3.2.tgz#8f76b70777dd53eb669c6f58c997bf0a9972e444" + integrity sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg== + +supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== @@ -10352,11 +10273,6 @@ timsort@^0.3.0: resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= -tiny-emitter@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423" - integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q== - tiny-invariant@^1.0.6: version "1.2.0" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.2.0.tgz#a1141f86b672a9148c72e978a19a73b9b94a15a9" @@ -10452,6 +10368,11 @@ tsconfig-paths@^3.12.0: minimist "^1.2.0" strip-bom "^3.0.0" +tslib@2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + tslib@^1.8.1, tslib@^1.9.3: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" @@ -10467,6 +10388,11 @@ tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== +tslib@^2.6.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.0.tgz#d124c86c3c05a40a91e6fdea4021bd31d377971b" + integrity sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" From 986dc92d05d085e89fe2832b35d9a2d8ad277f9d Mon Sep 17 00:00:00 2001 From: Harri Lehtola <1781172+peruukki@users.noreply.github.com> Date: Sun, 20 Oct 2024 16:53:55 +0300 Subject: [PATCH 156/185] chore: Bump dependencies to reduce vulnerabilities in /ui (#4654) * chore: Bump transitive word-wrap from 1.2.3 to 1.2.5 in /ui This resolves a vulnerability in word-wrap: https://github.com/advisories/GHSA-j8xg-fqg3-53r7. Signed-off-by: Harri Lehtola * chore: Bump protobufjs-cli from 1.0.2 to 1.1.3 in /ui The older version depended on taffydb that has a vulnerability with no patched version available. The latest version no longer uses it. Signed-off-by: Harri Lehtola * chore: Bump transitive dependencies of msw in /ui This resolves 1 critical and 1 high level vulnerability in @xmldom/xmldom and path-to-regexp. Signed-off-by: Harri Lehtola * chore: Bump vulnerable rollup packages to latest versions in /ui This resolves 1 high and 9 moderate level vulnerabilities reported by `yarn audit`. Signed-off-by: Harri Lehtola * chore: Bump @babel packages to latest in /ui This resolves 16 high level vulnerabilities reported by `yarn audit`. Signed-off-by: Harri Lehtola * chore: Use browserslist default configuration in Feast UI "defaults" (https://browsersl.ist/#q=defaults) is the recommended starting point these days, it's a shorthand for "> 0.5%, last 2 versions, Firefox ESR, not dead". Using it gets rid of a couple autoprefixer related warnings that started appearing after dependency updates. Check the previous production configuration at https://browsersl.ist/#q=%3E0.2%25%2C+not+dead%2C+not+op_mini+all if you want to compare what changed. Signed-off-by: Harri Lehtola * chore: Bump react-scripts from 5.0.0 to 5.0.1 in /ui This resolves 2 critical, 11 high, 7 moderate and 1 low level vulnerability reported by `yarn audit`. @babel/plugin-proposal-private-property-in-object is added to devDependencies due to this warning when running the tests: > One of your dependencies, babel-preset-react-app, is importing the > "@babel/plugin-proposal-private-property-in-object" package without > declaring it in its dependencies. This is currently working because > "@babel/plugin-proposal-private-property-in-object" is already in your > node_modules folder for unrelated reasons, but it may break at any time. > > babel-preset-react-app is part of the create-react-app project, which > is not maintianed (sic) anymore. It is thus unlikely that this bug will > ever be fixed. Add "@babel/plugin-proposal-private-property-in-object" to > your devDependencies to work around this error. This will make this message > go away. Signed-off-by: Harri Lehtola --------- Signed-off-by: Harri Lehtola --- ui/package.json | 30 +- ui/yarn.lock | 8358 +++++++++++++++++++++++++---------------------- 2 files changed, 4513 insertions(+), 3875 deletions(-) diff --git a/ui/package.json b/ui/package.json index 169aa11bf0..5de1537862 100644 --- a/ui/package.json +++ b/ui/package.json @@ -34,7 +34,7 @@ "react-code-blocks": "^0.1.6", "react-query": "^3.39.3", "react-router-dom": "<6.4.0", - "react-scripts": "^5.0.0", + "react-scripts": "^5.0.1", "tslib": "^2.3.1", "use-query-params": "^1.2.3", "zod": "^3.11.6" @@ -59,22 +59,14 @@ "chroma-js": "/node_modules/chroma-js/dist/chroma.min.cjs" } }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] - }, + "browserslist": [ + "defaults" + ], "devDependencies": { - "@babel/core": "^7.17.5", - "@babel/preset-env": "^7.16.11", - "@babel/preset-react": "^7.16.7", + "@babel/core": "^7.25.8", + "@babel/plugin-proposal-private-property-in-object": "^7.21.11", + "@babel/preset-env": "^7.25.8", + "@babel/preset-react": "^7.25.7", "@rollup/plugin-babel": "^5.3.1", "@rollup/plugin-commonjs": "^21.0.2", "@rollup/plugin-json": "^4.1.0", @@ -89,15 +81,15 @@ "@types/react": "^18.3.11", "@types/react-dom": "^18.3.0", "msw": "^0.36.8", - "protobufjs-cli": "^1.0.2", + "protobufjs-cli": "^1.1.3", "react": "^18.3.1", "react-dom": "^18.3.1", "rimraf": "^3.0.2", "rollup": "^2.68.0", - "rollup-plugin-copy": "^3.4.0", + "rollup-plugin-copy": "^3.5.0", "rollup-plugin-import-css": "^3.0.2", "rollup-plugin-svg": "^2.0.0", - "rollup-plugin-svgo": "^1.1.0", + "rollup-plugin-svgo": "^2.0.0", "rollup-plugin-terser": "^7.0.2", "typescript": "^4.9.5" }, diff --git a/ui/yarn.lock b/ui/yarn.lock index af2f2101cf..9c8c874e20 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -7,564 +7,364 @@ resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.0.tgz#728c484f4e10df03d5a3acd0d8adcbbebff8ad63" integrity sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ== -"@ampproject/remapping@^2.1.0": - version "2.1.2" - resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.1.2.tgz#4edca94973ded9630d20101cd8559cedb8d8bd34" - integrity sha512-hoyByceqwKirw7w3Z7gnIIZC3Wx3J484Y3L/cMpXFbr7d9ZQj2mODrirNzcJa+SM3UlpWXYvKV4RlRpFXlWgXg== +"@alloc/quick-lru@^5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30" + integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw== + +"@ampproject/remapping@^2.2.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" + integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== dependencies: - "@jridgewell/trace-mapping" "^0.3.0" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" "@apideck/better-ajv-errors@^0.3.1": - version "0.3.2" - resolved "https://registry.yarnpkg.com/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.2.tgz#cd6d3814eda8aee38ee2e3fa6457be43af4f8361" - integrity sha512-JdEazx7qiVqTBzzBl5rolRwl5cmhihjfIcpqRzIZjtT6b18liVmDn/VlWpqW4C/qP2hrFFMLRV1wlex8ZVBPTg== + version "0.3.6" + resolved "https://registry.yarnpkg.com/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz#957d4c28e886a64a8141f7522783be65733ff097" + integrity sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA== dependencies: json-schema "^0.4.0" jsonpointer "^5.0.0" leven "^3.1.0" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.8.3": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.7.tgz#44416b6bd7624b998f5b1af5d470856c40138789" - integrity sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg== - dependencies: - "@babel/highlight" "^7.16.7" - -"@babel/code-frame@^7.22.13": - version "7.22.13" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" - integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w== +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.25.7", "@babel/code-frame@^7.8.3": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.25.7.tgz#438f2c524071531d643c6f0188e1e28f130cebc7" + integrity sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g== dependencies: - "@babel/highlight" "^7.22.13" - chalk "^2.4.2" + "@babel/highlight" "^7.25.7" + picocolors "^1.0.0" -"@babel/compat-data@^7.13.11", "@babel/compat-data@^7.16.4", "@babel/compat-data@^7.16.8": - version "7.16.8" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.16.8.tgz#31560f9f29fdf1868de8cb55049538a1b9732a60" - integrity sha512-m7OkX0IdKLKPpBlJtF561YJal5y/jyI5fNfWbPxh2D/nbzzGI4qRyrD8xO2jB24u7l+5I2a43scCG2IrfjC50Q== - -"@babel/core@^7.1.0", "@babel/core@^7.11.1", "@babel/core@^7.12.3", "@babel/core@^7.16.0", "@babel/core@^7.7.2", "@babel/core@^7.8.0": - version "7.16.12" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.16.12.tgz#5edc53c1b71e54881315923ae2aedea2522bb784" - integrity sha512-dK5PtG1uiN2ikk++5OzSYsitZKny4wOCD0nrO4TqnW4BVBTQ2NGS3NgilvT/TEyxTST7LNyWV/T4tXDoD3fOgg== - dependencies: - "@babel/code-frame" "^7.16.7" - "@babel/generator" "^7.16.8" - "@babel/helper-compilation-targets" "^7.16.7" - "@babel/helper-module-transforms" "^7.16.7" - "@babel/helpers" "^7.16.7" - "@babel/parser" "^7.16.12" - "@babel/template" "^7.16.7" - "@babel/traverse" "^7.16.10" - "@babel/types" "^7.16.8" - convert-source-map "^1.7.0" +"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.25.7", "@babel/compat-data@^7.25.8": + version "7.25.8" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.25.8.tgz#0376e83df5ab0eb0da18885c0140041f0747a402" + integrity sha512-ZsysZyXY4Tlx+Q53XdnOFmqwfB9QDTHYxaZYajWRoBLuLEAwI2UIbtxOjWh/cFaa9IKUlcB+DDuoskLuKu56JA== + +"@babel/core@^7.1.0", "@babel/core@^7.11.1", "@babel/core@^7.12.3", "@babel/core@^7.16.0", "@babel/core@^7.25.8", "@babel/core@^7.7.2", "@babel/core@^7.8.0": + version "7.25.8" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.25.8.tgz#a57137d2a51bbcffcfaeba43cb4dd33ae3e0e1c6" + integrity sha512-Oixnb+DzmRT30qu9d3tJSQkxuygWm32DFykT4bRoORPa9hZ/L4KhVB/XiRm6KG+roIEM7DBQlmg27kw2HZkdZg== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.25.7" + "@babel/generator" "^7.25.7" + "@babel/helper-compilation-targets" "^7.25.7" + "@babel/helper-module-transforms" "^7.25.7" + "@babel/helpers" "^7.25.7" + "@babel/parser" "^7.25.8" + "@babel/template" "^7.25.7" + "@babel/traverse" "^7.25.7" + "@babel/types" "^7.25.8" + convert-source-map "^2.0.0" debug "^4.1.0" gensync "^1.0.0-beta.2" - json5 "^2.1.2" - semver "^6.3.0" - source-map "^0.5.0" - -"@babel/core@^7.17.5": - version "7.17.5" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.17.5.tgz#6cd2e836058c28f06a4ca8ee7ed955bbf37c8225" - integrity sha512-/BBMw4EvjmyquN5O+t5eh0+YqB3XXJkYD2cjKpYtWOfFy4lQ4UozNSmxAcWT8r2XtZs0ewG+zrfsqeR15i1ajA== - dependencies: - "@ampproject/remapping" "^2.1.0" - "@babel/code-frame" "^7.16.7" - "@babel/generator" "^7.17.3" - "@babel/helper-compilation-targets" "^7.16.7" - "@babel/helper-module-transforms" "^7.16.7" - "@babel/helpers" "^7.17.2" - "@babel/parser" "^7.17.3" - "@babel/template" "^7.16.7" - "@babel/traverse" "^7.17.3" - "@babel/types" "^7.17.0" - convert-source-map "^1.7.0" - debug "^4.1.0" - gensync "^1.0.0-beta.2" - json5 "^2.1.2" - semver "^6.3.0" + json5 "^2.2.3" + semver "^6.3.1" "@babel/eslint-parser@^7.16.3": - version "7.16.5" - resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.16.5.tgz#48d3485091d6e36915358e4c0d0b2ebe6da90462" - integrity sha512-mUqYa46lgWqHKQ33Q6LNCGp/wPR3eqOYTUixHFsfrSQqRxH0+WOzca75iEjFr5RDGH1dDz622LaHhLOzOuQRUA== + version "7.25.8" + resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.25.8.tgz#0119dec46be547d7a339978dedb9d29e517c2443" + integrity sha512-Po3VLMN7fJtv0nsOjBDSbO1J71UhzShE9MuOSkWEV9IZQXzhZklYtzKZ8ZD/Ij3a0JBv1AG3Ny2L3jvAHQVOGg== dependencies: - eslint-scope "^5.1.1" + "@nicolo-ribaudo/eslint-scope-5-internals" "5.1.1-v1" eslint-visitor-keys "^2.1.0" - semver "^6.3.0" - -"@babel/generator@^7.16.8", "@babel/generator@^7.7.2": - version "7.16.8" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.16.8.tgz#359d44d966b8cd059d543250ce79596f792f2ebe" - integrity sha512-1ojZwE9+lOXzcWdWmO6TbUzDfqLD39CmEhN8+2cX9XkDo5yW1OpgfejfliysR2AWLpMamTiOiAp/mtroaymhpw== - dependencies: - "@babel/types" "^7.16.8" - jsesc "^2.5.1" - source-map "^0.5.0" - -"@babel/generator@^7.17.3": - version "7.17.3" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.17.3.tgz#a2c30b0c4f89858cb87050c3ffdfd36bdf443200" - integrity sha512-+R6Dctil/MgUsZsZAkYgK+ADNSZzJRRy0TvY65T71z/CR854xHQ1EweBYXdfT+HNeN7w0cSJJEzgxZMv40pxsg== - dependencies: - "@babel/types" "^7.17.0" - jsesc "^2.5.1" - source-map "^0.5.0" + semver "^6.3.1" -"@babel/generator@^7.23.0": - version "7.23.0" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.0.tgz#df5c386e2218be505b34837acbcb874d7a983420" - integrity sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g== - dependencies: - "@babel/types" "^7.23.0" - "@jridgewell/gen-mapping" "^0.3.2" - "@jridgewell/trace-mapping" "^0.3.17" - jsesc "^2.5.1" - -"@babel/helper-annotate-as-pure@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz#bb2339a7534a9c128e3102024c60760a3a7f3862" - integrity sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw== +"@babel/generator@^7.25.7", "@babel/generator@^7.7.2": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.25.7.tgz#de86acbeb975a3e11ee92dd52223e6b03b479c56" + integrity sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA== dependencies: - "@babel/types" "^7.16.7" + "@babel/types" "^7.25.7" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^3.0.2" -"@babel/helper-builder-binary-assignment-operator-visitor@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.16.7.tgz#38d138561ea207f0f69eb1626a418e4f7e6a580b" - integrity sha512-C6FdbRaxYjwVu/geKW4ZeQ0Q31AftgRcdSnZ5/jsH6BzCJbtvXvhpfkbkThYSuutZA7nCXpPR6AD9zd1dprMkA== +"@babel/helper-annotate-as-pure@^7.18.6", "@babel/helper-annotate-as-pure@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.7.tgz#63f02dbfa1f7cb75a9bdb832f300582f30bb8972" + integrity sha512-4xwU8StnqnlIhhioZf1tqnVWeQ9pvH/ujS8hRfw/WOza+/a+1qv69BWNy+oY231maTCWgKWhfBU7kDpsds6zAA== dependencies: - "@babel/helper-explode-assignable-expression" "^7.16.7" - "@babel/types" "^7.16.7" + "@babel/types" "^7.25.7" -"@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.7.tgz#06e66c5f299601e6c7da350049315e83209d551b" - integrity sha512-mGojBwIWcwGD6rfqgRXVlVYmPAv7eOpIemUG3dGnDdCY4Pae70ROij3XmfrH6Fa1h1aiDylpglbZyktfzyo/hA== +"@babel/helper-builder-binary-assignment-operator-visitor@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.25.7.tgz#d721650c1f595371e0a23ee816f1c3c488c0d622" + integrity sha512-12xfNeKNH7jubQNm7PAkzlLwEmCs1tfuX3UjIw6vP6QXi+leKh6+LyC/+Ed4EIQermwd58wsyh070yjDHFlNGg== dependencies: - "@babel/compat-data" "^7.16.4" - "@babel/helper-validator-option" "^7.16.7" - browserslist "^4.17.5" - semver "^6.3.0" + "@babel/traverse" "^7.25.7" + "@babel/types" "^7.25.7" -"@babel/helper-create-class-features-plugin@^7.16.10", "@babel/helper-create-class-features-plugin@^7.16.7": - version "7.16.10" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.16.10.tgz#8a6959b9cc818a88815ba3c5474619e9c0f2c21c" - integrity sha512-wDeej0pu3WN/ffTxMNCPW5UCiOav8IcLRxSIyp/9+IF2xJUM9h/OYjg0IJLHaL6F8oU8kqMz9nc1vryXhMsgXg== +"@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.7.tgz#11260ac3322dda0ef53edfae6e97b961449f5fa4" + integrity sha512-DniTEax0sv6isaw6qSQSfV4gVRNtw2rte8HHM45t9ZR0xILaufBRNkpMifCRiAPyvL4ACD6v0gfCwCmtOQaV4A== dependencies: - "@babel/helper-annotate-as-pure" "^7.16.7" - "@babel/helper-environment-visitor" "^7.16.7" - "@babel/helper-function-name" "^7.16.7" - "@babel/helper-member-expression-to-functions" "^7.16.7" - "@babel/helper-optimise-call-expression" "^7.16.7" - "@babel/helper-replace-supers" "^7.16.7" - "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/compat-data" "^7.25.7" + "@babel/helper-validator-option" "^7.25.7" + browserslist "^4.24.0" + lru-cache "^5.1.1" + semver "^6.3.1" -"@babel/helper-create-regexp-features-plugin@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.16.7.tgz#0cb82b9bac358eb73bfbd73985a776bfa6b14d48" - integrity sha512-fk5A6ymfp+O5+p2yCkXAu5Kyj6v0xh0RBeNcAkYUMDvvAAoxvSKXn+Jb37t/yWFiQVDFK1ELpUTD8/aLhCPu+g== +"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.21.0", "@babel/helper-create-class-features-plugin@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.7.tgz#5d65074c76cae75607421c00d6bd517fe1892d6b" + integrity sha512-bD4WQhbkx80mAyj/WCm4ZHcF4rDxkoLFO6ph8/5/mQ3z4vAzltQXAmbc7GvVJx5H+lk5Mi5EmbTeox5nMGCsbw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.25.7" + "@babel/helper-member-expression-to-functions" "^7.25.7" + "@babel/helper-optimise-call-expression" "^7.25.7" + "@babel/helper-replace-supers" "^7.25.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.7" + "@babel/traverse" "^7.25.7" + semver "^6.3.1" + +"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.7.tgz#dcb464f0e2cdfe0c25cc2a0a59c37ab940ce894e" + integrity sha512-byHhumTj/X47wJ6C6eLpK7wW/WBEcnUeb7D0FNc/jFQnQVw7DOso3Zz5u9x/zLrFVkHa89ZGDbkAa1D54NdrCQ== dependencies: - "@babel/helper-annotate-as-pure" "^7.16.7" - regexpu-core "^4.7.1" + "@babel/helper-annotate-as-pure" "^7.25.7" + regexpu-core "^6.1.1" + semver "^6.3.1" -"@babel/helper-define-polyfill-provider@^0.3.1": - version "0.3.1" - resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.1.tgz#52411b445bdb2e676869e5a74960d2d3826d2665" - integrity sha512-J9hGMpJQmtWmj46B3kBHmL38UhJGhYX7eqkcq+2gsstyYt341HmPeWspihX43yVRA0mS+8GGk2Gckc7bY/HCmA== +"@babel/helper-define-polyfill-provider@^0.6.2": + version "0.6.2" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz#18594f789c3594acb24cfdb4a7f7b7d2e8bd912d" + integrity sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ== dependencies: - "@babel/helper-compilation-targets" "^7.13.0" - "@babel/helper-module-imports" "^7.12.13" - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/traverse" "^7.13.0" + "@babel/helper-compilation-targets" "^7.22.6" + "@babel/helper-plugin-utils" "^7.22.5" debug "^4.1.1" lodash.debounce "^4.0.8" resolve "^1.14.2" - semver "^6.1.2" -"@babel/helper-environment-visitor@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz#ff484094a839bde9d89cd63cba017d7aae80ecd7" - integrity sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag== - dependencies: - "@babel/types" "^7.16.7" - -"@babel/helper-environment-visitor@^7.22.20": - version "7.22.20" - resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" - integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== - -"@babel/helper-explode-assignable-expression@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.16.7.tgz#12a6d8522fdd834f194e868af6354e8650242b7a" - integrity sha512-KyUenhWMC8VrxzkGP0Jizjo4/Zx+1nNZhgocs+gLzyZyB8SHidhoq9KK/8Ato4anhwsivfkBLftky7gvzbZMtQ== - dependencies: - "@babel/types" "^7.16.7" - -"@babel/helper-function-name@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz#f1ec51551fb1c8956bc8dd95f38523b6cf375f8f" - integrity sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA== - dependencies: - "@babel/helper-get-function-arity" "^7.16.7" - "@babel/template" "^7.16.7" - "@babel/types" "^7.16.7" - -"@babel/helper-function-name@^7.23.0": - version "7.23.0" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" - integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== - dependencies: - "@babel/template" "^7.22.15" - "@babel/types" "^7.23.0" - -"@babel/helper-get-function-arity@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz#ea08ac753117a669f1508ba06ebcc49156387419" - integrity sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw== - dependencies: - "@babel/types" "^7.16.7" - -"@babel/helper-hoist-variables@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz#86bcb19a77a509c7b77d0e22323ef588fa58c246" - integrity sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg== - dependencies: - "@babel/types" "^7.16.7" - -"@babel/helper-hoist-variables@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" - integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== - dependencies: - "@babel/types" "^7.22.5" - -"@babel/helper-member-expression-to-functions@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.16.7.tgz#42b9ca4b2b200123c3b7e726b0ae5153924905b0" - integrity sha512-VtJ/65tYiU/6AbMTDwyoXGPKHgTsfRarivm+YbB5uAzKUyuPjgZSgAFeG87FCigc7KNHu2Pegh1XIT3lXjvz3Q== +"@babel/helper-member-expression-to-functions@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.7.tgz#541a33b071f0355a63a0fa4bdf9ac360116b8574" + integrity sha512-O31Ssjd5K6lPbTX9AAYpSKrZmLeagt9uwschJd+Ixo6QiRyfpvgtVQp8qrDR9UNFjZ8+DO34ZkdrN+BnPXemeA== dependencies: - "@babel/types" "^7.16.7" + "@babel/traverse" "^7.25.7" + "@babel/types" "^7.25.7" -"@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz#25612a8091a999704461c8a222d0efec5d091437" - integrity sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg== +"@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.25.7.tgz#dba00d9523539152906ba49263e36d7261040472" + integrity sha512-o0xCgpNmRohmnoWKQ0Ij8IdddjyBFE4T2kagL/x6M3+4zUgc+4qTOUBoNe4XxDskt1HPKO007ZPiMgLDq2s7Kw== dependencies: - "@babel/types" "^7.16.7" + "@babel/traverse" "^7.25.7" + "@babel/types" "^7.25.7" -"@babel/helper-module-transforms@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.16.7.tgz#7665faeb721a01ca5327ddc6bba15a5cb34b6a41" - integrity sha512-gaqtLDxJEFCeQbYp9aLAefjhkKdjKcdh6DB7jniIGU3Pz52WAmP268zK0VgPz9hUNkMSYeH976K2/Y6yPadpng== - dependencies: - "@babel/helper-environment-visitor" "^7.16.7" - "@babel/helper-module-imports" "^7.16.7" - "@babel/helper-simple-access" "^7.16.7" - "@babel/helper-split-export-declaration" "^7.16.7" - "@babel/helper-validator-identifier" "^7.16.7" - "@babel/template" "^7.16.7" - "@babel/traverse" "^7.16.7" - "@babel/types" "^7.16.7" - -"@babel/helper-optimise-call-expression@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.16.7.tgz#a34e3560605abbd31a18546bd2aad3e6d9a174f2" - integrity sha512-EtgBhg7rd/JcnpZFXpBy0ze1YRfdm7BnBX4uKMBd3ixa3RGAE002JZB66FJyNH7g0F38U05pXmA5P8cBh7z+1w== +"@babel/helper-module-transforms@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.25.7.tgz#2ac9372c5e001b19bc62f1fe7d96a18cb0901d1a" + integrity sha512-k/6f8dKG3yDz/qCwSM+RKovjMix563SLxQFo0UhRNo239SP6n9u5/eLtKD6EAjwta2JHJ49CsD8pms2HdNiMMQ== dependencies: - "@babel/types" "^7.16.7" + "@babel/helper-module-imports" "^7.25.7" + "@babel/helper-simple-access" "^7.25.7" + "@babel/helper-validator-identifier" "^7.25.7" + "@babel/traverse" "^7.25.7" -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.13.0", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz#aa3a8ab4c3cceff8e65eb9e73d87dc4ff320b2f5" - integrity sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA== - -"@babel/helper-remap-async-to-generator@^7.16.8": - version "7.16.8" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.8.tgz#29ffaade68a367e2ed09c90901986918d25e57e3" - integrity sha512-fm0gH7Flb8H51LqJHy3HJ3wnE1+qtYR2A99K06ahwrawLdOFsCEWjZOrYricXJHoPSudNKxrMBUPEIPxiIIvBw== +"@babel/helper-optimise-call-expression@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.7.tgz#1de1b99688e987af723eed44fa7fc0ee7b97d77a" + integrity sha512-VAwcwuYhv/AT+Vfr28c9y6SHzTan1ryqrydSTFGjU0uDJHw3uZ+PduI8plCLkRsDnqK2DMEDmwrOQRsK/Ykjng== dependencies: - "@babel/helper-annotate-as-pure" "^7.16.7" - "@babel/helper-wrap-function" "^7.16.8" - "@babel/types" "^7.16.8" + "@babel/types" "^7.25.7" -"@babel/helper-replace-supers@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.16.7.tgz#e9f5f5f32ac90429c1a4bdec0f231ef0c2838ab1" - integrity sha512-y9vsWilTNaVnVh6xiJfABzsNpgDPKev9HnAgz6Gb1p6UUwf9NepdlsV7VXGCftJM+jqD5f7JIEubcpLjZj5dBw== - dependencies: - "@babel/helper-environment-visitor" "^7.16.7" - "@babel/helper-member-expression-to-functions" "^7.16.7" - "@babel/helper-optimise-call-expression" "^7.16.7" - "@babel/traverse" "^7.16.7" - "@babel/types" "^7.16.7" +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.25.7", "@babel/helper-plugin-utils@^7.8.0": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.7.tgz#8ec5b21812d992e1ef88a9b068260537b6f0e36c" + integrity sha512-eaPZai0PiqCi09pPs3pAFfl/zYgGaE6IdXtYvmf0qlcDTd3WCtO7JWCcRd64e0EQrcYgiHibEZnOGsSY4QSgaw== -"@babel/helper-simple-access@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.16.7.tgz#d656654b9ea08dbb9659b69d61063ccd343ff0f7" - integrity sha512-ZIzHVyoeLMvXMN/vok/a4LWRy8G2v205mNP0XOuf9XRLyX5/u9CnVulUtDgUTama3lT+bf/UqucuZjqiGuTS1g== +"@babel/helper-remap-async-to-generator@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.7.tgz#9efdc39df5f489bcd15533c912b6c723a0a65021" + integrity sha512-kRGE89hLnPfcz6fTrlNU+uhgcwv0mBE4Gv3P9Ke9kLVJYpi4AMVVEElXvB5CabrPZW4nCM8P8UyyjrzCM0O2sw== dependencies: - "@babel/types" "^7.16.7" + "@babel/helper-annotate-as-pure" "^7.25.7" + "@babel/helper-wrap-function" "^7.25.7" + "@babel/traverse" "^7.25.7" -"@babel/helper-skip-transparent-expression-wrappers@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.16.0.tgz#0ee3388070147c3ae051e487eca3ebb0e2e8bb09" - integrity sha512-+il1gTy0oHwUsBQZyJvukbB4vPMdcYBrFHa0Uc4AizLxbq6BOYC51Rv4tWocX9BLBDLZ4kc6qUFpQ6HRgL+3zw== +"@babel/helper-replace-supers@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.25.7.tgz#38cfda3b6e990879c71d08d0fef9236b62bd75f5" + integrity sha512-iy8JhqlUW9PtZkd4pHM96v6BdJ66Ba9yWSE4z0W4TvSZwLBPkyDsiIU3ENe4SmrzRBs76F7rQXTy1lYC49n6Lw== dependencies: - "@babel/types" "^7.16.0" + "@babel/helper-member-expression-to-functions" "^7.25.7" + "@babel/helper-optimise-call-expression" "^7.25.7" + "@babel/traverse" "^7.25.7" -"@babel/helper-split-export-declaration@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz#0b648c0c42da9d3920d85ad585f2778620b8726b" - integrity sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw== +"@babel/helper-simple-access@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.25.7.tgz#5eb9f6a60c5d6b2e0f76057004f8dacbddfae1c0" + integrity sha512-FPGAkJmyoChQeM+ruBGIDyrT2tKfZJO8NcxdC+CWNJi7N8/rZpSxK7yvBJ5O/nF1gfu5KzN7VKG3YVSLFfRSxQ== dependencies: - "@babel/types" "^7.16.7" + "@babel/traverse" "^7.25.7" + "@babel/types" "^7.25.7" -"@babel/helper-split-export-declaration@^7.22.6": - version "7.22.6" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" - integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== +"@babel/helper-skip-transparent-expression-wrappers@^7.20.0", "@babel/helper-skip-transparent-expression-wrappers@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.7.tgz#382831c91038b1a6d32643f5f49505b8442cb87c" + integrity sha512-pPbNbchZBkPMD50K0p3JGcFMNLVUCuU/ABybm/PGNj4JiHrpmNyqqCphBk4i19xXtNV0JhldQJJtbSW5aUvbyA== dependencies: - "@babel/types" "^7.22.5" - -"@babel/helper-string-parser@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" - integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== - -"@babel/helper-validator-identifier@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad" - integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw== - -"@babel/helper-validator-identifier@^7.22.20": - version "7.22.20" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" - integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== + "@babel/traverse" "^7.25.7" + "@babel/types" "^7.25.7" -"@babel/helper-validator-option@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz#b203ce62ce5fe153899b617c08957de860de4d23" - integrity sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ== +"@babel/helper-string-parser@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz#d50e8d37b1176207b4fe9acedec386c565a44a54" + integrity sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g== -"@babel/helper-wrap-function@^7.16.8": - version "7.16.8" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.16.8.tgz#58afda087c4cd235de92f7ceedebca2c41274200" - integrity sha512-8RpyRVIAW1RcDDGTA+GpPAwV22wXCfKOoM9bet6TLkGIFTkRQSkH1nMQ5Yet4MpoXe1ZwHPVtNasc2w0uZMqnw== - dependencies: - "@babel/helper-function-name" "^7.16.7" - "@babel/template" "^7.16.7" - "@babel/traverse" "^7.16.8" - "@babel/types" "^7.16.8" +"@babel/helper-validator-identifier@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz#77b7f60c40b15c97df735b38a66ba1d7c3e93da5" + integrity sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg== -"@babel/helpers@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.16.7.tgz#7e3504d708d50344112767c3542fc5e357fffefc" - integrity sha512-9ZDoqtfY7AuEOt3cxchfii6C7GDyyMBffktR5B2jvWv8u2+efwvpnVKXMWzNehqy68tKgAfSwfdw/lWpthS2bw== - dependencies: - "@babel/template" "^7.16.7" - "@babel/traverse" "^7.16.7" - "@babel/types" "^7.16.7" +"@babel/helper-validator-option@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.25.7.tgz#97d1d684448228b30b506d90cace495d6f492729" + integrity sha512-ytbPLsm+GjArDYXJ8Ydr1c/KJuutjF2besPNbIZnZ6MKUxi/uTA22t2ymmA4WFjZFpjiAMO0xuuJPqK2nvDVfQ== -"@babel/helpers@^7.17.2": - version "7.17.2" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.17.2.tgz#23f0a0746c8e287773ccd27c14be428891f63417" - integrity sha512-0Qu7RLR1dILozr/6M0xgj+DFPmi6Bnulgm9M8BVa9ZCWxDqlSnqt3cf8IDPB5m45sVXUZ0kuQAgUrdSFFH79fQ== +"@babel/helper-wrap-function@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.25.7.tgz#9f6021dd1c4fdf4ad515c809967fc4bac9a70fe7" + integrity sha512-MA0roW3JF2bD1ptAaJnvcabsVlNQShUaThyJbCDD4bCp8NEgiFvpoqRI2YS22hHlc2thjO/fTg2ShLMC3jygAg== dependencies: - "@babel/template" "^7.16.7" - "@babel/traverse" "^7.17.0" - "@babel/types" "^7.17.0" + "@babel/template" "^7.25.7" + "@babel/traverse" "^7.25.7" + "@babel/types" "^7.25.7" -"@babel/highlight@^7.16.7": - version "7.16.10" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.16.10.tgz#744f2eb81579d6eea753c227b0f570ad785aba88" - integrity sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw== +"@babel/helpers@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.25.7.tgz#091b52cb697a171fe0136ab62e54e407211f09c2" + integrity sha512-Sv6pASx7Esm38KQpF/U/OXLwPPrdGHNKoeblRxgZRLXnAtnkEe4ptJPDtAZM7fBLadbc1Q07kQpSiGQ0Jg6tRA== dependencies: - "@babel/helper-validator-identifier" "^7.16.7" - chalk "^2.0.0" - js-tokens "^4.0.0" + "@babel/template" "^7.25.7" + "@babel/types" "^7.25.7" -"@babel/highlight@^7.22.13": - version "7.22.20" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54" - integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg== +"@babel/highlight@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.25.7.tgz#20383b5f442aa606e7b5e3043b0b1aafe9f37de5" + integrity sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw== dependencies: - "@babel/helper-validator-identifier" "^7.22.20" + "@babel/helper-validator-identifier" "^7.25.7" chalk "^2.4.2" js-tokens "^4.0.0" + picocolors "^1.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.16.12", "@babel/parser@^7.16.7": - version "7.16.12" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.16.12.tgz#9474794f9a650cf5e2f892444227f98e28cdf8b6" - integrity sha512-VfaV15po8RiZssrkPweyvbGVSe4x2y+aciFCgn0n0/SJMR22cwofRV1mtnJQYcSB1wUTaA/X1LnA3es66MCO5A== - -"@babel/parser@^7.17.3": - version "7.17.3" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.3.tgz#b07702b982990bf6fdc1da5049a23fece4c5c3d0" - integrity sha512-7yJPvPV+ESz2IUTPbOL+YkIGyCqOyNIzdguKQuJGnH7bg1WTIifuM21YqokFt/THWh1AkCRn9IgoykTRCBVpzA== - -"@babel/parser@^7.22.15", "@babel/parser@^7.23.0": - version "7.23.0" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719" - integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw== - -"@babel/parser@^7.9.4": - version "7.19.0" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.19.0.tgz#497fcafb1d5b61376959c1c338745ef0577aa02c" - integrity sha512-74bEXKX2h+8rrfQUfsBfuZZHzsEs6Eql4pqy/T4Nn6Y9wNPggQOqD6z6pn5Bl8ZfysKouFZT/UXEH94ummEeQw== - -"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.16.7.tgz#4eda6d6c2a0aa79c70fa7b6da67763dfe2141050" - integrity sha512-anv/DObl7waiGEnC24O9zqL0pSuI9hljihqiDuFHC8d7/bjr/4RLGPWuc8rYOff/QPzbEPSkzG8wGG9aDuhHRg== - dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.16.7.tgz#cc001234dfc139ac45f6bcf801866198c8c72ff9" - integrity sha512-di8vUHRdf+4aJ7ltXhaDbPoszdkh59AQtJM5soLsuHpQJdFQZOA4uGj0V2u/CZ8bJ/u8ULDL5yq6FO/bCXnKHw== - dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.16.0" - "@babel/plugin-proposal-optional-chaining" "^7.16.7" - -"@babel/plugin-proposal-async-generator-functions@^7.16.8": - version "7.16.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.16.8.tgz#3bdd1ebbe620804ea9416706cd67d60787504bc8" - integrity sha512-71YHIvMuiuqWJQkebWJtdhQTfd4Q4mF76q2IX37uZPkG9+olBxsX+rH1vkhFto4UeJZ9dPY2s+mDvhDm1u2BGQ== +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.15", "@babel/parser@^7.20.7", "@babel/parser@^7.25.7", "@babel/parser@^7.25.8": + version "7.25.8" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.25.8.tgz#f6aaf38e80c36129460c1657c0762db584c9d5e2" + integrity sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ== dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/helper-remap-async-to-generator" "^7.16.8" - "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/types" "^7.25.8" -"@babel/plugin-proposal-class-properties@^7.16.0", "@babel/plugin-proposal-class-properties@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.16.7.tgz#925cad7b3b1a2fcea7e59ecc8eb5954f961f91b0" - integrity sha512-IobU0Xme31ewjYOShSIqd/ZGM/r/cuOz2z0MDbNrhF5FW+ZVgi0f2lyeoj9KFPDOAqsYxmLWZte1WOwlvY9aww== +"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.7.tgz#93969ac50ef4d68b2504b01b758af714e4cbdd64" + integrity sha512-UV9Lg53zyebzD1DwQoT9mzkEKa922LNUp5YkTJ6Uta0RbyXaQNUgcvSt7qIu1PpPzVb6rd10OVNTzkyBGeVmxQ== dependencies: - "@babel/helper-create-class-features-plugin" "^7.16.7" - "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/traverse" "^7.25.7" -"@babel/plugin-proposal-class-static-block@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.16.7.tgz#712357570b612106ef5426d13dc433ce0f200c2a" - integrity sha512-dgqJJrcZoG/4CkMopzhPJjGxsIe9A8RlkQLnL/Vhhx8AA9ZuaRwGSlscSh42hazc7WSrya/IK7mTeoF0DP9tEw== +"@babel/plugin-bugfix-safari-class-field-initializer-scope@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.7.tgz#a338d611adb9dcd599b8b1efa200c88ebeffe046" + integrity sha512-GDDWeVLNxRIkQTnJn2pDOM1pkCgYdSqPeT1a9vh9yIqu2uzzgw1zcqEb+IJOhy+dTBMlNdThrDIksr2o09qrrQ== dependencies: - "@babel/helper-create-class-features-plugin" "^7.16.7" - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/plugin-syntax-class-static-block" "^7.14.5" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-proposal-decorators@^7.16.4": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.16.7.tgz#922907d2e3e327f5b07d2246bcfc0bd438f360d2" - integrity sha512-DoEpnuXK14XV9btI1k8tzNGCutMclpj4yru8aXKoHlVmbO1s+2A+g2+h4JhcjrxkFJqzbymnLG6j/niOf3iFXQ== +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.7.tgz#c5f755e911dfac7ef6957300c0f9c4a8c18c06f4" + integrity sha512-wxyWg2RYaSUYgmd9MR0FyRGyeOMQE/Uzr1wzd/g5cf5bwi9A4v6HFdDm7y1MgDtod/fLOSTZY6jDgV0xU9d5bA== dependencies: - "@babel/helper-create-class-features-plugin" "^7.16.7" - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/plugin-syntax-decorators" "^7.16.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-proposal-dynamic-import@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.16.7.tgz#c19c897eaa46b27634a00fee9fb7d829158704b2" - integrity sha512-I8SW9Ho3/8DRSdmDdH3gORdyUuYnk1m4cMxUAdu5oy4n3OfN8flDEH+d60iG7dUfi0KkYwSvoalHzzdRzpWHTg== +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.7.tgz#3b7ea04492ded990978b6deaa1dfca120ad4455a" + integrity sha512-Xwg6tZpLxc4iQjorYsyGMyfJE7nP5MV8t/Ka58BgiA7Jw0fRqQNcANlLfdJ/yvBt9z9LD2We+BEkT7vLqZRWng== dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.7" + "@babel/plugin-transform-optional-chaining" "^7.25.7" -"@babel/plugin-proposal-export-namespace-from@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.16.7.tgz#09de09df18445a5786a305681423ae63507a6163" - integrity sha512-ZxdtqDXLRGBL64ocZcs7ovt71L3jhC1RGSyR996svrCi3PYqHNkb3SwPJCs8RIzD86s+WPpt2S73+EHCGO+NUA== +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.7.tgz#9622b1d597a703aa3a921e6f58c9c2d9a028d2c5" + integrity sha512-UVATLMidXrnH+GMUIuxq55nejlj02HP7F5ETyBONzP6G87fPBogG4CH6kxrSrdIuAjdwNO9VzyaYsrZPscWUrw== dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/traverse" "^7.25.7" -"@babel/plugin-proposal-json-strings@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.16.7.tgz#9732cb1d17d9a2626a08c5be25186c195b6fa6e8" - integrity sha512-lNZ3EEggsGY78JavgbHsK9u5P3pQaW7k4axlgFLYkMd7UBsiNahCITShLjNQschPyjtO6dADrL24757IdhBrsQ== +"@babel/plugin-proposal-class-properties@^7.16.0": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz#b110f59741895f7ec21a6fff696ec46265c446a3" + integrity sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ== dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/helper-create-class-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-proposal-logical-assignment-operators@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.16.7.tgz#be23c0ba74deec1922e639832904be0bea73cdea" - integrity sha512-K3XzyZJGQCr00+EtYtrDjmwX7o7PLK6U9bi1nCwkQioRFVUv6dJoxbQjtWVtP+bCPy82bONBKG8NPyQ4+i6yjg== +"@babel/plugin-proposal-decorators@^7.16.4": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.25.7.tgz#dabfd82df5dff3a8fc61a434233bf8227c88402c" + integrity sha512-q1mqqqH0e1lhmsEQHV5U8OmdueBC2y0RFr2oUzZoFRtN3MvPmt2fsFRcNQAoGLTSNdHBFUYGnlgcRFhkBbKjPw== dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/helper-create-class-features-plugin" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/plugin-syntax-decorators" "^7.25.7" -"@babel/plugin-proposal-nullish-coalescing-operator@^7.16.0", "@babel/plugin-proposal-nullish-coalescing-operator@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.16.7.tgz#141fc20b6857e59459d430c850a0011e36561d99" - integrity sha512-aUOrYU3EVtjf62jQrCj63pYZ7k6vns2h/DQvHPWGmsJRYzWXZ6/AsfgpiRy6XiuIDADhJzP2Q9MwSMKauBQ+UQ== +"@babel/plugin-proposal-nullish-coalescing-operator@^7.16.0": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz#fdd940a99a740e577d6c753ab6fbb43fdb9467e1" + integrity sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA== dependencies: - "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" -"@babel/plugin-proposal-numeric-separator@^7.16.0", "@babel/plugin-proposal-numeric-separator@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.16.7.tgz#d6b69f4af63fb38b6ca2558442a7fb191236eba9" - integrity sha512-vQgPMknOIgiuVqbokToyXbkY/OmmjAzr/0lhSIbG/KmnzXPGwW/AdhdKpi+O4X/VkWiWjnkKOBiqJrTaC98VKw== +"@babel/plugin-proposal-numeric-separator@^7.16.0": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz#899b14fbafe87f053d2c5ff05b36029c62e13c75" + integrity sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q== dependencies: - "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-syntax-numeric-separator" "^7.10.4" -"@babel/plugin-proposal-object-rest-spread@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.16.7.tgz#94593ef1ddf37021a25bdcb5754c4a8d534b01d8" - integrity sha512-3O0Y4+dw94HA86qSg9IHfyPktgR7q3gpNVAeiKQd+8jBKFaU5NQS1Yatgo4wY+UFNuLjvxcSmzcsHqrhgTyBUA== - dependencies: - "@babel/compat-data" "^7.16.4" - "@babel/helper-compilation-targets" "^7.16.7" - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.16.7" - -"@babel/plugin-proposal-optional-catch-binding@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.16.7.tgz#c623a430674ffc4ab732fd0a0ae7722b67cb74cf" - integrity sha512-eMOH/L4OvWSZAE1VkHbr1vckLG1WUcHGJSLqqQwl2GaUqG6QjddvrOaTUMNYiv77H5IKPMZ9U9P7EaHwvAShfA== - dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - -"@babel/plugin-proposal-optional-chaining@^7.16.0", "@babel/plugin-proposal-optional-chaining@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.16.7.tgz#7cd629564724816c0e8a969535551f943c64c39a" - integrity sha512-eC3xy+ZrUcBtP7x+sq62Q/HYd674pPTb/77XZMb5wbDPGWIdUbSr4Agr052+zaUPSb+gGRnjxXfKFvx5iMJ+DA== +"@babel/plugin-proposal-optional-chaining@^7.16.0": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz#886f5c8978deb7d30f678b2e24346b287234d3ea" + integrity sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA== dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.16.0" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" "@babel/plugin-syntax-optional-chaining" "^7.8.3" -"@babel/plugin-proposal-private-methods@^7.16.0", "@babel/plugin-proposal-private-methods@^7.16.11": - version "7.16.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.16.11.tgz#e8df108288555ff259f4527dbe84813aac3a1c50" - integrity sha512-F/2uAkPlXDr8+BHpZvo19w3hLFKge+k75XUprE6jaqKxjGkSYcK+4c+bup5PdW/7W/Rpjwql7FTVEDW+fRAQsw== +"@babel/plugin-proposal-private-methods@^7.16.0": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz#5209de7d213457548a98436fa2882f52f4be6bea" + integrity sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA== dependencies: - "@babel/helper-create-class-features-plugin" "^7.16.10" - "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-create-class-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-proposal-private-property-in-object@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.16.7.tgz#b0b8cef543c2c3d57e59e2c611994861d46a3fce" - integrity sha512-rMQkjcOFbm+ufe3bTZLyOfsOUOxyvLXZJCTARhJr+8UMSoZmqTe1K1BgkFcrW37rAchWg57yI69ORxiWvUINuQ== - dependencies: - "@babel/helper-annotate-as-pure" "^7.16.7" - "@babel/helper-create-class-features-plugin" "^7.16.7" - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/plugin-syntax-private-property-in-object" "^7.14.5" +"@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": + version "7.21.0-placeholder-for-preset-env.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703" + integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w== -"@babel/plugin-proposal-unicode-property-regex@^7.16.7", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.16.7.tgz#635d18eb10c6214210ffc5ff4932552de08188a2" - integrity sha512-QRK0YI/40VLhNVGIjRNAAQkEHws0cswSdFFjpFyt943YmJIU1da9uW63Iu6NFV6CxTZW5eTDCrwZUstBWgp/Rg== +"@babel/plugin-proposal-private-property-in-object@^7.21.11": + version "7.21.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz#69d597086b6760c4126525cfa154f34631ff272c" + integrity sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.16.7" - "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-create-class-features-plugin" "^7.21.0" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" @@ -580,7 +380,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-class-properties@^7.12.13", "@babel/plugin-syntax-class-properties@^7.8.3": +"@babel/plugin-syntax-class-properties@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== @@ -594,35 +394,35 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-syntax-decorators@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.16.7.tgz#f66a0199f16de7c1ef5192160ccf5d069739e3d3" - integrity sha512-vQ+PxL+srA7g6Rx6I1e15m55gftknl2X8GCUW1JTlkTaXZLJOS0UcaY0eK9jYT7IYf4awn6qwyghVHLDz1WyMw== +"@babel/plugin-syntax-decorators@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.25.7.tgz#cf26fdde4e750688e133c0e33ead2506377e88f7" + integrity sha512-oXduHo642ZhstLVYTe2z2GSJIruU0c/W3/Ghr6A5yGMsVrvdnxO1z+3pbTcT7f3/Clnt+1z8D/w1r1f1SHaCHw== dependencies: - "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-syntax-dynamic-import@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" - integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== +"@babel/plugin-syntax-flow@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.25.7.tgz#7d1255201b55d7644c57e0eb354aaf9f8b8d2d02" + integrity sha512-fyoj6/YdVtlv2ROig/J0fP7hh/wNO1MJGm1NR70Pg7jbkF+jOUL9joorqaCOQh06Y+LfgTagHzC8KqZ3MF782w== dependencies: - "@babel/helper-plugin-utils" "^7.8.0" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-syntax-export-namespace-from@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" - integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== +"@babel/plugin-syntax-import-assertions@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.25.7.tgz#8ce248f9f4ed4b7ed4cb2e0eb4ed9efd9f52921f" + integrity sha512-ZvZQRmME0zfJnDQnVBKYzHxXT7lYBB3Revz1GuS7oLXWMgqUPX4G+DDbT30ICClht9WKV34QVrZhSw6WdklwZQ== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-syntax-flow@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.16.7.tgz#202b147e5892b8452bbb0bb269c7ed2539ab8832" - integrity sha512-UDo3YGQO0jH6ytzVwgSLv9i/CzMcUjbKenL67dTrAZPPv6GFAtDhe6jqnvmoKzC/7htNTohhos+onPtDMqJwaQ== +"@babel/plugin-syntax-import-attributes@^7.24.7", "@babel/plugin-syntax-import-attributes@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.25.7.tgz#d78dd0499d30df19a598e63ab895e21b909bc43f" + integrity sha512-AqVo+dguCgmpi/3mYBdu9lkngOBlQ2w2vnNpa6gfiCxQZLzV4ZbhsXitJ2Yblkoe1VQwtHSaNmIaGll/26YWRw== dependencies: - "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-syntax-import-meta@^7.8.3": +"@babel/plugin-syntax-import-meta@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== @@ -636,14 +436,14 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-jsx@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.16.7.tgz#50b6571d13f764266a113d77c82b4a6508bbe665" - integrity sha512-Esxmk7YjA8QysKeT3VhTXvF6y77f/a91SIs4pWb4H2eWGQkCKFgQaG6hdoEVZtGsrAcb2K5BW66XsOErD4WU3Q== +"@babel/plugin-syntax-jsx@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.7.tgz#5352d398d11ea5e7ef330c854dea1dae0bf18165" + integrity sha512-ruZOnKO+ajVL/MVx+PwNBPOkrnXTXoWMtte1MBpegfCArhqOe3Bj52avVj1huLLxNKYKXYaSxZ2F+woK1ekXfw== dependencies: - "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-syntax-logical-assignment-operators@^7.10.4", "@babel/plugin-syntax-logical-assignment-operators@^7.8.3": +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== @@ -657,7 +457,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-numeric-separator@^7.10.4", "@babel/plugin-syntax-numeric-separator@^7.8.3": +"@babel/plugin-syntax-numeric-separator@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== @@ -692,460 +492,587 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-syntax-top-level-await@^7.14.5", "@babel/plugin-syntax-top-level-await@^7.8.3": +"@babel/plugin-syntax-top-level-await@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== dependencies: "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-syntax-typescript@^7.16.7", "@babel/plugin-syntax-typescript@^7.7.2": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.16.7.tgz#39c9b55ee153151990fb038651d58d3fd03f98f8" - integrity sha512-YhUIJHHGkqPgEcMYkPCKTyGUdoGKWtopIycQyjJH8OjvRgOYsXsaKehLVPScKJWAULPxMa4N1vCe6szREFlZ7A== +"@babel/plugin-syntax-typescript@^7.25.7", "@babel/plugin-syntax-typescript@^7.7.2": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.7.tgz#bfc05b0cc31ebd8af09964650cee723bb228108b" + integrity sha512-rR+5FDjpCHqqZN2bzZm18bVYGaejGq5ZkpVCJLXor/+zlSrSoc4KWcHI0URVWjl/68Dyr1uwZUz/1njycEAv9g== dependencies: - "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-arrow-functions@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.16.7.tgz#44125e653d94b98db76369de9c396dc14bef4154" - integrity sha512-9ffkFFMbvzTvv+7dTp/66xvZAWASuPD5Tl9LK3Z9vhOmANo6j94rik+5YMBt4CwHVMWLWpMsriIc2zsa3WW3xQ== +"@babel/plugin-syntax-unicode-sets-regex@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz#d49a3b3e6b52e5be6740022317580234a6a47357" + integrity sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg== dependencies: - "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-transform-async-to-generator@^7.16.8": - version "7.16.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.16.8.tgz#b83dff4b970cf41f1b819f8b49cc0cfbaa53a808" - integrity sha512-MtmUmTJQHCnyJVrScNzNlofQJ3dLFuobYn3mwOTKHnSCMtbNsqvF71GQmJfFjdrXSsAA7iysFmYWw4bXZ20hOg== +"@babel/plugin-transform-arrow-functions@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.7.tgz#1b9ed22e6890a0e9ff470371c73b8c749bcec386" + integrity sha512-EJN2mKxDwfOUCPxMO6MUI58RN3ganiRAG/MS/S3HfB6QFNjroAMelQo/gybyYq97WerCBAZoyrAoW8Tzdq2jWg== dependencies: - "@babel/helper-module-imports" "^7.16.7" - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/helper-remap-async-to-generator" "^7.16.8" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-block-scoped-functions@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.16.7.tgz#4d0d57d9632ef6062cdf354bb717102ee042a620" - integrity sha512-JUuzlzmF40Z9cXyytcbZEZKckgrQzChbQJw/5PuEHYeqzCsvebDx0K0jWnIIVcmmDOAVctCgnYs0pMcrYj2zJg== +"@babel/plugin-transform-async-generator-functions@^7.25.8": + version "7.25.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.8.tgz#3331de02f52cc1f2c75b396bec52188c85b0b1ec" + integrity sha512-9ypqkozyzpG+HxlH4o4gdctalFGIjjdufzo7I2XPda0iBnZ6a+FO0rIEQcdSPXp02CkvGsII1exJhmROPQd5oA== dependencies: - "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-remap-async-to-generator" "^7.25.7" + "@babel/traverse" "^7.25.7" -"@babel/plugin-transform-block-scoping@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.16.7.tgz#f50664ab99ddeaee5bc681b8f3a6ea9d72ab4f87" - integrity sha512-ObZev2nxVAYA4bhyusELdo9hb3H+A56bxH3FZMbEImZFiEDYVHXQSJ1hQKFlDnlt8G9bBrCZ5ZpURZUrV4G5qQ== +"@babel/plugin-transform-async-to-generator@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.7.tgz#a44c7323f8d4285a6c568dd43c5c361d6367ec52" + integrity sha512-ZUCjAavsh5CESCmi/xCpX1qcCaAglzs/7tmuvoFnJgA1dM7gQplsguljoTg+Ru8WENpX89cQyAtWoaE0I3X3Pg== dependencies: - "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-module-imports" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-remap-async-to-generator" "^7.25.7" -"@babel/plugin-transform-classes@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.16.7.tgz#8f4b9562850cd973de3b498f1218796eb181ce00" - integrity sha512-WY7og38SFAGYRe64BrjKf8OrE6ulEHtr5jEYaZMwox9KebgqPi67Zqz8K53EKk1fFEJgm96r32rkKZ3qA2nCWQ== - dependencies: - "@babel/helper-annotate-as-pure" "^7.16.7" - "@babel/helper-environment-visitor" "^7.16.7" - "@babel/helper-function-name" "^7.16.7" - "@babel/helper-optimise-call-expression" "^7.16.7" - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/helper-replace-supers" "^7.16.7" - "@babel/helper-split-export-declaration" "^7.16.7" +"@babel/plugin-transform-block-scoped-functions@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.7.tgz#e0b8843d5571719a2f1bf7e284117a3379fcc17c" + integrity sha512-xHttvIM9fvqW+0a3tZlYcZYSBpSWzGBFIt/sYG3tcdSzBB8ZeVgz2gBP7Df+sM0N1850jrviYSSeUuc+135dmQ== + dependencies: + "@babel/helper-plugin-utils" "^7.25.7" + +"@babel/plugin-transform-block-scoping@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.7.tgz#6dab95e98adf780ceef1b1c3ab0e55cd20dd410a" + integrity sha512-ZEPJSkVZaeTFG/m2PARwLZQ+OG0vFIhPlKHK/JdIMy8DbRJ/htz6LRrTFtdzxi9EHmcwbNPAKDnadpNSIW+Aow== + dependencies: + "@babel/helper-plugin-utils" "^7.25.7" + +"@babel/plugin-transform-class-properties@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.7.tgz#a389cfca7a10ac80e3ff4c75fca08bd097ad1523" + integrity sha512-mhyfEW4gufjIqYFo9krXHJ3ElbFLIze5IDp+wQTxoPd+mwFb1NxatNAwmv8Q8Iuxv7Zc+q8EkiMQwc9IhyGf4g== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" + +"@babel/plugin-transform-class-static-block@^7.25.8": + version "7.25.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.25.8.tgz#a8af22028920fe404668031eceb4c3aadccb5262" + integrity sha512-e82gl3TCorath6YLf9xUwFehVvjvfqFhdOo4+0iVIVju+6XOi5XHkqB3P2AXnSwoeTX0HBoXq5gJFtvotJzFnQ== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" + +"@babel/plugin-transform-classes@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.7.tgz#5103206cf80d02283bbbd044509ea3b65d0906bb" + integrity sha512-9j9rnl+YCQY0IGoeipXvnk3niWicIB6kCsWRGLwX241qSXpbA4MKxtp/EdvFxsc4zI5vqfLxzOd0twIJ7I99zg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.25.7" + "@babel/helper-compilation-targets" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-replace-supers" "^7.25.7" + "@babel/traverse" "^7.25.7" globals "^11.1.0" -"@babel/plugin-transform-computed-properties@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.16.7.tgz#66dee12e46f61d2aae7a73710f591eb3df616470" - integrity sha512-gN72G9bcmenVILj//sv1zLNaPyYcOzUho2lIJBMh/iakJ9ygCo/hEF9cpGb61SCMEDxbbyBoVQxrt+bWKu5KGw== +"@babel/plugin-transform-computed-properties@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.7.tgz#7f621f0aa1354b5348a935ab12e3903842466f65" + integrity sha512-QIv+imtM+EtNxg/XBKL3hiWjgdLjMOmZ+XzQwSgmBfKbfxUjBzGgVPklUuE55eq5/uVoh8gg3dqlrwR/jw3ZeA== dependencies: - "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/template" "^7.25.7" -"@babel/plugin-transform-destructuring@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.16.7.tgz#ca9588ae2d63978a4c29d3f33282d8603f618e23" - integrity sha512-VqAwhTHBnu5xBVDCvrvqJbtLUa++qZaWC0Fgr2mqokBlulZARGyIvZDoqbPlPaKImQ9dKAcCzbv+ul//uqu70A== +"@babel/plugin-transform-destructuring@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.7.tgz#f6f26a9feefb5aa41fd45b6f5838901b5333d560" + integrity sha512-xKcfLTlJYUczdaM1+epcdh1UGewJqr9zATgrNHcLBcV2QmfvPPEixo/sK/syql9cEmbr7ulu5HMFG5vbbt/sEA== dependencies: - "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-dotall-regex@^7.16.7", "@babel/plugin-transform-dotall-regex@^7.4.4": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.16.7.tgz#6b2d67686fab15fb6a7fd4bd895d5982cfc81241" - integrity sha512-Lyttaao2SjZF6Pf4vk1dVKv8YypMpomAbygW+mU5cYP3S5cWTfCJjG8xV6CFdzGFlfWK81IjL9viiTvpb6G7gQ== +"@babel/plugin-transform-dotall-regex@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.7.tgz#9d775c4a3ff1aea64045300fcd4309b4a610ef02" + integrity sha512-kXzXMMRzAtJdDEgQBLF4oaiT6ZCU3oWHgpARnTKDAqPkDJ+bs3NrZb310YYevR5QlRo3Kn7dzzIdHbZm1VzJdQ== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.16.7" - "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-create-regexp-features-plugin" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-duplicate-keys@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.16.7.tgz#2207e9ca8f82a0d36a5a67b6536e7ef8b08823c9" - integrity sha512-03DvpbRfvWIXyK0/6QiR1KMTWeT6OcQ7tbhjrXyFS02kjuX/mu5Bvnh5SDSWHxyawit2g5aWhKwI86EE7GUnTw== +"@babel/plugin-transform-duplicate-keys@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.7.tgz#fbba7d1155eab76bd4f2a038cbd5d65883bd7a93" + integrity sha512-by+v2CjoL3aMnWDOyCIg+yxU9KXSRa9tN6MbqggH5xvymmr9p4AMjYkNlQy4brMceBnUyHZ9G8RnpvT8wP7Cfg== dependencies: - "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-exponentiation-operator@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.16.7.tgz#efa9862ef97e9e9e5f653f6ddc7b665e8536fe9b" - integrity sha512-8UYLSlyLgRixQvlYH3J2ekXFHDFLQutdy7FfFAMm3CPZ6q9wHCwnUyiXpQCe3gVVnQlHc5nsuiEVziteRNTXEA== +"@babel/plugin-transform-duplicate-named-capturing-groups-regex@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.7.tgz#102b31608dcc22c08fbca1894e104686029dc141" + integrity sha512-HvS6JF66xSS5rNKXLqkk7L9c/jZ/cdIVIcoPVrnl8IsVpLggTjXs8OWekbLHs/VtYDDh5WXnQyeE3PPUGm22MA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" + +"@babel/plugin-transform-dynamic-import@^7.25.8": + version "7.25.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.8.tgz#f1edbe75b248cf44c70c8ca8ed3818a668753aaa" + integrity sha512-gznWY+mr4ZQL/EWPcbBQUP3BXS5FwZp8RUOw06BaRn8tQLzN4XLIxXejpHN9Qo8x8jjBmAAKp6FoS51AgkSA/A== dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.16.7" - "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-plugin-utils" "^7.25.7" + +"@babel/plugin-transform-exponentiation-operator@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.25.7.tgz#5961a3a23a398faccd6cddb34a2182807d75fb5f" + integrity sha512-yjqtpstPfZ0h/y40fAXRv2snciYr0OAoMXY/0ClC7tm4C/nG5NJKmIItlaYlLbIVAWNfrYuy9dq1bE0SbX0PEg== + dependencies: + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" + +"@babel/plugin-transform-export-namespace-from@^7.25.8": + version "7.25.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.8.tgz#d1988c3019a380b417e0516418b02804d3858145" + integrity sha512-sPtYrduWINTQTW7FtOy99VCTWp4H23UX7vYcut7S4CIMEXU+54zKX9uCoGkLsWXteyaMXzVHgzWbLfQ1w4GZgw== + dependencies: + "@babel/helper-plugin-utils" "^7.25.7" "@babel/plugin-transform-flow-strip-types@^7.16.0": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.16.7.tgz#291fb140c78dabbf87f2427e7c7c332b126964b8" - integrity sha512-mzmCq3cNsDpZZu9FADYYyfZJIOrSONmHcop2XEKPdBNMa4PDC4eEvcOvzZaCNcjKu72v0XQlA5y1g58aLRXdYg== + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.25.7.tgz#32be871a80e10bbe6d8b1c8a7eeedbbc896d5e80" + integrity sha512-q8Td2PPc6/6I73g96SreSUCKEcwMXCwcXSIAVTyTTN6CpJe0dMj8coxu1fg1T9vfBLi6Rsi6a4ECcFBbKabS5w== dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/plugin-syntax-flow" "^7.16.7" + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/plugin-syntax-flow" "^7.25.7" -"@babel/plugin-transform-for-of@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.16.7.tgz#649d639d4617dff502a9a158c479b3b556728d8c" - integrity sha512-/QZm9W92Ptpw7sjI9Nx1mbcsWz33+l8kuMIQnDwgQBG5s3fAfQvkRjQ7NqXhtNcKOnPkdICmUHyCaWW06HCsqg== +"@babel/plugin-transform-for-of@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.7.tgz#0acfea0f27aa290818b5b48a5a44b3f03fc13669" + integrity sha512-n/TaiBGJxYFWvpJDfsxSj9lEEE44BFM1EPGz4KEiTipTgkoFVVcCmzAL3qA7fdQU96dpo4gGf5HBx/KnDvqiHw== dependencies: - "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.7" -"@babel/plugin-transform-function-name@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.16.7.tgz#5ab34375c64d61d083d7d2f05c38d90b97ec65cf" - integrity sha512-SU/C68YVwTRxqWj5kgsbKINakGag0KTgq9f2iZEXdStoAbOzLHEBRYzImmA6yFo8YZhJVflvXmIHUO7GWHmxxA== +"@babel/plugin-transform-function-name@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.7.tgz#7e394ccea3693902a8b50ded8b6ae1fa7b8519fd" + integrity sha512-5MCTNcjCMxQ63Tdu9rxyN6cAWurqfrDZ76qvVPrGYdBxIj+EawuuxTu/+dgJlhK5eRz3v1gLwp6XwS8XaX2NiQ== dependencies: - "@babel/helper-compilation-targets" "^7.16.7" - "@babel/helper-function-name" "^7.16.7" - "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-compilation-targets" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/traverse" "^7.25.7" -"@babel/plugin-transform-literals@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.16.7.tgz#254c9618c5ff749e87cb0c0cef1a0a050c0bdab1" - integrity sha512-6tH8RTpTWI0s2sV6uq3e/C9wPo4PTqqZps4uF0kzQ9/xPLFQtipynvmT1g/dOfEJ+0EQsHhkQ/zyRId8J2b8zQ== +"@babel/plugin-transform-json-strings@^7.25.8": + version "7.25.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.8.tgz#6fb3ec383a2ea92652289fdba653e3f9de722694" + integrity sha512-4OMNv7eHTmJ2YXs3tvxAfa/I43di+VcF+M4Wt66c88EAED1RoGaf1D64cL5FkRpNL+Vx9Hds84lksWvd/wMIdA== dependencies: - "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-member-expression-literals@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.16.7.tgz#6e5dcf906ef8a098e630149d14c867dd28f92384" - integrity sha512-mBruRMbktKQwbxaJof32LT9KLy2f3gH+27a5XSuXo6h7R3vqltl0PgZ80C8ZMKw98Bf8bqt6BEVi3svOh2PzMw== +"@babel/plugin-transform-literals@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.7.tgz#70cbdc742f2cfdb1a63ea2cbd018d12a60b213c3" + integrity sha512-fwzkLrSu2fESR/cm4t6vqd7ebNIopz2QHGtjoU+dswQo/P6lwAG04Q98lliE3jkz/XqnbGFLnUcE0q0CVUf92w== dependencies: - "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-modules-amd@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.16.7.tgz#b28d323016a7daaae8609781d1f8c9da42b13186" - integrity sha512-KaaEtgBL7FKYwjJ/teH63oAmE3lP34N3kshz8mm4VMAw7U3PxjVwwUmxEFksbgsNUaO3wId9R2AVQYSEGRa2+g== +"@babel/plugin-transform-logical-assignment-operators@^7.25.8": + version "7.25.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.8.tgz#01868ff92daa9e525b4c7902aa51979082a05710" + integrity sha512-f5W0AhSbbI+yY6VakT04jmxdxz+WsID0neG7+kQZbCOjuyJNdL5Nn4WIBm4hRpKnUcO9lP0eipUhFN12JpoH8g== dependencies: - "@babel/helper-module-transforms" "^7.16.7" - "@babel/helper-plugin-utils" "^7.16.7" - babel-plugin-dynamic-import-node "^2.3.3" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-modules-commonjs@^7.16.8": - version "7.16.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.16.8.tgz#cdee19aae887b16b9d331009aa9a219af7c86afe" - integrity sha512-oflKPvsLT2+uKQopesJt3ApiaIS2HW+hzHFcwRNtyDGieAeC/dIHZX8buJQ2J2X1rxGPy4eRcUijm3qcSPjYcA== +"@babel/plugin-transform-member-expression-literals@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.7.tgz#0a36c3fbd450cc9e6485c507f005fa3d1bc8fca5" + integrity sha512-Std3kXwpXfRV0QtQy5JJcRpkqP8/wG4XL7hSKZmGlxPlDqmpXtEPRmhF7ztnlTCtUN3eXRUJp+sBEZjaIBVYaw== dependencies: - "@babel/helper-module-transforms" "^7.16.7" - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/helper-simple-access" "^7.16.7" - babel-plugin-dynamic-import-node "^2.3.3" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-modules-systemjs@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.16.7.tgz#887cefaef88e684d29558c2b13ee0563e287c2d7" - integrity sha512-DuK5E3k+QQmnOqBR9UkusByy5WZWGRxfzV529s9nPra1GE7olmxfqO2FHobEOYSPIjPBTr4p66YDcjQnt8cBmw== +"@babel/plugin-transform-modules-amd@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.7.tgz#bb4e543b5611f6c8c685a2fd485408713a3adf3d" + integrity sha512-CgselSGCGzjQvKzghCvDTxKHP3iooenLpJDO842ehn5D2G5fJB222ptnDwQho0WjEvg7zyoxb9P+wiYxiJX5yA== dependencies: - "@babel/helper-hoist-variables" "^7.16.7" - "@babel/helper-module-transforms" "^7.16.7" - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/helper-validator-identifier" "^7.16.7" - babel-plugin-dynamic-import-node "^2.3.3" + "@babel/helper-module-transforms" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-modules-umd@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.16.7.tgz#23dad479fa585283dbd22215bff12719171e7618" - integrity sha512-EMh7uolsC8O4xhudF2F6wedbSHm1HHZ0C6aJ7K67zcDNidMzVcxWdGr+htW9n21klm+bOn+Rx4CBsAntZd3rEQ== +"@babel/plugin-transform-modules-commonjs@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.25.7.tgz#173f0c791bb7407c092ce6d77ee90eb3f2d1d2fd" + integrity sha512-L9Gcahi0kKFYXvweO6n0wc3ZG1ChpSFdgG+eV1WYZ3/dGbJK7vvk91FgGgak8YwRgrCuihF8tE/Xg07EkL5COg== dependencies: - "@babel/helper-module-transforms" "^7.16.7" - "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-module-transforms" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-simple-access" "^7.25.7" -"@babel/plugin-transform-named-capturing-groups-regex@^7.16.8": - version "7.16.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.16.8.tgz#7f860e0e40d844a02c9dcf9d84965e7dfd666252" - integrity sha512-j3Jw+n5PvpmhRR+mrgIh04puSANCk/T/UA3m3P1MjJkhlK906+ApHhDIqBQDdOgL/r1UYpz4GNclTXxyZrYGSw== +"@babel/plugin-transform-modules-systemjs@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.7.tgz#8b14d319a177cc9c85ef8b0512afd429d9e2e60b" + integrity sha512-t9jZIvBmOXJsiuyOwhrIGs8dVcD6jDyg2icw1VL4A/g+FnWyJKwUfSSU2nwJuMV2Zqui856El9u+ElB+j9fV1g== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.16.7" + "@babel/helper-module-transforms" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-validator-identifier" "^7.25.7" + "@babel/traverse" "^7.25.7" -"@babel/plugin-transform-new-target@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.16.7.tgz#9967d89a5c243818e0800fdad89db22c5f514244" - integrity sha512-xiLDzWNMfKoGOpc6t3U+etCE2yRnn3SM09BXqWPIZOBpL2gvVrBWUKnsJx0K/ADi5F5YC5f8APFfWrz25TdlGg== +"@babel/plugin-transform-modules-umd@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.7.tgz#00ee7a7e124289549381bfb0e24d87fd7f848367" + integrity sha512-p88Jg6QqsaPh+EB7I9GJrIqi1Zt4ZBHUQtjw3z1bzEXcLh6GfPqzZJ6G+G1HBGKUNukT58MnKG7EN7zXQBCODw== dependencies: - "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-module-transforms" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-object-super@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.16.7.tgz#ac359cf8d32cf4354d27a46867999490b6c32a94" - integrity sha512-14J1feiQVWaGvRxj2WjyMuXS2jsBkgB3MdSN5HuC2G5nRspa5RK9COcs82Pwy5BuGcjb+fYaUj94mYcOj7rCvw== +"@babel/plugin-transform-named-capturing-groups-regex@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.7.tgz#a2f3f6d7f38693b462542951748f0a72a34d196d" + integrity sha512-BtAT9LzCISKG3Dsdw5uso4oV1+v2NlVXIIomKJgQybotJY3OwCwJmkongjHgwGKoZXd0qG5UZ12JUlDQ07W6Ow== dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/helper-replace-supers" "^7.16.7" + "@babel/helper-create-regexp-features-plugin" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-parameters@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.16.7.tgz#a1721f55b99b736511cb7e0152f61f17688f331f" - integrity sha512-AT3MufQ7zZEhU2hwOA11axBnExW0Lszu4RL/tAlUJBuNoRak+wehQW8h6KcXOcgjY42fHtDxswuMhMjFEuv/aw== +"@babel/plugin-transform-new-target@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.7.tgz#52b2bde523b76c548749f38dc3054f1f45e82bc9" + integrity sha512-CfCS2jDsbcZaVYxRFo2qtavW8SpdzmBXC2LOI4oO0rP+JSRDxxF3inF4GcPsLgfb5FjkhXG5/yR/lxuRs2pySA== dependencies: - "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-property-literals@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.16.7.tgz#2dadac85155436f22c696c4827730e0fe1057a55" - integrity sha512-z4FGr9NMGdoIl1RqavCqGG+ZuYjfZ/hkCIeuH6Do7tXmSm0ls11nYVSJqFEUOSJbDab5wC6lRE/w6YjVcr6Hqw== +"@babel/plugin-transform-nullish-coalescing-operator@^7.25.8": + version "7.25.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.25.8.tgz#befb4900c130bd52fccf2b926314557987f1b552" + integrity sha512-Z7WJJWdQc8yCWgAmjI3hyC+5PXIubH9yRKzkl9ZEG647O9szl9zvmKLzpbItlijBnVhTUf1cpyWBsZ3+2wjWPQ== + dependencies: + "@babel/helper-plugin-utils" "^7.25.7" + +"@babel/plugin-transform-numeric-separator@^7.25.8": + version "7.25.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.8.tgz#91e370486371637bd42161052f2602c701386891" + integrity sha512-rm9a5iEFPS4iMIy+/A/PiS0QN0UyjPIeVvbU5EMZFKJZHt8vQnasbpo3T3EFcxzCeYO0BHfc4RqooCZc51J86Q== + dependencies: + "@babel/helper-plugin-utils" "^7.25.7" + +"@babel/plugin-transform-object-rest-spread@^7.25.8": + version "7.25.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.8.tgz#0904ac16bcce41df4db12d915d6780f85c7fb04b" + integrity sha512-LkUu0O2hnUKHKE7/zYOIjByMa4VRaV2CD/cdGz0AxU9we+VA3kDDggKEzI0Oz1IroG+6gUP6UmWEHBMWZU316g== + dependencies: + "@babel/helper-compilation-targets" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/plugin-transform-parameters" "^7.25.7" + +"@babel/plugin-transform-object-super@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.7.tgz#582a9cea8cf0a1e02732be5b5a703a38dedf5661" + integrity sha512-pWT6UXCEW3u1t2tcAGtE15ornCBvopHj9Bps9D2DsH15APgNVOTwwczGckX+WkAvBmuoYKRCFa4DK+jM8vh5AA== + dependencies: + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-replace-supers" "^7.25.7" + +"@babel/plugin-transform-optional-catch-binding@^7.25.8": + version "7.25.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.8.tgz#2649b86a3bb202c6894ec81a6ddf41b94d8f3103" + integrity sha512-EbQYweoMAHOn7iJ9GgZo14ghhb9tTjgOc88xFgYngifx7Z9u580cENCV159M4xDh3q/irbhSjZVpuhpC2gKBbg== + dependencies: + "@babel/helper-plugin-utils" "^7.25.7" + +"@babel/plugin-transform-optional-chaining@^7.25.7", "@babel/plugin-transform-optional-chaining@^7.25.8": + version "7.25.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.8.tgz#f46283b78adcc5b6ab988a952f989e7dce70653f" + integrity sha512-q05Bk7gXOxpTHoQ8RSzGSh/LHVB9JEIkKnk3myAWwZHnYiTGYtbdrYkIsS8Xyh4ltKf7GNUSgzs/6P2bJtBAQg== + dependencies: + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.7" + +"@babel/plugin-transform-parameters@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.7.tgz#80c38b03ef580f6d6bffe1c5254bb35986859ac7" + integrity sha512-FYiTvku63me9+1Nz7TOx4YMtW3tWXzfANZtrzHhUZrz4d47EEtMQhzFoZWESfXuAMMT5mwzD4+y1N8ONAX6lMQ== + dependencies: + "@babel/helper-plugin-utils" "^7.25.7" + +"@babel/plugin-transform-private-methods@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.7.tgz#c790a04f837b4bd61d6b0317b43aa11ff67dce80" + integrity sha512-KY0hh2FluNxMLwOCHbxVOKfdB5sjWG4M183885FmaqWWiGMhRZq4DQRKH6mHdEucbJnyDyYiZNwNG424RymJjA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" + +"@babel/plugin-transform-private-property-in-object@^7.25.8": + version "7.25.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.8.tgz#1234f856ce85e061f9688764194e51ea7577c434" + integrity sha512-8Uh966svuB4V8RHHg0QJOB32QK287NBksJOByoKmHMp1TAobNniNalIkI2i5IPj5+S9NYCG4VIjbEuiSN8r+ow== + dependencies: + "@babel/helper-annotate-as-pure" "^7.25.7" + "@babel/helper-create-class-features-plugin" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" + +"@babel/plugin-transform-property-literals@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.7.tgz#a8612b4ea4e10430f00012ecf0155662c7d6550d" + integrity sha512-lQEeetGKfFi0wHbt8ClQrUSUMfEeI3MMm74Z73T9/kuz990yYVtfofjf3NuA42Jy3auFOpbjDyCSiIkTs1VIYw== dependencies: - "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-plugin-utils" "^7.25.7" "@babel/plugin-transform-react-constant-elements@^7.12.1": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.16.7.tgz#19e9e4c2df2f6c3e6b3aea11778297d81db8df62" - integrity sha512-lF+cfsyTgwWkcw715J88JhMYJ5GpysYNLhLP1PkvkhTRN7B3e74R/1KsDxFxhRpSn0UUD3IWM4GvdBR2PEbbQQ== + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.25.7.tgz#b7f18dcdfac137a635a3f1242ea7c931df82a666" + integrity sha512-/qXt69Em8HgsjCLu7G3zdIQn7A2QwmYND7Wa0LTp09Na+Zn8L5d0A7wSXrKi18TJRc/Q5S1i1De/SU1LzVkSvA== dependencies: - "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-react-display-name@^7.16.0", "@babel/plugin-transform-react-display-name@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.16.7.tgz#7b6d40d232f4c0f550ea348593db3b21e2404340" - integrity sha512-qgIg8BcZgd0G/Cz916D5+9kqX0c7nPZyXaP8R2tLNN5tkyIZdG5fEwBrxwplzSnjC1jvQmyMNVwUCZPcbGY7Pg== +"@babel/plugin-transform-react-display-name@^7.16.0", "@babel/plugin-transform-react-display-name@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.25.7.tgz#2753e875a1b702fb1d806c4f5d4c194d64cadd88" + integrity sha512-r0QY7NVU8OnrwE+w2IWiRom0wwsTbjx4+xH2RTd7AVdof3uurXOF+/mXHQDRk+2jIvWgSaCHKMgggfvM4dyUGA== dependencies: - "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-react-jsx-development@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.16.7.tgz#43a00724a3ed2557ed3f276a01a929e6686ac7b8" - integrity sha512-RMvQWvpla+xy6MlBpPlrKZCMRs2AGiHOGHY3xRwl0pEeim348dDyxeH4xBsMPbIMhujeq7ihE702eM2Ew0Wo+A== +"@babel/plugin-transform-react-jsx-development@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.25.7.tgz#2fbd77887b8fa2942d7cb61edf1029ea1b048554" + integrity sha512-5yd3lH1PWxzW6IZj+p+Y4OLQzz0/LzlOG8vGqonHfVR3euf1vyzyMUJk9Ac+m97BH46mFc/98t9PmYLyvgL3qg== dependencies: - "@babel/plugin-transform-react-jsx" "^7.16.7" + "@babel/plugin-transform-react-jsx" "^7.25.7" -"@babel/plugin-transform-react-jsx@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.16.7.tgz#86a6a220552afd0e4e1f0388a68a372be7add0d4" - integrity sha512-8D16ye66fxiE8m890w0BpPpngG9o9OVBBy0gH2E+2AR7qMR2ZpTYJEqLxAsoroenMId0p/wMW+Blc0meDgu0Ag== +"@babel/plugin-transform-react-jsx@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.25.7.tgz#f5e2af6020a562fe048dd343e571c4428e6c5632" + integrity sha512-vILAg5nwGlR9EXE8JIOX4NHXd49lrYbN8hnjffDtoULwpL9hUx/N55nqh2qd0q6FyNDfjl9V79ecKGvFbcSA0Q== dependencies: - "@babel/helper-annotate-as-pure" "^7.16.7" - "@babel/helper-module-imports" "^7.16.7" - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/plugin-syntax-jsx" "^7.16.7" - "@babel/types" "^7.16.7" + "@babel/helper-annotate-as-pure" "^7.25.7" + "@babel/helper-module-imports" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/plugin-syntax-jsx" "^7.25.7" + "@babel/types" "^7.25.7" -"@babel/plugin-transform-react-pure-annotations@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.16.7.tgz#232bfd2f12eb551d6d7d01d13fe3f86b45eb9c67" - integrity sha512-hs71ToC97k3QWxswh2ElzMFABXHvGiJ01IB1TbYQDGeWRKWz/MPUTh5jGExdHvosYKpnJW5Pm3S4+TA3FyX+GA== +"@babel/plugin-transform-react-pure-annotations@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.25.7.tgz#6d0b8dadb2d3c5cbb8ade68c5efd49470b0d65f7" + integrity sha512-6YTHJ7yjjgYqGc8S+CbEXhLICODk0Tn92j+vNJo07HFk9t3bjFgAKxPLFhHwF2NjmQVSI1zBRfBWUeVBa2osfA== dependencies: - "@babel/helper-annotate-as-pure" "^7.16.7" - "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-annotate-as-pure" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-regenerator@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.16.7.tgz#9e7576dc476cb89ccc5096fff7af659243b4adeb" - integrity sha512-mF7jOgGYCkSJagJ6XCujSQg+6xC1M77/03K2oBmVJWoFGNUtnVJO4WHKJk3dnPC8HCcj4xBQP1Egm8DWh3Pb3Q== +"@babel/plugin-transform-regenerator@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.7.tgz#6eb006e6d26f627bc2f7844a9f19770721ad6f3e" + integrity sha512-mgDoQCRjrY3XK95UuV60tZlFCQGXEtMg8H+IsW72ldw1ih1jZhzYXbJvghmAEpg5UVhhnCeia1CkGttUvCkiMQ== dependencies: - regenerator-transform "^0.14.2" + "@babel/helper-plugin-utils" "^7.25.7" + regenerator-transform "^0.15.2" -"@babel/plugin-transform-reserved-words@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.16.7.tgz#1d798e078f7c5958eec952059c460b220a63f586" - integrity sha512-KQzzDnZ9hWQBjwi5lpY5v9shmm6IVG0U9pB18zvMu2i4H90xpT4gmqwPYsn8rObiadYe2M0gmgsiOIF5A/2rtg== +"@babel/plugin-transform-reserved-words@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.7.tgz#dc56b25e02afaabef3ce0c5b06b0916e8523e995" + integrity sha512-3OfyfRRqiGeOvIWSagcwUTVk2hXBsr/ww7bLn6TRTuXnexA+Udov2icFOxFX9abaj4l96ooYkcNN1qi2Zvqwng== dependencies: - "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-plugin-utils" "^7.25.7" "@babel/plugin-transform-runtime@^7.16.4": - version "7.16.10" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.16.10.tgz#53d9fd3496daedce1dd99639097fa5d14f4c7c2c" - integrity sha512-9nwTiqETv2G7xI4RvXHNfpGdr8pAA+Q/YtN3yLK7OoK7n9OibVm/xymJ838a9A6E/IciOLPj82lZk0fW6O4O7w== + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.25.7.tgz#435a4fab67273f00047dc806e05069c9c6344e12" + integrity sha512-Y9p487tyTzB0yDYQOtWnC+9HGOuogtP3/wNpun1xJXEEvI6vip59BSBTsHnekZLqxmPcgsrAKt46HAAb//xGhg== dependencies: - "@babel/helper-module-imports" "^7.16.7" - "@babel/helper-plugin-utils" "^7.16.7" - babel-plugin-polyfill-corejs2 "^0.3.0" - babel-plugin-polyfill-corejs3 "^0.5.0" - babel-plugin-polyfill-regenerator "^0.3.0" - semver "^6.3.0" + "@babel/helper-module-imports" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" + babel-plugin-polyfill-corejs2 "^0.4.10" + babel-plugin-polyfill-corejs3 "^0.10.6" + babel-plugin-polyfill-regenerator "^0.6.1" + semver "^6.3.1" -"@babel/plugin-transform-shorthand-properties@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.16.7.tgz#e8549ae4afcf8382f711794c0c7b6b934c5fbd2a" - integrity sha512-hah2+FEnoRoATdIb05IOXf+4GzXYTq75TVhIn1PewihbpyrNWUt2JbudKQOETWw6QpLe+AIUpJ5MVLYTQbeeUg== +"@babel/plugin-transform-shorthand-properties@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.7.tgz#92690a9c671915602d91533c278cc8f6bf12275f" + integrity sha512-uBbxNwimHi5Bv3hUccmOFlUy3ATO6WagTApenHz9KzoIdn0XeACdB12ZJ4cjhuB2WSi80Ez2FWzJnarccriJeA== dependencies: - "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-spread@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.16.7.tgz#a303e2122f9f12e0105daeedd0f30fb197d8ff44" - integrity sha512-+pjJpgAngb53L0iaA5gU/1MLXJIfXcYepLgXB3esVRf4fqmj8f2cxM3/FKaHsZms08hFQJkFccEWuIpm429TXg== +"@babel/plugin-transform-spread@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.7.tgz#df83e899a9fc66284ee601a7b738568435b92998" + integrity sha512-Mm6aeymI0PBh44xNIv/qvo8nmbkpZze1KvR8MkEqbIREDxoiWTi18Zr2jryfRMwDfVZF9foKh060fWgni44luw== dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.16.0" + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.7" -"@babel/plugin-transform-sticky-regex@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.16.7.tgz#c84741d4f4a38072b9a1e2e3fd56d359552e8660" - integrity sha512-NJa0Bd/87QV5NZZzTuZG5BPJjLYadeSZ9fO6oOUoL4iQx+9EEuw/eEM92SrsT19Yc2jgB1u1hsjqDtH02c3Drw== +"@babel/plugin-transform-sticky-regex@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.7.tgz#341c7002bef7f29037be7fb9684e374442dd0d17" + integrity sha512-ZFAeNkpGuLnAQ/NCsXJ6xik7Id+tHuS+NT+ue/2+rn/31zcdnupCdmunOizEaP0JsUmTFSTOPoQY7PkK2pttXw== dependencies: - "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-template-literals@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.16.7.tgz#f3d1c45d28967c8e80f53666fc9c3e50618217ab" - integrity sha512-VwbkDDUeenlIjmfNeDX/V0aWrQH2QiVyJtwymVQSzItFDTpxfyJh3EVaQiS0rIN/CqbLGr0VcGmuwyTdZtdIsA== +"@babel/plugin-transform-template-literals@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.7.tgz#e566c581bb16d8541dd8701093bb3457adfce16b" + integrity sha512-SI274k0nUsFFmyQupiO7+wKATAmMFf8iFgq2O+vVFXZ0SV9lNfT1NGzBEhjquFmD8I9sqHLguH+gZVN3vww2AA== dependencies: - "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-typeof-symbol@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.16.7.tgz#9cdbe622582c21368bd482b660ba87d5545d4f7e" - integrity sha512-p2rOixCKRJzpg9JB4gjnG4gjWkWa89ZoYUnl9snJ1cWIcTH/hvxZqfO+WjG6T8DRBpctEol5jw1O5rA8gkCokQ== +"@babel/plugin-transform-typeof-symbol@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.7.tgz#debb1287182efd20488f126be343328c679b66eb" + integrity sha512-OmWmQtTHnO8RSUbL0NTdtpbZHeNTnm68Gj5pA4Y2blFNh+V4iZR68V1qL9cI37J21ZN7AaCnkfdHtLExQPf2uA== dependencies: - "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-typescript@^7.16.7": - version "7.16.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.16.8.tgz#591ce9b6b83504903fa9dd3652c357c2ba7a1ee0" - integrity sha512-bHdQ9k7YpBDO2d0NVfkj51DpQcvwIzIusJ7mEUaMlbZq3Kt/U47j24inXZHQ5MDiYpCs+oZiwnXyKedE8+q7AQ== +"@babel/plugin-transform-typescript@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.25.7.tgz#8fc7c3d28ddd36bce45b9b48594129d0e560cfbe" + integrity sha512-VKlgy2vBzj8AmEzunocMun2fF06bsSWV+FvVXohtL6FGve/+L217qhHxRTVGHEDO/YR8IANcjzgJsd04J8ge5Q== dependencies: - "@babel/helper-create-class-features-plugin" "^7.16.7" - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/plugin-syntax-typescript" "^7.16.7" + "@babel/helper-annotate-as-pure" "^7.25.7" + "@babel/helper-create-class-features-plugin" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.7" + "@babel/plugin-syntax-typescript" "^7.25.7" -"@babel/plugin-transform-unicode-escapes@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.16.7.tgz#da8717de7b3287a2c6d659750c964f302b31ece3" - integrity sha512-TAV5IGahIz3yZ9/Hfv35TV2xEm+kaBDaZQCn2S/hG9/CZ0DktxJv9eKfPc7yYCvOYR4JGx1h8C+jcSOvgaaI/Q== +"@babel/plugin-transform-unicode-escapes@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.7.tgz#973592b6d13a914794e1de8cf1383e50e0f87f81" + integrity sha512-BN87D7KpbdiABA+t3HbVqHzKWUDN3dymLaTnPFAMyc8lV+KN3+YzNhVRNdinaCPA4AUqx7ubXbQ9shRjYBl3SQ== dependencies: - "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-transform-unicode-regex@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.16.7.tgz#0f7aa4a501198976e25e82702574c34cfebe9ef2" - integrity sha512-oC5tYYKw56HO75KZVLQ+R/Nl3Hro9kf8iG0hXoaHP7tjAyCpvqBiSNe6vGrZni1Z6MggmUOC6A7VP7AVmw225Q== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.16.7" - "@babel/helper-plugin-utils" "^7.16.7" - -"@babel/preset-env@^7.11.0", "@babel/preset-env@^7.12.1", "@babel/preset-env@^7.16.11", "@babel/preset-env@^7.16.4": - version "7.16.11" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.16.11.tgz#5dd88fd885fae36f88fd7c8342475c9f0abe2982" - integrity sha512-qcmWG8R7ZW6WBRPZK//y+E3Cli151B20W1Rv7ln27vuPaXU/8TKms6jFdiJtF7UDTxcrb7mZd88tAeK9LjdT8g== - dependencies: - "@babel/compat-data" "^7.16.8" - "@babel/helper-compilation-targets" "^7.16.7" - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/helper-validator-option" "^7.16.7" - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.16.7" - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.16.7" - "@babel/plugin-proposal-async-generator-functions" "^7.16.8" - "@babel/plugin-proposal-class-properties" "^7.16.7" - "@babel/plugin-proposal-class-static-block" "^7.16.7" - "@babel/plugin-proposal-dynamic-import" "^7.16.7" - "@babel/plugin-proposal-export-namespace-from" "^7.16.7" - "@babel/plugin-proposal-json-strings" "^7.16.7" - "@babel/plugin-proposal-logical-assignment-operators" "^7.16.7" - "@babel/plugin-proposal-nullish-coalescing-operator" "^7.16.7" - "@babel/plugin-proposal-numeric-separator" "^7.16.7" - "@babel/plugin-proposal-object-rest-spread" "^7.16.7" - "@babel/plugin-proposal-optional-catch-binding" "^7.16.7" - "@babel/plugin-proposal-optional-chaining" "^7.16.7" - "@babel/plugin-proposal-private-methods" "^7.16.11" - "@babel/plugin-proposal-private-property-in-object" "^7.16.7" - "@babel/plugin-proposal-unicode-property-regex" "^7.16.7" - "@babel/plugin-syntax-async-generators" "^7.8.4" - "@babel/plugin-syntax-class-properties" "^7.12.13" - "@babel/plugin-syntax-class-static-block" "^7.14.5" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" - "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - "@babel/plugin-syntax-json-strings" "^7.8.3" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - "@babel/plugin-syntax-private-property-in-object" "^7.14.5" - "@babel/plugin-syntax-top-level-await" "^7.14.5" - "@babel/plugin-transform-arrow-functions" "^7.16.7" - "@babel/plugin-transform-async-to-generator" "^7.16.8" - "@babel/plugin-transform-block-scoped-functions" "^7.16.7" - "@babel/plugin-transform-block-scoping" "^7.16.7" - "@babel/plugin-transform-classes" "^7.16.7" - "@babel/plugin-transform-computed-properties" "^7.16.7" - "@babel/plugin-transform-destructuring" "^7.16.7" - "@babel/plugin-transform-dotall-regex" "^7.16.7" - "@babel/plugin-transform-duplicate-keys" "^7.16.7" - "@babel/plugin-transform-exponentiation-operator" "^7.16.7" - "@babel/plugin-transform-for-of" "^7.16.7" - "@babel/plugin-transform-function-name" "^7.16.7" - "@babel/plugin-transform-literals" "^7.16.7" - "@babel/plugin-transform-member-expression-literals" "^7.16.7" - "@babel/plugin-transform-modules-amd" "^7.16.7" - "@babel/plugin-transform-modules-commonjs" "^7.16.8" - "@babel/plugin-transform-modules-systemjs" "^7.16.7" - "@babel/plugin-transform-modules-umd" "^7.16.7" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.16.8" - "@babel/plugin-transform-new-target" "^7.16.7" - "@babel/plugin-transform-object-super" "^7.16.7" - "@babel/plugin-transform-parameters" "^7.16.7" - "@babel/plugin-transform-property-literals" "^7.16.7" - "@babel/plugin-transform-regenerator" "^7.16.7" - "@babel/plugin-transform-reserved-words" "^7.16.7" - "@babel/plugin-transform-shorthand-properties" "^7.16.7" - "@babel/plugin-transform-spread" "^7.16.7" - "@babel/plugin-transform-sticky-regex" "^7.16.7" - "@babel/plugin-transform-template-literals" "^7.16.7" - "@babel/plugin-transform-typeof-symbol" "^7.16.7" - "@babel/plugin-transform-unicode-escapes" "^7.16.7" - "@babel/plugin-transform-unicode-regex" "^7.16.7" - "@babel/preset-modules" "^0.1.5" - "@babel/types" "^7.16.8" - babel-plugin-polyfill-corejs2 "^0.3.0" - babel-plugin-polyfill-corejs3 "^0.5.0" - babel-plugin-polyfill-regenerator "^0.3.0" - core-js-compat "^3.20.2" - semver "^6.3.0" +"@babel/plugin-transform-unicode-property-regex@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.7.tgz#25349197cce964b1343f74fa7cfdf791a1b1919e" + integrity sha512-IWfR89zcEPQGB/iB408uGtSPlQd3Jpq11Im86vUgcmSTcoWAiQMCTOa2K2yNNqFJEBVICKhayctee65Ka8OB0w== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" -"@babel/preset-modules@^0.1.5": - version "0.1.5" - resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.5.tgz#ef939d6e7f268827e1841638dc6ff95515e115d9" - integrity sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA== +"@babel/plugin-transform-unicode-regex@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.7.tgz#f93a93441baf61f713b6d5552aaa856bfab34809" + integrity sha512-8JKfg/hiuA3qXnlLx8qtv5HWRbgyFx2hMMtpDDuU2rTckpKkGu4ycK5yYHwuEa16/quXfoxHBIApEsNyMWnt0g== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" + +"@babel/plugin-transform-unicode-sets-regex@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.7.tgz#d1b3295d29e0f8f4df76abc909ad1ebee919560c" + integrity sha512-YRW8o9vzImwmh4Q3Rffd09bH5/hvY0pxg+1H1i0f7APoUeg12G7+HhLj9ZFNIrYkgBXhIijPJ+IXypN0hLTIbw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" + +"@babel/preset-env@^7.11.0", "@babel/preset-env@^7.12.1", "@babel/preset-env@^7.16.4", "@babel/preset-env@^7.25.8": + version "7.25.8" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.25.8.tgz#dc6b719627fb29cd9cccbbbe041802fd575b524c" + integrity sha512-58T2yulDHMN8YMUxiLq5YmWUnlDCyY1FsHM+v12VMx+1/FlrUj5tY50iDCpofFQEM8fMYOaY9YRvym2jcjn1Dg== + dependencies: + "@babel/compat-data" "^7.25.8" + "@babel/helper-compilation-targets" "^7.25.7" + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-validator-option" "^7.25.7" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.25.7" + "@babel/plugin-bugfix-safari-class-field-initializer-scope" "^7.25.7" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.25.7" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.25.7" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.25.7" + "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" + "@babel/plugin-syntax-import-assertions" "^7.25.7" + "@babel/plugin-syntax-import-attributes" "^7.25.7" + "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" + "@babel/plugin-transform-arrow-functions" "^7.25.7" + "@babel/plugin-transform-async-generator-functions" "^7.25.8" + "@babel/plugin-transform-async-to-generator" "^7.25.7" + "@babel/plugin-transform-block-scoped-functions" "^7.25.7" + "@babel/plugin-transform-block-scoping" "^7.25.7" + "@babel/plugin-transform-class-properties" "^7.25.7" + "@babel/plugin-transform-class-static-block" "^7.25.8" + "@babel/plugin-transform-classes" "^7.25.7" + "@babel/plugin-transform-computed-properties" "^7.25.7" + "@babel/plugin-transform-destructuring" "^7.25.7" + "@babel/plugin-transform-dotall-regex" "^7.25.7" + "@babel/plugin-transform-duplicate-keys" "^7.25.7" + "@babel/plugin-transform-duplicate-named-capturing-groups-regex" "^7.25.7" + "@babel/plugin-transform-dynamic-import" "^7.25.8" + "@babel/plugin-transform-exponentiation-operator" "^7.25.7" + "@babel/plugin-transform-export-namespace-from" "^7.25.8" + "@babel/plugin-transform-for-of" "^7.25.7" + "@babel/plugin-transform-function-name" "^7.25.7" + "@babel/plugin-transform-json-strings" "^7.25.8" + "@babel/plugin-transform-literals" "^7.25.7" + "@babel/plugin-transform-logical-assignment-operators" "^7.25.8" + "@babel/plugin-transform-member-expression-literals" "^7.25.7" + "@babel/plugin-transform-modules-amd" "^7.25.7" + "@babel/plugin-transform-modules-commonjs" "^7.25.7" + "@babel/plugin-transform-modules-systemjs" "^7.25.7" + "@babel/plugin-transform-modules-umd" "^7.25.7" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.25.7" + "@babel/plugin-transform-new-target" "^7.25.7" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.25.8" + "@babel/plugin-transform-numeric-separator" "^7.25.8" + "@babel/plugin-transform-object-rest-spread" "^7.25.8" + "@babel/plugin-transform-object-super" "^7.25.7" + "@babel/plugin-transform-optional-catch-binding" "^7.25.8" + "@babel/plugin-transform-optional-chaining" "^7.25.8" + "@babel/plugin-transform-parameters" "^7.25.7" + "@babel/plugin-transform-private-methods" "^7.25.7" + "@babel/plugin-transform-private-property-in-object" "^7.25.8" + "@babel/plugin-transform-property-literals" "^7.25.7" + "@babel/plugin-transform-regenerator" "^7.25.7" + "@babel/plugin-transform-reserved-words" "^7.25.7" + "@babel/plugin-transform-shorthand-properties" "^7.25.7" + "@babel/plugin-transform-spread" "^7.25.7" + "@babel/plugin-transform-sticky-regex" "^7.25.7" + "@babel/plugin-transform-template-literals" "^7.25.7" + "@babel/plugin-transform-typeof-symbol" "^7.25.7" + "@babel/plugin-transform-unicode-escapes" "^7.25.7" + "@babel/plugin-transform-unicode-property-regex" "^7.25.7" + "@babel/plugin-transform-unicode-regex" "^7.25.7" + "@babel/plugin-transform-unicode-sets-regex" "^7.25.7" + "@babel/preset-modules" "0.1.6-no-external-plugins" + babel-plugin-polyfill-corejs2 "^0.4.10" + babel-plugin-polyfill-corejs3 "^0.10.6" + babel-plugin-polyfill-regenerator "^0.6.1" + core-js-compat "^3.38.1" + semver "^6.3.1" + +"@babel/preset-modules@0.1.6-no-external-plugins": + version "0.1.6-no-external-plugins" + resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz#ccb88a2c49c817236861fee7826080573b8a923a" + integrity sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" - "@babel/plugin-transform-dotall-regex" "^7.4.4" "@babel/types" "^7.4.4" esutils "^2.0.2" -"@babel/preset-react@^7.12.5", "@babel/preset-react@^7.16.0", "@babel/preset-react@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.16.7.tgz#4c18150491edc69c183ff818f9f2aecbe5d93852" - integrity sha512-fWpyI8UM/HE6DfPBzD8LnhQ/OcH8AgTaqcqP2nGOXEUV+VKBR5JRN9hCk9ai+zQQ57vtm9oWeXguBCPNUjytgA== +"@babel/preset-react@^7.12.5", "@babel/preset-react@^7.16.0", "@babel/preset-react@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.25.7.tgz#081cbe1dea363b732764d06a0fdda67ffa17735d" + integrity sha512-GjV0/mUEEXpi1U5ZgDprMRRgajGMRW3G5FjMr5KLKD8nT2fTG8+h/klV3+6Dm5739QE+K5+2e91qFKAYI3pmRg== dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/helper-validator-option" "^7.16.7" - "@babel/plugin-transform-react-display-name" "^7.16.7" - "@babel/plugin-transform-react-jsx" "^7.16.7" - "@babel/plugin-transform-react-jsx-development" "^7.16.7" - "@babel/plugin-transform-react-pure-annotations" "^7.16.7" + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-validator-option" "^7.25.7" + "@babel/plugin-transform-react-display-name" "^7.25.7" + "@babel/plugin-transform-react-jsx" "^7.25.7" + "@babel/plugin-transform-react-jsx-development" "^7.25.7" + "@babel/plugin-transform-react-pure-annotations" "^7.25.7" "@babel/preset-typescript@^7.16.0": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.16.7.tgz#ab114d68bb2020afc069cd51b37ff98a046a70b9" - integrity sha512-WbVEmgXdIyvzB77AQjGBEyYPZx+8tTsO50XtfozQrkW8QB2rLJpH2lgx0TRw5EJrBxOZQ+wCcyPVQvS8tjEHpQ== - dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/helper-validator-option" "^7.16.7" - "@babel/plugin-transform-typescript" "^7.16.7" - -"@babel/runtime-corejs3@^7.10.2": - version "7.16.8" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.16.8.tgz#ea533d96eda6fdc76b1812248e9fbd0c11d4a1a7" - integrity sha512-3fKhuICS1lMz0plI5ktOE/yEtBRMVxplzRkdn6mJQ197XiY0JnrzYV0+Mxozq3JZ8SBV9Ecurmw1XsGbwOf+Sg== + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.25.7.tgz#43c5b68eccb856ae5b52274b77b1c3c413cde1b7" + integrity sha512-rkkpaXJZOFN45Fb+Gki0c+KMIglk4+zZXOoMJuyEK8y8Kkc8Jd3BDmP7qPsz0zQMJj+UD7EprF+AqAXcILnexw== dependencies: - core-js-pure "^3.20.2" - regenerator-runtime "^0.13.4" + "@babel/helper-plugin-utils" "^7.25.7" + "@babel/helper-validator-option" "^7.25.7" + "@babel/plugin-syntax-jsx" "^7.25.7" + "@babel/plugin-transform-modules-commonjs" "^7.25.7" + "@babel/plugin-transform-typescript" "^7.25.7" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.16.3", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.13", "@babel/runtime@^7.7.6", "@babel/runtime@^7.9.2": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.7.tgz#03ff99f64106588c9c403c6ecb8c3bafbbdff1fa" integrity sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ== @@ -1159,6 +1086,13 @@ dependencies: regenerator-runtime "^0.13.11" +"@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.23.8", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.7.tgz#7ffb53c37a8f247c8c4d335e89cdf16a2e0d0fb6" + integrity sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/runtime@^7.12.1", "@babel/runtime@^7.18.3", "@babel/runtime@^7.24.1": version "7.25.6" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.6.tgz#9afc3289f7184d8d7f98b099884c26317b9264d2" @@ -1166,70 +1100,35 @@ dependencies: regenerator-runtime "^0.14.0" -"@babel/runtime@^7.12.5", "@babel/runtime@^7.23.8", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2": +"@babel/template@^7.25.7", "@babel/template@^7.3.3": version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.7.tgz#7ffb53c37a8f247c8c4d335e89cdf16a2e0d0fb6" - integrity sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w== + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.7.tgz#27f69ce382855d915b14ab0fe5fb4cbf88fa0769" + integrity sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA== dependencies: - regenerator-runtime "^0.14.0" + "@babel/code-frame" "^7.25.7" + "@babel/parser" "^7.25.7" + "@babel/types" "^7.25.7" -"@babel/template@^7.16.7", "@babel/template@^7.3.3": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155" - integrity sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w== - dependencies: - "@babel/code-frame" "^7.16.7" - "@babel/parser" "^7.16.7" - "@babel/types" "^7.16.7" - -"@babel/template@^7.22.15": - version "7.22.15" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" - integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w== - dependencies: - "@babel/code-frame" "^7.22.13" - "@babel/parser" "^7.22.15" - "@babel/types" "^7.22.15" - -"@babel/traverse@^7.13.0", "@babel/traverse@^7.16.10", "@babel/traverse@^7.16.7", "@babel/traverse@^7.16.8", "@babel/traverse@^7.17.0", "@babel/traverse@^7.17.3", "@babel/traverse@^7.7.2": - version "7.23.2" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8" - integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw== - dependencies: - "@babel/code-frame" "^7.22.13" - "@babel/generator" "^7.23.0" - "@babel/helper-environment-visitor" "^7.22.20" - "@babel/helper-function-name" "^7.23.0" - "@babel/helper-hoist-variables" "^7.22.5" - "@babel/helper-split-export-declaration" "^7.22.6" - "@babel/parser" "^7.23.0" - "@babel/types" "^7.23.0" - debug "^4.1.0" +"@babel/traverse@^7.25.7", "@babel/traverse@^7.7.2": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.7.tgz#83e367619be1cab8e4f2892ef30ba04c26a40fa8" + integrity sha512-jatJPT1Zjqvh/1FyJs6qAHL+Dzb7sTb+xr7Q+gM1b+1oBsMsQQ4FkVKb6dFlJvLlVssqkRzV05Jzervt9yhnzg== + dependencies: + "@babel/code-frame" "^7.25.7" + "@babel/generator" "^7.25.7" + "@babel/parser" "^7.25.7" + "@babel/template" "^7.25.7" + "@babel/types" "^7.25.7" + debug "^4.3.1" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.12.6", "@babel/types@^7.16.0", "@babel/types@^7.16.7", "@babel/types@^7.16.8", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": - version "7.16.8" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.16.8.tgz#0ba5da91dd71e0a4e7781a30f22770831062e3c1" - integrity sha512-smN2DQc5s4M7fntyjGtyIPbRJv6wW4rU/94fmYJ7PKQuZkC0qGMHXJbg6sNGt12JmVr4k5YaptI/XtiLJBnmIg== - dependencies: - "@babel/helper-validator-identifier" "^7.16.7" - to-fast-properties "^2.0.0" - -"@babel/types@^7.17.0": - version "7.17.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.17.0.tgz#a826e368bccb6b3d84acd76acad5c0d87342390b" - integrity sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw== +"@babel/types@^7.0.0", "@babel/types@^7.12.6", "@babel/types@^7.20.7", "@babel/types@^7.25.7", "@babel/types@^7.25.8", "@babel/types@^7.3.3", "@babel/types@^7.4.4": + version "7.25.8" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.25.8.tgz#5cf6037258e8a9bcad533f4979025140cb9993e1" + integrity sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg== dependencies: - "@babel/helper-validator-identifier" "^7.16.7" - to-fast-properties "^2.0.0" - -"@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0": - version "7.23.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb" - integrity sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg== - dependencies: - "@babel/helper-string-parser" "^7.22.5" - "@babel/helper-validator-identifier" "^7.22.20" + "@babel/helper-string-parser" "^7.25.7" + "@babel/helper-validator-identifier" "^7.25.7" to-fast-properties "^2.0.0" "@base2/pretty-print-object@1.0.1": @@ -1243,9 +1142,115 @@ integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== "@csstools/normalize.css@*": - version "12.0.0" - resolved "https://registry.yarnpkg.com/@csstools/normalize.css/-/normalize.css-12.0.0.tgz#a9583a75c3f150667771f30b60d9f059473e62c4" - integrity sha512-M0qqxAcwCsIVfpFQSlGN5XjXWu8l5JDZN+fPt1LeW5SZexQTgnaEvgXAY+CeygRw0EeppWHi12JxESWiWrB0Sg== + version "12.1.1" + resolved "https://registry.yarnpkg.com/@csstools/normalize.css/-/normalize.css-12.1.1.tgz#f0ad221b7280f3fc814689786fd9ee092776ef8f" + integrity sha512-YAYeJ+Xqh7fUou1d1j9XHl44BmsuThiTr4iNrgCQ3J27IbhXsxXDGZ1cXv8Qvs99d4rBbLiSKy3+WZiet32PcQ== + +"@csstools/postcss-cascade-layers@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-1.1.1.tgz#8a997edf97d34071dd2e37ea6022447dd9e795ad" + integrity sha512-+KdYrpKC5TgomQr2DlZF4lDEpHcoxnj5IGddYYfBWJAKfj1JtuHUIqMa+E1pJJ+z3kvDViWMqyqPlG4Ja7amQA== + dependencies: + "@csstools/selector-specificity" "^2.0.2" + postcss-selector-parser "^6.0.10" + +"@csstools/postcss-color-function@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-color-function/-/postcss-color-function-1.1.1.tgz#2bd36ab34f82d0497cfacdc9b18d34b5e6f64b6b" + integrity sha512-Bc0f62WmHdtRDjf5f3e2STwRAl89N2CLb+9iAwzrv4L2hncrbDwnQD9PCq0gtAt7pOI2leIV08HIBUd4jxD8cw== + dependencies: + "@csstools/postcss-progressive-custom-properties" "^1.1.0" + postcss-value-parser "^4.2.0" + +"@csstools/postcss-font-format-keywords@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-1.0.1.tgz#677b34e9e88ae997a67283311657973150e8b16a" + integrity sha512-ZgrlzuUAjXIOc2JueK0X5sZDjCtgimVp/O5CEqTcs5ShWBa6smhWYbS0x5cVc/+rycTDbjjzoP0KTDnUneZGOg== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-hwb-function@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@csstools/postcss-hwb-function/-/postcss-hwb-function-1.0.2.tgz#ab54a9fce0ac102c754854769962f2422ae8aa8b" + integrity sha512-YHdEru4o3Rsbjmu6vHy4UKOXZD+Rn2zmkAmLRfPet6+Jz4Ojw8cbWxe1n42VaXQhD3CQUXXTooIy8OkVbUcL+w== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-ic-unit@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-ic-unit/-/postcss-ic-unit-1.0.1.tgz#28237d812a124d1a16a5acc5c3832b040b303e58" + integrity sha512-Ot1rcwRAaRHNKC9tAqoqNZhjdYBzKk1POgWfhN4uCOE47ebGcLRqXjKkApVDpjifL6u2/55ekkpnFcp+s/OZUw== + dependencies: + "@csstools/postcss-progressive-custom-properties" "^1.1.0" + postcss-value-parser "^4.2.0" + +"@csstools/postcss-is-pseudo-class@^2.0.7": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-2.0.7.tgz#846ae6c0d5a1eaa878fce352c544f9c295509cd1" + integrity sha512-7JPeVVZHd+jxYdULl87lvjgvWldYu+Bc62s9vD/ED6/QTGjy0jy0US/f6BG53sVMTBJ1lzKZFpYmofBN9eaRiA== + dependencies: + "@csstools/selector-specificity" "^2.0.0" + postcss-selector-parser "^6.0.10" + +"@csstools/postcss-nested-calc@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-nested-calc/-/postcss-nested-calc-1.0.0.tgz#d7e9d1d0d3d15cf5ac891b16028af2a1044d0c26" + integrity sha512-JCsQsw1wjYwv1bJmgjKSoZNvf7R6+wuHDAbi5f/7MbFhl2d/+v+TvBTU4BJH3G1X1H87dHl0mh6TfYogbT/dJQ== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-normalize-display-values@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-1.0.1.tgz#15da54a36e867b3ac5163ee12c1d7f82d4d612c3" + integrity sha512-jcOanIbv55OFKQ3sYeFD/T0Ti7AMXc9nM1hZWu8m/2722gOTxFg7xYu4RDLJLeZmPUVQlGzo4jhzvTUq3x4ZUw== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-oklab-function@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-oklab-function/-/postcss-oklab-function-1.1.1.tgz#88cee0fbc8d6df27079ebd2fa016ee261eecf844" + integrity sha512-nJpJgsdA3dA9y5pgyb/UfEzE7W5Ka7u0CX0/HIMVBNWzWemdcTH3XwANECU6anWv/ao4vVNLTMxhiPNZsTK6iA== + dependencies: + "@csstools/postcss-progressive-custom-properties" "^1.1.0" + postcss-value-parser "^4.2.0" + +"@csstools/postcss-progressive-custom-properties@^1.1.0", "@csstools/postcss-progressive-custom-properties@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-1.3.0.tgz#542292558384361776b45c85226b9a3a34f276fa" + integrity sha512-ASA9W1aIy5ygskZYuWams4BzafD12ULvSypmaLJT2jvQ8G0M3I8PRQhC0h7mG0Z3LI05+agZjqSR9+K9yaQQjA== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-stepped-value-functions@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-1.0.1.tgz#f8772c3681cc2befed695e2b0b1d68e22f08c4f4" + integrity sha512-dz0LNoo3ijpTOQqEJLY8nyaapl6umbmDcgj4AD0lgVQ572b2eqA1iGZYTTWhrcrHztWDDRAX2DGYyw2VBjvCvQ== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-text-decoration-shorthand@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-1.0.0.tgz#ea96cfbc87d921eca914d3ad29340d9bcc4c953f" + integrity sha512-c1XwKJ2eMIWrzQenN0XbcfzckOLLJiczqy+YvfGmzoVXd7pT9FfObiSEfzs84bpE/VqfpEuAZ9tCRbZkZxxbdw== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-trigonometric-functions@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-1.0.2.tgz#94d3e4774c36d35dcdc88ce091336cb770d32756" + integrity sha512-woKaLO///4bb+zZC2s80l+7cm07M7268MsyG3M0ActXXEFi6SuhvriQYcb58iiKGbjwwIU7n45iRLEHypB47Og== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-unset-value@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@csstools/postcss-unset-value/-/postcss-unset-value-1.0.2.tgz#c99bb70e2cdc7312948d1eb41df2412330b81f77" + integrity sha512-c8J4roPBILnelAsdLr4XOAR/GsTm0GJi4XpcfvoWk3U6KiTCqiFYc63KhRMQQX35jYMp4Ao8Ij9+IZRgMfJp1g== + +"@csstools/selector-specificity@^2.0.0", "@csstools/selector-specificity@^2.0.2": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz#2cbcf822bf3764c9658c4d2e568bd0c0cb748016" + integrity sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw== "@elastic/datemath@^5.0.3": version "5.0.3" @@ -1409,21 +1414,38 @@ resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz#5e13fac887f08c44f76b0ccaf3370eb00fec9bb6" integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg== -"@eslint/eslintrc@^1.0.5": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.0.5.tgz#33f1b838dbf1f923bfa517e008362b78ddbbf318" - integrity sha512-BLxsnmK3KyPunz5wmCCpqy0YelEoxxGmH73Is+Z74oOTMtExcjkr3dDR6quwrjh1YspA8DH9gnX1o069KiS9AQ== +"@eslint-community/eslint-utils@^4.2.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" + integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== + dependencies: + eslint-visitor-keys "^3.3.0" + +"@eslint-community/regexpp@^4.4.0", "@eslint-community/regexpp@^4.6.1": + version "4.11.1" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.11.1.tgz#a547badfc719eb3e5f4b556325e542fbe9d7a18f" + integrity sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q== + +"@eslint/eslintrc@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" + integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== dependencies: ajv "^6.12.4" debug "^4.3.2" - espree "^9.2.0" - globals "^13.9.0" - ignore "^4.0.6" + espree "^9.6.0" + globals "^13.19.0" + ignore "^5.2.0" import-fresh "^3.2.1" js-yaml "^4.1.0" - minimatch "^3.0.4" + minimatch "^3.1.2" strip-json-comments "^3.1.1" +"@eslint/js@8.57.1": + version "8.57.1" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" + integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== + "@hello-pangea/dnd@^16.6.0": version "16.6.0" resolved "https://registry.yarnpkg.com/@hello-pangea/dnd/-/dnd-16.6.0.tgz#7509639c7bd13f55e537b65a9dcfcd54e7c99ac7" @@ -1437,19 +1459,36 @@ redux "^4.2.1" use-memo-one "^1.1.3" -"@humanwhocodes/config-array@^0.9.2": - version "0.9.3" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.9.3.tgz#f2564c744b387775b436418491f15fce6601f63e" - integrity sha512-3xSMlXHh03hCcCmFc0rbKp3Ivt2PFEJnQUJDDMTJQ2wkECZWdq4GePs2ctc5H8zV+cHPaq8k2vU8mrQjA6iHdQ== +"@humanwhocodes/config-array@^0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748" + integrity sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw== dependencies: - "@humanwhocodes/object-schema" "^1.2.1" - debug "^4.1.1" - minimatch "^3.0.4" + "@humanwhocodes/object-schema" "^2.0.3" + debug "^4.3.1" + minimatch "^3.0.5" -"@humanwhocodes/object-schema@^1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" - integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/object-schema@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" + integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== + +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" @@ -1467,168 +1506,197 @@ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== -"@jest/console@^27.4.6": - version "27.4.6" - resolved "https://registry.yarnpkg.com/@jest/console/-/console-27.4.6.tgz#0742e6787f682b22bdad56f9db2a8a77f6a86107" - integrity sha512-jauXyacQD33n47A44KrlOVeiXHEXDqapSdfb9kTekOchH/Pd18kBIO1+xxJQRLuG+LUuljFCwTG92ra4NW7SpA== +"@jest/console@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-27.5.1.tgz#260fe7239602fe5130a94f1aa386eff54b014bba" + integrity sha512-kZ/tNpS3NXn0mlXXXPNuDZnb4c0oZ20r4K5eemM2k30ZC3G0T02nXUvyhf5YdbXWHPEJLc9qGLxEZ216MdL+Zg== dependencies: - "@jest/types" "^27.4.2" + "@jest/types" "^27.5.1" "@types/node" "*" chalk "^4.0.0" - jest-message-util "^27.4.6" - jest-util "^27.4.2" + jest-message-util "^27.5.1" + jest-util "^27.5.1" slash "^3.0.0" -"@jest/core@^27.4.7": - version "27.4.7" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-27.4.7.tgz#84eabdf42a25f1fa138272ed229bcf0a1b5e6913" - integrity sha512-n181PurSJkVMS+kClIFSX/LLvw9ExSb+4IMtD6YnfxZVerw9ANYtW0bPrm0MJu2pfe9SY9FJ9FtQ+MdZkrZwjg== +"@jest/console@^28.1.3": + version "28.1.3" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-28.1.3.tgz#2030606ec03a18c31803b8a36382762e447655df" + integrity sha512-QPAkP5EwKdK/bxIr6C1I4Vs0rm2nHiANzj/Z5X2JQkrZo6IqvC4ldZ9K95tF0HdidhA8Bo6egxSzUFPYKcEXLw== dependencies: - "@jest/console" "^27.4.6" - "@jest/reporters" "^27.4.6" - "@jest/test-result" "^27.4.6" - "@jest/transform" "^27.4.6" - "@jest/types" "^27.4.2" + "@jest/types" "^28.1.3" + "@types/node" "*" + chalk "^4.0.0" + jest-message-util "^28.1.3" + jest-util "^28.1.3" + slash "^3.0.0" + +"@jest/core@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-27.5.1.tgz#267ac5f704e09dc52de2922cbf3af9edcd64b626" + integrity sha512-AK6/UTrvQD0Cd24NSqmIA6rKsu0tKIxfiCducZvqxYdmMisOYAsdItspT+fQDQYARPf8XgjAFZi0ogW2agH5nQ== + dependencies: + "@jest/console" "^27.5.1" + "@jest/reporters" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" emittery "^0.8.1" exit "^0.1.2" - graceful-fs "^4.2.4" - jest-changed-files "^27.4.2" - jest-config "^27.4.7" - jest-haste-map "^27.4.6" - jest-message-util "^27.4.6" - jest-regex-util "^27.4.0" - jest-resolve "^27.4.6" - jest-resolve-dependencies "^27.4.6" - jest-runner "^27.4.6" - jest-runtime "^27.4.6" - jest-snapshot "^27.4.6" - jest-util "^27.4.2" - jest-validate "^27.4.6" - jest-watcher "^27.4.6" + graceful-fs "^4.2.9" + jest-changed-files "^27.5.1" + jest-config "^27.5.1" + jest-haste-map "^27.5.1" + jest-message-util "^27.5.1" + jest-regex-util "^27.5.1" + jest-resolve "^27.5.1" + jest-resolve-dependencies "^27.5.1" + jest-runner "^27.5.1" + jest-runtime "^27.5.1" + jest-snapshot "^27.5.1" + jest-util "^27.5.1" + jest-validate "^27.5.1" + jest-watcher "^27.5.1" micromatch "^4.0.4" rimraf "^3.0.0" slash "^3.0.0" strip-ansi "^6.0.0" -"@jest/environment@^27.4.6": - version "27.4.6" - resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-27.4.6.tgz#1e92885d64f48c8454df35ed9779fbcf31c56d8b" - integrity sha512-E6t+RXPfATEEGVidr84WngLNWZ8ffCPky8RqqRK6u1Bn0LK92INe0MDttyPl/JOzaq92BmDzOeuqk09TvM22Sg== +"@jest/environment@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-27.5.1.tgz#d7425820511fe7158abbecc010140c3fd3be9c74" + integrity sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA== dependencies: - "@jest/fake-timers" "^27.4.6" - "@jest/types" "^27.4.2" + "@jest/fake-timers" "^27.5.1" + "@jest/types" "^27.5.1" "@types/node" "*" - jest-mock "^27.4.6" + jest-mock "^27.5.1" -"@jest/fake-timers@^27.4.6": - version "27.4.6" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-27.4.6.tgz#e026ae1671316dbd04a56945be2fa251204324e8" - integrity sha512-mfaethuYF8scV8ntPpiVGIHQgS0XIALbpY2jt2l7wb/bvq4Q5pDLk4EP4D7SAvYT1QrPOPVZAtbdGAOOyIgs7A== +"@jest/fake-timers@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-27.5.1.tgz#76979745ce0579c8a94a4678af7a748eda8ada74" + integrity sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ== dependencies: - "@jest/types" "^27.4.2" + "@jest/types" "^27.5.1" "@sinonjs/fake-timers" "^8.0.1" "@types/node" "*" - jest-message-util "^27.4.6" - jest-mock "^27.4.6" - jest-util "^27.4.2" + jest-message-util "^27.5.1" + jest-mock "^27.5.1" + jest-util "^27.5.1" -"@jest/globals@^27.4.6": - version "27.4.6" - resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-27.4.6.tgz#3f09bed64b0fd7f5f996920258bd4be8f52f060a" - integrity sha512-kAiwMGZ7UxrgPzu8Yv9uvWmXXxsy0GciNejlHvfPIfWkSxChzv6bgTS3YqBkGuHcis+ouMFI2696n2t+XYIeFw== +"@jest/globals@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-27.5.1.tgz#7ac06ce57ab966566c7963431cef458434601b2b" + integrity sha512-ZEJNB41OBQQgGzgyInAv0UUfDDj3upmHydjieSxFvTRuZElrx7tXg/uVQ5hYVEwiXs3+aMsAeEc9X7xiSKCm4Q== dependencies: - "@jest/environment" "^27.4.6" - "@jest/types" "^27.4.2" - expect "^27.4.6" + "@jest/environment" "^27.5.1" + "@jest/types" "^27.5.1" + expect "^27.5.1" -"@jest/reporters@^27.4.6": - version "27.4.6" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-27.4.6.tgz#b53dec3a93baf9b00826abf95b932de919d6d8dd" - integrity sha512-+Zo9gV81R14+PSq4wzee4GC2mhAN9i9a7qgJWL90Gpx7fHYkWpTBvwWNZUXvJByYR9tAVBdc8VxDWqfJyIUrIQ== +"@jest/reporters@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-27.5.1.tgz#ceda7be96170b03c923c37987b64015812ffec04" + integrity sha512-cPXh9hWIlVJMQkVk84aIvXuBB4uQQmFqZiacloFuGiP3ah1sbCxCosidXFDfqG8+6fO1oR2dTJTlsOy4VFmUfw== dependencies: "@bcoe/v8-coverage" "^0.2.3" - "@jest/console" "^27.4.6" - "@jest/test-result" "^27.4.6" - "@jest/transform" "^27.4.6" - "@jest/types" "^27.4.2" + "@jest/console" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" "@types/node" "*" chalk "^4.0.0" collect-v8-coverage "^1.0.0" exit "^0.1.2" glob "^7.1.2" - graceful-fs "^4.2.4" + graceful-fs "^4.2.9" istanbul-lib-coverage "^3.0.0" istanbul-lib-instrument "^5.1.0" istanbul-lib-report "^3.0.0" istanbul-lib-source-maps "^4.0.0" istanbul-reports "^3.1.3" - jest-haste-map "^27.4.6" - jest-resolve "^27.4.6" - jest-util "^27.4.2" - jest-worker "^27.4.6" + jest-haste-map "^27.5.1" + jest-resolve "^27.5.1" + jest-util "^27.5.1" + jest-worker "^27.5.1" slash "^3.0.0" source-map "^0.6.0" string-length "^4.0.1" terminal-link "^2.0.0" v8-to-istanbul "^8.1.0" -"@jest/source-map@^27.4.0": - version "27.4.0" - resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-27.4.0.tgz#2f0385d0d884fb3e2554e8f71f8fa957af9a74b6" - integrity sha512-Ntjx9jzP26Bvhbm93z/AKcPRj/9wrkI88/gK60glXDx1q+IeI0rf7Lw2c89Ch6ofonB0On/iRDreQuQ6te9pgQ== +"@jest/schemas@^28.1.3": + version "28.1.3" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-28.1.3.tgz#ad8b86a66f11f33619e3d7e1dcddd7f2d40ff905" + integrity sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg== + dependencies: + "@sinclair/typebox" "^0.24.1" + +"@jest/source-map@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-27.5.1.tgz#6608391e465add4205eae073b55e7f279e04e8cf" + integrity sha512-y9NIHUYF3PJRlHk98NdC/N1gl88BL08aQQgu4k4ZopQkCw9t9cV8mtl3TV8b/YCB8XaVTFrmUTAJvjsntDireg== dependencies: callsites "^3.0.0" - graceful-fs "^4.2.4" + graceful-fs "^4.2.9" source-map "^0.6.0" -"@jest/test-result@^27.4.6": - version "27.4.6" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-27.4.6.tgz#b3df94c3d899c040f602cea296979844f61bdf69" - integrity sha512-fi9IGj3fkOrlMmhQqa/t9xum8jaJOOAi/lZlm6JXSc55rJMXKHxNDN1oCP39B0/DhNOa2OMupF9BcKZnNtXMOQ== +"@jest/test-result@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-27.5.1.tgz#56a6585fa80f7cdab72b8c5fc2e871d03832f5bb" + integrity sha512-EW35l2RYFUcUQxFJz5Cv5MTOxlJIQs4I7gxzi2zVU7PJhOwfYq1MdC5nhSmYjX1gmMmLPvB3sIaC+BkcHRBfag== dependencies: - "@jest/console" "^27.4.6" - "@jest/types" "^27.4.2" + "@jest/console" "^27.5.1" + "@jest/types" "^27.5.1" "@types/istanbul-lib-coverage" "^2.0.0" collect-v8-coverage "^1.0.0" -"@jest/test-sequencer@^27.4.6": - version "27.4.6" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-27.4.6.tgz#447339b8a3d7b5436f50934df30854e442a9d904" - integrity sha512-3GL+nsf6E1PsyNsJuvPyIz+DwFuCtBdtvPpm/LMXVkBJbdFvQYCDpccYT56qq5BGniXWlE81n2qk1sdXfZebnw== +"@jest/test-result@^28.1.3": + version "28.1.3" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-28.1.3.tgz#5eae945fd9f4b8fcfce74d239e6f725b6bf076c5" + integrity sha512-kZAkxnSE+FqE8YjW8gNuoVkkC9I7S1qmenl8sGcDOLropASP+BkcGKwhXoyqQuGOGeYY0y/ixjrd/iERpEXHNg== dependencies: - "@jest/test-result" "^27.4.6" - graceful-fs "^4.2.4" - jest-haste-map "^27.4.6" - jest-runtime "^27.4.6" + "@jest/console" "^28.1.3" + "@jest/types" "^28.1.3" + "@types/istanbul-lib-coverage" "^2.0.0" + collect-v8-coverage "^1.0.0" -"@jest/transform@^27.4.6": - version "27.4.6" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-27.4.6.tgz#153621940b1ed500305eacdb31105d415dc30231" - integrity sha512-9MsufmJC8t5JTpWEQJ0OcOOAXaH5ioaIX6uHVBLBMoCZPfKKQF+EqP8kACAvCZ0Y1h2Zr3uOccg8re+Dr5jxyw== +"@jest/test-sequencer@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-27.5.1.tgz#4057e0e9cea4439e544c6353c6affe58d095745b" + integrity sha512-LCheJF7WB2+9JuCS7VB/EmGIdQuhtqjRNI9A43idHv3E4KltCTsPsLxvdaubFHSYwY/fNjMWjl6vNRhDiN7vpQ== + dependencies: + "@jest/test-result" "^27.5.1" + graceful-fs "^4.2.9" + jest-haste-map "^27.5.1" + jest-runtime "^27.5.1" + +"@jest/transform@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-27.5.1.tgz#6c3501dcc00c4c08915f292a600ece5ecfe1f409" + integrity sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw== dependencies: "@babel/core" "^7.1.0" - "@jest/types" "^27.4.2" + "@jest/types" "^27.5.1" babel-plugin-istanbul "^6.1.1" chalk "^4.0.0" convert-source-map "^1.4.0" fast-json-stable-stringify "^2.0.0" - graceful-fs "^4.2.4" - jest-haste-map "^27.4.6" - jest-regex-util "^27.4.0" - jest-util "^27.4.2" + graceful-fs "^4.2.9" + jest-haste-map "^27.5.1" + jest-regex-util "^27.5.1" + jest-util "^27.5.1" micromatch "^4.0.4" pirates "^4.0.4" slash "^3.0.0" source-map "^0.6.1" write-file-atomic "^3.0.0" -"@jest/types@^27.4.2": - version "27.4.2" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.4.2.tgz#96536ebd34da6392c2b7c7737d693885b5dd44a5" - integrity sha512-j35yw0PMTPpZsUoOBiuHzr1zTYoad1cVIE0ajEjcrJONxxrko/IRGKkXx3os0Nsi4Hu3+5VmDbVfq5WhG/pWAg== +"@jest/types@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.5.1.tgz#3c79ec4a8ba61c170bf937bcf9e98a9df175ec80" + integrity sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw== dependencies: "@types/istanbul-lib-coverage" "^2.0.0" "@types/istanbul-reports" "^3.0.0" @@ -1636,25 +1704,19 @@ "@types/yargs" "^16.0.0" chalk "^4.0.0" -"@jridgewell/gen-mapping@^0.3.0": - version "0.3.2" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" - integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== +"@jest/types@^28.1.3": + version "28.1.3" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-28.1.3.tgz#b05de80996ff12512bc5ceb1d208285a7d11748b" + integrity sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ== dependencies: - "@jridgewell/set-array" "^1.0.1" - "@jridgewell/sourcemap-codec" "^1.4.10" - "@jridgewell/trace-mapping" "^0.3.9" - -"@jridgewell/gen-mapping@^0.3.2": - version "0.3.3" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" - integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== - dependencies: - "@jridgewell/set-array" "^1.0.1" - "@jridgewell/sourcemap-codec" "^1.4.10" - "@jridgewell/trace-mapping" "^0.3.9" + "@jest/schemas" "^28.1.3" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" -"@jridgewell/gen-mapping@^0.3.5": +"@jridgewell/gen-mapping@^0.3.2", "@jridgewell/gen-mapping@^0.3.5": version "0.3.5" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== @@ -1663,34 +1725,16 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.24" -"@jridgewell/resolve-uri@3.1.0", "@jridgewell/resolve-uri@^3.0.3": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" - integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== - "@jridgewell/resolve-uri@^3.1.0": version "3.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== -"@jridgewell/set-array@^1.0.1": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" - integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== - "@jridgewell/set-array@^1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== -"@jridgewell/source-map@^0.3.2": - version "0.3.2" - resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb" - integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw== - dependencies: - "@jridgewell/gen-mapping" "^0.3.0" - "@jridgewell/trace-mapping" "^0.3.9" - "@jridgewell/source-map@^0.3.3": version "0.3.6" resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.6.tgz#9d71ca886e32502eb9362c9a74a46787c36df81a" @@ -1699,32 +1743,11 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.25" -"@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10": - version "1.4.14" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" - integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== - -"@jridgewell/sourcemap-codec@^1.4.14": +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": version "1.5.0" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== -"@jridgewell/trace-mapping@^0.3.0": - version "0.3.4" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.4.tgz#f6a0832dffd5b8a6aaa633b7d9f8e8e94c83a0c3" - integrity sha512-vFv9ttIedivx0ux3QSjhgtCVjPZd5l46ZOMDSCwnH1yUO2e964gO8LZGyv2QkqcgR6TnBU1v+1IFqmeoG+0UJQ== - dependencies: - "@jridgewell/resolve-uri" "^3.0.3" - "@jridgewell/sourcemap-codec" "^1.4.10" - -"@jridgewell/trace-mapping@^0.3.17": - version "0.3.18" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz#25783b2086daf6ff1dcb53c9249ae480e4dd4cd6" - integrity sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA== - dependencies: - "@jridgewell/resolve-uri" "3.1.0" - "@jridgewell/sourcemap-codec" "1.4.14" - "@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": version "0.3.25" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" @@ -1733,13 +1756,17 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@jridgewell/trace-mapping@^0.3.9": - version "0.3.14" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz#b231a081d8f66796e475ad588a1ef473112701ed" - integrity sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ== +"@jsdoc/salty@^0.2.1": + version "0.2.8" + resolved "https://registry.yarnpkg.com/@jsdoc/salty/-/salty-0.2.8.tgz#8d29923a9429694a437a50ab75004b576131d597" + integrity sha512-5e+SFVavj1ORKlKaKr2BmTOekmXbelU7dC0cDkQLqag7xfuTPuGMUFx7KWJuv4bYZrTsoL2Z18VVCOKYxzoHcg== dependencies: - "@jridgewell/resolve-uri" "^3.0.3" - "@jridgewell/sourcemap-codec" "^1.4.10" + lodash "^4.17.21" + +"@leichtgewicht/ip-codec@^2.0.1": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz#4fc56c15c580b9adb7dc3c333a134e540b44bfb1" + integrity sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw== "@mapbox/hast-util-table-cell-style@^0.2.0": version "0.2.0" @@ -1768,6 +1795,13 @@ outvariant "^1.2.0" strict-event-emitter "^0.2.0" +"@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1": + version "5.1.1-v1" + resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129" + integrity sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg== + dependencies: + eslint-scope "5.1.1" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -1781,7 +1815,7 @@ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== -"@nodelib/fs.walk@^1.2.3": +"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": version "1.2.8" resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== @@ -1794,19 +1828,22 @@ resolved "https://registry.yarnpkg.com/@open-draft/until/-/until-1.0.3.tgz#db9cc719191a62e7d9200f6e7bab21c5b848adca" integrity sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q== +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + "@pmmmwh/react-refresh-webpack-plugin@^0.5.3": - version "0.5.4" - resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.4.tgz#df0d0d855fc527db48aac93c218a0bf4ada41f99" - integrity sha512-zZbZeHQDnoTlt2AF+diQT0wsSXpvWiaIOZwBRdltNFhG1+I3ozyaw7U/nBiUwyJ0D+zwdXp0E3bWOl38Ag2BMw== + version "0.5.15" + resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.15.tgz#f126be97c30b83ed777e2aeabd518bc592e6e7c4" + integrity sha512-LFWllMA55pzB9D34w/wXUCf8+c+IYKuJDgxiZ3qMhl64KRMBHYM1I3VdGaD2BV5FNPV2/S2596bppxHbv2ZydQ== dependencies: - ansi-html-community "^0.0.8" - common-path-prefix "^3.0.0" - core-js-pure "^3.8.1" + ansi-html "^0.0.9" + core-js-pure "^3.23.3" error-stack-parser "^2.0.6" - find-up "^5.0.0" html-entities "^2.1.0" - loader-utils "^2.0.0" - schema-utils "^3.0.0" + loader-utils "^2.0.4" + schema-utils "^4.2.0" source-map "^0.7.3" "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": @@ -1862,15 +1899,7 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== -"@rollup/plugin-babel@^5.2.0": - version "5.3.0" - resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz#9cb1c5146ddd6a4968ad96f209c50c62f92f9879" - integrity sha512-9uIC8HZOnVLrLHxayq/PTzw+uS25E14KPUBh5ktF+18Mjo5yK0ToMMx6epY0uEgkjwJw0aBW4x2horYXh8juWw== - dependencies: - "@babel/helper-module-imports" "^7.10.4" - "@rollup/pluginutils" "^3.1.0" - -"@rollup/plugin-babel@^5.3.1": +"@rollup/plugin-babel@^5.2.0", "@rollup/plugin-babel@^5.3.1": version "5.3.1" resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz#04bc0608f4aa4b2e4b1aebf284344d0f68fda283" integrity sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q== @@ -1955,15 +1984,25 @@ estree-walker "^2.0.1" picomatch "^2.2.2" -"@rushstack/eslint-patch@^1.1.0": +"@rtsao/scc@^1.1.0": version "1.1.0" - resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.1.0.tgz#7f698254aadf921e48dda8c0a6b304026b8a9323" - integrity sha512-JLo+Y592QzIE+q7Dl2pMUtt4q8SKYI5jDrZxrozEQxnGVOyYE+GWK9eLkwTaeN9DDctlaRAQ3TBmzZ1qdLE30A== + resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" + integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== + +"@rushstack/eslint-patch@^1.1.0": + version "1.10.4" + resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.10.4.tgz#427d5549943a9c6fce808e39ea64dbe60d4047f1" + integrity sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA== + +"@sinclair/typebox@^0.24.1": + version "0.24.51" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.51.tgz#645f33fe4e02defe26f2f5c0410e1c094eac7f5f" + integrity sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA== "@sinonjs/commons@^1.7.0": - version "1.8.3" - resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d" - integrity sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ== + version "1.8.6" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.6.tgz#80c516a4dc264c2a69115e7578d62581ff455ed9" + integrity sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ== dependencies: type-detect "4.0.8" @@ -2142,65 +2181,65 @@ integrity sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw== "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14": - version "7.1.18" - resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.18.tgz#1a29abcc411a9c05e2094c98f9a1b7da6cdf49f8" - integrity sha512-S7unDjm/C7z2A2R9NzfKCK1I+BAALDtxEmsJBwlB3EzNfb929ykjL++1CK9LO++EIp2fQrC8O+BwjKvz6UeDyQ== + version "7.20.5" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" + integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== dependencies: - "@babel/parser" "^7.1.0" - "@babel/types" "^7.0.0" + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" "@types/babel__generator" "*" "@types/babel__template" "*" "@types/babel__traverse" "*" "@types/babel__generator@*": - version "7.6.4" - resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.4.tgz#1f20ce4c5b1990b37900b63f050182d28c2439b7" - integrity sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg== + version "7.6.8" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.8.tgz#f836c61f48b1346e7d2b0d93c6dacc5b9535d3ab" + integrity sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw== dependencies: "@babel/types" "^7.0.0" "@types/babel__template@*": - version "7.4.1" - resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.1.tgz#3d1a48fd9d6c0edfd56f2ff578daed48f36c8969" - integrity sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g== + version "7.4.4" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.4.tgz#5672513701c1b2199bc6dad636a9d7491586766f" + integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A== dependencies: "@babel/parser" "^7.1.0" "@babel/types" "^7.0.0" "@types/babel__traverse@*", "@types/babel__traverse@^7.0.4", "@types/babel__traverse@^7.0.6": - version "7.14.2" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.14.2.tgz#ffcd470bbb3f8bf30481678fb5502278ca833a43" - integrity sha512-K2waXdXBi2302XUdcHcR1jCeU0LL4TD9HRs/gk0N2Xvrht+G/BfJa4QObBQZfhMdxiCpV3COl5Nfq4uKTeTnJA== + version "7.20.6" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.20.6.tgz#8dc9f0ae0f202c08d8d4dab648912c8d6038e3f7" + integrity sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg== dependencies: - "@babel/types" "^7.3.0" + "@babel/types" "^7.20.7" "@types/body-parser@*": - version "1.19.2" - resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" - integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g== + version "1.19.5" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" + integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== dependencies: "@types/connect" "*" "@types/node" "*" "@types/bonjour@^3.5.9": - version "3.5.10" - resolved "https://registry.yarnpkg.com/@types/bonjour/-/bonjour-3.5.10.tgz#0f6aadfe00ea414edc86f5d106357cda9701e275" - integrity sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw== + version "3.5.13" + resolved "https://registry.yarnpkg.com/@types/bonjour/-/bonjour-3.5.13.tgz#adf90ce1a105e81dd1f9c61fdc5afda1bfb92956" + integrity sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ== dependencies: "@types/node" "*" "@types/connect-history-api-fallback@^1.3.5": - version "1.3.5" - resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz#d1f7a8a09d0ed5a57aee5ae9c18ab9b803205dae" - integrity sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw== + version "1.5.4" + resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz#7de71645a103056b48ac3ce07b3520b819c1d5b3" + integrity sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw== dependencies: "@types/express-serve-static-core" "*" "@types/node" "*" "@types/connect@*": - version "3.4.35" - resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" - integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== + version "3.4.38" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== dependencies: "@types/node" "*" @@ -2209,52 +2248,68 @@ resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== -"@types/eslint@^7.28.2": - version "7.29.0" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.29.0.tgz#e56ddc8e542815272720bb0b4ccc2aff9c3e1c78" - integrity sha512-VNcvioYDH8/FxaeTKkM4/TiTwt6pBV9E3OfGmvaw8tPl0rrHCJ4Ll15HRT+pMiFAf/MLQvAzC+6RzUMEL9Ceng== +"@types/eslint@^7.29.0 || ^8.4.1": + version "8.56.12" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.56.12.tgz#1657c814ffeba4d2f84c0d4ba0f44ca7ea1ca53a" + integrity sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g== dependencies: "@types/estree" "*" "@types/json-schema" "*" -"@types/estree@*": - version "0.0.50" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.50.tgz#1e0caa9364d3fccd2931c3ed96fdbeaa5d4cca83" - integrity sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw== +"@types/estree@*", "@types/estree@^1.0.5": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" + integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== "@types/estree@0.0.39": version "0.0.39" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== -"@types/estree@^1.0.5": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" - integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== +"@types/express-serve-static-core@*", "@types/express-serve-static-core@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.0.0.tgz#91f06cda1049e8f17eeab364798ed79c97488a1c" + integrity sha512-AbXMTZGt40T+KON9/Fdxx0B2WK5hsgxcfXJLr5bFpZ7b4JCex2WyQPTEKdXqfHiY5nKKBScZ7yCoO6Pvgxfvnw== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" -"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.18": - version "4.17.28" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz#c47def9f34ec81dc6328d0b1b5303d1ec98d86b8" - integrity sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig== +"@types/express-serve-static-core@^4.17.33": + version "4.19.6" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz#e01324c2a024ff367d92c66f48553ced0ab50267" + integrity sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A== dependencies: "@types/node" "*" "@types/qs" "*" "@types/range-parser" "*" + "@types/send" "*" "@types/express@*": - version "4.17.13" - resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034" - integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA== + version "5.0.0" + resolved "https://registry.yarnpkg.com/@types/express/-/express-5.0.0.tgz#13a7d1f75295e90d19ed6e74cab3678488eaa96c" + integrity sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^5.0.0" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/express@^4.17.13": + version "4.17.21" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" + integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== dependencies: "@types/body-parser" "*" - "@types/express-serve-static-core" "^4.17.18" + "@types/express-serve-static-core" "^4.17.33" "@types/qs" "*" "@types/serve-static" "*" "@types/fs-extra@^8.0.1": - version "8.1.2" - resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-8.1.2.tgz#7125cc2e4bdd9bd2fc83005ffdb1d0ba00cca61f" - integrity sha512-SvSrYXfWSc7R4eqnOzbQF4TZmfpNSM9FrSWLU3EUnWBuyZqNBOrv1B1JA3byUDPUl9z4Ab3jeZG2eDdySlgNMg== + version "8.1.5" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-8.1.5.tgz#33aae2962d3b3ec9219b5aca2555ee00274f5927" + integrity sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ== dependencies: "@types/node" "*" @@ -2267,9 +2322,9 @@ "@types/node" "*" "@types/graceful-fs@^4.1.2": - version "4.1.5" - resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15" - integrity sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw== + version "4.1.9" + resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.9.tgz#2a06bc0f68a20ab37b3e36aa238be6abdf49e8b4" + integrity sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ== dependencies: "@types/node" "*" @@ -2293,37 +2348,42 @@ resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#4fc33a00c1d0c16987b1a20cf92d20614c55ac35" integrity sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg== +"@types/http-errors@*": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" + integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== + "@types/http-proxy@^1.17.8": - version "1.17.8" - resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.8.tgz#968c66903e7e42b483608030ee85800f22d03f55" - integrity sha512-5kPLG5BKpWYkw/LVOGWpiq3nEVqxiN32rTgI53Sk12/xHFQ2rG3ehI9IO+O3W2QoKeyB92dJkoka8SUm6BX1pA== + version "1.17.15" + resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.15.tgz#12118141ce9775a6499ecb4c01d02f90fc839d36" + integrity sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ== dependencies: "@types/node" "*" "@types/inquirer@^8.1.3": - version "8.2.0" - resolved "https://registry.yarnpkg.com/@types/inquirer/-/inquirer-8.2.0.tgz#b9566d048f5ff65159f2ed97aff45fe0f00b35ec" - integrity sha512-BNoMetRf3gmkpAlV5we+kxyZTle7YibdOntIZbU5pyIfMdcwy784KfeZDAcuyMznkh5OLa17RVXZOGA5LTlkgQ== + version "8.2.10" + resolved "https://registry.yarnpkg.com/@types/inquirer/-/inquirer-8.2.10.tgz#9444dce2d764c35bc5bb4d742598aaa4acb6561b" + integrity sha512-IdD5NmHyVjWM8SHWo/kPBgtzXatwPkfwzyP3fN1jF2g9BWt5WO+8hL2F4o2GKIYsU40PpqeevuUWvkS/roXJkA== dependencies: "@types/through" "*" rxjs "^7.2.0" "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" - integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g== + version "2.0.6" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" + integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w== "@types/istanbul-lib-report@*": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686" - integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg== + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz#53047614ae72e19fc0401d872de3ae2b4ce350bf" + integrity sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA== dependencies: "@types/istanbul-lib-coverage" "*" "@types/istanbul-reports@^3.0.0": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz#9153fe98bba2bd565a63add9436d6f0d7f8468ff" - integrity sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw== + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz#0f03e3d2f670fbdac586e34b433783070cc16f54" + integrity sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ== dependencies: "@types/istanbul-lib-report" "*" @@ -2336,37 +2396,37 @@ pretty-format "^27.0.0" "@types/js-levenshtein@^1.1.0": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@types/js-levenshtein/-/js-levenshtein-1.1.1.tgz#ba05426a43f9e4e30b631941e0aa17bf0c890ed5" - integrity sha512-qC4bCqYGy1y/NP7dDVr7KJarn+PbX1nSpwA7JXdu0HxT3QYjO8MJ+cntENtHFVy2dRAyBV23OZ6MxsW1AM1L8g== + version "1.1.3" + resolved "https://registry.yarnpkg.com/@types/js-levenshtein/-/js-levenshtein-1.1.3.tgz#a6fd0bdc8255b274e5438e0bfb25f154492d1106" + integrity sha512-jd+Q+sD20Qfu9e2aEXogiO3vpOC1PYJOUdyN9gvs4Qrvkg4wF43L5OhqrPeokdv8TL0/mXoYfpkcoGZMNN2pkQ== "@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": - version "7.0.9" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" - integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" - integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= + integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@types/linkify-it@*": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.2.tgz#fd2cd2edbaa7eaac7e7f3c1748b52a19143846c9" - integrity sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA== +"@types/linkify-it@^5": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-5.0.0.tgz#21413001973106cda1c3a9b91eedd4ccd5469d76" + integrity sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q== "@types/lodash@^4.14.202": version "4.17.9" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.9.tgz#0dc4902c229f6b8e2ac5456522104d7b1a230290" integrity sha512-w9iWudx1XWOHW5lQRS9iKpK/XuRhnN+0T7HvdCCd802FYkT1AMTnxndJHGrNJwRoRHkslGr4S29tjm1cT7x/7w== -"@types/markdown-it@^12.2.3": - version "12.2.3" - resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-12.2.3.tgz#0d6f6e5e413f8daaa26522904597be3d6cd93b51" - integrity sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ== +"@types/markdown-it@^14.1.1": + version "14.1.2" + resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-14.1.2.tgz#57f2532a0800067d9b934f3521429a2e8bfb4c61" + integrity sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog== dependencies: - "@types/linkify-it" "*" - "@types/mdurl" "*" + "@types/linkify-it" "^5" + "@types/mdurl" "^2" "@types/mdast@^3.0.0": version "3.0.10" @@ -2375,22 +2435,36 @@ dependencies: "@types/unist" "*" -"@types/mdurl@*": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-1.0.2.tgz#e2ce9d83a613bacf284c7be7d491945e39e1f8e9" - integrity sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA== +"@types/mdurl@^2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-2.0.0.tgz#d43878b5b20222682163ae6f897b20447233bdfd" + integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg== "@types/mime@^1": - version "1.3.2" - resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" - integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" + integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== "@types/minimatch@*": - version "3.0.5" - resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40" - integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ== + version "5.1.2" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca" + integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA== + +"@types/node-forge@^1.3.0": + version "1.3.11" + resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.11.tgz#0972ea538ddb0f4d9c2fa0ec5db5724773a604da" + integrity sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ== + dependencies: + "@types/node" "*" + +"@types/node@*": + version "22.7.7" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.7.7.tgz#6cd9541c3dccb4f7e8b141b491443f4a1570e307" + integrity sha512-SRxCrrg9CL/y54aiMCG3edPKdprgMVGDXjA3gB8UmmBW5TcXzRUYAh8EWzTnSJFAd1rgImPELza+A3bJ+qxz8Q== + dependencies: + undici-types "~6.19.2" -"@types/node@*", "@types/node@>=13.7.0": +"@types/node@>=13.7.0": version "18.7.18" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.18.tgz#633184f55c322e4fb08612307c274ee6d5ed3154" integrity sha512-m+6nTEOadJZuTPkKR/SYK3A2d7FZrgElol9UP1Kae90VVU4a6mxnPuLiIW1m4Cq4gZ/nWb9GrdVXJCoCazDAbg== @@ -2406,9 +2480,9 @@ integrity sha512-kH8I7OSSwQu9DS9JYdFWbuvhVzvFRoCPCkGxNwoGgaPeDfEPJlcxNvEOypZhQ3XXHsGbfIuYcxcJxKUfJHnRfw== "@types/parse-json@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" - integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" + integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw== "@types/parse5@^5.0.0": version "5.0.3" @@ -2416,9 +2490,9 @@ integrity sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw== "@types/prettier@^2.1.5": - version "2.4.3" - resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.4.3.tgz#a3c65525b91fca7da00ab1a3ac2b5a2a4afbffbf" - integrity sha512-QzSuZMBuG5u8HqYz01qtMdg/Jfctlnvj1z/lYnIDXs/golxw0fxtRAHd9KrzjR7Yxz1qVeI00o0kiO3PmVdJ9w== + version "2.7.3" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.3.tgz#3e51a17e291d01d17d3fc61422015a933af7a08f" + integrity sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA== "@types/prismjs@*": version "1.26.0" @@ -2431,19 +2505,19 @@ integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== "@types/q@^1.5.1": - version "1.5.5" - resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.5.tgz#75a2a8e7d8ab4b230414505d92335d1dcb53a6df" - integrity sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ== + version "1.5.8" + resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.8.tgz#95f6c6a08f2ad868ba230ead1d2d7f7be3db3837" + integrity sha512-hroOstUScF6zhIi+5+x0dzqrHA1EJi+Irri6b1fxolMTqqHIV/Cg77EtnQcZqZCu8hR3mX2BzIxN4/GzI68Kfw== "@types/qs@*": - version "6.9.7" - resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" - integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== + version "6.9.16" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.16.tgz#52bba125a07c0482d26747d5d4947a64daf8f794" + integrity sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A== "@types/range-parser@*": - version "1.2.4" - resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" - integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== + version "1.2.7" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" + integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== "@types/react-dom@^18.3.0": version "18.3.0" @@ -2490,49 +2564,63 @@ dependencies: "@types/node" "*" -"@types/retry@^0.12.0": - version "0.12.1" - resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.1.tgz#d8f1c0d0dc23afad6dc16a9e993a0865774b4065" - integrity sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g== +"@types/retry@0.12.0": + version "0.12.0" + resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" + integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== "@types/scheduler@*": version "0.16.2" resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== +"@types/semver@^7.3.12": + version "7.5.8" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" + integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ== + +"@types/send@*": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a" + integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + "@types/serve-index@^1.9.1": - version "1.9.1" - resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.1.tgz#1b5e85370a192c01ec6cec4735cf2917337a6278" - integrity sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg== + version "1.9.4" + resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.4.tgz#e6ae13d5053cb06ed36392110b4f9a49ac4ec898" + integrity sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug== dependencies: "@types/express" "*" -"@types/serve-static@*": - version "1.13.10" - resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.10.tgz#f5e0ce8797d2d7cc5ebeda48a52c96c4fa47a8d9" - integrity sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ== +"@types/serve-static@*", "@types/serve-static@^1.13.10": + version "1.15.7" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.7.tgz#22174bbd74fb97fe303109738e9b5c2f3064f714" + integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw== dependencies: - "@types/mime" "^1" + "@types/http-errors" "*" "@types/node" "*" + "@types/send" "*" "@types/set-cookie-parser@^2.4.0": - version "2.4.2" - resolved "https://registry.yarnpkg.com/@types/set-cookie-parser/-/set-cookie-parser-2.4.2.tgz#b6a955219b54151bfebd4521170723df5e13caad" - integrity sha512-fBZgytwhYAUkj/jC/FAV4RQ5EerRup1YQsXQCh8rZfiHkc4UahC192oH0smGwsXol3cL3A5oETuAHeQHmhXM4w== + version "2.4.10" + resolved "https://registry.yarnpkg.com/@types/set-cookie-parser/-/set-cookie-parser-2.4.10.tgz#ad3a807d6d921db9720621ea3374c5d92020bcbc" + integrity sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw== dependencies: "@types/node" "*" "@types/sockjs@^0.3.33": - version "0.3.33" - resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.33.tgz#570d3a0b99ac995360e3136fd6045113b1bd236f" - integrity sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw== + version "0.3.36" + resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.36.tgz#ce322cf07bcc119d4cbf7f88954f3a3bd0f67535" + integrity sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q== dependencies: "@types/node" "*" "@types/stack-utils@^2.0.0": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" - integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" + integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== "@types/stylis@4.2.5": version "4.2.5" @@ -2540,16 +2628,16 @@ integrity sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw== "@types/through@*": - version "0.0.30" - resolved "https://registry.yarnpkg.com/@types/through/-/through-0.0.30.tgz#e0e42ce77e897bd6aead6f6ea62aeb135b8a3895" - integrity sha512-FvnCJljyxhPM3gkRgWmxmDZyAQSiBQQWLI0A0VFL0K7W1oRUrPJSqNO0NvTnLkBcotdlp3lKvaT0JrnyRDkzOg== + version "0.0.33" + resolved "https://registry.yarnpkg.com/@types/through/-/through-0.0.33.tgz#14ebf599320e1c7851e7d598149af183c6b9ea56" + integrity sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ== dependencies: "@types/node" "*" "@types/trusted-types@^2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.2.tgz#fc25ad9943bcac11cceb8168db4f275e0e72e756" - integrity sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg== + version "2.0.7" + resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11" + integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== "@types/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2", "@types/unist@^2.0.3": version "2.0.6" @@ -2561,111 +2649,127 @@ resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43" integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA== -"@types/ws@^8.2.2": - version "8.2.2" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.2.2.tgz#7c5be4decb19500ae6b3d563043cd407bf366c21" - integrity sha512-NOn5eIcgWLOo6qW8AcuLZ7G8PycXu0xTxxkS6Q18VWFxgPUSOwV0pBj2a/4viNZVu25i7RIB7GttdkAIUUXOOg== +"@types/ws@^8.5.5": + version "8.5.12" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.12.tgz#619475fe98f35ccca2a2f6c137702d85ec247b7e" + integrity sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ== dependencies: "@types/node" "*" "@types/yargs-parser@*": - version "20.2.1" - resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.1.tgz#3b9ce2489919d9e4fea439b76916abc34b2df129" - integrity sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw== + version "21.0.3" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15" + integrity sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ== "@types/yargs@^16.0.0": - version "16.0.4" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-16.0.4.tgz#26aad98dd2c2a38e421086ea9ad42b9e51642977" - integrity sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw== + version "16.0.9" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-16.0.9.tgz#ba506215e45f7707e6cbcaf386981155b7ab956e" + integrity sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA== dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@^5.5.0": - version "5.10.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.10.1.tgz#870195d0f2146b36d11fc71131b75aba52354c69" - integrity sha512-xN3CYqFlyE/qOcy978/L0xLR2HlcAGIyIK5sMOasxaaAPfQRj/MmMV6OC3I7NZO84oEUdWCOju34Z9W8E0pFDQ== +"@types/yargs@^17.0.8": + version "17.0.33" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.33.tgz#8c32303da83eec050a84b3c7ae7b9f922d13e32d" + integrity sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA== dependencies: - "@typescript-eslint/scope-manager" "5.10.1" - "@typescript-eslint/type-utils" "5.10.1" - "@typescript-eslint/utils" "5.10.1" - debug "^4.3.2" - functional-red-black-tree "^1.0.1" - ignore "^5.1.8" - regexpp "^3.2.0" - semver "^7.3.5" + "@types/yargs-parser" "*" + +"@typescript-eslint/eslint-plugin@^5.5.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz#aeef0328d172b9e37d9bab6dbc13b87ed88977db" + integrity sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag== + dependencies: + "@eslint-community/regexpp" "^4.4.0" + "@typescript-eslint/scope-manager" "5.62.0" + "@typescript-eslint/type-utils" "5.62.0" + "@typescript-eslint/utils" "5.62.0" + debug "^4.3.4" + graphemer "^1.4.0" + ignore "^5.2.0" + natural-compare-lite "^1.4.0" + semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/experimental-utils@^5.0.0", "@typescript-eslint/experimental-utils@^5.9.0": - version "5.10.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.10.1.tgz#49fa5a7800ed08ea70aef14fccb14fbae85116ab" - integrity sha512-Ryeb8nkJa/1zKl8iujNtJC8tgj6PgaY0sDUnrTqbmC70nrKKkZaHfiRDTcqICmCSCEQyLQcJAoh0AukLaIaGTw== +"@typescript-eslint/experimental-utils@^5.0.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.62.0.tgz#14559bf73383a308026b427a4a6129bae2146741" + integrity sha512-RTXpeB3eMkpoclG3ZHft6vG/Z30azNHuqY6wKPBHlVMZFuEvrtlEDe8gMqDb+SO+9hjC/pLekeSCryf9vMZlCw== dependencies: - "@typescript-eslint/utils" "5.10.1" + "@typescript-eslint/utils" "5.62.0" "@typescript-eslint/parser@^5.5.0": - version "5.10.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.10.1.tgz#4ce9633cc33fc70bc13786cb793c1a76fe5ad6bd" - integrity sha512-GReo3tjNBwR5RnRO0K2wDIDN31cM3MmDtgyQ85oAxAmC5K3j/g85IjP+cDfcqDsDDBf1HNKQAD0WqOYL8jXqUA== - dependencies: - "@typescript-eslint/scope-manager" "5.10.1" - "@typescript-eslint/types" "5.10.1" - "@typescript-eslint/typescript-estree" "5.10.1" - debug "^4.3.2" - -"@typescript-eslint/scope-manager@5.10.1": - version "5.10.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.10.1.tgz#f0539c73804d2423506db2475352a4dec36cd809" - integrity sha512-Lyvi559Gvpn94k7+ElXNMEnXu/iundV5uFmCUNnftbFrUbAJ1WBoaGgkbOBm07jVZa682oaBU37ao/NGGX4ZDg== - dependencies: - "@typescript-eslint/types" "5.10.1" - "@typescript-eslint/visitor-keys" "5.10.1" - -"@typescript-eslint/type-utils@5.10.1": - version "5.10.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.10.1.tgz#5e526c00142585e40ab1503e83f1ff608c367405" - integrity sha512-AfVJkV8uck/UIoDqhu+ptEdBoQATON9GXnhOpPLzkQRJcSChkvD//qsz9JVffl2goxX+ybs5klvacE9vmrQyCw== - dependencies: - "@typescript-eslint/utils" "5.10.1" - debug "^4.3.2" + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.62.0.tgz#1b63d082d849a2fcae8a569248fbe2ee1b8a56c7" + integrity sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA== + dependencies: + "@typescript-eslint/scope-manager" "5.62.0" + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/typescript-estree" "5.62.0" + debug "^4.3.4" + +"@typescript-eslint/scope-manager@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz#d9457ccc6a0b8d6b37d0eb252a23022478c5460c" + integrity sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w== + dependencies: + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/visitor-keys" "5.62.0" + +"@typescript-eslint/type-utils@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz#286f0389c41681376cdad96b309cedd17d70346a" + integrity sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew== + dependencies: + "@typescript-eslint/typescript-estree" "5.62.0" + "@typescript-eslint/utils" "5.62.0" + debug "^4.3.4" tsutils "^3.21.0" -"@typescript-eslint/types@5.10.1": - version "5.10.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.10.1.tgz#dca9bd4cb8c067fc85304a31f38ec4766ba2d1ea" - integrity sha512-ZvxQ2QMy49bIIBpTqFiOenucqUyjTQ0WNLhBM6X1fh1NNlYAC6Kxsx8bRTY3jdYsYg44a0Z/uEgQkohbR0H87Q== +"@typescript-eslint/types@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f" + integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== -"@typescript-eslint/typescript-estree@5.10.1": - version "5.10.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.10.1.tgz#b268e67be0553f8790ba3fe87113282977adda15" - integrity sha512-PwIGnH7jIueXv4opcwEbVGDATjGPO1dx9RkUl5LlHDSe+FXxPwFL5W/qYd5/NHr7f6lo/vvTrAzd0KlQtRusJQ== +"@typescript-eslint/typescript-estree@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b" + integrity sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA== dependencies: - "@typescript-eslint/types" "5.10.1" - "@typescript-eslint/visitor-keys" "5.10.1" - debug "^4.3.2" - globby "^11.0.4" + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/visitor-keys" "5.62.0" + debug "^4.3.4" + globby "^11.1.0" is-glob "^4.0.3" - semver "^7.3.5" + semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/utils@5.10.1": - version "5.10.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.10.1.tgz#fa682a33af47080ba2c4368ee0ad2128213a1196" - integrity sha512-RRmlITiUbLuTRtn/gcPRi4202niF+q7ylFLCKu4c+O/PcpRvZ/nAUwQ2G00bZgpWkhrNLNnvhZLbDn8Ml0qsQw== +"@typescript-eslint/utils@5.62.0", "@typescript-eslint/utils@^5.58.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.62.0.tgz#141e809c71636e4a75daa39faed2fb5f4b10df86" + integrity sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ== dependencies: + "@eslint-community/eslint-utils" "^4.2.0" "@types/json-schema" "^7.0.9" - "@typescript-eslint/scope-manager" "5.10.1" - "@typescript-eslint/types" "5.10.1" - "@typescript-eslint/typescript-estree" "5.10.1" + "@types/semver" "^7.3.12" + "@typescript-eslint/scope-manager" "5.62.0" + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/typescript-estree" "5.62.0" eslint-scope "^5.1.1" - eslint-utils "^3.0.0" + semver "^7.3.7" -"@typescript-eslint/visitor-keys@5.10.1": - version "5.10.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.10.1.tgz#29102de692f59d7d34ecc457ed59ab5fc558010b" - integrity sha512-NjQ0Xinhy9IL979tpoTRuLKxMc0zJC7QVSdeerXs2/QvOy2yRkzX5dRb10X5woNUdJgU8G3nYRDlI33sq1K4YQ== +"@typescript-eslint/visitor-keys@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz#2174011917ce582875954ffe2f6912d5931e353e" + integrity sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw== dependencies: - "@typescript-eslint/types" "5.10.1" - eslint-visitor-keys "^3.0.0" + "@typescript-eslint/types" "5.62.0" + eslint-visitor-keys "^3.3.0" + +"@ungap/structured-clone@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" + integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== "@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1": version "1.12.1" @@ -2789,9 +2893,9 @@ "@xtuc/long" "4.2.2" "@xmldom/xmldom@^0.7.2": - version "0.7.5" - resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.7.5.tgz#09fa51e356d07d0be200642b0e4f91d8e6dd408d" - integrity sha512-V3BIhmY36fXZ1OtVcI9W+FxQqxVLsPKcNjWigIaa81dLC9IolJl5Mt4Cvhmr0flUnjSpTdrbMTSbXqYqV5dT6A== + version "0.7.13" + resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.7.13.tgz#ff34942667a4e19a9f4a0996a76814daac364cf3" + integrity sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g== "@xtuc/ieee754@^1.2.0": version "1.2.0" @@ -2804,9 +2908,9 @@ integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== abab@^2.0.3, abab@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" - integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q== + version "2.0.6" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" + integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: version "1.3.8" @@ -2829,54 +2933,30 @@ acorn-import-attributes@^1.9.5: resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef" integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ== -acorn-jsx@^5.3.1, acorn-jsx@^5.3.2: +acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn-node@^1.6.1: - version "1.8.2" - resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8" - integrity sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A== - dependencies: - acorn "^7.0.0" - acorn-walk "^7.0.0" - xtend "^4.0.2" - -acorn-walk@^7.0.0, acorn-walk@^7.1.1: +acorn-walk@^7.1.1: version "7.2.0" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== -acorn@^7.0.0, acorn@^7.1.1: +acorn@^7.1.1: version "7.4.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.2.4, acorn@^8.5.0, acorn@^8.7.0: - version "8.7.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30" - integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== - -acorn@^8.7.1: - version "8.8.2" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" - integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== - -acorn@^8.8.0: - version "8.8.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" - integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w== - -acorn@^8.8.2: - version "8.12.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" - integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== +acorn@^8.2.4, acorn@^8.7.1, acorn@^8.8.2, acorn@^8.9.0: + version "8.13.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.13.0.tgz#2a30d670818ad16ddd6a35d3842dacec9e5d7ca3" + integrity sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w== address@^1.0.1, address@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/address/-/address-1.1.2.tgz#bf1116c9c758c51b7a933d296b72c221ed9428b6" - integrity sha512-aT6camzM4xEA54YVJYSqxz1kv4IHnQZRtThJJHhUMRExaU5spC7jX5ugSwTaTgJliIgs4VhZOk7htClvQ/LmRA== + version "1.2.2" + resolved "https://registry.yarnpkg.com/address/-/address-1.2.2.tgz#2b5248dac5485a6390532c6a517fda2e3faac89e" + integrity sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA== adjust-sourcemap-loader@^4.0.0: version "4.0.0" @@ -2893,14 +2973,6 @@ agent-base@6: dependencies: debug "4" -aggregate-error@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" - integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== - dependencies: - clean-stack "^2.0.0" - indent-string "^4.0.0" - ajv-formats@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" @@ -2913,14 +2985,14 @@ ajv-keywords@^3.4.1, ajv-keywords@^3.5.2: resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== -ajv-keywords@^5.0.0: +ajv-keywords@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== dependencies: fast-deep-equal "^3.1.3" -ajv@^6.10.0, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5: +ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -2930,15 +3002,15 @@ ajv@^6.10.0, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.0, ajv@^8.6.0, ajv@^8.8.0: - version "8.9.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.9.0.tgz#738019146638824dea25edcf299dcba1b0e7eb18" - integrity sha512-qOKJyNj/h+OWx7s5DePL6Zu1KeM9jPZhwBqs+7DzP6bGOvqzVCSf0xueYmVuaC/oQ/VtS2zLMLHdQFbkka+XDQ== +ajv@^8.0.0, ajv@^8.6.0, ajv@^8.9.0: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" + integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== dependencies: - fast-deep-equal "^3.1.1" + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" json-schema-traverse "^1.0.0" require-from-string "^2.0.2" - uri-js "^4.2.2" ansi-escapes@^4.2.1, ansi-escapes@^4.3.1: version "4.3.2" @@ -2952,15 +3024,20 @@ ansi-html-community@^0.0.8: resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw== +ansi-html@^0.0.9: + version "0.0.9" + resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.9.tgz#6512d02342ae2cc68131952644a129cb734cd3f0" + integrity sha512-ozbS3LuenHVxNRh/wdnN16QapUHzauqSomAl1jwwJRRsGwFwtj644lIhxfWu0Fy0acCij2+AEgHvjscq3dlVXg== + ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== ansi-regex@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" - integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== + version "6.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.1.0.tgz#95ec409c69619d6cb1b8b34f14b660ef28ebd654" + integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== ansi-styles@^3.2.1: version "3.2.1" @@ -2981,18 +3058,28 @@ ansi-styles@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== +ansi-styles@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + +any-promise@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" + integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== + anymatch@^3.0.3, anymatch@~3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" - integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== dependencies: normalize-path "^3.0.0" picomatch "^2.0.4" -arg@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.1.tgz#eb0c9a8f77786cad2af8ff2b862899842d7b6adb" - integrity sha512-e0hDa9H2Z9AwFkk2qDlwhoMYE4eToKarchkQHovNdLTCYMHZHeRjI71crOh+dio4K6u1IcwubQqo79Ga4CyAQA== +arg@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c" + integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== argparse@^1.0.7: version "1.0.10" @@ -3020,38 +3107,41 @@ aria-query@5.3.0: dependencies: dequal "^2.0.3" -aria-query@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b" - integrity sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA== - dependencies: - "@babel/runtime" "^7.10.2" - "@babel/runtime-corejs3" "^7.10.2" - aria-query@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.0.0.tgz#210c21aaf469613ee8c9a62c7f86525e058db52c" integrity sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg== +aria-query@~5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.1.3.tgz#19db27cd101152773631396f7a95a3b58c22c35e" + integrity sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ== + dependencies: + deep-equal "^2.0.5" + +array-buffer-byte-length@^1.0.0, array-buffer-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f" + integrity sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg== + dependencies: + call-bind "^1.0.5" + is-array-buffer "^3.0.4" + array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== -array-flatten@^2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099" - integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== - -array-includes@^3.1.3, array-includes@^3.1.4: - version "3.1.4" - resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.4.tgz#f5b493162c760f3539631f005ba2bb46acb45ba9" - integrity sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw== +array-includes@^3.1.6, array-includes@^3.1.8: + version "3.1.8" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.8.tgz#5e370cbe172fdd5dd6530c1d4aadda25281ba97d" + integrity sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ== dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.1" - get-intrinsic "^1.1.1" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.4" is-string "^1.0.7" array-union@^2.1.0: @@ -3059,50 +3149,107 @@ array-union@^2.1.0: resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== -array.prototype.flat@^1.2.5: +array.prototype.findlast@^1.2.5: version "1.2.5" - resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.5.tgz#07e0975d84bbc7c48cd1879d609e682598d33e13" - integrity sha512-KaYU+S+ndVqyUnignHftkwc58o3uVU1jzczILJ1tN2YaIZpFIKBiP/x/j97E5MVPsaCloPbqWLB/8qCTVvT2qg== + resolved "https://registry.yarnpkg.com/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz#3e4fbcb30a15a7f5bf64cf2faae22d139c2e4904" + integrity sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ== dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.0" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-shim-unscopables "^1.0.2" -array.prototype.flatmap@^1.2.5: +array.prototype.findlastindex@^1.2.5: version "1.2.5" - resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.2.5.tgz#908dc82d8a406930fdf38598d51e7411d18d4446" - integrity sha512-08u6rVyi1Lj7oqWbS9nUxliETrtIROT4XGTA4D/LWGten6E3ocm7cy9SIrmNHOL5XVbVuckUp3X6Xyg8/zpvHA== + resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz#8c35a755c72908719453f87145ca011e39334d0d" + integrity sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ== dependencies: - call-bind "^1.0.0" - define-properties "^1.1.3" - es-abstract "^1.19.0" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-shim-unscopables "^1.0.2" + +array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz#1476217df8cff17d72ee8f3ba06738db5b387d18" + integrity sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + +array.prototype.flatmap@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz#c9a7c6831db8e719d6ce639190146c24bbd3e527" + integrity sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + +array.prototype.reduce@^1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/array.prototype.reduce/-/array.prototype.reduce-1.0.7.tgz#6aadc2f995af29cb887eb866d981dc85ab6f7dc7" + integrity sha512-mzmiUCVwtiD4lgxYP8g7IYy8El8p2CSMePvIbTS7gchKir/L1fgJrk0yDKmAX6mnRQFKNADYIk8nNlTris5H1Q== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-array-method-boxes-properly "^1.0.0" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + is-string "^1.0.7" + +array.prototype.tosorted@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz#fe954678ff53034e717ea3352a03f0b0b86f7ffc" + integrity sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.3" + es-errors "^1.3.0" + es-shim-unscopables "^1.0.2" + +arraybuffer.prototype.slice@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz#097972f4255e41bc3425e37dc3f6421cf9aefde6" + integrity sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A== + dependencies: + array-buffer-byte-length "^1.0.1" + call-bind "^1.0.5" + define-properties "^1.2.1" + es-abstract "^1.22.3" + es-errors "^1.2.1" + get-intrinsic "^1.2.3" + is-array-buffer "^3.0.4" + is-shared-array-buffer "^1.0.2" asap@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" - integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= - -ast-types-flow@^0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad" - integrity sha1-9wtzXGvKGlycItmCw+Oef+ujva0= + integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== -async@^2.6.2: - version "2.6.4" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221" - integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA== - dependencies: - lodash "^4.17.14" +ast-types-flow@^0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.8.tgz#0a85e1c92695769ac13a428bb653e7538bea27d6" + integrity sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ== async@^3.2.3: - version "3.2.3" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9" - integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g== + version "3.2.6" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" + integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== at-least-node@^1.0.0: version "1.0.0" @@ -3114,59 +3261,59 @@ attr-accept@^2.2.2: resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b" integrity sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg== -autoprefixer@^10.4.2: - version "10.4.2" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.2.tgz#25e1df09a31a9fba5c40b578936b90d35c9d4d3b" - integrity sha512-9fOPpHKuDW1w/0EKfRmVnxTDt8166MAnLI3mgZ1JCnhNtYWxcJ6Ud5CO/AVOZi/AvFa8DY9RTy3h3+tFBlrrdQ== +autoprefixer@^10.4.13: + version "10.4.20" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.20.tgz#5caec14d43976ef42e32dcb4bd62878e96be5b3b" + integrity sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g== dependencies: - browserslist "^4.19.1" - caniuse-lite "^1.0.30001297" - fraction.js "^4.1.2" + browserslist "^4.23.3" + caniuse-lite "^1.0.30001646" + fraction.js "^4.3.7" normalize-range "^0.1.2" - picocolors "^1.0.0" + picocolors "^1.0.1" postcss-value-parser "^4.2.0" -axe-core@^4.3.5: - version "4.3.5" - resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.3.5.tgz#78d6911ba317a8262bfee292aeafcc1e04b49cc5" - integrity sha512-WKTW1+xAzhMS5dJsxWkliixlO/PqC4VhmO9T4juNYcaTg9jzWiJsou6m5pxWYGfigWbwzJWeFY6z47a+4neRXA== +available-typed-arrays@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== + dependencies: + possible-typed-array-names "^1.0.0" -axobject-query@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" - integrity sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA== +axe-core@^4.10.0: + version "4.10.1" + resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.10.1.tgz#7d2589b0183f05b0f23e55c2f4cdf97b5bdc66d9" + integrity sha512-qPC9o+kD8Tir0lzNGLeghbOrWMr3ZJpaRlCIb6Uobt/7N4FiEDvqUMnxzCHRHmg8vOg14kr5gVNyScRmbMaJ9g== -babel-jest@^27.4.2, babel-jest@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.4.6.tgz#4d024e69e241cdf4f396e453a07100f44f7ce314" - integrity sha512-qZL0JT0HS1L+lOuH+xC2DVASR3nunZi/ozGhpgauJHgmI7f8rudxf6hUjEHympdQ/J64CdKmPkgfJ+A3U6QCrg== +axobject-query@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-4.1.0.tgz#28768c76d0e3cff21bc62a9e2d0b6ac30042a1ee" + integrity sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ== + +babel-jest@^27.4.2, babel-jest@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.5.1.tgz#a1bf8d61928edfefd21da27eb86a695bfd691444" + integrity sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg== dependencies: - "@jest/transform" "^27.4.6" - "@jest/types" "^27.4.2" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" "@types/babel__core" "^7.1.14" babel-plugin-istanbul "^6.1.1" - babel-preset-jest "^27.4.0" + babel-preset-jest "^27.5.1" chalk "^4.0.0" - graceful-fs "^4.2.4" + graceful-fs "^4.2.9" slash "^3.0.0" babel-loader@^8.2.3: - version "8.2.3" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.2.3.tgz#8986b40f1a64cacfcb4b8429320085ef68b1342d" - integrity sha512-n4Zeta8NC3QAsuyiizu0GkmRcQ6clkV9WFUnUf1iXP//IeSKbWjofW3UHyZVwlOB4y039YQKefawyTn64Zwbuw== + version "8.4.1" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.4.1.tgz#6ccb75c66e62c3b144e1c5f2eaec5b8f6c08c675" + integrity sha512-nXzRChX+Z1GoE6yWavBQg6jDslyFF3SDjl2paADuoQtQW10JqShJt62R6eJQ5m/pjJFDT8xgKIWSP85OY8eXeA== dependencies: find-cache-dir "^3.3.1" - loader-utils "^1.4.0" + loader-utils "^2.0.4" make-dir "^3.1.0" schema-utils "^2.6.5" -babel-plugin-dynamic-import-node@^2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3" - integrity sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ== - dependencies: - object.assign "^4.1.0" - babel-plugin-istanbul@^6.1.1: version "6.1.1" resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" @@ -3178,10 +3325,10 @@ babel-plugin-istanbul@^6.1.1: istanbul-lib-instrument "^5.0.4" test-exclude "^6.0.0" -babel-plugin-jest-hoist@^27.4.0: - version "27.4.0" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.4.0.tgz#d7831fc0f93573788d80dee7e682482da4c730d6" - integrity sha512-Jcu7qS4OX5kTWBc45Hz7BMmgXuJqRnhatqpUhnzGC3OBYpOmf2tv6jFNwZpwM7wU7MUuv2r9IPS/ZlYOuburVw== +babel-plugin-jest-hoist@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz#9be98ecf28c331eb9f5df9c72d6f89deb8181c2e" + integrity sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ== dependencies: "@babel/template" "^7.3.3" "@babel/types" "^7.3.3" @@ -3202,29 +3349,29 @@ babel-plugin-named-asset-import@^0.3.8: resolved "https://registry.yarnpkg.com/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.8.tgz#6b7fa43c59229685368683c28bc9734f24524cc2" integrity sha512-WXiAc++qo7XcJ1ZnTYGtLxmBCVbddAml3CEXgWaBzNzLNoxtQ8AiGEFDMOhot9XjTCQbvP5E77Fj9Gk924f00Q== -babel-plugin-polyfill-corejs2@^0.3.0: - version "0.3.1" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.1.tgz#440f1b70ccfaabc6b676d196239b138f8a2cfba5" - integrity sha512-v7/T6EQcNfVLfcN2X8Lulb7DjprieyLWJK/zOWH5DUYcAgex9sP3h25Q+DLsX9TloXe3y1O8l2q2Jv9q8UVB9w== +babel-plugin-polyfill-corejs2@^0.4.10: + version "0.4.11" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz#30320dfe3ffe1a336c15afdcdafd6fd615b25e33" + integrity sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q== dependencies: - "@babel/compat-data" "^7.13.11" - "@babel/helper-define-polyfill-provider" "^0.3.1" - semver "^6.1.1" + "@babel/compat-data" "^7.22.6" + "@babel/helper-define-polyfill-provider" "^0.6.2" + semver "^6.3.1" -babel-plugin-polyfill-corejs3@^0.5.0: - version "0.5.1" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.1.tgz#d66183bf10976ea677f4149a7fcc4d8df43d4060" - integrity sha512-TihqEe4sQcb/QcPJvxe94/9RZuLQuF1+To4WqQcRvc+3J3gLCPIPgDKzGLG6zmQLfH3nn25heRuDNkS2KR4I8A== +babel-plugin-polyfill-corejs3@^0.10.6: + version "0.10.6" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz#2deda57caef50f59c525aeb4964d3b2f867710c7" + integrity sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA== dependencies: - "@babel/helper-define-polyfill-provider" "^0.3.1" - core-js-compat "^3.20.0" + "@babel/helper-define-polyfill-provider" "^0.6.2" + core-js-compat "^3.38.0" -babel-plugin-polyfill-regenerator@^0.3.0: - version "0.3.1" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.3.1.tgz#2c0678ea47c75c8cc2fbb1852278d8fb68233990" - integrity sha512-Y2B06tvgHYt1x0yz17jGkGeeMr5FeKUu+ASJ+N6nB5lQ8Dapfg42i0OVrf8PNGJ3zKL4A23snMi1IRwrqqND7A== +babel-plugin-polyfill-regenerator@^0.6.1: + version "0.6.2" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz#addc47e240edd1da1058ebda03021f382bba785e" + integrity sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg== dependencies: - "@babel/helper-define-polyfill-provider" "^0.3.1" + "@babel/helper-define-polyfill-provider" "^0.6.2" babel-plugin-transform-react-remove-prop-types@^0.4.24: version "0.4.24" @@ -3232,29 +3379,32 @@ babel-plugin-transform-react-remove-prop-types@^0.4.24: integrity sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA== babel-preset-current-node-syntax@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b" - integrity sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ== + version "1.1.0" + resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz#9a929eafece419612ef4ae4f60b1862ebad8ef30" + integrity sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw== dependencies: "@babel/plugin-syntax-async-generators" "^7.8.4" "@babel/plugin-syntax-bigint" "^7.8.3" - "@babel/plugin-syntax-class-properties" "^7.8.3" - "@babel/plugin-syntax-import-meta" "^7.8.3" + "@babel/plugin-syntax-class-properties" "^7.12.13" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + "@babel/plugin-syntax-import-attributes" "^7.24.7" + "@babel/plugin-syntax-import-meta" "^7.10.4" "@babel/plugin-syntax-json-strings" "^7.8.3" - "@babel/plugin-syntax-logical-assignment-operators" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - "@babel/plugin-syntax-numeric-separator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" "@babel/plugin-syntax-object-rest-spread" "^7.8.3" "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" "@babel/plugin-syntax-optional-chaining" "^7.8.3" - "@babel/plugin-syntax-top-level-await" "^7.8.3" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + "@babel/plugin-syntax-top-level-await" "^7.14.5" -babel-preset-jest@^27.4.0: - version "27.4.0" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-27.4.0.tgz#70d0e676a282ccb200fbabd7f415db5fdf393bca" - integrity sha512-NK4jGYpnBvNxcGo7/ZpZJr51jCGT+3bwwpVIDY2oNfTxJJldRtB4VAcYdgp1loDE50ODuTu+yBjpMAswv5tlpg== +babel-preset-jest@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz#91f10f58034cb7989cb4f962b69fa6eef6a6bc81" + integrity sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag== dependencies: - babel-plugin-jest-hoist "^27.4.0" + babel-plugin-jest-hoist "^27.5.1" babel-preset-current-node-syntax "^1.0.0" babel-preset-react-app@^10.0.1: @@ -3297,16 +3447,17 @@ base64-js@^1.3.1: batch@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" - integrity sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY= + integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== bfj@^7.0.2: - version "7.0.2" - resolved "https://registry.yarnpkg.com/bfj/-/bfj-7.0.2.tgz#1988ce76f3add9ac2913fd8ba47aad9e651bfbb2" - integrity sha512-+e/UqUzwmzJamNF50tBV6tZPTORow7gQ96iFow+8b562OdMpEK0BcJEq2OSPEDmAbSMBQ7PKZ87ubFkgxpYWgw== + version "7.1.0" + resolved "https://registry.yarnpkg.com/bfj/-/bfj-7.1.0.tgz#c5177d522103f9040e1b12980fe8c38cf41d3f8b" + integrity sha512-I6MMLkn+anzNdCUp9hMRyui1HaNEUCco50lxbvNS4+EyXg8lN3nJ48PjPWtbH8UVS9CuMoaKE9U2V3l29DaRQw== dependencies: - bluebird "^3.5.5" - check-types "^11.1.1" + bluebird "^3.7.2" + check-types "^11.2.3" hoopy "^0.1.4" + jsonpath "^1.1.1" tryer "^1.0.1" big-integer@^1.6.16: @@ -3320,9 +3471,9 @@ big.js@^5.2.2: integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== binary-extensions@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" - integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== bl@^4.1.0: version "4.1.0" @@ -3333,15 +3484,15 @@ bl@^4.1.0: inherits "^2.0.4" readable-stream "^3.4.0" -bluebird@^3.5.5, bluebird@^3.7.2: +bluebird@^3.7.2: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== -body-parser@1.20.2: - version "1.20.2" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" - integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== +body-parser@1.20.3: + version "1.20.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" + integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== dependencies: bytes "3.1.2" content-type "~1.0.5" @@ -3351,27 +3502,23 @@ body-parser@1.20.2: http-errors "2.0.0" iconv-lite "0.4.24" on-finished "2.4.1" - qs "6.11.0" + qs "6.13.0" raw-body "2.5.2" type-is "~1.6.18" unpipe "1.0.0" -bonjour@^3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5" - integrity sha1-jokKGD2O6aI5OzhExpGkK897yfU= +bonjour-service@^1.0.11: + version "1.2.1" + resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.2.1.tgz#eb41b3085183df3321da1264719fbada12478d02" + integrity sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw== dependencies: - array-flatten "^2.1.0" - deep-equal "^1.0.1" - dns-equal "^1.0.0" - dns-txt "^2.0.2" - multicast-dns "^6.0.1" - multicast-dns-service-types "^1.1.0" + fast-deep-equal "^3.1.3" + multicast-dns "^7.2.5" boolbase@^1.0.0, boolbase@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" - integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= + integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== brace-expansion@^1.1.7: version "1.1.11" @@ -3388,7 +3535,7 @@ brace-expansion@^2.0.1: dependencies: balanced-match "^1.0.0" -braces@^3.0.1, braces@~3.0.2: +braces@^3.0.3, braces@~3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== @@ -3414,24 +3561,13 @@ browser-process-hrtime@^1.0.0: resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== -browserslist@^4.0.0, browserslist@^4.16.6, browserslist@^4.17.5, browserslist@^4.18.1, browserslist@^4.19.1: - version "4.19.1" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.19.1.tgz#4ac0435b35ab655896c31d53018b6dd5e9e4c9a3" - integrity sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A== - dependencies: - caniuse-lite "^1.0.30001286" - electron-to-chromium "^1.4.17" - escalade "^3.1.1" - node-releases "^2.0.1" - picocolors "^1.0.0" - -browserslist@^4.21.10: - version "4.23.3" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.3.tgz#debb029d3c93ebc97ffbc8d9cbb03403e227c800" - integrity sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA== +browserslist@^4.0.0, browserslist@^4.18.1, browserslist@^4.21.10, browserslist@^4.21.4, browserslist@^4.23.3, browserslist@^4.24.0: + version "4.24.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.0.tgz#a1325fe4bc80b64fda169629fc01b3d6cecd38d4" + integrity sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A== dependencies: - caniuse-lite "^1.0.30001646" - electron-to-chromium "^1.5.4" + caniuse-lite "^1.0.30001663" + electron-to-chromium "^1.5.28" node-releases "^2.0.18" update-browserslist-db "^1.1.0" @@ -3447,11 +3583,6 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== -buffer-indexof@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" - integrity sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g== - buffer@^5.5.0: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" @@ -3461,27 +3592,30 @@ buffer@^5.5.0: ieee754 "^1.1.13" builtin-modules@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.2.0.tgz#45d5db99e7ee5e6bc4f362e008bf917ab5049887" - integrity sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA== + version "3.3.0" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" + integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" - integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= + integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== bytes@3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== -call-bind@^1.0.0, call-bind@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" - integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== +call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== dependencies: - function-bind "^1.1.1" - get-intrinsic "^1.0.2" + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" callsites@^3.0.0: version "3.1.0" @@ -3526,15 +3660,10 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001286, caniuse-lite@^1.0.30001297, caniuse-lite@^1.0.30001299: - version "1.0.30001416" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001416.tgz" - integrity sha512-06wzzdAkCPZO+Qm4e/eNghZBDfVNDsCgw33T27OwBH9unE9S478OYw//Q2L7Npf/zBzs7rjZOszIFQkwQKAEqA== - -caniuse-lite@^1.0.30001646: - version "1.0.30001657" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001657.tgz#29fd504bffca719d1c6b63a1f6f840be1973a660" - integrity sha512-DPbJAlP8/BAXy3IgiWmZKItubb3TYGP0WscQQlVGIfT4s/YlFYVuJgyOsQNP7rJRChx/qdMeLJQJP0Sgg2yjNA== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001663: + version "1.0.30001669" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001669.tgz#fda8f1d29a8bfdc42de0c170d7f34a9cf19ed7a3" + integrity sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w== case-sensitive-paths-webpack-plugin@^2.4.0: version "2.4.0" @@ -3561,7 +3690,7 @@ chalk@4.1.1: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.2: +chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -3592,9 +3721,9 @@ char-regex@^1.0.2: integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== char-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-2.0.0.tgz#16f98f3f874edceddd300fda5d58df380a7641a6" - integrity sha512-oGu2QekBMXgyQNWPDRQ001bjvDnZe4/zBTz37TMbiKz1NbNiyiH5hRkobe7npRN6GfbGbxMYFck/vQ1r9c1VMA== + version "2.0.1" + resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-2.0.1.tgz#6dafdb25f9d3349914079f010ba8d0e6ff9cd01e" + integrity sha512-oSvEeo6ZUD7NepqAat3RqoucZ5SeqLJgOvVIwkafu6IP3V0pO38s/ypdVUmDDK6qIIHNlYHJAKX9E7R7HoKElw== character-entities-html4@^1.0.0: version "1.1.4" @@ -3621,15 +3750,15 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== -check-types@^11.1.1: - version "11.1.2" - resolved "https://registry.yarnpkg.com/check-types/-/check-types-11.1.2.tgz#86a7c12bf5539f6324eb0e70ca8896c0e38f3e2f" - integrity sha512-tzWzvgePgLORb9/3a0YenggReLKAIb2owL03H2Xdoe5pKcUyWRSEQ8xfCar8t2SIAuEDwtmx2da1YB52YuHQMQ== +check-types@^11.2.3: + version "11.2.3" + resolved "https://registry.yarnpkg.com/check-types/-/check-types-11.2.3.tgz#1ffdf68faae4e941fce252840b1787b8edc93b71" + integrity sha512-+67P1GkJRaxQD6PKK0Et9DhwQB+vGg3PM5+aavopCpZT1lj9jeqfvpgTLAWErNj8qApkkmXlu/Ug74kmhagkXg== -chokidar@^3.4.2, chokidar@^3.5.2, chokidar@^3.5.3: - version "3.5.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" - integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== +chokidar@^3.4.2, chokidar@^3.5.3: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== dependencies: anymatch "~3.1.2" braces "~3.0.2" @@ -3647,19 +3776,19 @@ chroma-js@^2.4.2: integrity sha512-BLHvCB9s8Z1EV4ethr6xnkl/P2YRFOGqfgvuMG/MyCbZPrTA+NeiByY6XvgF0zP4/2deU2CXnWyMa3zu1LqQ3A== chrome-trace-event@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" - integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== + version "1.0.4" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz#05bffd7ff928465093314708c93bdfa9bd1f0f5b" + integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== ci-info@^3.2.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.3.0.tgz#b4ed1fb6818dea4803a55c623041f9165d2066b2" - integrity sha512-riT/3vI5YpVH6/qomlDnJow6TBee2PBKSEpx3O32EGPYbWGIRsIlGRms3Sm74wYE1JMo8RnO04Hb12+v1J5ICw== + version "3.9.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" + integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== cjs-module-lexer@^1.0.0: - version "1.2.2" - resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40" - integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== + version "1.4.1" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz#707413784dbb3a72aa11c2f2b042a0bef4004170" + integrity sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA== classnames@^2.5.1: version "2.5.1" @@ -3667,17 +3796,12 @@ classnames@^2.5.1: integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== clean-css@^5.2.2: - version "5.2.4" - resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.2.4.tgz#982b058f8581adb2ae062520808fb2429bd487a4" - integrity sha512-nKseG8wCzEuji/4yrgM/5cthL9oTDc5UOQyFMvW/Q53oP6gLH690o1NbuTh6Y18nujr7BxlsFuS7gXLnLzKJGg== + version "5.3.3" + resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.3.tgz#b330653cd3bd6b75009cc25c714cae7b93351ccd" + integrity sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg== dependencies: source-map "~0.6.0" -clean-stack@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" - integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== - cli-cursor@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" @@ -3686,9 +3810,9 @@ cli-cursor@^3.1.0: restore-cursor "^3.1.0" cli-spinners@^2.5.0: - version "2.6.1" - resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.1.tgz#adc954ebe281c37a6319bfa401e6dd2488ffb70d" - integrity sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g== + version "2.9.2" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.2.tgz#1773a8f4b9c4d6ac31563df53b3fc1d79462fe41" + integrity sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg== cli-width@^3.0.0: version "3.0.0" @@ -3704,15 +3828,24 @@ cliui@^7.0.2: strip-ansi "^6.0.0" wrap-ansi "^7.0.0" +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + clone@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" - integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= + integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" - integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= + integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== coa@^2.0.2: version "2.0.2" @@ -3729,9 +3862,9 @@ collapse-white-space@^1.0.2: integrity sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ== collect-v8-coverage@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59" - integrity sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg== + version "1.0.2" + resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz#c0b29bcd33bcd0779a1344c2136051e6afd3d9e9" + integrity sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q== color-convert@^1.9.0: version "1.9.3" @@ -3750,17 +3883,17 @@ color-convert@^2.0.1: color-name@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== -color-name@^1.1.4, color-name@~1.1.4: +color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== colord@^2.9.1: - version "2.9.2" - resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.2.tgz#25e2bacbbaa65991422c07ea209e2089428effb1" - integrity sha512-Uqbg+J445nc1TKn4FoDPS6ZZqAvEDnwrH42yo8B40JSOgSLxMZ/gt3h4nmCtPLQeXhjJJkqBx7SCY35WnIixaQ== + version "2.9.3" + resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43" + integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw== colorette@^1.1.0: version "1.4.0" @@ -3768,9 +3901,9 @@ colorette@^1.1.0: integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g== colorette@^2.0.10: - version "2.0.16" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.16.tgz#713b9af84fdb000139f04546bd4a93f62a5085da" - integrity sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g== + version "2.0.20" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== combined-stream@^1.0.8: version "1.0.8" @@ -3789,6 +3922,11 @@ commander@^2.20.0: resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== +commander@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" + integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== + commander@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" @@ -3799,11 +3937,6 @@ commander@^8.3.0: resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== -common-path-prefix@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/common-path-prefix/-/common-path-prefix-3.0.0.tgz#7d007a7e07c58c4b4d5f433131a19141b29f11e0" - integrity sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w== - common-tags@^1.8.0: version "1.8.2" resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.2.tgz#94ebb3c076d26032745fd54face7f688ef5ac9c6" @@ -3812,7 +3945,7 @@ common-tags@^1.8.0: commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" - integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= + integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== compressible@~2.0.16: version "2.0.18" @@ -3844,72 +3977,64 @@ confusing-browser-globals@^1.0.11: resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz#ae40e9b57cdd3915408a2805ebd3a5585608dc81" integrity sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA== -connect-history-api-fallback@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" - integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== +connect-history-api-fallback@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" + integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== content-disposition@0.5.4: version "0.5.4" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== dependencies: - safe-buffer "5.2.1" - -content-type@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" - integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + safe-buffer "5.2.1" -content-type@~1.0.5: +content-type@~1.0.4, content-type@~1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== -convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" - integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA== - dependencies: - safe-buffer "~5.1.1" - -convert-source-map@^1.5.0: +convert-source-map@^1.4.0, convert-source-map@^1.5.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: version "1.9.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== -cookie@0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" - integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== +cookie@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9" + integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== cookie@^0.4.1: version "0.4.2" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== -core-js-compat@^3.20.0, core-js-compat@^3.20.2: - version "3.20.3" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.20.3.tgz#d71f85f94eb5e4bea3407412e549daa083d23bd6" - integrity sha512-c8M5h0IkNZ+I92QhIpuSijOxGAcj3lgpsWdkCqmUTZNwidujF4r3pi6x1DCN+Vcs5qTS2XWWMfWSuCqyupX8gw== +core-js-compat@^3.38.0, core-js-compat@^3.38.1: + version "3.38.1" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.38.1.tgz#2bc7a298746ca5a7bcb9c164bcb120f2ebc09a09" + integrity sha512-JRH6gfXxGmrzF3tZ57lFx97YARxCXPaMzPo6jELZhv88pBH5VXpQ+y0znKGlFnzuaihqhLbefxSJxWJMPtfDzw== dependencies: - browserslist "^4.19.1" - semver "7.0.0" + browserslist "^4.23.3" -core-js-pure@^3.20.2, core-js-pure@^3.8.1: - version "3.20.3" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.20.3.tgz#6cc4f36da06c61d95254efc54024fe4797fd5d02" - integrity sha512-Q2H6tQ5MtPtcC7f3HxJ48i4Q7T9ybPKgvWyuH7JXIoNa2pm0KuBnycsET/qw1SLLZYfbsbrZQNMeIOClb+6WIA== +core-js-pure@^3.23.3: + version "3.38.1" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.38.1.tgz#e8534062a54b7221344884ba9b52474be495ada3" + integrity sha512-BY8Etc1FZqdw1glX0XNOq2FDwfrg/VGqoZOZCdaL+UmdaqDwQwYXkMJT4t6In+zfEfOJDcM9T0KdbBeJg8KKCQ== core-js@^3.19.2: - version "3.20.3" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.20.3.tgz#c710d0a676e684522f3db4ee84e5e18a9d11d69a" - integrity sha512-vVl8j8ph6tRS3B8qir40H7yw7voy17xL0piAjlbBUsH7WIfzoedL/ZOr1OV9FyZQLWXsayOJyV4tnRyXR85/ag== + version "3.38.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.38.1.tgz#aa375b79a286a670388a1a363363d53677c0383e" + integrity sha512-OP35aUorbU3Zvlx7pjsFdu1rGNnD4pgw/CWoYzRY3t2EzoVT7shKHY1dlAy3f41cGIO7ZDPQimhGFTlEYkG/Hw== core-util-is@~1.0.0: version "1.0.3" @@ -3927,10 +4052,10 @@ cosmiconfig@^6.0.0: path-type "^4.0.0" yaml "^1.7.2" -cosmiconfig@^7.0.0, cosmiconfig@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.1.tgz#714d756522cace867867ccb4474c5d01bbae5d6d" - integrity sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ== +cosmiconfig@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" + integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA== dependencies: "@types/parse-json" "^4.0.0" import-fresh "^3.2.1" @@ -3938,7 +4063,7 @@ cosmiconfig@^7.0.0, cosmiconfig@^7.0.1: path-type "^4.0.0" yaml "^1.10.0" -cross-spawn@^7.0.2, cross-spawn@^7.0.3: +cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -3952,12 +4077,12 @@ crypto-random-string@^2.0.0: resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== -css-blank-pseudo@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/css-blank-pseudo/-/css-blank-pseudo-3.0.2.tgz#f8660f6a48b17888a9277e53f25cc5abec1f0169" - integrity sha512-hOb1LFjRR+8ocA071xUSmg5VslJ8NGo/I2qpUpdeAYyBVCgupS5O8SEVo4SxEMYyFBNodBkzG3T1iqW9HCXxew== +css-blank-pseudo@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz#36523b01c12a25d812df343a32c322d2a2324561" + integrity sha512-VS90XWtsHGqoM0t4KpH053c4ehxZ2E6HtGI7x68YFV0pTo/QmkV/YFA+NnlvK8guxZVNWGQhVNJGC39Q8XF4OQ== dependencies: - postcss-selector-parser "^6.0.8" + postcss-selector-parser "^6.0.9" css-box-model@^1.2.1: version "1.2.1" @@ -3971,33 +4096,31 @@ css-color-keywords@^1.0.0: resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05" integrity sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg== -css-declaration-sorter@^6.0.3: - version "6.1.4" - resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.1.4.tgz#b9bfb4ed9a41f8dcca9bf7184d849ea94a8294b4" - integrity sha512-lpfkqS0fctcmZotJGhnxkIyJWvBXgpyi2wsFd4J8VB7wzyrT6Ch/3Q+FMNJpjK4gu1+GN5khOnpU2ZVKrLbhCw== - dependencies: - timsort "^0.3.0" +css-declaration-sorter@^6.3.1: + version "6.4.1" + resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz#28beac7c20bad7f1775be3a7129d7eae409a3a71" + integrity sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g== -css-has-pseudo@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/css-has-pseudo/-/css-has-pseudo-3.0.3.tgz#4824a34cb92dae7e09ea1d3fd19691b653412098" - integrity sha512-0gDYWEKaGacwxCqvQ3Ypg6wGdD1AztbMm5h1JsactG2hP2eiflj808QITmuWBpE7sjSEVrAlZhPTVd/nNMj/hQ== +css-has-pseudo@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/css-has-pseudo/-/css-has-pseudo-3.0.4.tgz#57f6be91ca242d5c9020ee3e51bbb5b89fc7af73" + integrity sha512-Vse0xpR1K9MNlp2j5w1pgWIJtm1a8qS0JwS9goFYcImjlHEmywP9VUF05aGBXzGpDJF86QXk4L0ypBmwPhGArw== dependencies: - postcss-selector-parser "^6.0.8" + postcss-selector-parser "^6.0.9" css-loader@^6.5.1: - version "6.5.1" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.5.1.tgz#0c43d4fbe0d97f699c91e9818cb585759091d1b1" - integrity sha512-gEy2w9AnJNnD9Kuo4XAP9VflW/ujKoS9c/syO+uWMlm5igc7LysKzPXaDoR2vroROkSwsTS2tGr1yGGEbZOYZQ== + version "6.11.0" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.11.0.tgz#33bae3bf6363d0a7c2cf9031c96c744ff54d85ba" + integrity sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g== dependencies: icss-utils "^5.1.0" - postcss "^8.2.15" - postcss-modules-extract-imports "^3.0.0" - postcss-modules-local-by-default "^4.0.0" - postcss-modules-scope "^3.0.0" + postcss "^8.4.33" + postcss-modules-extract-imports "^3.1.0" + postcss-modules-local-by-default "^4.0.5" + postcss-modules-scope "^3.2.0" postcss-modules-values "^4.0.0" - postcss-value-parser "^4.1.0" - semver "^7.3.5" + postcss-value-parser "^4.2.0" + semver "^7.5.4" css-minimizer-webpack-plugin@^3.2.0: version "3.4.1" @@ -4011,10 +4134,10 @@ css-minimizer-webpack-plugin@^3.2.0: serialize-javascript "^6.0.0" source-map "^0.6.1" -css-prefers-color-scheme@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.2.tgz#d5c03a980caab92d8beeee176a8795d331e0c727" - integrity sha512-gv0KQBEM+q/XdoKyznovq3KW7ocO7k+FhPP+hQR1MenJdu0uPGS6IZa9PzlbqBeS6XcZJNAoqoFxlAUW461CrA== +css-prefers-color-scheme@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz#ca8a22e5992c10a5b9d315155e7caee625903349" + integrity sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA== css-select-base-adapter@^0.1.1: version "0.1.1" @@ -4032,13 +4155,13 @@ css-select@^2.0.0: nth-check "^1.0.2" css-select@^4.1.3: - version "4.2.1" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.2.1.tgz#9e665d6ae4c7f9d65dbe69d0316e3221fb274cdd" - integrity sha512-/aUslKhzkTNCQUB2qTX84lVmfia9NyjP3WpDGtj/WxhwBzWBYUV3DgUpurHTme8UTPcPlAD1DJ+b0nN/t50zDQ== + version "4.3.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b" + integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ== dependencies: boolbase "^1.0.0" - css-what "^5.1.0" - domhandler "^4.3.0" + css-what "^6.0.1" + domhandler "^4.3.1" domutils "^2.8.0" nth-check "^2.0.1" @@ -4051,22 +4174,6 @@ css-to-react-native@3.2.0: css-color-keywords "^1.0.0" postcss-value-parser "^4.0.2" -css-tree@1.0.0-alpha.29: - version "1.0.0-alpha.29" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.29.tgz#3fa9d4ef3142cbd1c301e7664c1f352bd82f5a39" - integrity sha512-sRNb1XydwkW9IOci6iB2xmy8IGCj6r/fr+JWitvJ2JxQRPzN3T4AGGVWCMlVmVwM1gtgALJRmGIlWv5ppnGGkg== - dependencies: - mdn-data "~1.1.0" - source-map "^0.5.3" - -css-tree@1.0.0-alpha.33: - version "1.0.0-alpha.33" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.33.tgz#970e20e5a91f7a378ddd0fc58d0b6c8d4f3be93e" - integrity sha512-SPt57bh5nQnpsTBsx/IXbO14sRc9xXu5MtMAVuo0BaQQmyf0NupNPPSoMaqiAF5tDFafYsTkfeH4Q/HCKXkg4w== - dependencies: - mdn-data "2.0.4" - source-map "^0.5.3" - css-tree@1.0.0-alpha.37: version "1.0.0-alpha.37" resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.37.tgz#98bebd62c4c1d9f960ec340cf9f7522e30709a22" @@ -4088,82 +4195,75 @@ css-what@^3.2.1: resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4" integrity sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ== -css-what@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.1.0.tgz#3f7b707aadf633baf62c2ceb8579b545bb40f7fe" - integrity sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw== +css-what@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" + integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== css.escape@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" integrity sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s= -cssdb@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-5.1.0.tgz#ec728d5f5c0811debd0820cbebda505d43003400" - integrity sha512-/vqjXhv1x9eGkE/zO6o8ZOI7dgdZbLVLUGyVRbPgk6YipXbW87YzUCcO+Jrmi5bwJlAH6oD+MNeZyRgXea1GZw== +cssdb@^7.1.0: + version "7.11.2" + resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-7.11.2.tgz#127a2f5b946ee653361a5af5333ea85a39df5ae5" + integrity sha512-lhQ32TFkc1X4eTefGfYPvgovRSzIMofHkigfH8nWtyRL4XJLsRhJFreRvEgKzept7x1rjBuy3J/MurXLaFxW/A== cssesc@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== -cssnano-preset-default@^5.1.11: - version "5.1.11" - resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-5.1.11.tgz#db10fb1ecee310e8285c5aca45bd8237be206828" - integrity sha512-ETet5hqHxmzQq2ynXMOQofKuLm7VOjMiOB7E2zdtm/hSeCKlD9fabzIUV4GoPcRyJRHi+4kGf0vsfGYbQ4nmPw== - dependencies: - css-declaration-sorter "^6.0.3" - cssnano-utils "^3.0.1" - postcss-calc "^8.2.0" - postcss-colormin "^5.2.4" - postcss-convert-values "^5.0.3" - postcss-discard-comments "^5.0.2" - postcss-discard-duplicates "^5.0.2" - postcss-discard-empty "^5.0.2" - postcss-discard-overridden "^5.0.3" - postcss-merge-longhand "^5.0.5" - postcss-merge-rules "^5.0.5" - postcss-minify-font-values "^5.0.3" - postcss-minify-gradients "^5.0.5" - postcss-minify-params "^5.0.4" - postcss-minify-selectors "^5.1.2" - postcss-normalize-charset "^5.0.2" - postcss-normalize-display-values "^5.0.2" - postcss-normalize-positions "^5.0.3" - postcss-normalize-repeat-style "^5.0.3" - postcss-normalize-string "^5.0.3" - postcss-normalize-timing-functions "^5.0.2" - postcss-normalize-unicode "^5.0.3" - postcss-normalize-url "^5.0.4" - postcss-normalize-whitespace "^5.0.3" - postcss-ordered-values "^5.0.4" - postcss-reduce-initial "^5.0.2" - postcss-reduce-transforms "^5.0.3" - postcss-svgo "^5.0.3" - postcss-unique-selectors "^5.0.3" - -cssnano-utils@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/cssnano-utils/-/cssnano-utils-3.0.1.tgz#d3cc0a142d3d217f8736837ec0a2ccff6a89c6ea" - integrity sha512-VNCHL364lh++/ono+S3j9NlUK+d97KNkxI77NlqZU2W3xd2/qmyN61dsa47pTpb55zuU4G4lI7qFjAXZJH1OAQ== +cssnano-preset-default@^5.2.14: + version "5.2.14" + resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-5.2.14.tgz#309def4f7b7e16d71ab2438052093330d9ab45d8" + integrity sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A== + dependencies: + css-declaration-sorter "^6.3.1" + cssnano-utils "^3.1.0" + postcss-calc "^8.2.3" + postcss-colormin "^5.3.1" + postcss-convert-values "^5.1.3" + postcss-discard-comments "^5.1.2" + postcss-discard-duplicates "^5.1.0" + postcss-discard-empty "^5.1.1" + postcss-discard-overridden "^5.1.0" + postcss-merge-longhand "^5.1.7" + postcss-merge-rules "^5.1.4" + postcss-minify-font-values "^5.1.0" + postcss-minify-gradients "^5.1.1" + postcss-minify-params "^5.1.4" + postcss-minify-selectors "^5.2.1" + postcss-normalize-charset "^5.1.0" + postcss-normalize-display-values "^5.1.0" + postcss-normalize-positions "^5.1.1" + postcss-normalize-repeat-style "^5.1.1" + postcss-normalize-string "^5.1.0" + postcss-normalize-timing-functions "^5.1.0" + postcss-normalize-unicode "^5.1.1" + postcss-normalize-url "^5.1.0" + postcss-normalize-whitespace "^5.1.1" + postcss-ordered-values "^5.1.3" + postcss-reduce-initial "^5.1.2" + postcss-reduce-transforms "^5.1.0" + postcss-svgo "^5.1.0" + postcss-unique-selectors "^5.1.1" + +cssnano-utils@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cssnano-utils/-/cssnano-utils-3.1.0.tgz#95684d08c91511edfc70d2636338ca37ef3a6861" + integrity sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA== cssnano@^5.0.6: - version "5.0.16" - resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-5.0.16.tgz#4ee97d30411693f3de24cef70b36f7ae2a843e04" - integrity sha512-ryhRI9/B9VFCwPbb1z60LLK5/ldoExi7nwdnJzpkLZkm2/r7j2X3jfY+ZvDVJhC/0fPZlrAguYdHNFg0iglPKQ== + version "5.1.15" + resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-5.1.15.tgz#ded66b5480d5127fcb44dac12ea5a983755136bf" + integrity sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw== dependencies: - cssnano-preset-default "^5.1.11" + cssnano-preset-default "^5.2.14" lilconfig "^2.0.3" yaml "^1.10.2" -csso@^3.5.1: - version "3.5.1" - resolved "https://registry.yarnpkg.com/csso/-/csso-3.5.1.tgz#7b9eb8be61628973c1b261e169d2f024008e758b" - integrity sha512-vrqULLffYU1Q2tLdJvaCYbONStnfkfimRxXNaGjxMldI0C7JPBC4rB1RyjhfdZ4m1frm8pM9uRPKH3d2knZ8gg== - dependencies: - css-tree "1.0.0-alpha.29" - csso@^4.0.2, csso@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529" @@ -4198,7 +4298,7 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.10.tgz#2ad3a7bed70f35b965707c092e5f30b327c290e5" integrity sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA== -damerau-levenshtein@^1.0.7: +damerau-levenshtein@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== @@ -4212,21 +4312,48 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" -debug@2.6.9, debug@^2.6.0, debug@^2.6.9: +data-view-buffer@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.1.tgz#8ea6326efec17a2e42620696e671d7d5a8bc66b2" + integrity sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +data-view-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz#90721ca95ff280677eb793749fce1011347669e2" + integrity sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +data-view-byte-offset@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz#5e0bbfb4828ed2d1b9b400cd8a7d119bca0ff18a" + integrity sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +debug@2.6.9, debug@^2.6.0: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== dependencies: ms "2.0.0" -debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2: - version "4.3.3" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" - integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: + version "4.3.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== dependencies: - ms "2.1.2" + ms "^2.1.3" -debug@^3.1.1, debug@^3.2.7: +debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== @@ -4234,9 +4361,9 @@ debug@^3.1.1, debug@^3.2.7: ms "^2.1.1" decimal.js@^10.2.1: - version "10.3.1" - resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783" - integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ== + version "10.4.3" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" + integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== decode-uri-component@^0.2.0: version "0.2.2" @@ -4246,19 +4373,31 @@ decode-uri-component@^0.2.0: dedent@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" - integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= + integrity sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA== -deep-equal@^1.0.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a" - integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g== - dependencies: - is-arguments "^1.0.4" - is-date-object "^1.0.1" - is-regex "^1.0.4" - object-is "^1.0.1" +deep-equal@^2.0.5: + version "2.2.3" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.3.tgz#af89dafb23a396c7da3e862abc0be27cf51d56e1" + integrity sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA== + dependencies: + array-buffer-byte-length "^1.0.0" + call-bind "^1.0.5" + es-get-iterator "^1.1.3" + get-intrinsic "^1.2.2" + is-arguments "^1.1.1" + is-array-buffer "^3.0.2" + is-date-object "^1.0.5" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + isarray "^2.0.5" + object-is "^1.1.5" object-keys "^1.1.1" - regexp.prototype.flags "^1.2.0" + object.assign "^4.1.4" + regexp.prototype.flags "^1.5.1" + side-channel "^1.0.4" + which-boxed-primitive "^1.0.2" + which-collection "^1.0.1" + which-typed-array "^1.1.13" deep-is@^0.1.3, deep-is@~0.1.3: version "0.1.4" @@ -4266,9 +4405,9 @@ deep-is@^0.1.3, deep-is@~0.1.3: integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== deepmerge@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" - integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== + version "4.3.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== default-gateway@^6.0.3: version "6.0.3" @@ -4278,47 +4417,39 @@ default-gateway@^6.0.3: execa "^5.0.0" defaults@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d" - integrity sha1-xlYFHpgX2f8I7YgUd/P+QBnz730= + version "1.0.4" + resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.4.tgz#b0b02062c1e2aa62ff5d9528f0f98baa90978d7a" + integrity sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A== dependencies: clone "^1.0.2" +define-data-property@^1.0.1, define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + define-lazy-prop@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== -define-properties@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" - integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== - dependencies: - object-keys "^1.0.12" - -defined@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" - integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM= - -del@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/del/-/del-6.0.0.tgz#0b40d0332cea743f1614f818be4feb717714c952" - integrity sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ== +define-properties@^1.1.3, define-properties@^1.2.0, define-properties@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== dependencies: - globby "^11.0.1" - graceful-fs "^4.2.4" - is-glob "^4.0.1" - is-path-cwd "^2.2.0" - is-path-inside "^3.0.2" - p-map "^4.0.0" - rimraf "^3.0.2" - slash "^3.0.0" + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== depd@2.0.0: version "2.0.0" @@ -4328,7 +4459,7 @@ depd@2.0.0: depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" - integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== dequal@^2.0.3: version "2.0.3" @@ -4363,24 +4494,15 @@ detect-port-alt@^1.1.6: address "^1.0.1" debug "^2.6.0" -detective@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/detective/-/detective-5.2.0.tgz#feb2a77e85b904ecdea459ad897cc90a99bd2a7b" - integrity sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg== - dependencies: - acorn-node "^1.6.1" - defined "^1.0.0" - minimist "^1.1.1" - didyoumean@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw== -diff-sequences@^27.4.0: - version "27.4.0" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.4.0.tgz#d783920ad8d06ec718a060d00196dfef25b132a5" - integrity sha512-YqiQzkrsmHMH5uuh8OdQFU9/ZpADnwzml8z0O5HvRNda+5UZsaX/xN+AAxfR2hWq1Y7HZnAzO9J5lJXOuDz2Ww== +diff-sequences@^27.4.0, diff-sequences@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327" + integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ== dir-glob@^3.0.1: version "3.0.1" @@ -4394,25 +4516,12 @@ dlv@^1.1.3: resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79" integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA== -dns-equal@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" - integrity sha1-s55/HabrCnW6nBcySzR1PEfgZU0= - -dns-packet@^1.3.1: - version "1.3.4" - resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.4.tgz#e3455065824a2507ba886c55a89963bb107dec6f" - integrity sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA== - dependencies: - ip "^1.1.0" - safe-buffer "^5.0.1" - -dns-txt@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/dns-txt/-/dns-txt-2.0.2.tgz#b91d806f5d27188e4ab3e7d107d881a1cc4642b6" - integrity sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY= +dns-packet@^5.2.2: + version "5.6.1" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.6.1.tgz#ae888ad425a9d1478a0674256ab866de1012cf2f" + integrity sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw== dependencies: - buffer-indexof "^1.0.0" + "@leichtgewicht/ip-codec" "^2.0.1" doctrine@^2.1.0: version "2.1.0" @@ -4454,9 +4563,9 @@ dom-serializer@0: entities "^2.0.0" dom-serializer@^1.0.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.2.tgz#6206437d32ceefaec7161803230c7a20bc1b4d91" - integrity sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig== + version "1.4.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30" + integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag== dependencies: domelementtype "^2.0.1" domhandler "^4.2.0" @@ -4468,9 +4577,9 @@ domelementtype@1: integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== domelementtype@^2.0.1, domelementtype@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57" - integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A== + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== domexception@^2.0.1: version "2.0.1" @@ -4479,10 +4588,10 @@ domexception@^2.0.1: dependencies: webidl-conversions "^5.0.0" -domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.0.tgz#16c658c626cf966967e306f966b431f77d4a5626" - integrity sha512-fC0aXNQXqKSFTr2wDNZDhsEYjCiYsDWl3D01kwt25hm1YIPyDGHvvi3rw+PLqHAl/m71MaiF7d5zvBr0p5UB2g== +domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c" + integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ== dependencies: domelementtype "^2.2.0" @@ -4526,6 +4635,11 @@ duplexer@^0.1.2: resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -4538,15 +4652,15 @@ ejs@^3.1.6: dependencies: jake "^10.8.5" -electron-to-chromium@^1.4.17: - version "1.4.57" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.57.tgz#2b2766df76ac8dbc0a1d41249bc5684a31849892" - integrity sha512-FNC+P5K1n6pF+M0zIK+gFCoXcJhhzDViL3DRIGy2Fv5PohuSES1JHR7T+GlwxSxlzx4yYbsuzCZvHxcBSRCIOw== +electron-to-chromium@^1.5.28: + version "1.5.41" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.41.tgz#eae1ba6c49a1a61d84cf8263351d3513b2bcc534" + integrity sha512-dfdv/2xNjX0P8Vzme4cfzHqnPm5xsZXwsolTYr0eyW18IUmNyG08vL+fttvinTfhKfIKdRoqkDIC9e9iWQCNYQ== -electron-to-chromium@^1.5.4: - version "1.5.15" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.15.tgz#3c969a29b03682db7a3032283ec8be6e75effe50" - integrity sha512-Z4rIDoImwEJW+YYKnPul4DzqsWVqYetYVN3XqDmRpgV0mjz0hYTaeeh+8/9CL1bk3AHYmF4freW/NTiVoXA2gA== +emittery@^0.10.2: + version "0.10.2" + resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.10.2.tgz#902eec8aedb8c41938c46e9385e9db7e03182933" + integrity sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw== emittery@^0.8.1: version "0.8.1" @@ -4578,6 +4692,11 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== +encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + enhanced-resolve@^5.17.1: version "5.17.1" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" @@ -4591,10 +4710,10 @@ entities@^2.0.0: resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== -entities@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" - integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== +entities@^4.4.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== error-ex@^1.3.1: version "1.3.2" @@ -4604,43 +4723,144 @@ error-ex@^1.3.1: is-arrayish "^0.2.1" error-stack-parser@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.0.6.tgz#5a99a707bd7a4c58a797902d48d82803ede6aad8" - integrity sha512-d51brTeqC+BHlwF0BhPtcYgF5nlzf9ZZ0ZIUQNZpc9ZB9qw5IJ2diTrBY9jlCJkTLITYPjmiX6OWCwH+fuyNgQ== + version "2.1.4" + resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.1.4.tgz#229cb01cdbfa84440bfa91876285b94680188286" + integrity sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ== + dependencies: + stackframe "^1.3.4" + +es-abstract@^1.17.2, es-abstract@^1.17.5, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.1, es-abstract@^1.23.2, es-abstract@^1.23.3: + version "1.23.3" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0" + integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A== + dependencies: + array-buffer-byte-length "^1.0.1" + arraybuffer.prototype.slice "^1.0.3" + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + data-view-buffer "^1.0.1" + data-view-byte-length "^1.0.1" + data-view-byte-offset "^1.0.0" + es-define-property "^1.0.0" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-set-tostringtag "^2.0.3" + es-to-primitive "^1.2.1" + function.prototype.name "^1.1.6" + get-intrinsic "^1.2.4" + get-symbol-description "^1.0.2" + globalthis "^1.0.3" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + has-proto "^1.0.3" + has-symbols "^1.0.3" + hasown "^2.0.2" + internal-slot "^1.0.7" + is-array-buffer "^3.0.4" + is-callable "^1.2.7" + is-data-view "^1.0.1" + is-negative-zero "^2.0.3" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.3" + is-string "^1.0.7" + is-typed-array "^1.1.13" + is-weakref "^1.0.2" + object-inspect "^1.13.1" + object-keys "^1.1.1" + object.assign "^4.1.5" + regexp.prototype.flags "^1.5.2" + safe-array-concat "^1.1.2" + safe-regex-test "^1.0.3" + string.prototype.trim "^1.2.9" + string.prototype.trimend "^1.0.8" + string.prototype.trimstart "^1.0.8" + typed-array-buffer "^1.0.2" + typed-array-byte-length "^1.0.1" + typed-array-byte-offset "^1.0.2" + typed-array-length "^1.0.6" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.15" + +es-array-method-boxes-properly@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e" + integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA== + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== dependencies: - stackframe "^1.1.1" + get-intrinsic "^1.2.4" + +es-errors@^1.2.1, es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== -es-abstract@^1.17.2, es-abstract@^1.19.0, es-abstract@^1.19.1: - version "1.19.1" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.1.tgz#d4885796876916959de78edaa0df456627115ec3" - integrity sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w== +es-get-iterator@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6" + integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw== dependencies: call-bind "^1.0.2" - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - get-intrinsic "^1.1.1" - get-symbol-description "^1.0.0" - has "^1.0.3" - has-symbols "^1.0.2" - internal-slot "^1.0.3" - is-callable "^1.2.4" - is-negative-zero "^2.0.1" - is-regex "^1.1.4" - is-shared-array-buffer "^1.0.1" + get-intrinsic "^1.1.3" + has-symbols "^1.0.3" + is-arguments "^1.1.1" + is-map "^2.0.2" + is-set "^2.0.2" is-string "^1.0.7" - is-weakref "^1.0.1" - object-inspect "^1.11.0" - object-keys "^1.1.1" - object.assign "^4.1.2" - string.prototype.trimend "^1.0.4" - string.prototype.trimstart "^1.0.4" - unbox-primitive "^1.0.1" + isarray "^2.0.5" + stop-iteration-iterator "^1.0.0" + +es-iterator-helpers@^1.0.19: + version "1.1.0" + resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.1.0.tgz#f6d745d342aea214fe09497e7152170dc333a7a6" + integrity sha512-/SurEfycdyssORP/E+bj4sEu1CWw4EmLDsHynHwSXQ7utgbrMRWW195pTrCjFgFCddf/UkYm3oqKPRq5i8bJbw== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.3" + es-errors "^1.3.0" + es-set-tostringtag "^2.0.3" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + globalthis "^1.0.4" + has-property-descriptors "^1.0.2" + has-proto "^1.0.3" + has-symbols "^1.0.3" + internal-slot "^1.0.7" + iterator.prototype "^1.1.3" + safe-array-concat "^1.1.2" es-module-lexer@^1.2.1: version "1.5.4" resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.4.tgz#a8efec3a3da991e60efa6b633a7cad6ab8d26b78" integrity sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw== +es-object-atoms@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" + integrity sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw== + dependencies: + es-errors "^1.3.0" + +es-set-tostringtag@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777" + integrity sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ== + dependencies: + get-intrinsic "^1.2.4" + has-tostringtag "^1.0.2" + hasown "^2.0.1" + +es-shim-unscopables@^1.0.0, es-shim-unscopables@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz#1f6942e71ecc7835ed1c8a83006d8771a63a3763" + integrity sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw== + dependencies: + hasown "^2.0.0" + es-to-primitive@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" @@ -4650,12 +4870,7 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" -escalade@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" - integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== - -escalade@^3.1.2: +escalade@^3.1.1, escalade@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== @@ -4668,7 +4883,7 @@ escape-html@~1.0.3: escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== escape-string-regexp@^2.0.0: version "2.0.0" @@ -4680,7 +4895,7 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== -escodegen@^1.13.0: +escodegen@^1.13.0, escodegen@^1.8.1: version "1.14.3" resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw== @@ -4693,21 +4908,20 @@ escodegen@^1.13.0: source-map "~0.6.1" escodegen@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd" - integrity sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw== + version "2.1.0" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.1.0.tgz#ba93bbb7a43986d29d6041f99f5262da773e2e17" + integrity sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w== dependencies: esprima "^4.0.1" estraverse "^5.2.0" esutils "^2.0.2" - optionator "^0.8.1" optionalDependencies: source-map "~0.6.1" -eslint-config-react-app@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/eslint-config-react-app/-/eslint-config-react-app-7.0.0.tgz#0fa96d5ec1dfb99c029b1554362ab3fa1c3757df" - integrity sha512-xyymoxtIt1EOsSaGag+/jmcywRuieQoA2JbPCjnw9HukFj9/97aGPoZVFioaotzk1K5Qt9sHO5EutZbkrAXS0g== +eslint-config-react-app@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz#73ba3929978001c5c86274c017ea57eb5fa644b4" + integrity sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA== dependencies: "@babel/core" "^7.16.0" "@babel/eslint-parser" "^7.16.3" @@ -4724,21 +4938,21 @@ eslint-config-react-app@^7.0.0: eslint-plugin-react-hooks "^4.3.0" eslint-plugin-testing-library "^5.0.1" -eslint-import-resolver-node@^0.3.6: - version "0.3.6" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz#4048b958395da89668252001dbd9eca6b83bacbd" - integrity sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw== +eslint-import-resolver-node@^0.3.9: + version "0.3.9" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac" + integrity sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g== dependencies: debug "^3.2.7" - resolve "^1.20.0" + is-core-module "^2.13.0" + resolve "^1.22.4" -eslint-module-utils@^2.7.2: - version "2.7.3" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.3.tgz#ad7e3a10552fdd0642e1e55292781bd6e34876ee" - integrity sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ== +eslint-module-utils@^2.12.0: + version "2.12.0" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz#fe4cfb948d61f49203d7b08871982b65b9af0b0b" + integrity sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg== dependencies: debug "^3.2.7" - find-up "^2.1.0" eslint-plugin-flowtype@^8.0.3: version "8.0.3" @@ -4749,23 +4963,29 @@ eslint-plugin-flowtype@^8.0.3: string-natural-compare "^3.0.1" eslint-plugin-import@^2.25.3: - version "2.25.4" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.25.4.tgz#322f3f916a4e9e991ac7af32032c25ce313209f1" - integrity sha512-/KJBASVFxpu0xg1kIBn9AUa8hQVnszpwgE7Ld0lKAlx7Ie87yzEzCgSkekt+le/YVhiaosO4Y14GDAOc41nfxA== - dependencies: - array-includes "^3.1.4" - array.prototype.flat "^1.2.5" - debug "^2.6.9" + version "2.31.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz#310ce7e720ca1d9c0bb3f69adfd1c6bdd7d9e0e7" + integrity sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A== + dependencies: + "@rtsao/scc" "^1.1.0" + array-includes "^3.1.8" + array.prototype.findlastindex "^1.2.5" + array.prototype.flat "^1.3.2" + array.prototype.flatmap "^1.3.2" + debug "^3.2.7" doctrine "^2.1.0" - eslint-import-resolver-node "^0.3.6" - eslint-module-utils "^2.7.2" - has "^1.0.3" - is-core-module "^2.8.0" + eslint-import-resolver-node "^0.3.9" + eslint-module-utils "^2.12.0" + hasown "^2.0.2" + is-core-module "^2.15.1" is-glob "^4.0.3" - minimatch "^3.0.4" - object.values "^1.1.5" - resolve "^1.20.0" - tsconfig-paths "^3.12.0" + minimatch "^3.1.2" + object.fromentries "^2.0.8" + object.groupby "^1.0.3" + object.values "^1.2.0" + semver "^6.3.1" + string.prototype.trimend "^1.0.8" + tsconfig-paths "^3.15.0" eslint-plugin-jest@^25.3.0: version "25.7.0" @@ -4775,54 +4995,62 @@ eslint-plugin-jest@^25.3.0: "@typescript-eslint/experimental-utils" "^5.0.0" eslint-plugin-jsx-a11y@^6.5.1: - version "6.5.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.5.1.tgz#cdbf2df901040ca140b6ec14715c988889c2a6d8" - integrity sha512-sVCFKX9fllURnXT2JwLN5Qgo24Ug5NF6dxhkmxsMEUZhXRcGg+X3e1JbJ84YePQKBl5E0ZjAH5Q4rkdcGY99+g== - dependencies: - "@babel/runtime" "^7.16.3" - aria-query "^4.2.2" - array-includes "^3.1.4" - ast-types-flow "^0.0.7" - axe-core "^4.3.5" - axobject-query "^2.2.0" - damerau-levenshtein "^1.0.7" + version "6.10.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.0.tgz#36fb9dead91cafd085ddbe3829602fb10ef28339" + integrity sha512-ySOHvXX8eSN6zz8Bywacm7CvGNhUtdjvqfQDVe6020TUK34Cywkw7m0KsCCk1Qtm9G1FayfTN1/7mMYnYO2Bhg== + dependencies: + aria-query "~5.1.3" + array-includes "^3.1.8" + array.prototype.flatmap "^1.3.2" + ast-types-flow "^0.0.8" + axe-core "^4.10.0" + axobject-query "^4.1.0" + damerau-levenshtein "^1.0.8" emoji-regex "^9.2.2" - has "^1.0.3" - jsx-ast-utils "^3.2.1" - language-tags "^1.0.5" - minimatch "^3.0.4" + es-iterator-helpers "^1.0.19" + hasown "^2.0.2" + jsx-ast-utils "^3.3.5" + language-tags "^1.0.9" + minimatch "^3.1.2" + object.fromentries "^2.0.8" + safe-regex-test "^1.0.3" + string.prototype.includes "^2.0.0" eslint-plugin-react-hooks@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.3.0.tgz#318dbf312e06fab1c835a4abef00121751ac1172" - integrity sha512-XslZy0LnMn+84NEG9jSGR6eGqaZB3133L8xewQo3fQagbQuGt7a63gf+P1NGKZavEYEC3UXaWEAA/AqDkuN6xA== + version "4.6.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz#c829eb06c0e6f484b3fbb85a97e57784f328c596" + integrity sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ== eslint-plugin-react@^7.27.1: - version "7.28.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.28.0.tgz#8f3ff450677571a659ce76efc6d80b6a525adbdf" - integrity sha512-IOlFIRHzWfEQQKcAD4iyYDndHwTQiCMcJVJjxempf203jnNLUnW34AXLrV33+nEXoifJE2ZEGmcjKPL8957eSw== - dependencies: - array-includes "^3.1.4" - array.prototype.flatmap "^1.2.5" + version "7.37.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.37.1.tgz#56493d7d69174d0d828bc83afeffe96903fdadbd" + integrity sha512-xwTnwDqzbDRA8uJ7BMxPs/EXRB3i8ZfnOIp8BsxEQkT0nHPp+WWceqGgo6rKb9ctNi8GJLDT4Go5HAWELa/WMg== + dependencies: + array-includes "^3.1.8" + array.prototype.findlast "^1.2.5" + array.prototype.flatmap "^1.3.2" + array.prototype.tosorted "^1.1.4" doctrine "^2.1.0" + es-iterator-helpers "^1.0.19" estraverse "^5.3.0" + hasown "^2.0.2" jsx-ast-utils "^2.4.1 || ^3.0.0" - minimatch "^3.0.4" - object.entries "^1.1.5" - object.fromentries "^2.0.5" - object.hasown "^1.1.0" - object.values "^1.1.5" - prop-types "^15.7.2" - resolve "^2.0.0-next.3" - semver "^6.3.0" - string.prototype.matchall "^4.0.6" + minimatch "^3.1.2" + object.entries "^1.1.8" + object.fromentries "^2.0.8" + object.values "^1.2.0" + prop-types "^15.8.1" + resolve "^2.0.0-next.5" + semver "^6.3.1" + string.prototype.matchall "^4.0.11" + string.prototype.repeat "^1.0.0" eslint-plugin-testing-library@^5.0.1: - version "5.0.4" - resolved "https://registry.yarnpkg.com/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.0.4.tgz#1f18b6e7d51db8452203bcbc909efbb571e964b8" - integrity sha512-zA/NfAENCsJXujvwwiap5gsqLp2U6X7m2XA5nOksl4zzb6GpUmRNAleCll58rEP0brFVj7DZBprlIlMGIhoC7Q== + version "5.11.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.11.1.tgz#5b46cdae96d4a78918711c0b4792f90088e62d20" + integrity sha512-5eX9e1Kc2PqVRed3taaLnAAqPZGEX75C+M/rXzUAI3wIg/ZxzUm1OVAwfe/O+vE+6YXOLetSe9g5GKD2ecXipw== dependencies: - "@typescript-eslint/experimental-utils" "^5.9.0" + "@typescript-eslint/utils" "^5.58.0" eslint-scope@5.1.1, eslint-scope@^5.1.1: version "5.1.1" @@ -4832,115 +5060,102 @@ eslint-scope@5.1.1, eslint-scope@^5.1.1: esrecurse "^4.3.0" estraverse "^4.1.1" -eslint-scope@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.0.tgz#c1f6ea30ac583031f203d65c73e723b01298f153" - integrity sha512-aWwkhnS0qAXqNOgKOK0dJ2nvzEbhEvpy8OlJ9kZ0FeZnA6zpjv1/Vei+puGFFX7zkPCkHHXb7IDX3A+7yPrRWg== +eslint-scope@^7.2.2: + version "7.2.2" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" + integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== dependencies: esrecurse "^4.3.0" estraverse "^5.2.0" -eslint-utils@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" - integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== - dependencies: - eslint-visitor-keys "^2.0.0" - -eslint-visitor-keys@^2.0.0, eslint-visitor-keys@^2.1.0: +eslint-visitor-keys@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== -eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.1.0, eslint-visitor-keys@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.2.0.tgz#6fbb166a6798ee5991358bc2daa1ba76cc1254a1" - integrity sha512-IOzT0X126zn7ALX0dwFiUQEdsfzrm4+ISsQS8nukaJXwEyYKRSnEIIDULYg1mCtGp7UUXgfGl7BIolXREQK+XQ== - -eslint-visitor-keys@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" - integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== eslint-webpack-plugin@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/eslint-webpack-plugin/-/eslint-webpack-plugin-3.1.1.tgz#83dad2395e5f572d6f4d919eedaa9cf902890fcb" - integrity sha512-xSucskTN9tOkfW7so4EaiFIkulWLXwCB/15H917lR6pTv0Zot6/fetFucmENRb7J5whVSFKIvwnrnsa78SG2yg== + version "3.2.0" + resolved "https://registry.yarnpkg.com/eslint-webpack-plugin/-/eslint-webpack-plugin-3.2.0.tgz#1978cdb9edc461e4b0195a20da950cf57988347c" + integrity sha512-avrKcGncpPbPSUHX6B3stNGzkKFto3eL+DKM4+VyMrVnhPc3vRczVlCq3uhuFOdRvDHTVXuzwk1ZKUrqDQHQ9w== dependencies: - "@types/eslint" "^7.28.2" - jest-worker "^27.3.1" - micromatch "^4.0.4" + "@types/eslint" "^7.29.0 || ^8.4.1" + jest-worker "^28.0.2" + micromatch "^4.0.5" normalize-path "^3.0.0" - schema-utils "^3.1.1" + schema-utils "^4.0.0" eslint@^8.3.0: - version "8.8.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.8.0.tgz#9762b49abad0cb4952539ffdb0a046392e571a2d" - integrity sha512-H3KXAzQGBH1plhYS3okDix2ZthuYJlQQEGE5k0IKuEqUSiyu4AmxxlJ2MtTYeJ3xB4jDhcYCwGOg2TXYdnDXlQ== - dependencies: - "@eslint/eslintrc" "^1.0.5" - "@humanwhocodes/config-array" "^0.9.2" - ajv "^6.10.0" + version "8.57.1" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.1.tgz#7df109654aba7e3bbe5c8eae533c5e461d3c6ca9" + integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.6.1" + "@eslint/eslintrc" "^2.1.4" + "@eslint/js" "8.57.1" + "@humanwhocodes/config-array" "^0.13.0" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + "@ungap/structured-clone" "^1.2.0" + ajv "^6.12.4" chalk "^4.0.0" cross-spawn "^7.0.2" debug "^4.3.2" doctrine "^3.0.0" escape-string-regexp "^4.0.0" - eslint-scope "^7.1.0" - eslint-utils "^3.0.0" - eslint-visitor-keys "^3.2.0" - espree "^9.3.0" - esquery "^1.4.0" + eslint-scope "^7.2.2" + eslint-visitor-keys "^3.4.3" + espree "^9.6.1" + esquery "^1.4.2" esutils "^2.0.2" fast-deep-equal "^3.1.3" file-entry-cache "^6.0.1" - functional-red-black-tree "^1.0.1" - glob-parent "^6.0.1" - globals "^13.6.0" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.19.0" + graphemer "^1.4.0" ignore "^5.2.0" - import-fresh "^3.0.0" imurmurhash "^0.1.4" is-glob "^4.0.0" + is-path-inside "^3.0.3" js-yaml "^4.1.0" json-stable-stringify-without-jsonify "^1.0.1" levn "^0.4.1" lodash.merge "^4.6.2" - minimatch "^3.0.4" + minimatch "^3.1.2" natural-compare "^1.4.0" - optionator "^0.9.1" - regexpp "^3.2.0" + optionator "^0.9.3" strip-ansi "^6.0.1" - strip-json-comments "^3.1.0" text-table "^0.2.0" - v8-compile-cache "^2.0.3" -espree@^9.0.0: - version "9.4.0" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.4.0.tgz#cd4bc3d6e9336c433265fc0aa016fc1aaf182f8a" - integrity sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw== +espree@^9.0.0, espree@^9.6.0, espree@^9.6.1: + version "9.6.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" + integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== dependencies: - acorn "^8.8.0" + acorn "^8.9.0" acorn-jsx "^5.3.2" - eslint-visitor-keys "^3.3.0" + eslint-visitor-keys "^3.4.1" -espree@^9.2.0, espree@^9.3.0: - version "9.3.0" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.0.tgz#c1240d79183b72aaee6ccfa5a90bc9111df085a8" - integrity sha512-d/5nCsb0JcqsSEeQzFZ8DH1RmxPcglRWh24EFTlUEmCKoehXGdpsx0RkHDubqUI8LSAIKMQp4r9SzQ3n+sm4HQ== - dependencies: - acorn "^8.7.0" - acorn-jsx "^5.3.1" - eslint-visitor-keys "^3.1.0" +esprima@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-1.2.2.tgz#76a0fd66fcfe154fd292667dc264019750b1657b" + integrity sha512-+JpPZam9w5DuJ3Q67SqsMGtiHKENSMRVoxvArfJZK01/BfLEObtZ6orJa/MtoGNR/rfMgp5837T41PAmTwAv/A== esprima@^4.0.0, esprima@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -esquery@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" - integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== +esquery@^1.4.2: + version "1.6.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" + integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== dependencies: estraverse "^5.1.0" @@ -5014,49 +5229,49 @@ execa@^5.0.0: exit@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" - integrity sha1-BjJjj42HfMghB9MKD/8aF8uhzQw= + integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== -expect@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/expect/-/expect-27.4.6.tgz#f335e128b0335b6ceb4fcab67ece7cbd14c942e6" - integrity sha512-1M/0kAALIaj5LaG66sFJTbRsWTADnylly82cu4bspI0nl+pgP4E6Bh/aqdHlTUjul06K7xQnnrAoqfxVU0+/ag== +expect@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/expect/-/expect-27.5.1.tgz#83ce59f1e5bdf5f9d2b94b61d2050db48f3fef74" + integrity sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw== dependencies: - "@jest/types" "^27.4.2" - jest-get-type "^27.4.0" - jest-matcher-utils "^27.4.6" - jest-message-util "^27.4.6" + "@jest/types" "^27.5.1" + jest-get-type "^27.5.1" + jest-matcher-utils "^27.5.1" + jest-message-util "^27.5.1" -express@^4.17.1: - version "4.19.2" - resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" - integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== +express@^4.17.3: + version "4.21.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.21.1.tgz#9dae5dda832f16b4eec941a4e44aa89ec481b281" + integrity sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ== dependencies: accepts "~1.3.8" array-flatten "1.1.1" - body-parser "1.20.2" + body-parser "1.20.3" content-disposition "0.5.4" content-type "~1.0.4" - cookie "0.6.0" + cookie "0.7.1" cookie-signature "1.0.6" debug "2.6.9" depd "2.0.0" - encodeurl "~1.0.2" + encodeurl "~2.0.0" escape-html "~1.0.3" etag "~1.8.1" - finalhandler "1.2.0" + finalhandler "1.3.1" fresh "0.5.2" http-errors "2.0.0" - merge-descriptors "1.0.1" + merge-descriptors "1.0.3" methods "~1.1.2" on-finished "2.4.1" parseurl "~1.3.3" - path-to-regexp "0.1.7" + path-to-regexp "0.1.10" proxy-addr "~2.0.7" - qs "6.11.0" + qs "6.13.0" range-parser "~1.2.1" safe-buffer "5.2.1" - send "0.18.0" - serve-static "1.15.0" + send "0.19.0" + serve-static "1.16.2" setprototypeof "1.2.0" statuses "2.0.1" type-is "~1.6.18" @@ -5082,10 +5297,10 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@^3.0.3, fast-glob@^3.2.11, fast-glob@^3.2.9: - version "3.2.11" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9" - integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew== +fast-glob@^3.0.3, fast-glob@^3.2.9, fast-glob@^3.3.0: + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" @@ -5101,12 +5316,17 @@ fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" - integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +fast-uri@^3.0.1: + version "3.0.3" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.3.tgz#892a1c91802d5d7860de728f18608a0573142241" + integrity sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw== fastq@^1.6.0: - version "1.13.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" - integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw== + version "1.17.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" + integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== dependencies: reusify "^1.0.4" @@ -5125,9 +5345,9 @@ faye-websocket@^0.11.3: websocket-driver ">=0.5.1" fb-watchman@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz#fc84fb39d2709cf3ff6d743706157bb5708a8a85" - integrity sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg== + version "2.0.2" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" + integrity sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA== dependencies: bser "2.1.1" @@ -5160,10 +5380,10 @@ file-selector@^0.4.0: dependencies: tslib "^2.0.3" -filelist@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.3.tgz#448607750376484932f67ef1b9ff07386b036c83" - integrity sha512-LwjCsruLWQULGYKy7TX0OPtrL9kLpojOFKc5VCTxdFTV7w5zbsgqVKfnkKG7Qgjtq50gKfO56hJv88OfcGb70Q== +filelist@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5" + integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q== dependencies: minimatch "^5.0.1" @@ -5184,13 +5404,13 @@ filter-obj@^1.1.0: resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-1.1.0.tgz#9b311112bc6c6127a16e016c6c5d7f19e0805c5b" integrity sha1-mzERErxsYSehbgFsbF1/GeCAXFs= -finalhandler@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" - integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== +finalhandler@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019" + integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== dependencies: debug "2.6.9" - encodeurl "~1.0.2" + encodeurl "~2.0.0" escape-html "~1.0.3" on-finished "2.4.1" parseurl "~1.3.3" @@ -5211,13 +5431,6 @@ find-root@^1.1.0: resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== -find-up@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" - integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= - dependencies: - locate-path "^2.0.0" - find-up@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" @@ -5242,17 +5455,18 @@ find-up@^5.0.0: path-exists "^4.0.0" flat-cache@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" - integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + version "3.2.0" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" + integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== dependencies: - flatted "^3.1.0" + flatted "^3.2.9" + keyv "^4.5.3" rimraf "^3.0.2" -flatted@^3.1.0: - version "3.2.5" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3" - integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg== +flatted@^3.2.9: + version "3.3.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" + integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== focus-lock@^1.3.5: version "1.3.5" @@ -5262,14 +5476,29 @@ focus-lock@^1.3.5: tslib "^2.0.3" follow-redirects@^1.0.0: - version "1.15.6" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" - integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== + version "1.15.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" + integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== + +for-each@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== + dependencies: + is-callable "^1.1.3" + +foreground-child@^3.1.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.0.tgz#0ac8644c06e431439f8561db8ecf29a7b5519c77" + integrity sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg== + dependencies: + cross-spawn "^7.0.0" + signal-exit "^4.0.1" fork-ts-checker-webpack-plugin@^6.5.0: - version "6.5.0" - resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.0.tgz#0282b335fa495a97e167f69018f566ea7d2a2b5e" - integrity sha512-cS178Y+xxtIjEUorcHddKS7yCMlrDPV31mt47blKKRfMd70Kxu5xruAFE2o9sDY6wVC5deuob/u/alD04YYHnw== + version "6.5.3" + resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz#eda2eff6e22476a2688d10661688c47f611b37f3" + integrity sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ== dependencies: "@babel/code-frame" "^7.8.3" "@types/json-schema" "^7.0.5" @@ -5286,9 +5515,9 @@ fork-ts-checker-webpack-plugin@^6.5.0: tapable "^1.0.0" form-data@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" - integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== + version "3.0.2" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.2.tgz#83ad9ced7c03feaad97e293d6f6091011e1659c8" + integrity sha512-sJe+TQb2vIaIyO783qN6BlMYWMw3WBOHA1Ay2qxsnjuafEOQFJ2JakedOQirT6D5XPRxDvS7AHYyem9fTpb4LQ== dependencies: asynckit "^0.4.0" combined-stream "^1.0.8" @@ -5304,10 +5533,10 @@ forwarded@0.2.0: resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== -fraction.js@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.1.2.tgz#13e420a92422b6cf244dff8690ed89401029fbe8" - integrity sha512-o2RiJQ6DZaR/5+Si0qJUIy637QMRudSi9kU/FFzx9EZazrIdnBgpU+3sEWCxAVhH2RtxW2Oz+T4p2o8uOPVcgA== +fraction.js@^4.3.7: + version "4.3.7" + resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" + integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== fresh@0.5.2: version "0.5.2" @@ -5315,9 +5544,9 @@ fresh@0.5.2: integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== fs-extra@^10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.0.tgz#9ff61b655dde53fb34a82df84bb214ce802e17c1" - integrity sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ== + version "10.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" + integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== dependencies: graceful-fs "^4.2.0" jsonfile "^6.0.1" @@ -5342,15 +5571,10 @@ fs-extra@^9.0.0, fs-extra@^9.0.1: jsonfile "^6.0.1" universalify "^2.0.0" -fs-monkey@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.3.tgz#ae3ac92d53bb328efe0e9a1d9541f6ad8d48e2d3" - integrity sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q== - fs-monkey@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.5.tgz#fe450175f0db0d7ea758102e1d84096acb925788" - integrity sha512-8uMbBjrhzW76TYgEV27Y5E//W2f/lTFmx78P2w19FZSxarhI/798APGQyuGCwmkNxgwGRhrLfvWyLBvNtuOmew== + version "1.0.6" + resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.6.tgz#8ead082953e88d992cf3ff844faa907b26756da2" + integrity sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg== fs.realpath@^1.0.0: version "1.0.0" @@ -5358,19 +5582,29 @@ fs.realpath@^1.0.0: integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== fsevents@^2.3.2, fsevents@~2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== -functional-red-black-tree@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" - integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= +function.prototype.name@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" + integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + functions-have-names "^1.2.3" + +functions-have-names@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== gensync@^1.0.0-beta.2: version "1.0.0-beta.2" @@ -5382,23 +5616,16 @@ get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.0.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.3.tgz#063c84329ad93e83893c7f4f243ef63ffa351385" - integrity sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A== +get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== dependencies: - function-bind "^1.1.1" - has "^1.0.3" + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" has-symbols "^1.0.3" - -get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" - integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== - dependencies: - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.1" + hasown "^2.0.0" get-nonce@^1.0.0: version "1.0.1" @@ -5420,13 +5647,14 @@ get-stream@^6.0.0: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== -get-symbol-description@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" - integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== +get-symbol-description@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" + integrity sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg== dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.1.1" + call-bind "^1.0.5" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" @@ -5435,7 +5663,7 @@ glob-parent@^5.1.2, glob-parent@~5.1.2: dependencies: is-glob "^4.0.1" -glob-parent@^6.0.1, glob-parent@^6.0.2: +glob-parent@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== @@ -5447,19 +5675,19 @@ glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@^7.1.1, glob@^7.1.2, glob@^7.1.4, glob@^7.1.6: - version "7.2.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" - integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== +glob@^10.3.10: + version "10.4.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" + integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" + foreground-child "^3.1.0" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^1.11.1" -glob@^7.1.3: +glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -5472,9 +5700,9 @@ glob@^7.1.3: path-is-absolute "^1.0.0" glob@^8.0.0: - version "8.0.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-8.0.3.tgz#415c6eb2deed9e502c68fa44a272e6da6eeca42e" - integrity sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ== + version "8.1.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" @@ -5503,13 +5731,21 @@ globals@^11.1.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -globals@^13.6.0, globals@^13.9.0: - version "13.12.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.12.0.tgz#4d733760304230a0082ed96e21e5c565f898089e" - integrity sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg== +globals@^13.19.0: + version "13.24.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" + integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== dependencies: type-fest "^0.20.2" +globalthis@^1.0.3, globalthis@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" + integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== + dependencies: + define-properties "^1.2.1" + gopd "^1.0.1" + globby@10.0.1: version "10.0.1" resolved "https://registry.yarnpkg.com/globby/-/globby-10.0.1.tgz#4782c34cb75dd683351335c5829cc3420e606b22" @@ -5524,7 +5760,7 @@ globby@10.0.1: merge2 "^1.2.3" slash "^3.0.0" -globby@^11.0.1, globby@^11.0.4: +globby@^11.0.4, globby@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== @@ -5536,25 +5772,27 @@ globby@^11.0.1, globby@^11.0.4: merge2 "^1.4.1" slash "^3.0.0" -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6: - version "4.2.9" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" - integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== - -graceful-fs@^4.1.9: - version "4.2.10" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" - integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" -graceful-fs@^4.2.11: +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + graphql@^15.5.1: - version "15.8.0" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.8.0.tgz#33410e96b012fa3bdb1091cc99a94769db212b38" - integrity sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw== + version "15.9.0" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.9.0.tgz#4e8ca830cfd30b03d44d3edd9cac2b0690304b53" + integrity sha512-GCOQdvm7XxV1S4U4CGrsdlEN37245eC8P9zaYCMr6K1BG0IPGy5lUwmJsEOGyl1GD6HXjOtl2keCP9asRBwNvA== gzip-size@^6.0.0: version "6.0.0" @@ -5573,44 +5811,51 @@ harmony-reflect@^1.4.6: resolved "https://registry.yarnpkg.com/harmony-reflect/-/harmony-reflect-1.6.2.tgz#31ecbd32e648a34d030d86adb67d4d47547fe710" integrity sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g== -has-bigints@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" - integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA== +has-bigints@^1.0.1, has-bigints@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" + integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== has-flag@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-symbols@^1.0.1, has-symbols@^1.0.2: +has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" - integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1, has-proto@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== -has-symbols@^1.0.3: +has-symbols@^1.0.1, has-symbols@^1.0.2, has-symbols@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== -has-tostringtag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" - integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== +has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== dependencies: - has-symbols "^1.0.2" + has-symbols "^1.0.3" -has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== +hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== dependencies: - function-bind "^1.1.1" + function-bind "^1.1.2" hast-to-hyperscript@^9.0.0: version "9.0.1" @@ -5749,7 +5994,7 @@ hoopy@^0.1.4: hpack.js@^2.1.6: version "2.1.6" resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" - integrity sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI= + integrity sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ== dependencies: inherits "^2.0.1" obuf "^1.0.0" @@ -5764,9 +6009,9 @@ html-encoding-sniffer@^2.0.1: whatwg-encoding "^1.0.5" html-entities@^2.1.0, html-entities@^2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.3.2.tgz#760b404685cb1d794e4f4b744332e3b00dcfe488" - integrity sha512-c3Ab/url5ksaT0WyleslpBEthOzWhrjQbg75y7XUsfSzi3Dgzt0l8w5e7DylRn15MTlMMD58dTfzddNS2kcAjQ== + version "2.5.2" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.5.2.tgz#201a3cf95d3a15be7099521620d19dfb4f65359f" + integrity sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA== html-escaper@^2.0.0: version "2.0.2" @@ -5792,9 +6037,9 @@ html-void-elements@^1.0.0: integrity sha512-uE/TxKuyNIcx44cIWnjr/rfIATDH7ZaOMmstu0CwhFG1Dunhlp4OC6/NMbhiwoq5BpW0ubi303qnEk/PZj614w== html-webpack-plugin@^5.5.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz#c3911936f57681c1f9f4d8b68c158cd9dfe52f50" - integrity sha512-sy88PC2cRTVxvETRgUHFrL4No3UxvcH8G1NepGhqaTT+GXN2kTamqasot0inS5hXeg1cMbFDt27zzo9p35lZVw== + version "5.6.2" + resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.6.2.tgz#174a67c8e55aa3fa2ba94c8e8e42894bfe4978ea" + integrity sha512-q7xp/FO9RGBVoTKNItkdX1jKLscLFkgn/dLVFNYbHVbfHLBk6DYW5nsQ8kCzIWcgKP/kUBocetjvav6lD8YfCQ== dependencies: "@types/html-minifier-terser" "^6.0.0" html-minifier-terser "^6.0.2" @@ -5815,7 +6060,7 @@ htmlparser2@^6.1.0: http-deceiver@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" - integrity sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc= + integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw== http-errors@2.0.0: version "2.0.0" @@ -5831,7 +6076,7 @@ http-errors@2.0.0: http-errors@~1.6.2: version "1.6.3" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" - integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0= + integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== dependencies: depd "~1.1.2" inherits "2.0.3" @@ -5839,9 +6084,9 @@ http-errors@~1.6.2: statuses ">= 1.4.0 < 2" http-parser-js@>=0.5.1: - version "0.5.5" - resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.5.tgz#d7c30d5d3c90d865b4a2e870181f9d6f22ac7ac5" - integrity sha512-x+JVEkO2PoM8qqpbPbOL3cqHPwerep7OwzK7Ay+sMQjKzaKCqWvjoXm5tqMP9tXWWTnTzAjIhXg+J99XYuPhPA== + version "0.5.8" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" + integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== http-proxy-agent@^4.0.1: version "4.0.1" @@ -5852,10 +6097,10 @@ http-proxy-agent@^4.0.1: agent-base "6" debug "4" -http-proxy-middleware@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.2.tgz#94d7593790aad6b3de48164f13792262f656c332" - integrity sha512-XtmDN5w+vdFTBZaYhdJAbMqn0DP/EhkUaAeo963mojwpKMMbw6nivtFKw07D7DDOH745L5k0VL0P8KRYNEVF/g== +http-proxy-middleware@^2.0.3: + version "2.0.7" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz#915f236d92ae98ef48278a95dedf17e991936ec6" + integrity sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA== dependencies: "@types/http-proxy" "^1.17.8" http-proxy "^1.18.1" @@ -5873,9 +6118,9 @@ http-proxy@^1.18.1: requires-port "^1.0.0" https-proxy-agent@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" - integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== dependencies: agent-base "6" debug "4" @@ -5904,15 +6149,15 @@ icss-utils@^5.0.0, icss-utils@^5.1.0: resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== -idb@^6.1.4: - version "6.1.5" - resolved "https://registry.yarnpkg.com/idb/-/idb-6.1.5.tgz#dbc53e7adf1ac7c59f9b2bf56e00b4ea4fce8c7b" - integrity sha512-IJtugpKkiVXQn5Y+LteyBCNk1N8xpGV3wWZk9EVtZWH8DYkjBn0bX1XnGP9RkyZF0sAcywa6unHqSWKe7q4LGw== +idb@^7.0.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.1.tgz#d910ded866d32c7ced9befc5bfdf36f572ced72b" + integrity sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ== identity-obj-proxy@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz#94d2bda96084453ef36fbc5aaec37e0f79f1fc14" - integrity sha1-lNK9qWCERT7zb7xarsN+D3nx/BQ= + integrity sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA== dependencies: harmony-reflect "^1.4.6" @@ -5921,22 +6166,17 @@ ieee754@^1.1.13: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -ignore@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" - integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== - -ignore@^5.1.1, ignore@^5.1.8, ignore@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" - integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== +ignore@^5.1.1, ignore@^5.2.0: + version "5.3.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== immer@^9.0.7: - version "9.0.12" - resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.12.tgz#2d33ddf3ee1d247deab9d707ca472c8c942a0f20" - integrity sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA== + version "9.0.21" + resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.21.tgz#1e025ea31a40f24fb064f1fef23e931496330176" + integrity sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA== -import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1: +import-fresh@^3.1.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== @@ -5945,9 +6185,9 @@ import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1: resolve-from "^4.0.0" import-local@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" - integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== + version "3.2.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.2.0.tgz#c3d5c745798c02a6f8b897726aba5100186ee260" + integrity sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA== dependencies: pkg-dir "^4.2.0" resolve-cwd "^3.0.0" @@ -5955,7 +6195,7 @@ import-local@^3.0.2: imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== indent-string@^4.0.0: version "4.0.0" @@ -5978,7 +6218,7 @@ inherits@2, inherits@2.0.4, inherits@^2.0.0, inherits@^2.0.1, inherits@^2.0.3, i inherits@2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== ini@^1.3.5: version "1.3.8" @@ -5991,9 +6231,9 @@ inline-style-parser@0.1.1: integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q== inquirer@^8.2.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.0.tgz#f44f008dd344bbfc4b30031f45d984e034a3ac3a" - integrity sha512-0crLweprevJ02tTuA6ThpoAERAGyVILC4sS74uib58Xf/zSr1/ZWtmm7D5CI+bSQEaA04f0K7idaHpQbSWgiVQ== + version "8.2.6" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.6.tgz#733b74888195d8d400a67ac332011b5fae5ea562" + integrity sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg== dependencies: ansi-escapes "^4.2.1" chalk "^4.1.1" @@ -6005,23 +6245,24 @@ inquirer@^8.2.0: mute-stream "0.0.8" ora "^5.4.1" run-async "^2.4.0" - rxjs "^7.2.0" + rxjs "^7.5.5" string-width "^4.1.0" strip-ansi "^6.0.0" through "^2.3.6" + wrap-ansi "^6.0.1" inter-ui@^3.19.3: version "3.19.3" resolved "https://registry.yarnpkg.com/inter-ui/-/inter-ui-3.19.3.tgz#cf4b4b6d30de8d5463e2462588654b325206488c" integrity sha512-5FG9fjuYOXocIfjzcCBhICL5cpvwEetseL3FU6tP3d6Bn7g8wODhB+I9RNGRTizCT7CUG4GOK54OPxqq3msQgg== -internal-slot@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" - integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA== +internal-slot@^1.0.4, internal-slot@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" + integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== dependencies: - get-intrinsic "^1.1.0" - has "^1.0.3" + es-errors "^1.3.0" + hasown "^2.0.0" side-channel "^1.0.4" invariant@^2.2.4: @@ -6031,20 +6272,15 @@ invariant@^2.2.4: dependencies: loose-envify "^1.0.0" -ip@^1.1.0: - version "1.1.9" - resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.9.tgz#8dfbcc99a754d07f425310b86a99546b1151e396" - integrity sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ== - ipaddr.js@1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== ipaddr.js@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.0.1.tgz#eca256a7a877e917aeb368b0a7497ddf42ef81c0" - integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng== + version "2.2.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.2.0.tgz#d33fa7bac284f4de7af949638c9d68157c6b92e8" + integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA== is-alphabetical@^1.0.0: version "1.0.4" @@ -6059,7 +6295,7 @@ is-alphanumerical@^1.0.0: is-alphabetical "^1.0.0" is-decimal "^1.0.0" -is-arguments@^1.0.4: +is-arguments@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== @@ -6067,10 +6303,25 @@ is-arguments@^1.0.4: call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-array-buffer@^3.0.2, is-array-buffer@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" + integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.1" + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + +is-async-function@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.0.0.tgz#8e4418efd3e5d3a6ebb0164c05ef5afb69aa9646" + integrity sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA== + dependencies: + has-tostringtag "^1.0.0" is-bigint@^1.0.1: version "1.0.4" @@ -6099,19 +6350,26 @@ is-buffer@^2.0.0: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== -is-callable@^1.1.4, is-callable@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945" - integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== +is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== -is-core-module@^2.2.0, is-core-module@^2.8.0, is-core-module@^2.8.1: - version "2.8.1" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211" - integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA== +is-core-module@^2.13.0, is-core-module@^2.15.1, is-core-module@^2.8.1: + version "2.15.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37" + integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== dependencies: - has "^1.0.3" + hasown "^2.0.2" -is-date-object@^1.0.1: +is-data-view@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.1.tgz#4b4d3a511b70f3dc26d42c03ca9ca515d847759f" + integrity sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w== + dependencies: + is-typed-array "^1.1.13" + +is-date-object@^1.0.1, is-date-object@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== @@ -6131,7 +6389,14 @@ is-docker@^2.0.0, is-docker@^2.1.1: is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-finalizationregistry@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz#c8749b65f17c133313e661b1289b95ad3dbd62e6" + integrity sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw== + dependencies: + call-bind "^1.0.2" is-fullwidth-code-point@^3.0.0: version "3.0.0" @@ -6143,6 +6408,13 @@ is-generator-fn@^2.0.0: resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== +is-generator-function@^1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" + integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== + dependencies: + has-tostringtag "^1.0.0" + is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" @@ -6160,25 +6432,30 @@ is-interactive@^1.0.0: resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== +is-map@^2.0.2, is-map@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" + integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== + is-module@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" - integrity sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE= + integrity sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g== -is-negative-zero@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" - integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== +is-negative-zero@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" + integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== is-node-process@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-node-process/-/is-node-process-1.0.1.tgz#4fc7ac3a91e8aac58175fe0578abbc56f2831b23" - integrity sha512-5IcdXuf++TTNt3oGl9EBdkvndXA8gmc4bz/Y+mdEpWh3Mcn/+kOw6hI7LD5CocqJWMzeb0I0ClndRVNdEPuJXQ== + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-node-process/-/is-node-process-1.2.0.tgz#ea02a1b90ddb3934a19aea414e88edef7e11d134" + integrity sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw== is-number-object@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.6.tgz#6a7aaf838c7f0686a50b4553f7e54a96494e89f0" - integrity sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g== + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" + integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== dependencies: has-tostringtag "^1.0.0" @@ -6190,14 +6467,9 @@ is-number@^7.0.0: is-obj@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" - integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= - -is-path-cwd@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" - integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== + integrity sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg== -is-path-inside@^3.0.2: +is-path-inside@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== @@ -6234,7 +6506,7 @@ is-reference@^1.2.1: dependencies: "@types/estree" "*" -is-regex@^1.0.4, is-regex@^1.1.4: +is-regex@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== @@ -6245,17 +6517,24 @@ is-regex@^1.0.4, is-regex@^1.1.4: is-regexp@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" - integrity sha1-/S2INUXEa6xaYz57mgnof6LLUGk= + integrity sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA== is-root@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-root/-/is-root-2.1.0.tgz#809e18129cf1129644302a4f8544035d51984a9c" integrity sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg== -is-shared-array-buffer@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz#97b0c85fbdacb59c9c446fe653b82cf2b5b7cfe6" - integrity sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA== +is-set@^2.0.2, is-set@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" + integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== + +is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz#1237f1cba059cdb62431d378dcc37d9680181688" + integrity sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg== + dependencies: + call-bind "^1.0.7" is-stream@^2.0.0: version "2.0.1" @@ -6276,23 +6555,43 @@ is-symbol@^1.0.2, is-symbol@^1.0.3: dependencies: has-symbols "^1.0.2" +is-typed-array@^1.1.13: + version "1.1.13" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" + integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== + dependencies: + which-typed-array "^1.1.14" + is-typedarray@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== is-unicode-supported@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== -is-weakref@^1.0.1: +is-weakmap@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd" + integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== + +is-weakref@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== dependencies: call-bind "^1.0.2" +is-weakset@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.3.tgz#e801519df8c0c43e12ff2834eead84ec9e624007" + integrity sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ== + dependencies: + call-bind "^1.0.7" + get-intrinsic "^1.2.4" + is-whitespace-character@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz#0858edd94a95594c7c9dd0b5c174ec6e45ee4aa7" @@ -6310,25 +6609,30 @@ is-wsl@^2.2.0: dependencies: is-docker "^2.0.0" +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" - integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== + version "3.2.2" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756" + integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg== istanbul-lib-instrument@^5.0.4, istanbul-lib-instrument@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.1.0.tgz#7b49198b657b27a730b8e9cb601f1e1bff24c59a" - integrity sha512-czwUz525rkOFDJxfKK6mYfIs9zBKILyrZQxjz3ABhjQXhbhFsSbo1HW/BFcsDnfJYJWA6thRR5/TUY2qs5W99Q== + version "5.2.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz#d10c8885c2125574e1c231cacadf955675e1ce3d" + integrity sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg== dependencies: "@babel/core" "^7.12.3" "@babel/parser" "^7.14.7" @@ -6337,12 +6641,12 @@ istanbul-lib-instrument@^5.0.4, istanbul-lib-instrument@^5.1.0: semver "^6.3.0" istanbul-lib-report@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6" - integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw== + version "3.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz#908305bac9a5bd175ac6a74489eafd0fc2445a7d" + integrity sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw== dependencies: istanbul-lib-coverage "^3.0.0" - make-dir "^3.0.0" + make-dir "^4.0.0" supports-color "^7.1.0" istanbul-lib-source-maps@^4.0.0: @@ -6355,104 +6659,126 @@ istanbul-lib-source-maps@^4.0.0: source-map "^0.6.1" istanbul-reports@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.3.tgz#4bcae3103b94518117930d51283690960b50d3c2" - integrity sha512-x9LtDVtfm/t1GFiLl3NffC7hz+I1ragvgX1P/Lg1NlIagifZDKUkuuaAxH/qpwj2IuEfD8G2Bs/UKp+sZ/pKkg== + version "3.1.7" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.7.tgz#daed12b9e1dca518e15c056e1e537e741280fa0b" + integrity sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g== dependencies: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" +iterator.prototype@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.3.tgz#016c2abe0be3bbdb8319852884f60908ac62bf9c" + integrity sha512-FW5iMbeQ6rBGm/oKgzq2aW4KvAGpxPzYES8N4g4xNXUKpL1mclMvOe+76AcLDTvD+Ze+sOpVhgdAQEKF4L9iGQ== + dependencies: + define-properties "^1.2.1" + get-intrinsic "^1.2.1" + has-symbols "^1.0.3" + reflect.getprototypeof "^1.0.4" + set-function-name "^2.0.1" + +jackspeak@^3.1.2: + version "3.4.3" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" + integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + jake@^10.8.5: - version "10.8.5" - resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.5.tgz#f2183d2c59382cb274226034543b9c03b8164c46" - integrity sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw== + version "10.9.2" + resolved "https://registry.yarnpkg.com/jake/-/jake-10.9.2.tgz#6ae487e6a69afec3a5e167628996b59f35ae2b7f" + integrity sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA== dependencies: async "^3.2.3" chalk "^4.0.2" - filelist "^1.0.1" - minimatch "^3.0.4" + filelist "^1.0.4" + minimatch "^3.1.2" -jest-changed-files@^27.4.2: - version "27.4.2" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-27.4.2.tgz#da2547ea47c6e6a5f6ed336151bd2075736eb4a5" - integrity sha512-/9x8MjekuzUQoPjDHbBiXbNEBauhrPU2ct7m8TfCg69ywt1y/N+yYwGh3gCpnqUS3klYWDU/lSNgv+JhoD2k1A== +jest-changed-files@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-27.5.1.tgz#a348aed00ec9bf671cc58a66fcbe7c3dfd6a68f5" + integrity sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw== dependencies: - "@jest/types" "^27.4.2" + "@jest/types" "^27.5.1" execa "^5.0.0" throat "^6.0.1" -jest-circus@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-27.4.6.tgz#d3af34c0eb742a967b1919fbb351430727bcea6c" - integrity sha512-UA7AI5HZrW4wRM72Ro80uRR2Fg+7nR0GESbSI/2M+ambbzVuA63mn5T1p3Z/wlhntzGpIG1xx78GP2YIkf6PhQ== +jest-circus@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-27.5.1.tgz#37a5a4459b7bf4406e53d637b49d22c65d125ecc" + integrity sha512-D95R7x5UtlMA5iBYsOHFFbMD/GVA4R/Kdq15f7xYWUfWHBto9NYRsOvnSauTgdF+ogCpJ4tyKOXhUifxS65gdw== dependencies: - "@jest/environment" "^27.4.6" - "@jest/test-result" "^27.4.6" - "@jest/types" "^27.4.2" + "@jest/environment" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/types" "^27.5.1" "@types/node" "*" chalk "^4.0.0" co "^4.6.0" dedent "^0.7.0" - expect "^27.4.6" + expect "^27.5.1" is-generator-fn "^2.0.0" - jest-each "^27.4.6" - jest-matcher-utils "^27.4.6" - jest-message-util "^27.4.6" - jest-runtime "^27.4.6" - jest-snapshot "^27.4.6" - jest-util "^27.4.2" - pretty-format "^27.4.6" + jest-each "^27.5.1" + jest-matcher-utils "^27.5.1" + jest-message-util "^27.5.1" + jest-runtime "^27.5.1" + jest-snapshot "^27.5.1" + jest-util "^27.5.1" + pretty-format "^27.5.1" slash "^3.0.0" stack-utils "^2.0.3" throat "^6.0.1" -jest-cli@^27.4.7: - version "27.4.7" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-27.4.7.tgz#d00e759e55d77b3bcfea0715f527c394ca314e5a" - integrity sha512-zREYhvjjqe1KsGV15mdnxjThKNDgza1fhDT+iUsXWLCq3sxe9w5xnvyctcYVT5PcdLSjv7Y5dCwTS3FCF1tiuw== +jest-cli@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-27.5.1.tgz#278794a6e6458ea8029547e6c6cbf673bd30b145" + integrity sha512-Hc6HOOwYq4/74/c62dEE3r5elx8wjYqxY0r0G/nFrLDPMFRu6RA/u8qINOIkvhxG7mMQ5EJsOGfRpI8L6eFUVw== dependencies: - "@jest/core" "^27.4.7" - "@jest/test-result" "^27.4.6" - "@jest/types" "^27.4.2" + "@jest/core" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/types" "^27.5.1" chalk "^4.0.0" exit "^0.1.2" - graceful-fs "^4.2.4" + graceful-fs "^4.2.9" import-local "^3.0.2" - jest-config "^27.4.7" - jest-util "^27.4.2" - jest-validate "^27.4.6" + jest-config "^27.5.1" + jest-util "^27.5.1" + jest-validate "^27.5.1" prompts "^2.0.1" yargs "^16.2.0" -jest-config@^27.4.7: - version "27.4.7" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-27.4.7.tgz#4f084b2acbd172c8b43aa4cdffe75d89378d3972" - integrity sha512-xz/o/KJJEedHMrIY9v2ParIoYSrSVY6IVeE4z5Z3i101GoA5XgfbJz+1C8EYPsv7u7f39dS8F9v46BHDhn0vlw== +jest-config@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-27.5.1.tgz#5c387de33dca3f99ad6357ddeccd91bf3a0e4a41" + integrity sha512-5sAsjm6tGdsVbW9ahcChPAFCk4IlkQUknH5AvKjuLTSlcO/wCZKyFdn7Rg0EkC+OGgWODEy2hDpWB1PgzH0JNA== dependencies: "@babel/core" "^7.8.0" - "@jest/test-sequencer" "^27.4.6" - "@jest/types" "^27.4.2" - babel-jest "^27.4.6" + "@jest/test-sequencer" "^27.5.1" + "@jest/types" "^27.5.1" + babel-jest "^27.5.1" chalk "^4.0.0" ci-info "^3.2.0" deepmerge "^4.2.2" glob "^7.1.1" - graceful-fs "^4.2.4" - jest-circus "^27.4.6" - jest-environment-jsdom "^27.4.6" - jest-environment-node "^27.4.6" - jest-get-type "^27.4.0" - jest-jasmine2 "^27.4.6" - jest-regex-util "^27.4.0" - jest-resolve "^27.4.6" - jest-runner "^27.4.6" - jest-util "^27.4.2" - jest-validate "^27.4.6" + graceful-fs "^4.2.9" + jest-circus "^27.5.1" + jest-environment-jsdom "^27.5.1" + jest-environment-node "^27.5.1" + jest-get-type "^27.5.1" + jest-jasmine2 "^27.5.1" + jest-regex-util "^27.5.1" + jest-resolve "^27.5.1" + jest-runner "^27.5.1" + jest-util "^27.5.1" + jest-validate "^27.5.1" micromatch "^4.0.4" - pretty-format "^27.4.6" + parse-json "^5.2.0" + pretty-format "^27.5.1" slash "^3.0.0" + strip-json-comments "^3.1.1" -jest-diff@^27.0.0, jest-diff@^27.4.6: +jest-diff@^27.0.0: version "27.4.6" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.4.6.tgz#93815774d2012a2cbb6cf23f84d48c7a2618f98d" integrity sha512-zjaB0sh0Lb13VyPsd92V7HkqF6yKRH9vm33rwBt7rPYrpQvS1nCvlIy2pICbKta+ZjWngYLNn4cCK4nyZkjS/w== @@ -6462,313 +6788,368 @@ jest-diff@^27.0.0, jest-diff@^27.4.6: jest-get-type "^27.4.0" pretty-format "^27.4.6" -jest-docblock@^27.4.0: - version "27.4.0" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-27.4.0.tgz#06c78035ca93cbbb84faf8fce64deae79a59f69f" - integrity sha512-7TBazUdCKGV7svZ+gh7C8esAnweJoG+SvcF6Cjqj4l17zA2q1cMwx2JObSioubk317H+cjcHgP+7fTs60paulg== +jest-diff@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.5.1.tgz#a07f5011ac9e6643cf8a95a462b7b1ecf6680def" + integrity sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw== + dependencies: + chalk "^4.0.0" + diff-sequences "^27.5.1" + jest-get-type "^27.5.1" + pretty-format "^27.5.1" + +jest-docblock@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-27.5.1.tgz#14092f364a42c6108d42c33c8cf30e058e25f6c0" + integrity sha512-rl7hlABeTsRYxKiUfpHrQrG4e2obOiTQWfMEH3PxPjOtdsfLQO4ReWSZaQ7DETm4xu07rl4q/h4zcKXyU0/OzQ== dependencies: detect-newline "^3.0.0" -jest-each@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-27.4.6.tgz#e7e8561be61d8cc6dbf04296688747ab186c40ff" - integrity sha512-n6QDq8y2Hsmn22tRkgAk+z6MCX7MeVlAzxmZDshfS2jLcaBlyhpF3tZSJLR+kXmh23GEvS0ojMR8i6ZeRvpQcA== +jest-each@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-27.5.1.tgz#5bc87016f45ed9507fed6e4702a5b468a5b2c44e" + integrity sha512-1Ff6p+FbhT/bXQnEouYy00bkNSY7OUpfIcmdl8vZ31A1UUaurOLPA8a8BbJOF2RDUElwJhmeaV7LnagI+5UwNQ== dependencies: - "@jest/types" "^27.4.2" + "@jest/types" "^27.5.1" chalk "^4.0.0" - jest-get-type "^27.4.0" - jest-util "^27.4.2" - pretty-format "^27.4.6" + jest-get-type "^27.5.1" + jest-util "^27.5.1" + pretty-format "^27.5.1" -jest-environment-jsdom@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-27.4.6.tgz#c23a394eb445b33621dfae9c09e4c8021dea7b36" - integrity sha512-o3dx5p/kHPbUlRvSNjypEcEtgs6LmvESMzgRFQE6c+Prwl2JLA4RZ7qAnxc5VM8kutsGRTB15jXeeSbJsKN9iA== +jest-environment-jsdom@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-27.5.1.tgz#ea9ccd1fc610209655a77898f86b2b559516a546" + integrity sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw== dependencies: - "@jest/environment" "^27.4.6" - "@jest/fake-timers" "^27.4.6" - "@jest/types" "^27.4.2" + "@jest/environment" "^27.5.1" + "@jest/fake-timers" "^27.5.1" + "@jest/types" "^27.5.1" "@types/node" "*" - jest-mock "^27.4.6" - jest-util "^27.4.2" + jest-mock "^27.5.1" + jest-util "^27.5.1" jsdom "^16.6.0" -jest-environment-node@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-27.4.6.tgz#ee8cd4ef458a0ef09d087c8cd52ca5856df90242" - integrity sha512-yfHlZ9m+kzTKZV0hVfhVu6GuDxKAYeFHrfulmy7Jxwsq4V7+ZK7f+c0XP/tbVDMQW7E4neG2u147hFkuVz0MlQ== +jest-environment-node@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-27.5.1.tgz#dedc2cfe52fab6b8f5714b4808aefa85357a365e" + integrity sha512-Jt4ZUnxdOsTGwSRAfKEnE6BcwsSPNOijjwifq5sDFSA2kesnXTvNqKHYgM0hDq3549Uf/KzdXNYn4wMZJPlFLw== dependencies: - "@jest/environment" "^27.4.6" - "@jest/fake-timers" "^27.4.6" - "@jest/types" "^27.4.2" + "@jest/environment" "^27.5.1" + "@jest/fake-timers" "^27.5.1" + "@jest/types" "^27.5.1" "@types/node" "*" - jest-mock "^27.4.6" - jest-util "^27.4.2" + jest-mock "^27.5.1" + jest-util "^27.5.1" -jest-get-type@^27.4.0: - version "27.4.0" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.4.0.tgz#7503d2663fffa431638337b3998d39c5e928e9b5" - integrity sha512-tk9o+ld5TWq41DkK14L4wox4s2D9MtTpKaAVzXfr5CUKm5ZK2ExcaFE0qls2W71zE/6R2TxxrK9w2r6svAFDBQ== +jest-get-type@^27.4.0, jest-get-type@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1" + integrity sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw== -jest-haste-map@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.4.6.tgz#c60b5233a34ca0520f325b7e2cc0a0140ad0862a" - integrity sha512-0tNpgxg7BKurZeFkIOvGCkbmOHbLFf4LUQOxrQSMjvrQaQe3l6E8x6jYC1NuWkGo5WDdbr8FEzUxV2+LWNawKQ== +jest-haste-map@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.5.1.tgz#9fd8bd7e7b4fa502d9c6164c5640512b4e811e7f" + integrity sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng== dependencies: - "@jest/types" "^27.4.2" + "@jest/types" "^27.5.1" "@types/graceful-fs" "^4.1.2" "@types/node" "*" anymatch "^3.0.3" fb-watchman "^2.0.0" - graceful-fs "^4.2.4" - jest-regex-util "^27.4.0" - jest-serializer "^27.4.0" - jest-util "^27.4.2" - jest-worker "^27.4.6" + graceful-fs "^4.2.9" + jest-regex-util "^27.5.1" + jest-serializer "^27.5.1" + jest-util "^27.5.1" + jest-worker "^27.5.1" micromatch "^4.0.4" walker "^1.0.7" optionalDependencies: fsevents "^2.3.2" -jest-jasmine2@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-27.4.6.tgz#109e8bc036cb455950ae28a018f983f2abe50127" - integrity sha512-uAGNXF644I/whzhsf7/qf74gqy9OuhvJ0XYp8SDecX2ooGeaPnmJMjXjKt0mqh1Rl5dtRGxJgNrHlBQIBfS5Nw== +jest-jasmine2@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-27.5.1.tgz#a037b0034ef49a9f3d71c4375a796f3b230d1ac4" + integrity sha512-jtq7VVyG8SqAorDpApwiJJImd0V2wv1xzdheGHRGyuT7gZm6gG47QEskOlzsN1PG/6WNaCo5pmwMHDf3AkG2pQ== dependencies: - "@jest/environment" "^27.4.6" - "@jest/source-map" "^27.4.0" - "@jest/test-result" "^27.4.6" - "@jest/types" "^27.4.2" + "@jest/environment" "^27.5.1" + "@jest/source-map" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/types" "^27.5.1" "@types/node" "*" chalk "^4.0.0" co "^4.6.0" - expect "^27.4.6" + expect "^27.5.1" is-generator-fn "^2.0.0" - jest-each "^27.4.6" - jest-matcher-utils "^27.4.6" - jest-message-util "^27.4.6" - jest-runtime "^27.4.6" - jest-snapshot "^27.4.6" - jest-util "^27.4.2" - pretty-format "^27.4.6" + jest-each "^27.5.1" + jest-matcher-utils "^27.5.1" + jest-message-util "^27.5.1" + jest-runtime "^27.5.1" + jest-snapshot "^27.5.1" + jest-util "^27.5.1" + pretty-format "^27.5.1" throat "^6.0.1" -jest-leak-detector@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-27.4.6.tgz#ed9bc3ce514b4c582637088d9faf58a33bd59bf4" - integrity sha512-kkaGixDf9R7CjHm2pOzfTxZTQQQ2gHTIWKY/JZSiYTc90bZp8kSZnUMS3uLAfwTZwc0tcMRoEX74e14LG1WapA== +jest-leak-detector@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-27.5.1.tgz#6ec9d54c3579dd6e3e66d70e3498adf80fde3fb8" + integrity sha512-POXfWAMvfU6WMUXftV4HolnJfnPOGEu10fscNCA76KBpRRhcMN2c8d3iT2pxQS3HLbA+5X4sOUPzYO2NUyIlHQ== dependencies: - jest-get-type "^27.4.0" - pretty-format "^27.4.6" + jest-get-type "^27.5.1" + pretty-format "^27.5.1" -jest-matcher-utils@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.4.6.tgz#53ca7f7b58170638590e946f5363b988775509b8" - integrity sha512-XD4PKT3Wn1LQnRAq7ZsTI0VRuEc9OrCPFiO1XL7bftTGmfNF0DcEwMHRgqiu7NGf8ZoZDREpGrCniDkjt79WbA== +jest-matcher-utils@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz#9c0cdbda8245bc22d2331729d1091308b40cf8ab" + integrity sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw== dependencies: chalk "^4.0.0" - jest-diff "^27.4.6" - jest-get-type "^27.4.0" - pretty-format "^27.4.6" + jest-diff "^27.5.1" + jest-get-type "^27.5.1" + pretty-format "^27.5.1" -jest-message-util@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-27.4.6.tgz#9fdde41a33820ded3127465e1a5896061524da31" - integrity sha512-0p5szriFU0U74czRSFjH6RyS7UYIAkn/ntwMuOwTGWrQIOh5NzXXrq72LOqIkJKKvFbPq+byZKuBz78fjBERBA== +jest-message-util@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-27.5.1.tgz#bdda72806da10d9ed6425e12afff38cd1458b6cf" + integrity sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g== dependencies: "@babel/code-frame" "^7.12.13" - "@jest/types" "^27.4.2" + "@jest/types" "^27.5.1" "@types/stack-utils" "^2.0.0" chalk "^4.0.0" - graceful-fs "^4.2.4" + graceful-fs "^4.2.9" micromatch "^4.0.4" - pretty-format "^27.4.6" + pretty-format "^27.5.1" slash "^3.0.0" stack-utils "^2.0.3" -jest-mock@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-27.4.6.tgz#77d1ba87fbd33ccb8ef1f061697e7341b7635195" - integrity sha512-kvojdYRkst8iVSZ1EJ+vc1RRD9llueBjKzXzeCytH3dMM7zvPV/ULcfI2nr0v0VUgm3Bjt3hBCQvOeaBz+ZTHw== +jest-message-util@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-28.1.3.tgz#232def7f2e333f1eecc90649b5b94b0055e7c43d" + integrity sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g== + dependencies: + "@babel/code-frame" "^7.12.13" + "@jest/types" "^28.1.3" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^28.1.3" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-mock@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-27.5.1.tgz#19948336d49ef4d9c52021d34ac7b5f36ff967d6" + integrity sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og== dependencies: - "@jest/types" "^27.4.2" + "@jest/types" "^27.5.1" "@types/node" "*" jest-pnp-resolver@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c" - integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w== + version "1.2.3" + resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz#930b1546164d4ad5937d5540e711d4d38d4cad2e" + integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== -jest-regex-util@^27.0.0, jest-regex-util@^27.4.0: - version "27.4.0" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-27.4.0.tgz#e4c45b52653128843d07ad94aec34393ea14fbca" - integrity sha512-WeCpMpNnqJYMQoOjm1nTtsgbR4XHAk1u00qDoNBQoykM280+/TmgA5Qh5giC1ecy6a5d4hbSsHzpBtu5yvlbEg== +jest-regex-util@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-27.5.1.tgz#4da143f7e9fd1e542d4aa69617b38e4a78365b95" + integrity sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg== -jest-resolve-dependencies@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-27.4.6.tgz#fc50ee56a67d2c2183063f6a500cc4042b5e2327" - integrity sha512-W85uJZcFXEVZ7+MZqIPCscdjuctruNGXUZ3OHSXOfXR9ITgbUKeHj+uGcies+0SsvI5GtUfTw4dY7u9qjTvQOw== +jest-regex-util@^28.0.0: + version "28.0.2" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-28.0.2.tgz#afdc377a3b25fb6e80825adcf76c854e5bf47ead" + integrity sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw== + +jest-resolve-dependencies@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-27.5.1.tgz#d811ecc8305e731cc86dd79741ee98fed06f1da8" + integrity sha512-QQOOdY4PE39iawDn5rzbIePNigfe5B9Z91GDD1ae/xNDlu9kaat8QQ5EKnNmVWPV54hUdxCVwwj6YMgR2O7IOg== dependencies: - "@jest/types" "^27.4.2" - jest-regex-util "^27.4.0" - jest-snapshot "^27.4.6" + "@jest/types" "^27.5.1" + jest-regex-util "^27.5.1" + jest-snapshot "^27.5.1" -jest-resolve@^27.4.2, jest-resolve@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-27.4.6.tgz#2ec3110655e86d5bfcfa992e404e22f96b0b5977" - integrity sha512-SFfITVApqtirbITKFAO7jOVN45UgFzcRdQanOFzjnbd+CACDoyeX7206JyU92l4cRr73+Qy/TlW51+4vHGt+zw== +jest-resolve@^27.4.2, jest-resolve@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-27.5.1.tgz#a2f1c5a0796ec18fe9eb1536ac3814c23617b384" + integrity sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw== dependencies: - "@jest/types" "^27.4.2" + "@jest/types" "^27.5.1" chalk "^4.0.0" - graceful-fs "^4.2.4" - jest-haste-map "^27.4.6" + graceful-fs "^4.2.9" + jest-haste-map "^27.5.1" jest-pnp-resolver "^1.2.2" - jest-util "^27.4.2" - jest-validate "^27.4.6" + jest-util "^27.5.1" + jest-validate "^27.5.1" resolve "^1.20.0" resolve.exports "^1.1.0" slash "^3.0.0" -jest-runner@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-27.4.6.tgz#1d390d276ec417e9b4d0d081783584cbc3e24773" - integrity sha512-IDeFt2SG4DzqalYBZRgbbPmpwV3X0DcntjezPBERvnhwKGWTW7C5pbbA5lVkmvgteeNfdd/23gwqv3aiilpYPg== - dependencies: - "@jest/console" "^27.4.6" - "@jest/environment" "^27.4.6" - "@jest/test-result" "^27.4.6" - "@jest/transform" "^27.4.6" - "@jest/types" "^27.4.2" +jest-runner@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-27.5.1.tgz#071b27c1fa30d90540805c5645a0ec167c7b62e5" + integrity sha512-g4NPsM4mFCOwFKXO4p/H/kWGdJp9V8kURY2lX8Me2drgXqG7rrZAx5kv+5H7wtt/cdFIjhqYx1HrlqWHaOvDaQ== + dependencies: + "@jest/console" "^27.5.1" + "@jest/environment" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" "@types/node" "*" chalk "^4.0.0" emittery "^0.8.1" - exit "^0.1.2" - graceful-fs "^4.2.4" - jest-docblock "^27.4.0" - jest-environment-jsdom "^27.4.6" - jest-environment-node "^27.4.6" - jest-haste-map "^27.4.6" - jest-leak-detector "^27.4.6" - jest-message-util "^27.4.6" - jest-resolve "^27.4.6" - jest-runtime "^27.4.6" - jest-util "^27.4.2" - jest-worker "^27.4.6" + graceful-fs "^4.2.9" + jest-docblock "^27.5.1" + jest-environment-jsdom "^27.5.1" + jest-environment-node "^27.5.1" + jest-haste-map "^27.5.1" + jest-leak-detector "^27.5.1" + jest-message-util "^27.5.1" + jest-resolve "^27.5.1" + jest-runtime "^27.5.1" + jest-util "^27.5.1" + jest-worker "^27.5.1" source-map-support "^0.5.6" throat "^6.0.1" -jest-runtime@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-27.4.6.tgz#83ae923818e3ea04463b22f3597f017bb5a1cffa" - integrity sha512-eXYeoR/MbIpVDrjqy5d6cGCFOYBFFDeKaNWqTp0h6E74dK0zLHzASQXJpl5a2/40euBmKnprNLJ0Kh0LCndnWQ== - dependencies: - "@jest/environment" "^27.4.6" - "@jest/fake-timers" "^27.4.6" - "@jest/globals" "^27.4.6" - "@jest/source-map" "^27.4.0" - "@jest/test-result" "^27.4.6" - "@jest/transform" "^27.4.6" - "@jest/types" "^27.4.2" +jest-runtime@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-27.5.1.tgz#4896003d7a334f7e8e4a53ba93fb9bcd3db0a1af" + integrity sha512-o7gxw3Gf+H2IGt8fv0RiyE1+r83FJBRruoA+FXrlHw6xEyBsU8ugA6IPfTdVyA0w8HClpbK+DGJxH59UrNMx8A== + dependencies: + "@jest/environment" "^27.5.1" + "@jest/fake-timers" "^27.5.1" + "@jest/globals" "^27.5.1" + "@jest/source-map" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" chalk "^4.0.0" cjs-module-lexer "^1.0.0" collect-v8-coverage "^1.0.0" execa "^5.0.0" glob "^7.1.3" - graceful-fs "^4.2.4" - jest-haste-map "^27.4.6" - jest-message-util "^27.4.6" - jest-mock "^27.4.6" - jest-regex-util "^27.4.0" - jest-resolve "^27.4.6" - jest-snapshot "^27.4.6" - jest-util "^27.4.2" + graceful-fs "^4.2.9" + jest-haste-map "^27.5.1" + jest-message-util "^27.5.1" + jest-mock "^27.5.1" + jest-regex-util "^27.5.1" + jest-resolve "^27.5.1" + jest-snapshot "^27.5.1" + jest-util "^27.5.1" slash "^3.0.0" strip-bom "^4.0.0" -jest-serializer@^27.4.0: - version "27.4.0" - resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-27.4.0.tgz#34866586e1cae2388b7d12ffa2c7819edef5958a" - integrity sha512-RDhpcn5f1JYTX2pvJAGDcnsNTnsV9bjYPU8xcV+xPwOXnUPOQwf4ZEuiU6G9H1UztH+OapMgu/ckEVwO87PwnQ== +jest-serializer@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-27.5.1.tgz#81438410a30ea66fd57ff730835123dea1fb1f64" + integrity sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w== dependencies: "@types/node" "*" - graceful-fs "^4.2.4" + graceful-fs "^4.2.9" -jest-snapshot@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-27.4.6.tgz#e2a3b4fff8bdce3033f2373b2e525d8b6871f616" - integrity sha512-fafUCDLQfzuNP9IRcEqaFAMzEe7u5BF7mude51wyWv7VRex60WznZIC7DfKTgSIlJa8aFzYmXclmN328aqSDmQ== +jest-snapshot@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-27.5.1.tgz#b668d50d23d38054a51b42c4039cab59ae6eb6a1" + integrity sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA== dependencies: "@babel/core" "^7.7.2" "@babel/generator" "^7.7.2" "@babel/plugin-syntax-typescript" "^7.7.2" "@babel/traverse" "^7.7.2" "@babel/types" "^7.0.0" - "@jest/transform" "^27.4.6" - "@jest/types" "^27.4.2" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" "@types/babel__traverse" "^7.0.4" "@types/prettier" "^2.1.5" babel-preset-current-node-syntax "^1.0.0" chalk "^4.0.0" - expect "^27.4.6" - graceful-fs "^4.2.4" - jest-diff "^27.4.6" - jest-get-type "^27.4.0" - jest-haste-map "^27.4.6" - jest-matcher-utils "^27.4.6" - jest-message-util "^27.4.6" - jest-util "^27.4.2" + expect "^27.5.1" + graceful-fs "^4.2.9" + jest-diff "^27.5.1" + jest-get-type "^27.5.1" + jest-haste-map "^27.5.1" + jest-matcher-utils "^27.5.1" + jest-message-util "^27.5.1" + jest-util "^27.5.1" natural-compare "^1.4.0" - pretty-format "^27.4.6" + pretty-format "^27.5.1" semver "^7.3.2" -jest-util@^27.4.2: - version "27.4.2" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.4.2.tgz#ed95b05b1adfd761e2cda47e0144c6a58e05a621" - integrity sha512-YuxxpXU6nlMan9qyLuxHaMMOzXAl5aGZWCSzben5DhLHemYQxCc4YK+4L3ZrCutT8GPQ+ui9k5D8rUJoDioMnA== +jest-util@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.5.1.tgz#3ba9771e8e31a0b85da48fe0b0891fb86c01c2f9" + integrity sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw== dependencies: - "@jest/types" "^27.4.2" + "@jest/types" "^27.5.1" "@types/node" "*" chalk "^4.0.0" ci-info "^3.2.0" - graceful-fs "^4.2.4" + graceful-fs "^4.2.9" picomatch "^2.2.3" -jest-validate@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-27.4.6.tgz#efc000acc4697b6cf4fa68c7f3f324c92d0c4f1f" - integrity sha512-872mEmCPVlBqbA5dToC57vA3yJaMRfIdpCoD3cyHWJOMx+SJwLNw0I71EkWs41oza/Er9Zno9XuTkRYCPDUJXQ== +jest-util@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-28.1.3.tgz#f4f932aa0074f0679943220ff9cbba7e497028b0" + integrity sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ== + dependencies: + "@jest/types" "^28.1.3" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + +jest-validate@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-27.5.1.tgz#9197d54dc0bdb52260b8db40b46ae668e04df067" + integrity sha512-thkNli0LYTmOI1tDB3FI1S1RTp/Bqyd9pTarJwL87OIBFuqEb5Apv5EaApEudYg4g86e3CT6kM0RowkhtEnCBQ== dependencies: - "@jest/types" "^27.4.2" + "@jest/types" "^27.5.1" camelcase "^6.2.0" chalk "^4.0.0" - jest-get-type "^27.4.0" + jest-get-type "^27.5.1" leven "^3.1.0" - pretty-format "^27.4.6" + pretty-format "^27.5.1" jest-watch-typeahead@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/jest-watch-typeahead/-/jest-watch-typeahead-1.0.0.tgz#4de2ca1eb596acb1889752afbab84b74fcd99173" - integrity sha512-jxoszalAb394WElmiJTFBMzie/RDCF+W7Q29n5LzOPtcoQoHWfdUtHFkbhgf5NwWe8uMOxvKb/g7ea7CshfkTw== + version "1.1.0" + resolved "https://registry.yarnpkg.com/jest-watch-typeahead/-/jest-watch-typeahead-1.1.0.tgz#b4a6826dfb9c9420da2f7bc900de59dad11266a9" + integrity sha512-Va5nLSJTN7YFtC2jd+7wsoe1pNe5K4ShLux/E5iHEwlB9AxaxmggY7to9KUqKojhaJw3aXqt5WAb4jGPOolpEw== dependencies: ansi-escapes "^4.3.1" chalk "^4.0.0" - jest-regex-util "^27.0.0" - jest-watcher "^27.0.0" + jest-regex-util "^28.0.0" + jest-watcher "^28.0.0" slash "^4.0.0" string-length "^5.0.1" strip-ansi "^7.0.1" -jest-watcher@^27.0.0, jest-watcher@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-27.4.6.tgz#673679ebeffdd3f94338c24f399b85efc932272d" - integrity sha512-yKQ20OMBiCDigbD0quhQKLkBO+ObGN79MO4nT7YaCuQ5SM+dkBNWE8cZX0FjU6czwMvWw6StWbe+Wv4jJPJ+fw== +jest-watcher@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-27.5.1.tgz#71bd85fb9bde3a2c2ec4dc353437971c43c642a2" + integrity sha512-z676SuD6Z8o8qbmEGhoEUFOM1+jfEiL3DXHK/xgEiG2EyNYfFG60jluWcupY6dATjfEsKQuibReS1djInQnoVw== + dependencies: + "@jest/test-result" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + jest-util "^27.5.1" + string-length "^4.0.1" + +jest-watcher@^28.0.0: + version "28.1.3" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-28.1.3.tgz#c6023a59ba2255e3b4c57179fc94164b3e73abd4" + integrity sha512-t4qcqj9hze+jviFPUN3YAtAEeFnr/azITXQEMARf5cMwKY2SMBRnCQTXLixTl20OR6mLh9KLMrgVJgJISym+1g== dependencies: - "@jest/test-result" "^27.4.6" - "@jest/types" "^27.4.2" + "@jest/test-result" "^28.1.3" + "@jest/types" "^28.1.3" "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" - jest-util "^27.4.2" + emittery "^0.10.2" + jest-util "^28.1.3" string-length "^4.0.1" jest-worker@^26.2.1: @@ -6780,32 +7161,37 @@ jest-worker@^26.2.1: merge-stream "^2.0.0" supports-color "^7.0.0" -jest-worker@^27.0.2, jest-worker@^27.3.1, jest-worker@^27.4.1, jest-worker@^27.4.6: - version "27.4.6" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.4.6.tgz#5d2d93db419566cb680752ca0792780e71b3273e" - integrity sha512-gHWJF/6Xi5CTG5QCvROr6GcmpIqNYpDJyc8A1h/DyXqH1tD6SnRCM0d3U5msV31D2LB/U+E0M+W4oyvKV44oNw== +jest-worker@^27.0.2, jest-worker@^27.4.5, jest-worker@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== dependencies: "@types/node" "*" merge-stream "^2.0.0" supports-color "^8.0.0" -jest-worker@^27.4.5: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" - integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== +jest-worker@^28.0.2: + version "28.1.3" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-28.1.3.tgz#7e3c4ce3fa23d1bb6accb169e7f396f98ed4bb98" + integrity sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g== dependencies: "@types/node" "*" merge-stream "^2.0.0" supports-color "^8.0.0" jest@^27.4.3: - version "27.4.7" - resolved "https://registry.yarnpkg.com/jest/-/jest-27.4.7.tgz#87f74b9026a1592f2da05b4d258e57505f28eca4" - integrity sha512-8heYvsx7nV/m8m24Vk26Y87g73Ba6ueUd0MWed/NXMhSZIm62U/llVbS0PJe1SHunbyXjJ/BqG1z9bFjGUIvTg== + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest/-/jest-27.5.1.tgz#dadf33ba70a779be7a6fc33015843b51494f63fc" + integrity sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ== dependencies: - "@jest/core" "^27.4.7" + "@jest/core" "^27.5.1" import-local "^3.0.2" - jest-cli "^27.4.7" + jest-cli "^27.5.1" + +jiti@^1.21.0: + version "1.21.6" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.6.tgz#6c7f7398dd4b3142767f9a168af2f317a428d268" + integrity sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w== js-levenshtein@^1.1.6: version "1.1.6" @@ -6844,25 +7230,25 @@ js2xmlparser@^4.0.2: dependencies: xmlcreate "^2.0.4" -jsdoc@^3.6.3: - version "3.6.11" - resolved "https://registry.yarnpkg.com/jsdoc/-/jsdoc-3.6.11.tgz#8bbb5747e6f579f141a5238cbad4e95e004458ce" - integrity sha512-8UCU0TYeIYD9KeLzEcAu2q8N/mx9O3phAGl32nmHlE0LpaJL71mMkP4d+QE5zWfNt50qheHtOZ0qoxVrsX5TUg== +jsdoc@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/jsdoc/-/jsdoc-4.0.3.tgz#bfee86c6a82f6823e12b5e8be698fd99ae46c061" + integrity sha512-Nu7Sf35kXJ1MWDZIMAuATRQTg1iIPdzh7tqJ6jjvaU/GfDf+qi5UV8zJR3Mo+/pYFvm8mzay4+6O5EWigaQBQw== dependencies: - "@babel/parser" "^7.9.4" - "@types/markdown-it" "^12.2.3" + "@babel/parser" "^7.20.15" + "@jsdoc/salty" "^0.2.1" + "@types/markdown-it" "^14.1.1" bluebird "^3.7.2" catharsis "^0.9.0" escape-string-regexp "^2.0.0" js2xmlparser "^4.0.2" klaw "^3.0.0" - markdown-it "^12.3.2" - markdown-it-anchor "^8.4.1" + markdown-it "^14.1.0" + markdown-it-anchor "^8.6.7" marked "^4.0.10" mkdirp "^1.0.4" requizzle "^0.2.3" strip-json-comments "^3.1.0" - taffydb "2.6.2" underscore "~1.13.2" jsdom@^16.6.0: @@ -6898,15 +7284,15 @@ jsdom@^16.6.0: ws "^7.4.6" xml-name-validator "^3.0.0" -jsesc@^2.5.1: - version "2.5.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" - integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== +jsesc@^3.0.2, jsesc@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e" + integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g== -jsesc@~0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" - integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0= +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: version "2.3.1" @@ -6931,26 +7317,24 @@ json-schema@^0.4.0: json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" - integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== -json5@^1.0.1: +json5@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== dependencies: minimist "^1.2.0" -json5@^2.1.2, json5@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3" - integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA== - dependencies: - minimist "^1.2.5" +json5@^2.1.2, json5@^2.2.0, json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== jsonfile@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" - integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= + integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== optionalDependencies: graceful-fs "^4.1.6" @@ -6963,18 +7347,36 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" +jsonpath@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/jsonpath/-/jsonpath-1.1.1.tgz#0ca1ed8fb65bb3309248cc9d5466d12d5b0b9901" + integrity sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w== + dependencies: + esprima "1.2.2" + static-eval "2.0.2" + underscore "1.12.1" + jsonpointer@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.0.tgz#f802669a524ec4805fa7389eadbc9921d5dc8072" - integrity sha512-PNYZIdMjVIvVgDSYKTT63Y+KZ6IZvGRNNWcxwD+GNnUz1MKPfv30J8ueCjdwcN0nDx2SlshgyB7Oy0epAzVRRg== + version "5.0.1" + resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559" + integrity sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ== -"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz#720b97bfe7d901b927d87c3773637ae8ea48781b" - integrity sha512-uP5vu8xfy2F9A6LGC22KO7e2/vGTS1MhP+18f++ZNlf0Ohaxbc9nIEwHAsejlJKyzfZzU5UIhe5ItYkitcZnZA== +"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.5: + version "3.3.5" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a" + integrity sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ== + dependencies: + array-includes "^3.1.6" + array.prototype.flat "^1.3.1" + object.assign "^4.1.4" + object.values "^1.1.6" + +keyv@^4.5.3: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== dependencies: - array-includes "^3.1.3" - object.assign "^4.1.2" + json-buffer "3.0.1" kind-of@^6.0.2: version "6.0.3" @@ -6994,21 +7396,29 @@ kleur@^3.0.3: integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== klona@^2.0.4, klona@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.5.tgz#d166574d90076395d9963aa7a928fabb8d76afbc" - integrity sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ== + version "2.0.6" + resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.6.tgz#85bffbf819c03b2f53270412420a4555ef882e22" + integrity sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA== -language-subtag-registry@~0.3.2: - version "0.3.21" - resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz#04ac218bea46f04cb039084602c6da9e788dd45a" - integrity sha512-L0IqwlIXjilBVVYKFT37X9Ih11Um5NEl9cbJIuU/SwP/zEEAbBPOnEeeuxVMf45ydWQRDQN3Nqc96OgbH1K+Pg== +language-subtag-registry@^0.3.20: + version "0.3.23" + resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz#23529e04d9e3b74679d70142df3fd2eb6ec572e7" + integrity sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ== -language-tags@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.5.tgz#d321dbc4da30ba8bf3024e040fa5c14661f9193a" - integrity sha1-0yHbxNowuovzAk4ED6XBRmH5GTo= +language-tags@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.9.tgz#1ffdcd0ec0fafb4b1be7f8b11f306ad0f9c08777" + integrity sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA== dependencies: - language-subtag-registry "~0.3.2" + language-subtag-registry "^0.3.20" + +launch-editor@^2.6.0: + version "2.9.1" + resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.9.1.tgz#253f173bd441e342d4344b4dae58291abb425047" + integrity sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w== + dependencies: + picocolors "^1.0.0" + shell-quote "^1.8.1" leven@^3.1.0: version "3.1.0" @@ -7026,63 +7436,51 @@ levn@^0.4.1: levn@~0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" - integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= + integrity sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA== dependencies: prelude-ls "~1.1.2" type-check "~0.3.2" -lilconfig@^2.0.3, lilconfig@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.4.tgz#f4507d043d7058b380b6a8f5cb7bcd4b34cee082" - integrity sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA== +lilconfig@^2.0.3, lilconfig@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" + integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== + +lilconfig@^3.0.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.2.tgz#e4a7c3cb549e3a606c8dcc32e5ae1005e62c05cb" + integrity sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow== lines-and-columns@^1.1.6: version "1.2.4" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== -linkify-it@^3.0.1: - version "3.0.3" - resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-3.0.3.tgz#a98baf44ce45a550efb4d49c769d07524cc2fa2e" - integrity sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ== +linkify-it@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-5.0.0.tgz#9ef238bfa6dc70bd8e7f9572b52d369af569b421" + integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ== dependencies: - uc.micro "^1.0.1" + uc.micro "^2.0.0" loader-runner@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.2.0.tgz#d7022380d66d14c5fb1d496b89864ebcfd478384" - integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw== - -loader-utils@^1.4.0: - version "1.4.2" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.2.tgz#29a957f3a63973883eb684f10ffd3d151fec01a3" - integrity sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg== - dependencies: - big.js "^5.2.2" - emojis-list "^3.0.0" - json5 "^1.0.1" + version "4.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" + integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== -loader-utils@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.2.tgz#d6e3b4fb81870721ae4e0868ab11dd638368c129" - integrity sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A== +loader-utils@^2.0.0, loader-utils@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c" + integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw== dependencies: big.js "^5.2.2" emojis-list "^3.0.0" json5 "^2.1.2" loader-utils@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-3.2.0.tgz#bcecc51a7898bee7473d4bc6b845b23af8304d4f" - integrity sha512-HVl9ZqccQihZ7JM85dco1MvO9G+ONvxoGa9rkhzFsneGLKSUg1gJf9bWzhRhcvm2qChhWpebQhP44qxjKIUCaQ== - -locate-path@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" - integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= - dependencies: - p-locate "^2.0.0" - path-exists "^3.0.0" + version "3.3.1" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-3.3.1.tgz#735b9a19fd63648ca7adbd31c2327dfe281304e5" + integrity sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg== locate-path@^3.0.0: version "3.0.0" @@ -7109,12 +7507,12 @@ locate-path@^6.0.0: lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" - integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= + integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" - integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= + integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== lodash.merge@^4.6.2: version "4.6.2" @@ -7124,14 +7522,14 @@ lodash.merge@^4.6.2: lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" - integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= + integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" - integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= + integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== -lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0: +lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -7171,12 +7569,17 @@ lowlight@^1.17.0: fault "^1.0.0" highlight.js "~10.7.0" -lru-cache@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== +lru-cache@^10.2.0: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== dependencies: - yallist "^4.0.0" + yallist "^3.0.2" lz-string@^1.5.0: version "1.5.0" @@ -7184,19 +7587,26 @@ lz-string@^1.5.0: integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ== magic-string@^0.25.0, magic-string@^0.25.7: - version "0.25.7" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051" - integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA== + version "0.25.9" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c" + integrity sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ== dependencies: - sourcemap-codec "^1.4.4" + sourcemap-codec "^1.4.8" -make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0: +make-dir@^3.0.2, make-dir@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== dependencies: semver "^6.0.0" +make-dir@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e" + integrity sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw== + dependencies: + semver "^7.5.3" + makeerror@1.0.12: version "1.0.12" resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" @@ -7209,26 +7619,27 @@ markdown-escapes@^1.0.0: resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.4.tgz#c95415ef451499d7602b91095f3c8e8975f78535" integrity sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg== -markdown-it-anchor@^8.4.1: - version "8.6.5" - resolved "https://registry.yarnpkg.com/markdown-it-anchor/-/markdown-it-anchor-8.6.5.tgz#30c4bc5bbff327f15ce3c429010ec7ba75e7b5f8" - integrity sha512-PI1qEHHkTNWT+X6Ip9w+paonfIQ+QZP9sCeMYi47oqhH+EsW8CrJ8J7CzV19QVOj6il8ATGbK2nTECj22ZHGvQ== +markdown-it-anchor@^8.6.7: + version "8.6.7" + resolved "https://registry.yarnpkg.com/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz#ee6926daf3ad1ed5e4e3968b1740eef1c6399634" + integrity sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA== -markdown-it@^12.3.2: - version "12.3.2" - resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-12.3.2.tgz#bf92ac92283fe983fe4de8ff8abfb5ad72cd0c90" - integrity sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg== +markdown-it@^14.1.0: + version "14.1.0" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-14.1.0.tgz#3c3c5992883c633db4714ccb4d7b5935d98b7d45" + integrity sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg== dependencies: argparse "^2.0.1" - entities "~2.1.0" - linkify-it "^3.0.1" - mdurl "^1.0.1" - uc.micro "^1.0.5" + entities "^4.4.0" + linkify-it "^5.0.0" + mdurl "^2.0.0" + punycode.js "^2.3.1" + uc.micro "^2.1.0" marked@^4.0.10: - version "4.1.0" - resolved "https://registry.yarnpkg.com/marked/-/marked-4.1.0.tgz#3fc6e7485f21c1ca5d6ec4a39de820e146954796" - integrity sha512-+Z6KDjSPa6/723PQYyc1axYZpYYpDnECDaU6hkaf5gqBieBkMKYReL5hteF2QizhlMbgbo8umXl/clZ67+GlsA== + version "4.3.0" + resolved "https://registry.yarnpkg.com/marked/-/marked-4.3.0.tgz#796362821b019f734054582038b116481b456cf3" + integrity sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A== match-sorter@^6.0.2: version "6.3.4" @@ -7269,29 +7680,22 @@ mdn-data@2.0.4: resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b" integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA== -mdn-data@~1.1.0: - version "1.1.4" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-1.1.4.tgz#50b5d4ffc4575276573c4eedb8780812a8419f01" - integrity sha512-FSYbp3lyKjyj3E7fMl6rYvUdX0FBXaluGqlFoYESWQlyUTq8R+wp0rkFxoYFqZlHCvsUXGjyJmLQSnXToYhOSA== - -mdurl@^1.0.0, mdurl@^1.0.1: +mdurl@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= +mdurl@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0" + integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w== + media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== -memfs@^3.1.2: - version "3.4.1" - resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.4.1.tgz#b78092f466a0dce054d63d39275b24c71d3f1305" - integrity sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw== - dependencies: - fs-monkey "1.0.3" - -memfs@^3.4.3: +memfs@^3.1.2, memfs@^3.4.3: version "3.6.0" resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.6.0.tgz#d7a2110f86f79dd950a8b6df6d57bc984aa185f6" integrity sha512-EGowvkkgbMcIChjMTMkESFDbZeSh8xZ7kNSF0hAiAN4Jh6jgHCRS0Ga/+C8y6Au+oqpezRHCfPsmJ2+DwAgiwQ== @@ -7308,10 +7712,10 @@ memoize-one@^6.0.0: resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045" integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw== -merge-descriptors@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" - integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== +merge-descriptors@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" + integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== merge-stream@^2.0.0: version "2.0.0" @@ -7328,37 +7732,30 @@ methods@~1.1.2: resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== -micromatch@^4.0.2, micromatch@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" - integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== +micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== dependencies: - braces "^3.0.1" - picomatch "^2.2.3" + braces "^3.0.3" + picomatch "^2.3.1" microseconds@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/microseconds/-/microseconds-0.2.0.tgz#233b25f50c62a65d861f978a4a4f8ec18797dc39" integrity sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA== -mime-db@1.51.0, "mime-db@>= 1.43.0 < 2": - version "1.51.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.51.0.tgz#d9ff62451859b18342d960850dc3cfb77e63fb0c" - integrity sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g== - mime-db@1.52.0: version "1.52.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17: - version "2.1.34" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.34.tgz#5a712f9ec1503511a945803640fafe09d3793c24" - integrity sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A== - dependencies: - mime-db "1.51.0" +"mime-db@>= 1.43.0 < 2": + version "1.53.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.53.0.tgz#3cb63cd820fc29896d9d4e8c32ab4fcd74ccb447" + integrity sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg== -mime-types@~2.1.24, mime-types@~2.1.34: +mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== @@ -7381,55 +7778,61 @@ min-indent@^1.0.0: integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== mini-css-extract-plugin@^2.4.5: - version "2.5.3" - resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.5.3.tgz#c5c79f9b22ce9b4f164e9492267358dbe35376d9" - integrity sha512-YseMB8cs8U/KCaAGQoqYmfUuhhGW0a9p9XvWXrxVOkE3/IiISTLw4ALNt7JR5B2eYauFM+PQGSbXMDmVbR7Tfw== + version "2.9.1" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.1.tgz#4d184f12ce90582e983ccef0f6f9db637b4be758" + integrity sha512-+Vyi+GCCOHnrJ2VPS+6aPoXN2k2jgUzDRhTFLjjTBn23qyXJXkjUWQgTL+mXpF5/A8ixLdCc6kWsoeOjKGejKQ== dependencies: schema-utils "^4.0.0" + tapable "^2.2.1" minimalistic-assert@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== -minimatch@3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== - dependencies: - brace-expansion "^1.1.7" - -minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.1.1: +minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" -minimatch@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" - integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g== +minimatch@^5.0.1: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== dependencies: brace-expansion "^2.0.1" -minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5: - version "1.2.7" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" - integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== +minimist@^1.2.0, minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== -mkdirp@^0.5.5, mkdirp@~0.5.1: - version "0.5.5" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" - integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== - dependencies: - minimist "^1.2.5" +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== mkdirp@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +mkdirp@~0.5.1: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + moment@^2.29.1: version "2.29.4" resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" @@ -7440,12 +7843,7 @@ ms@2.0.0: resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== -ms@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -ms@2.1.3, ms@^2.1.1: +ms@2.1.3, ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -7476,17 +7874,12 @@ msw@^0.36.8: type-fest "^1.2.2" yargs "^17.3.0" -multicast-dns-service-types@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" - integrity sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE= - -multicast-dns@^6.0.1: - version "6.2.3" - resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.2.3.tgz#a0ec7bd9055c4282f790c3c82f4e28db3b31b229" - integrity sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g== +multicast-dns@^7.2.5: + version "7.2.5" + resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.5.tgz#77eb46057f4d7adbd16d9290fa7299f6fa64cced" + integrity sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg== dependencies: - dns-packet "^1.3.1" + dns-packet "^5.2.2" thunky "^1.0.2" mute-stream@0.0.8: @@ -7494,6 +7887,15 @@ mute-stream@0.0.8: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== +mz@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" + integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== + dependencies: + any-promise "^1.0.0" + object-assign "^4.0.1" + thenify-all "^1.0.0" + nano-time@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/nano-time/-/nano-time-1.0.0.tgz#b0554f69ad89e22d0907f7a12b0993a5d96137ef" @@ -7501,20 +7903,20 @@ nano-time@1.0.0: dependencies: big-integer "^1.6.16" -nanoid@^3.1.30: - version "3.2.0" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c" - integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA== - nanoid@^3.3.7: version "3.3.7" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== +natural-compare-lite@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" + integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" - integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== negotiator@0.6.3: version "0.6.3" @@ -7542,26 +7944,21 @@ node-emoji@^1.10.0: lodash "^4.17.21" node-fetch@^2.6.7: - version "2.6.7" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" - integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== dependencies: whatwg-url "^5.0.0" -node-forge@^1.2.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.0.tgz#37a874ea723855f37db091e6c186e5b67a01d4b2" - integrity sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA== +node-forge@^1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" + integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" - integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= - -node-releases@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.1.tgz#3d1d395f204f1f2f29a54358b9fb678765ad2fc5" - integrity sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA== + integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== node-releases@^2.0.18: version "2.0.18" @@ -7576,7 +7973,7 @@ normalize-path@^3.0.0, normalize-path@~3.0.0: normalize-range@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" - integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI= + integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== normalize-url@^6.0.1: version "6.1.0" @@ -7598,9 +7995,9 @@ nth-check@^1.0.2: boolbase "~1.0.0" nth-check@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.1.tgz#2efe162f5c3da06a28959fbd3db75dbeea9f0fc2" - integrity sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w== + version "2.1.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" + integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== dependencies: boolbase "^1.0.0" @@ -7610,96 +8007,97 @@ numeral@^2.0.6: integrity sha1-StCAk21EPCVhrtnyGX7//iX05QY= nwsapi@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" - integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ== + version "2.2.13" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.13.tgz#e56b4e98960e7a040e5474536587e599c4ff4655" + integrity sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ== -object-assign@^4.1.1: +object-assign@^4.0.1, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== -object-hash@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5" - integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== - -object-inspect@^1.11.0: - version "1.12.0" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0" - integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g== +object-hash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" + integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== -object-inspect@^1.9.0: - version "1.12.2" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" - integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== +object-inspect@^1.13.1: + version "1.13.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" + integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== -object-is@^1.0.1: - version "1.1.5" - resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" - integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== +object-is@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.6.tgz#1a6a53aed2dd8f7e6775ff870bea58545956ab07" + integrity sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q== dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" + call-bind "^1.0.7" + define-properties "^1.2.1" -object-keys@^1.0.12, object-keys@^1.1.1: +object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== -object.assign@^4.1.0, object.assign@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" - integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== +object.assign@^4.1.4, object.assign@^4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" + integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== dependencies: - call-bind "^1.0.0" - define-properties "^1.1.3" - has-symbols "^1.0.1" + call-bind "^1.0.5" + define-properties "^1.2.1" + has-symbols "^1.0.3" object-keys "^1.1.1" -object.entries@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.5.tgz#e1acdd17c4de2cd96d5a08487cfb9db84d881861" - integrity sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g== +object.entries@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.8.tgz#bffe6f282e01f4d17807204a24f8edd823599c41" + integrity sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ== dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.1" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" -object.fromentries@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.5.tgz#7b37b205109c21e741e605727fe8b0ad5fa08251" - integrity sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw== +object.fromentries@^2.0.8: + version "2.0.8" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.8.tgz#f7195d8a9b97bd95cbc1999ea939ecd1a2b00c65" + integrity sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ== dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.1" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" object.getownpropertydescriptors@^2.1.0: - version "2.1.3" - resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.3.tgz#b223cf38e17fefb97a63c10c91df72ccb386df9e" - integrity sha512-VdDoCwvJI4QdC6ndjpqFmoL3/+HxffFBbcJzKi5hwLLqqx3mdbedRpfZDdK0SrOSauj8X4GzBvnDZl4vTN7dOw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.1" - -object.hasown@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.0.tgz#7232ed266f34d197d15cac5880232f7a4790afe5" - integrity sha512-MhjYRfj3GBlhSkDHo6QmvgjRLXQ2zndabdf3nX0yTyZK9rPfxb6uRpAac8HXNLy1GpqWtZ81Qh4v3uOls2sRAg== + version "2.1.8" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.8.tgz#2f1fe0606ec1a7658154ccd4f728504f69667923" + integrity sha512-qkHIGe4q0lSYMv0XI4SsBTJz3WaURhLvd0lKSgtVuOsJ2krg4SgMw3PIRQFMp07yi++UR3se2mkcLqsBNpBb/A== + dependencies: + array.prototype.reduce "^1.0.6" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" + gopd "^1.0.1" + safe-array-concat "^1.1.2" + +object.groupby@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.3.tgz#9b125c36238129f6f7b61954a1e7176148d5002e" + integrity sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ== dependencies: - define-properties "^1.1.3" - es-abstract "^1.19.1" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" -object.values@^1.1.0, object.values@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.5.tgz#959f63e3ce9ef108720333082131e4a459b716ac" - integrity sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg== +object.values@^1.1.0, object.values@^1.1.6, object.values@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.0.tgz#65405a9d92cee68ac2d303002e0b8470a4d9ab1b" + integrity sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ== dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.1" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" oblivious-set@1.0.0: version "1.0.0" @@ -7738,9 +8136,9 @@ onetime@^5.1.0, onetime@^5.1.2: mimic-fn "^2.1.0" open@^8.0.9, open@^8.4.0: - version "8.4.0" - resolved "https://registry.yarnpkg.com/open/-/open-8.4.0.tgz#345321ae18f8138f82565a910fdc6b39e8c244f8" - integrity sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q== + version "8.4.2" + resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" + integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== dependencies: define-lazy-prop "^2.0.0" is-docker "^2.1.1" @@ -7758,17 +8156,17 @@ optionator@^0.8.1: type-check "~0.3.2" word-wrap "~1.2.3" -optionator@^0.9.1: - version "0.9.1" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" - integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== +optionator@^0.9.3: + version "0.9.4" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" + integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== dependencies: deep-is "^0.1.3" fast-levenshtein "^2.0.6" levn "^0.4.1" prelude-ls "^1.2.1" type-check "^0.4.0" - word-wrap "^1.2.3" + word-wrap "^1.2.5" ora@^5.4.1: version "5.4.1" @@ -7788,19 +8186,12 @@ ora@^5.4.1: os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" - integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= + integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== outvariant@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/outvariant/-/outvariant-1.2.1.tgz#e630f6cdc1dbf398ed857e36f219de4a005ccd35" - integrity sha512-bcILvFkvpMXh66+Ubax/inxbKRyWTUiiFIW2DWkiS79wakrLGn3Ydy+GvukadiyfZjaL6C7YhIem4EZSM282wA== - -p-limit@^1.1.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" - integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== - dependencies: - p-try "^1.0.0" + version "1.4.3" + resolved "https://registry.yarnpkg.com/outvariant/-/outvariant-1.4.3.tgz#221c1bfc093e8fec7075497e7799fdbf43d14873" + integrity sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA== p-limit@^2.0.0, p-limit@^2.2.0: version "2.3.0" @@ -7816,13 +8207,6 @@ p-limit@^3.0.2: dependencies: yocto-queue "^0.1.0" -p-locate@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" - integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= - dependencies: - p-limit "^1.1.0" - p-locate@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" @@ -7844,31 +8228,24 @@ p-locate@^5.0.0: dependencies: p-limit "^3.0.2" -p-map@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" - integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== - dependencies: - aggregate-error "^3.0.0" - p-retry@^4.5.0: - version "4.6.1" - resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.1.tgz#8fcddd5cdf7a67a0911a9cf2ef0e5df7f602316c" - integrity sha512-e2xXGNhZOZ0lfgR9kL34iGlU8N/KO0xZnQxVEwdeOvpqNDQfdnxIYizvWtK8RglUa3bGqI8g0R/BdfzLMxRkiA== + version "4.6.2" + resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16" + integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ== dependencies: - "@types/retry" "^0.12.0" + "@types/retry" "0.12.0" retry "^0.13.1" -p-try@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" - integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= - p-try@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +package-json-from-dist@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" + integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== + param-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5" @@ -7896,7 +8273,7 @@ parse-entities@^2.0.0: is-decimal "^1.0.0" is-hexadecimal "^1.0.0" -parse-json@^5.0.0: +parse-json@^5.0.0, parse-json@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== @@ -7927,7 +8304,7 @@ pascal-case@^3.1.2: path-exists@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" - integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= + integrity sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ== path-exists@^4.0.0: version "4.0.0" @@ -7944,20 +8321,28 @@ path-key@^3.0.0, path-key@^3.1.0: resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== -path-parse@^1.0.6, path-parse@^1.0.7: +path-parse@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-to-regexp@0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" - integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== +path-scurry@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + +path-to-regexp@0.1.10: + version "0.1.10" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b" + integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w== path-to-regexp@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.2.0.tgz#f7b3803336104c346889adece614669230645f38" - integrity sha512-f66KywYG6+43afgE/8j/GoiNyygk/bnoCbps++3ErRKsIYkGGupyv07R2Ok5m9i67Iqc+T2g1eAUGUPzWhYTyg== + version "6.3.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.3.0.tgz#2b6a26a337737a8e1416f9272ed0766b1c0389f4" + integrity sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ== path-type@^4.0.0: version "4.0.0" @@ -7967,32 +8352,32 @@ path-type@^4.0.0: performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" - integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== picocolors@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f" integrity sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA== -picocolors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" - integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== - -picocolors@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.0.tgz#5358b76a78cde483ba5cef6a9dc9671440b27d59" - integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw== +picocolors@^1.0.0, picocolors@^1.0.1, picocolors@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3: +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -pirates@^4.0.4: - version "4.0.5" - resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" - integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== +pify@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== + +pirates@^4.0.1, pirates@^4.0.4: + version "4.0.6" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" + integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== pkg-dir@^4.1.0, pkg-dir@^4.2.0: version "4.2.0" @@ -8008,130 +8393,137 @@ pkg-up@^3.1.0: dependencies: find-up "^3.0.0" -portfinder@^1.0.28: - version "1.0.28" - resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778" - integrity sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA== - dependencies: - async "^2.6.2" - debug "^3.1.1" - mkdirp "^0.5.5" +possible-typed-array-names@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" + integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== -postcss-attribute-case-insensitive@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.0.tgz#39cbf6babf3ded1e4abf37d09d6eda21c644105c" - integrity sha512-b4g9eagFGq9T5SWX4+USfVyjIb3liPnjhHHRMP7FMB2kFVpYyfEscV0wP3eaXhKlcHKUut8lt5BGoeylWA/dBQ== +postcss-attribute-case-insensitive@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.2.tgz#03d761b24afc04c09e757e92ff53716ae8ea2741" + integrity sha512-XIidXV8fDr0kKt28vqki84fRK8VW8eTuIa4PChv2MqKuT6C9UjmSKzen6KaWhWEoYvwxFCa7n/tC1SZ3tyq4SQ== dependencies: - postcss-selector-parser "^6.0.2" + postcss-selector-parser "^6.0.10" postcss-browser-comments@^4: version "4.0.0" resolved "https://registry.yarnpkg.com/postcss-browser-comments/-/postcss-browser-comments-4.0.0.tgz#bcfc86134df5807f5d3c0eefa191d42136b5e72a" integrity sha512-X9X9/WN3KIvY9+hNERUqX9gncsgBA25XaeR+jshHz2j8+sYyHktHw1JdKuMjeLpGktXidqDhA7b/qm1mrBDmgg== -postcss-calc@^8.2.0: - version "8.2.3" - resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-8.2.3.tgz#53b95ce93de19213c2a5fdd71277a81690ef41d0" - integrity sha512-EGM2EBBWqP57N0E7N7WOLT116PJ39dwHVU01WO4XPPQLJfkL2xVgkMZ+TZvCfapj/uJH07UEfKHQNPHzSw/14Q== +postcss-calc@^8.2.3: + version "8.2.4" + resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-8.2.4.tgz#77b9c29bfcbe8a07ff6693dc87050828889739a5" + integrity sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q== dependencies: - postcss-selector-parser "^6.0.2" - postcss-value-parser "^4.0.2" + postcss-selector-parser "^6.0.9" + postcss-value-parser "^4.2.0" -postcss-color-functional-notation@^4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/postcss-color-functional-notation/-/postcss-color-functional-notation-4.2.1.tgz#a25e9e1855e14d04319222a689f120b3240d39e0" - integrity sha512-62OBIXCjRXpQZcFOYIXwXBlpAVWrYk8ek1rcjvMING4Q2cf0ipyN9qT+BhHA6HmftGSEnFQu2qgKO3gMscl3Rw== +postcss-clamp@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/postcss-clamp/-/postcss-clamp-4.1.0.tgz#7263e95abadd8c2ba1bd911b0b5a5c9c93e02363" + integrity sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow== dependencies: postcss-value-parser "^4.2.0" -postcss-color-hex-alpha@^8.0.2: - version "8.0.2" - resolved "https://registry.yarnpkg.com/postcss-color-hex-alpha/-/postcss-color-hex-alpha-8.0.2.tgz#7a248b006dd47bd83063f662352d31fd982f74ec" - integrity sha512-gyx8RgqSmGVK156NAdKcsfkY3KPGHhKqvHTL3hhveFrBBToguKFzhyiuk3cljH6L4fJ0Kv+JENuPXs1Wij27Zw== +postcss-color-functional-notation@^4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/postcss-color-functional-notation/-/postcss-color-functional-notation-4.2.4.tgz#21a909e8d7454d3612d1659e471ce4696f28caec" + integrity sha512-2yrTAUZUab9s6CpxkxC4rVgFEVaR6/2Pipvi6qcgvnYiVqZcbDHEoBDhrXzyb7Efh2CCfHQNtcqWcIruDTIUeg== dependencies: postcss-value-parser "^4.2.0" -postcss-color-rebeccapurple@^7.0.2: - version "7.0.2" - resolved "https://registry.yarnpkg.com/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-7.0.2.tgz#5d397039424a58a9ca628762eb0b88a61a66e079" - integrity sha512-SFc3MaocHaQ6k3oZaFwH8io6MdypkUtEy/eXzXEB1vEQlO3S3oDc/FSZA8AsS04Z25RirQhlDlHLh3dn7XewWw== +postcss-color-hex-alpha@^8.0.4: + version "8.0.4" + resolved "https://registry.yarnpkg.com/postcss-color-hex-alpha/-/postcss-color-hex-alpha-8.0.4.tgz#c66e2980f2fbc1a63f5b079663340ce8b55f25a5" + integrity sha512-nLo2DCRC9eE4w2JmuKgVA3fGL3d01kGq752pVALF68qpGLmx2Qrk91QTKkdUqqp45T1K1XV8IhQpcu1hoAQflQ== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-color-rebeccapurple@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-7.1.1.tgz#63fdab91d878ebc4dd4b7c02619a0c3d6a56ced0" + integrity sha512-pGxkuVEInwLHgkNxUc4sdg4g3py7zUeCQ9sMfwyHAT+Ezk8a4OaaVZ8lIY5+oNqA/BXXgLyXv0+5wHP68R79hg== dependencies: postcss-value-parser "^4.2.0" -postcss-colormin@^5.2.4: - version "5.2.4" - resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-5.2.4.tgz#7726d3f3d24f111d39faff50a6500688225d5324" - integrity sha512-rYlC5015aNqVQt/B6Cy156g7sH5tRUJGmT9xeagYthtKehetbKx7jHxhyLpulP4bs4vbp8u/B2rac0J7S7qPQg== +postcss-colormin@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-5.3.1.tgz#86c27c26ed6ba00d96c79e08f3ffb418d1d1988f" + integrity sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ== dependencies: - browserslist "^4.16.6" + browserslist "^4.21.4" caniuse-api "^3.0.0" colord "^2.9.1" postcss-value-parser "^4.2.0" -postcss-convert-values@^5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-5.0.3.tgz#492db08a28af84d57651f10edc8f6c8fb2f6df40" - integrity sha512-fVkjHm2T0PSMqXUCIhHNWVGjhB9mHEWX2GboVs7j3iCgr6FpIl9c/IdXy0PHWZSQ9LFTRgmj98amxJE6KOnlsA== +postcss-convert-values@^5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz#04998bb9ba6b65aa31035d669a6af342c5f9d393" + integrity sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA== dependencies: + browserslist "^4.21.4" postcss-value-parser "^4.2.0" -postcss-custom-media@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-8.0.0.tgz#1be6aff8be7dc9bf1fe014bde3b71b92bb4552f1" - integrity sha512-FvO2GzMUaTN0t1fBULDeIvxr5IvbDXcIatt6pnJghc736nqNgsGao5NT+5+WVLAQiTt6Cb3YUms0jiPaXhL//g== +postcss-custom-media@^8.0.2: + version "8.0.2" + resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-8.0.2.tgz#c8f9637edf45fef761b014c024cee013f80529ea" + integrity sha512-7yi25vDAoHAkbhAzX9dHx2yc6ntS4jQvejrNcC+csQJAXjj15e7VcWfMgLqBNAbOvqi5uIa9huOVwdHbf+sKqg== + dependencies: + postcss-value-parser "^4.2.0" -postcss-custom-properties@^12.1.2: - version "12.1.3" - resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-12.1.3.tgz#8e37651c7188e72e6762eeae8db39755e84d3a64" - integrity sha512-rtu3otIeY532PnEuuBrIIe+N+pcdbX/7JMZfrcL09wc78YayrHw5E8UkDfvnlOhEUrI4ptCuzXQfj+Or6spbGA== +postcss-custom-properties@^12.1.10: + version "12.1.11" + resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-12.1.11.tgz#d14bb9b3989ac4d40aaa0e110b43be67ac7845cf" + integrity sha512-0IDJYhgU8xDv1KY6+VgUwuQkVtmYzRwu+dMjnmdMafXYv86SWqfxkc7qdDvWS38vsjaEtv8e0vGOUQrAiMBLpQ== dependencies: postcss-value-parser "^4.2.0" -postcss-custom-selectors@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/postcss-custom-selectors/-/postcss-custom-selectors-6.0.0.tgz#022839e41fbf71c47ae6e316cb0e6213012df5ef" - integrity sha512-/1iyBhz/W8jUepjGyu7V1OPcGbc636snN1yXEQCinb6Bwt7KxsiU7/bLQlp8GwAXzCh7cobBU5odNn/2zQWR8Q== +postcss-custom-selectors@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/postcss-custom-selectors/-/postcss-custom-selectors-6.0.3.tgz#1ab4684d65f30fed175520f82d223db0337239d9" + integrity sha512-fgVkmyiWDwmD3JbpCmB45SvvlCD6z9CG6Ie6Iere22W5aHea6oWa7EM2bpnv2Fj3I94L3VbtvX9KqwSi5aFzSg== dependencies: postcss-selector-parser "^6.0.4" -postcss-dir-pseudo-class@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-6.0.3.tgz#febfe305e75267913a53bf5094c7679f5cfa9b55" - integrity sha512-qiPm+CNAlgXiMf0J5IbBBEXA9l/Q5HGsNGkL3znIwT2ZFRLGY9U2fTUpa4lqCUXQOxaLimpacHeQC80BD2qbDw== +postcss-dir-pseudo-class@^6.0.5: + version "6.0.5" + resolved "https://registry.yarnpkg.com/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-6.0.5.tgz#2bf31de5de76added44e0a25ecf60ae9f7c7c26c" + integrity sha512-eqn4m70P031PF7ZQIvSgy9RSJ5uI2171O/OO/zcRNYpJbvaeKFUlar1aJ7rmgiQtbm0FSPsRewjpdS0Oew7MPA== dependencies: - postcss-selector-parser "^6.0.8" + postcss-selector-parser "^6.0.10" -postcss-discard-comments@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-5.0.2.tgz#811ed34e2b6c40713daab0beb4d7a04125927dcd" - integrity sha512-6VQ3pYTsJHEsN2Bic88Aa7J/Brn4Bv8j/rqaFQZkH+pcVkKYwxCIvoMQkykEW7fBjmofdTnQgcivt5CCBJhtrg== +postcss-discard-comments@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz#8df5e81d2925af2780075840c1526f0660e53696" + integrity sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ== -postcss-discard-duplicates@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-5.0.2.tgz#61076f3d256351bdaac8e20aade730fef0609f44" - integrity sha512-LKY81YjUjc78p6rbXIsnppsaFo8XzCoMZkXVILJU//sK0DgPkPSpuq/cZvHss3EtdKvWNYgWzQL+wiJFtEET4g== +postcss-discard-duplicates@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz#9eb4fe8456706a4eebd6d3b7b777d07bad03e848" + integrity sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw== -postcss-discard-empty@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-5.0.2.tgz#0676a9bcfc44bb00d338352a45ab80845a31d8f0" - integrity sha512-SxBsbTjlsKUvZLL+dMrdWauuNZU8TBq5IOL/DHa6jBUSXFEwmDqeXRfTIK/FQpPTa8MJMxEHjSV3UbiuyLARPQ== +postcss-discard-empty@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz#e57762343ff7f503fe53fca553d18d7f0c369c6c" + integrity sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A== -postcss-discard-overridden@^5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-5.0.3.tgz#004b9818cabb407e60616509267567150b327a3f" - integrity sha512-yRTXknIZA4k8Yo4FiF1xbsLj/VBxfXEWxJNIrtIy6HC9KQ4xJxcPtoaaskh6QptCGrrcGnhKsTsENTRPZOBu4g== +postcss-discard-overridden@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz#7e8c5b53325747e9d90131bb88635282fb4a276e" + integrity sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw== -postcss-double-position-gradients@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/postcss-double-position-gradients/-/postcss-double-position-gradients-3.0.4.tgz#2484b9785ef3ba81b0f03a279c52ec58fc5344c2" - integrity sha512-qz+s5vhKJlsHw8HjSs+HVk2QGFdRyC68KGRQGX3i+GcnUjhWhXQEmCXW6siOJkZ1giu0ddPwSO6I6JdVVVPoog== +postcss-double-position-gradients@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/postcss-double-position-gradients/-/postcss-double-position-gradients-3.1.2.tgz#b96318fdb477be95997e86edd29c6e3557a49b91" + integrity sha512-GX+FuE/uBR6eskOK+4vkXgT6pDkexLokPaz/AbJna9s5Kzp/yl488pKPjhy0obB475ovfT1Wv8ho7U/cHNaRgQ== dependencies: + "@csstools/postcss-progressive-custom-properties" "^1.1.0" postcss-value-parser "^4.2.0" -postcss-env-function@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/postcss-env-function/-/postcss-env-function-4.0.4.tgz#4e85359ca4fcdde4ec4b73752a41de818dbe91cc" - integrity sha512-0ltahRTPtXSIlEZFv7zIvdEib7HN0ZbUQxrxIKn8KbiRyhALo854I/CggU5lyZe6ZBvSTJ6Al2vkZecI2OhneQ== +postcss-env-function@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/postcss-env-function/-/postcss-env-function-4.0.6.tgz#7b2d24c812f540ed6eda4c81f6090416722a8e7a" + integrity sha512-kpA6FsLra+NqcFnL81TnsU+Z7orGtDTxcOhl6pwXeEq1yFPpRMkCDpHhrz8CFQDr/Wfm0jLiNQ1OsGGPjlqPwA== dependencies: postcss-value-parser "^4.2.0" @@ -8140,63 +8532,73 @@ postcss-flexbugs-fixes@^5.0.2: resolved "https://registry.yarnpkg.com/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-5.0.2.tgz#2028e145313074fc9abe276cb7ca14e5401eb49d" integrity sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ== -postcss-focus-visible@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/postcss-focus-visible/-/postcss-focus-visible-6.0.3.tgz#14635b71a6b9140f488f11f26cbc9965a13f6843" - integrity sha512-ozOsg+L1U8S+rxSHnJJiET6dNLyADcPHhEarhhtCI9DBLGOPG/2i4ddVoFch9LzrBgb8uDaaRI4nuid2OM82ZA== +postcss-focus-visible@^6.0.4: + version "6.0.4" + resolved "https://registry.yarnpkg.com/postcss-focus-visible/-/postcss-focus-visible-6.0.4.tgz#50c9ea9afa0ee657fb75635fabad25e18d76bf9e" + integrity sha512-QcKuUU/dgNsstIK6HELFRT5Y3lbrMLEOwG+A4s5cA+fx3A3y/JTq3X9LaOj3OC3ALH0XqyrgQIgey/MIZ8Wczw== dependencies: - postcss-selector-parser "^6.0.8" + postcss-selector-parser "^6.0.9" -postcss-focus-within@^5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/postcss-focus-within/-/postcss-focus-within-5.0.3.tgz#0b0bf425f14a646bbfd973b463e2d20d85a3a841" - integrity sha512-fk9y2uFS6/Kpp7/A9Hz9Z4rlFQ8+tzgBcQCXAFSrXFGAbKx+4ZZOmmfHuYjCOMegPWoz0pnC6fNzi8j7Xyqp5Q== +postcss-focus-within@^5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/postcss-focus-within/-/postcss-focus-within-5.0.4.tgz#5b1d2ec603195f3344b716c0b75f61e44e8d2e20" + integrity sha512-vvjDN++C0mu8jz4af5d52CB184ogg/sSxAFS+oUJQq2SuCe7T5U2iIsVJtsCp2d6R4j0jr5+q3rPkBVZkXD9fQ== dependencies: - postcss-selector-parser "^6.0.8" + postcss-selector-parser "^6.0.9" postcss-font-variant@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz#efd59b4b7ea8bb06127f2d031bfbb7f24d32fa66" integrity sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA== -postcss-gap-properties@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/postcss-gap-properties/-/postcss-gap-properties-3.0.2.tgz#562fbf43a6a721565b3ca0e01008690991d2f726" - integrity sha512-EaMy/pbxtQnKDsnbEjdqlkCkROTQZzolcLKgIE+3b7EuJfJydH55cZeHfm+MtIezXRqhR80VKgaztO/vHq94Fw== +postcss-gap-properties@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/postcss-gap-properties/-/postcss-gap-properties-3.0.5.tgz#f7e3cddcf73ee19e94ccf7cb77773f9560aa2fff" + integrity sha512-IuE6gKSdoUNcvkGIqdtjtcMtZIFyXZhmFd5RUlg97iVEvp1BZKV5ngsAjCjrVy+14uhGBQl9tzmi1Qwq4kqVOg== -postcss-image-set-function@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/postcss-image-set-function/-/postcss-image-set-function-4.0.4.tgz#ce91579ab2c1386d412ff5cd5e733c474b1f75ee" - integrity sha512-BlEo9gSTj66lXjRNByvkMK9dEdEGFXRfGjKRi9fo8s0/P3oEk74cAoonl/utiM50E2OPVb/XSu+lWvdW4KtE/Q== +postcss-image-set-function@^4.0.7: + version "4.0.7" + resolved "https://registry.yarnpkg.com/postcss-image-set-function/-/postcss-image-set-function-4.0.7.tgz#08353bd756f1cbfb3b6e93182c7829879114481f" + integrity sha512-9T2r9rsvYzm5ndsBE8WgtrMlIT7VbtTfE7b3BQnudUqnBcBo7L758oc+o+pdj/dUV0l5wjwSdjeOH2DZtfv8qw== dependencies: postcss-value-parser "^4.2.0" +postcss-import@^15.1.0: + version "15.1.0" + resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-15.1.0.tgz#41c64ed8cc0e23735a9698b3249ffdbf704adc70" + integrity sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew== + dependencies: + postcss-value-parser "^4.0.0" + read-cache "^1.0.0" + resolve "^1.1.7" + postcss-initial@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/postcss-initial/-/postcss-initial-4.0.1.tgz#529f735f72c5724a0fb30527df6fb7ac54d7de42" integrity sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ== -postcss-js@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-4.0.0.tgz#31db79889531b80dc7bc9b0ad283e418dce0ac00" - integrity sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ== +postcss-js@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-4.0.1.tgz#61598186f3703bab052f1c4f7d805f3991bee9d2" + integrity sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw== dependencies: camelcase-css "^2.0.1" -postcss-lab-function@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/postcss-lab-function/-/postcss-lab-function-4.0.3.tgz#633745b324afbcd5881da85fe2cef58b17487536" - integrity sha512-MH4tymWmefdZQ7uVG/4icfLjAQmH6o2NRYyVh2mKoB4RXJp9PjsyhZwhH4ouaCQHvg+qJVj3RzeAR1EQpIlXZA== +postcss-lab-function@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/postcss-lab-function/-/postcss-lab-function-4.2.1.tgz#6fe4c015102ff7cd27d1bd5385582f67ebdbdc98" + integrity sha512-xuXll4isR03CrQsmxyz92LJB2xX9n+pZJ5jE9JgcnmsCammLyKdlzrBin+25dy6wIjfhJpKBAN80gsTlCgRk2w== dependencies: + "@csstools/postcss-progressive-custom-properties" "^1.1.0" postcss-value-parser "^4.2.0" -postcss-load-config@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-3.1.1.tgz#2f53a17f2f543d9e63864460af42efdac0d41f87" - integrity sha512-c/9XYboIbSEUZpiD1UQD0IKiUe8n9WHYV7YFe7X7J+ZwCsEKkUJSFWjS9hBU1RR9THR7jMXst8sxiqP0jjo2mg== +postcss-load-config@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-4.0.2.tgz#7159dcf626118d33e299f485d6afe4aff7c4a3e3" + integrity sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ== dependencies: - lilconfig "^2.0.4" - yaml "^1.10.2" + lilconfig "^3.0.0" + yaml "^2.3.4" postcss-loader@^6.2.1: version "6.2.1" @@ -8207,84 +8609,84 @@ postcss-loader@^6.2.1: klona "^2.0.5" semver "^7.3.5" -postcss-logical@^5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/postcss-logical/-/postcss-logical-5.0.3.tgz#9934e0fb16af70adbd94217b24d2f315ceb5c2f0" - integrity sha512-P5NcHWYrif0vK8rgOy/T87vg0WRIj3HSknrvp1wzDbiBeoDPVmiVRmkown2eSQdpPveat/MC1ess5uhzZFVnqQ== +postcss-logical@^5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/postcss-logical/-/postcss-logical-5.0.4.tgz#ec75b1ee54421acc04d5921576b7d8db6b0e6f73" + integrity sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g== postcss-media-minmax@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz#7140bddec173e2d6d657edbd8554a55794e2a5b5" integrity sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ== -postcss-merge-longhand@^5.0.5: - version "5.0.5" - resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-5.0.5.tgz#cbc217ca22fb5a3e6ee22a6a1aa6920ec1f3c628" - integrity sha512-R2BCPJJ/U2oh1uTWEYn9CcJ7MMcQ1iIbj9wfr2s/zHu5om5MP/ewKdaunpfJqR1WYzqCsgnXuRoVXPAzxdqy8g== +postcss-merge-longhand@^5.1.7: + version "5.1.7" + resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz#24a1bdf402d9ef0e70f568f39bdc0344d568fb16" + integrity sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ== dependencies: postcss-value-parser "^4.2.0" - stylehacks "^5.0.2" + stylehacks "^5.1.1" -postcss-merge-rules@^5.0.5: - version "5.0.5" - resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-5.0.5.tgz#2a18669ec214019884a60f0a0d356803a8138366" - integrity sha512-3Oa26/Pb9VOFVksJjFG45SNoe4nhGvJ2Uc6TlRimqF8uhfOCEhVCaJ3rvEat5UFOn2UZqTY5Da8dFgCh3Iq0Ug== +postcss-merge-rules@^5.1.4: + version "5.1.4" + resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-5.1.4.tgz#2f26fa5cacb75b1402e213789f6766ae5e40313c" + integrity sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g== dependencies: - browserslist "^4.16.6" + browserslist "^4.21.4" caniuse-api "^3.0.0" - cssnano-utils "^3.0.1" + cssnano-utils "^3.1.0" postcss-selector-parser "^6.0.5" -postcss-minify-font-values@^5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-5.0.3.tgz#48c455c4cd980ecd07ac9bf3fc58e9d8a2ae4168" - integrity sha512-bC45rVzEwsLhv/cL1eCjoo2OOjbSk9I7HKFBYnBvtyuIZlf7uMipMATXtA0Fc3jwPo3wuPIW1jRJWKzflMh1sA== +postcss-minify-font-values@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz#f1df0014a726083d260d3bd85d7385fb89d1f01b" + integrity sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA== dependencies: postcss-value-parser "^4.2.0" -postcss-minify-gradients@^5.0.5: - version "5.0.5" - resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-5.0.5.tgz#a5572b9c98ed52cbd7414db24b873f8b9e418290" - integrity sha512-/YjvXs8PepsoiZAIpjstOO4IHKwFAqYNqbA1yVdqklM84tbUUneh6omJxGlRlF3mi6K5Pa067Mg6IwqEnYC8Zg== +postcss-minify-gradients@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz#f1fe1b4f498134a5068240c2f25d46fcd236ba2c" + integrity sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw== dependencies: colord "^2.9.1" - cssnano-utils "^3.0.1" + cssnano-utils "^3.1.0" postcss-value-parser "^4.2.0" -postcss-minify-params@^5.0.4: - version "5.0.4" - resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-5.0.4.tgz#230a4d04456609e614db1d48c2eebc21f6490a45" - integrity sha512-Z0vjod9lRZEmEPfEmA2sCfjbfEEFKefMD3RDIQSUfXK4LpCyWkX1CniUgyNvnjJFLDPSxtgKzozhHhPHKoeGkg== +postcss-minify-params@^5.1.4: + version "5.1.4" + resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz#c06a6c787128b3208b38c9364cfc40c8aa5d7352" + integrity sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw== dependencies: - browserslist "^4.16.6" - cssnano-utils "^3.0.1" + browserslist "^4.21.4" + cssnano-utils "^3.1.0" postcss-value-parser "^4.2.0" -postcss-minify-selectors@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-5.1.2.tgz#bc9698f713b9dab7f44f1ec30643fcbad9a043c0" - integrity sha512-gpn1nJDMCf3g32y/7kl+jsdamhiYT+/zmEt57RoT9GmzlixBNRPohI7k8UIHelLABhdLf3MSZhtM33xuH5eQOQ== +postcss-minify-selectors@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz#d4e7e6b46147b8117ea9325a915a801d5fe656c6" + integrity sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg== dependencies: postcss-selector-parser "^6.0.5" -postcss-modules-extract-imports@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d" - integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw== +postcss-modules-extract-imports@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz#b4497cb85a9c0c4b5aabeb759bb25e8d89f15002" + integrity sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q== -postcss-modules-local-by-default@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz#ebbb54fae1598eecfdf691a02b3ff3b390a5a51c" - integrity sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ== +postcss-modules-local-by-default@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz#f1b9bd757a8edf4d8556e8d0f4f894260e3df78f" + integrity sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw== dependencies: icss-utils "^5.0.0" postcss-selector-parser "^6.0.2" postcss-value-parser "^4.1.0" -postcss-modules-scope@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06" - integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg== +postcss-modules-scope@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz#a43d28289a169ce2c15c00c4e64c0858e43457d5" + integrity sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ== dependencies: postcss-selector-parser "^6.0.4" @@ -8295,80 +8697,81 @@ postcss-modules-values@^4.0.0: dependencies: icss-utils "^5.0.0" -postcss-nested@5.0.6: - version "5.0.6" - resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-5.0.6.tgz#466343f7fc8d3d46af3e7dba3fcd47d052a945bc" - integrity sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA== +postcss-nested@^6.0.1: + version "6.2.0" + resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-6.2.0.tgz#4c2d22ab5f20b9cb61e2c5c5915950784d068131" + integrity sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ== dependencies: - postcss-selector-parser "^6.0.6" + postcss-selector-parser "^6.1.1" -postcss-nesting@^10.1.2: - version "10.1.2" - resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-10.1.2.tgz#2e5f811b3d75602ea18a95dd445bde5297145141" - integrity sha512-dJGmgmsvpzKoVMtDMQQG/T6FSqs6kDtUDirIfl4KnjMCiY9/ETX8jdKyCd20swSRAbUYkaBKV20pxkzxoOXLqQ== +postcss-nesting@^10.2.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-10.2.0.tgz#0b12ce0db8edfd2d8ae0aaf86427370b898890be" + integrity sha512-EwMkYchxiDiKUhlJGzWsD9b2zvq/r2SSubcRrgP+jujMXFzqvANLt16lJANC+5uZ6hjI7lpRmI6O8JIl+8l1KA== dependencies: - postcss-selector-parser "^6.0.8" + "@csstools/selector-specificity" "^2.0.0" + postcss-selector-parser "^6.0.10" -postcss-normalize-charset@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-5.0.2.tgz#eb6130c8a8e950ce25f9ea512de1d9d6a6f81439" - integrity sha512-fEMhYXzO8My+gC009qDc/3bgnFP8Fv1Ic8uw4ec4YTlhIOw63tGPk1YFd7fk9bZUf1DAbkhiL/QPWs9JLqdF2g== +postcss-normalize-charset@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz#9302de0b29094b52c259e9b2cf8dc0879879f0ed" + integrity sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg== -postcss-normalize-display-values@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-5.0.2.tgz#8b5273c6c7d0a445e6ef226b8a5bb3204a55fb99" - integrity sha512-RxXoJPUR0shSjkMMzgEZDjGPrgXUVYyWA/YwQRicb48H15OClPuaDR7tYokLAlGZ2tCSENEN5WxjgxSD5m4cUw== +postcss-normalize-display-values@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz#72abbae58081960e9edd7200fcf21ab8325c3da8" + integrity sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA== dependencies: postcss-value-parser "^4.2.0" -postcss-normalize-positions@^5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-5.0.3.tgz#b63fcc4ff5fbf65934fafaf83270b2da214711d1" - integrity sha512-U+rmhjrNBvIGYqr/1tD4wXPFFMKUbXsYXvlUCzLi0tOCUS6LoeEAnmVXXJY/MEB/1CKZZwBSs2tmzGawcygVBA== +postcss-normalize-positions@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz#ef97279d894087b59325b45c47f1e863daefbb92" + integrity sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg== dependencies: postcss-value-parser "^4.2.0" -postcss-normalize-repeat-style@^5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.0.3.tgz#488c0ad8aac0fa4f66ef56cc8d604b3fd9bf705f" - integrity sha512-uk1+xYx0AMbA3nLSNhbDrqbf/rx+Iuq5tVad2VNyaxxJzx79oGieJ6D9F6AfOL2GtiIbP7vTYlpYHtG+ERFXTg== +postcss-normalize-repeat-style@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz#e9eb96805204f4766df66fd09ed2e13545420fb2" + integrity sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g== dependencies: postcss-value-parser "^4.2.0" -postcss-normalize-string@^5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-5.0.3.tgz#49e0a1d58a119d5435ef21893ad03136a6e8f0e6" - integrity sha512-Mf2V4JbIDboNGQhW6xW0YREDiYXoX3WrD3EjKkjvnpAJ6W4qqjLnK/c9aioyVFaWWHVdP5zVRw/9DI5S3oLDFw== +postcss-normalize-string@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz#411961169e07308c82c1f8c55f3e8a337757e228" + integrity sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w== dependencies: postcss-value-parser "^4.2.0" -postcss-normalize-timing-functions@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.0.2.tgz#db4f4f49721f47667afd1fdc5edb032f8d9cdb2e" - integrity sha512-Ao0PP6MoYsRU1LxeVUW740ioknvdIUmfr6uAA3xWlQJ9s69/Tupy8qwhuKG3xWfl+KvLMAP9p2WXF9cwuk/7Bg== +postcss-normalize-timing-functions@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz#d5614410f8f0b2388e9f240aa6011ba6f52dafbb" + integrity sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg== dependencies: postcss-value-parser "^4.2.0" -postcss-normalize-unicode@^5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-5.0.3.tgz#10f0d30093598a58c48a616491cc7fa53256dd43" - integrity sha512-uNC7BmS/7h6to2UWa4RFH8sOTzu2O9dVWPE/F9Vm9GdhONiD/c1kNaCLbmsFHlKWcEx7alNUChQ+jH/QAlqsQw== +postcss-normalize-unicode@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz#f67297fca3fea7f17e0d2caa40769afc487aa030" + integrity sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA== dependencies: - browserslist "^4.16.6" + browserslist "^4.21.4" postcss-value-parser "^4.2.0" -postcss-normalize-url@^5.0.4: - version "5.0.4" - resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-5.0.4.tgz#3b0322c425e31dd275174d0d5db0e466f50810fb" - integrity sha512-cNj3RzK2pgQQyNp7dzq0dqpUpQ/wYtdDZM3DepPmFjCmYIfceuD9VIAcOdvrNetjIU65g1B4uwdP/Krf6AFdXg== +postcss-normalize-url@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz#ed9d88ca82e21abef99f743457d3729a042adcdc" + integrity sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew== dependencies: normalize-url "^6.0.1" postcss-value-parser "^4.2.0" -postcss-normalize-whitespace@^5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.0.3.tgz#fb6bcc9ff2f834448b802657c7acd0956f4591d1" - integrity sha512-333JWRnX655fSoUbufJ10HJop3c8mrpKkCCUnEmgz/Cb/QEtW+/TMZwDAUt4lnwqP6tCCk0x0b58jqvDgiQm/A== +postcss-normalize-whitespace@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz#08a1a0d1ffa17a7cc6efe1e6c9da969cc4493cfa" + integrity sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA== dependencies: postcss-value-parser "^4.2.0" @@ -8381,89 +8784,112 @@ postcss-normalize@^10.0.1: postcss-browser-comments "^4" sanitize.css "*" -postcss-ordered-values@^5.0.4: - version "5.0.4" - resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-5.0.4.tgz#f799dca87a7f17526d31a20085e61768d0b00534" - integrity sha512-taKtGDZtyYUMVYkg+MuJeBUiTF6cGHZmo/qcW7ibvW79UlyKuSHbo6dpCIiqI+j9oJsXWzP+ovIxoyLDOeQFdw== +postcss-opacity-percentage@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/postcss-opacity-percentage/-/postcss-opacity-percentage-1.1.3.tgz#5b89b35551a556e20c5d23eb5260fbfcf5245da6" + integrity sha512-An6Ba4pHBiDtyVpSLymUUERMo2cU7s+Obz6BTrS+gxkbnSBNKSuD0AVUc+CpBMrpVPKKfoVz0WQCX+Tnst0i4A== + +postcss-ordered-values@^5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz#b6fd2bd10f937b23d86bc829c69e7732ce76ea38" + integrity sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ== dependencies: - cssnano-utils "^3.0.1" + cssnano-utils "^3.1.0" postcss-value-parser "^4.2.0" -postcss-overflow-shorthand@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/postcss-overflow-shorthand/-/postcss-overflow-shorthand-3.0.2.tgz#b4e9c89728cd1e4918173dfb95936b75f78d4148" - integrity sha512-odBMVt6PTX7jOE9UNvmnLrFzA9pXS44Jd5shFGGtSHY80QCuJF+14McSy0iavZggRZ9Oj//C9vOKQmexvyEJMg== +postcss-overflow-shorthand@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/postcss-overflow-shorthand/-/postcss-overflow-shorthand-3.0.4.tgz#7ed6486fec44b76f0eab15aa4866cda5d55d893e" + integrity sha512-otYl/ylHK8Y9bcBnPLo3foYFLL6a6Ak+3EQBPOTR7luMYCOsiVTUk1iLvNf6tVPNGXcoL9Hoz37kpfriRIFb4A== + dependencies: + postcss-value-parser "^4.2.0" postcss-page-break@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/postcss-page-break/-/postcss-page-break-3.0.4.tgz#7fbf741c233621622b68d435babfb70dd8c1ee5f" integrity sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ== -postcss-place@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/postcss-place/-/postcss-place-7.0.3.tgz#ca8040dfd937c7769a233a3bd6e66e139cf89e62" - integrity sha512-tDQ3m+GYoOar+KoQgj+pwPAvGHAp/Sby6vrFiyrELrMKQJ4AejL0NcS0mm296OKKYA2SRg9ism/hlT/OLhBrdQ== +postcss-place@^7.0.5: + version "7.0.5" + resolved "https://registry.yarnpkg.com/postcss-place/-/postcss-place-7.0.5.tgz#95dbf85fd9656a3a6e60e832b5809914236986c4" + integrity sha512-wR8igaZROA6Z4pv0d+bvVrvGY4GVHihBCBQieXFY3kuSuMyOmEnnfFzHl/tQuqHZkfkIVBEbDvYcFfHmpSet9g== dependencies: postcss-value-parser "^4.2.0" postcss-preset-env@^7.0.1: - version "7.2.3" - resolved "https://registry.yarnpkg.com/postcss-preset-env/-/postcss-preset-env-7.2.3.tgz#01b9b6eea0ff16c27a3d514f10105d56363428a6" - integrity sha512-Ok0DhLfwrcNGrBn8sNdy1uZqWRk/9FId0GiQ39W4ILop5GHtjJs8bu1MY9isPwHInpVEPWjb4CEcEaSbBLpfwA== - dependencies: - autoprefixer "^10.4.2" - browserslist "^4.19.1" - caniuse-lite "^1.0.30001299" - css-blank-pseudo "^3.0.2" - css-has-pseudo "^3.0.3" - css-prefers-color-scheme "^6.0.2" - cssdb "^5.0.0" - postcss-attribute-case-insensitive "^5.0.0" - postcss-color-functional-notation "^4.2.1" - postcss-color-hex-alpha "^8.0.2" - postcss-color-rebeccapurple "^7.0.2" - postcss-custom-media "^8.0.0" - postcss-custom-properties "^12.1.2" - postcss-custom-selectors "^6.0.0" - postcss-dir-pseudo-class "^6.0.3" - postcss-double-position-gradients "^3.0.4" - postcss-env-function "^4.0.4" - postcss-focus-visible "^6.0.3" - postcss-focus-within "^5.0.3" + version "7.8.3" + resolved "https://registry.yarnpkg.com/postcss-preset-env/-/postcss-preset-env-7.8.3.tgz#2a50f5e612c3149cc7af75634e202a5b2ad4f1e2" + integrity sha512-T1LgRm5uEVFSEF83vHZJV2z19lHg4yJuZ6gXZZkqVsqv63nlr6zabMH3l4Pc01FQCyfWVrh2GaUeCVy9Po+Aag== + dependencies: + "@csstools/postcss-cascade-layers" "^1.1.1" + "@csstools/postcss-color-function" "^1.1.1" + "@csstools/postcss-font-format-keywords" "^1.0.1" + "@csstools/postcss-hwb-function" "^1.0.2" + "@csstools/postcss-ic-unit" "^1.0.1" + "@csstools/postcss-is-pseudo-class" "^2.0.7" + "@csstools/postcss-nested-calc" "^1.0.0" + "@csstools/postcss-normalize-display-values" "^1.0.1" + "@csstools/postcss-oklab-function" "^1.1.1" + "@csstools/postcss-progressive-custom-properties" "^1.3.0" + "@csstools/postcss-stepped-value-functions" "^1.0.1" + "@csstools/postcss-text-decoration-shorthand" "^1.0.0" + "@csstools/postcss-trigonometric-functions" "^1.0.2" + "@csstools/postcss-unset-value" "^1.0.2" + autoprefixer "^10.4.13" + browserslist "^4.21.4" + css-blank-pseudo "^3.0.3" + css-has-pseudo "^3.0.4" + css-prefers-color-scheme "^6.0.3" + cssdb "^7.1.0" + postcss-attribute-case-insensitive "^5.0.2" + postcss-clamp "^4.1.0" + postcss-color-functional-notation "^4.2.4" + postcss-color-hex-alpha "^8.0.4" + postcss-color-rebeccapurple "^7.1.1" + postcss-custom-media "^8.0.2" + postcss-custom-properties "^12.1.10" + postcss-custom-selectors "^6.0.3" + postcss-dir-pseudo-class "^6.0.5" + postcss-double-position-gradients "^3.1.2" + postcss-env-function "^4.0.6" + postcss-focus-visible "^6.0.4" + postcss-focus-within "^5.0.4" postcss-font-variant "^5.0.0" - postcss-gap-properties "^3.0.2" - postcss-image-set-function "^4.0.4" + postcss-gap-properties "^3.0.5" + postcss-image-set-function "^4.0.7" postcss-initial "^4.0.1" - postcss-lab-function "^4.0.3" - postcss-logical "^5.0.3" + postcss-lab-function "^4.2.1" + postcss-logical "^5.0.4" postcss-media-minmax "^5.0.0" - postcss-nesting "^10.1.2" - postcss-overflow-shorthand "^3.0.2" + postcss-nesting "^10.2.0" + postcss-opacity-percentage "^1.1.2" + postcss-overflow-shorthand "^3.0.4" postcss-page-break "^3.0.4" - postcss-place "^7.0.3" - postcss-pseudo-class-any-link "^7.0.2" + postcss-place "^7.0.5" + postcss-pseudo-class-any-link "^7.1.6" postcss-replace-overflow-wrap "^4.0.0" - postcss-selector-not "^5.0.0" + postcss-selector-not "^6.0.1" + postcss-value-parser "^4.2.0" -postcss-pseudo-class-any-link@^7.0.2: - version "7.0.2" - resolved "https://registry.yarnpkg.com/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.0.2.tgz#6284c2f970715c78fe992d2fac1130e9991585c9" - integrity sha512-CG35J1COUH7OOBgpw5O+0koOLUd5N4vUGKUqSAuIe4GiuLHWU96Pqp+UPC8QITTd12zYAFx76pV7qWT/0Aj/TA== +postcss-pseudo-class-any-link@^7.1.6: + version "7.1.6" + resolved "https://registry.yarnpkg.com/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.1.6.tgz#2693b221902da772c278def85a4d9a64b6e617ab" + integrity sha512-9sCtZkO6f/5ML9WcTLcIyV1yz9D1rf0tWc+ulKcvV30s0iZKS/ONyETvoWsr6vnrmW+X+KmuK3gV/w5EWnT37w== dependencies: - postcss-selector-parser "^6.0.8" + postcss-selector-parser "^6.0.10" -postcss-reduce-initial@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-5.0.2.tgz#fa424ce8aa88a89bc0b6d0f94871b24abe94c048" - integrity sha512-v/kbAAQ+S1V5v9TJvbGkV98V2ERPdU6XvMcKMjqAlYiJ2NtsHGlKYLPjWWcXlaTKNxooId7BGxeraK8qXvzKtw== +postcss-reduce-initial@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-5.1.2.tgz#798cd77b3e033eae7105c18c9d371d989e1382d6" + integrity sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg== dependencies: - browserslist "^4.16.6" + browserslist "^4.21.4" caniuse-api "^3.0.0" -postcss-reduce-transforms@^5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-5.0.3.tgz#df60fab34698a43073e8b87938c71df7a3b040ac" - integrity sha512-yDnTUab5i7auHiNwdcL1f+pBnqQFf+7eC4cbC7D8Lc1FkvNZhtpkdad+9U4wDdFb84haupMf0rA/Zc5LcTe/3A== +postcss-reduce-transforms@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz#333b70e7758b802f3dd0ddfe98bb1ccfef96b6e9" + integrity sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ== dependencies: postcss-value-parser "^4.2.0" @@ -8472,37 +8898,37 @@ postcss-replace-overflow-wrap@^4.0.0: resolved "https://registry.yarnpkg.com/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz#d2df6bed10b477bf9c52fab28c568b4b29ca4319" integrity sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw== -postcss-selector-not@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/postcss-selector-not/-/postcss-selector-not-5.0.0.tgz#ac5fc506f7565dd872f82f5314c0f81a05630dc7" - integrity sha512-/2K3A4TCP9orP4TNS7u3tGdRFVKqz/E6pX3aGnriPG0jU78of8wsUcqE4QAhWEU0d+WnMSF93Ah3F//vUtK+iQ== +postcss-selector-not@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/postcss-selector-not/-/postcss-selector-not-6.0.1.tgz#8f0a709bf7d4b45222793fc34409be407537556d" + integrity sha512-1i9affjAe9xu/y9uqWH+tD4r6/hDaXJruk8xn2x1vzxC2U3J3LKO3zJW4CyxlNhA56pADJ/djpEwpH1RClI2rQ== dependencies: - balanced-match "^1.0.0" + postcss-selector-parser "^6.0.10" -postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.6, postcss-selector-parser@^6.0.8, postcss-selector-parser@^6.0.9: - version "6.0.9" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.9.tgz#ee71c3b9ff63d9cd130838876c13a2ec1a992b2f" - integrity sha512-UO3SgnZOVTwu4kyLR22UQ1xZh086RyNZppb7lLAKBFK8a32ttG5i87Y/P3+2bRSjZNyJ1B7hfFNo273tKe9YxQ== +postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.9, postcss-selector-parser@^6.1.1: + version "6.1.2" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz#27ecb41fb0e3b6ba7a1ec84fff347f734c7929de" + integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg== dependencies: cssesc "^3.0.0" util-deprecate "^1.0.2" -postcss-svgo@^5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-5.0.3.tgz#d945185756e5dfaae07f9edb0d3cae7ff79f9b30" - integrity sha512-41XZUA1wNDAZrQ3XgWREL/M2zSw8LJPvb5ZWivljBsUQAGoEKMYm6okHsTjJxKYI4M75RQEH4KYlEM52VwdXVA== +postcss-svgo@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-5.1.0.tgz#0a317400ced789f233a28826e77523f15857d80d" + integrity sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA== dependencies: - postcss-value-parser "^4.1.0" + postcss-value-parser "^4.2.0" svgo "^2.7.0" -postcss-unique-selectors@^5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-5.0.3.tgz#07fd116a8fbd9202e7030f7c4952e7b52c26c63d" - integrity sha512-V5tX2hadSSn+miVCluuK1IDGy+7jAXSOfRZ2DQ+s/4uQZb/orDYBjH0CHgFrXsRw78p4QTuEFA9kI6C956UnHQ== +postcss-unique-selectors@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz#a9f273d1eacd09e9aa6088f4b0507b18b1b541b6" + integrity sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA== dependencies: postcss-selector-parser "^6.0.5" -postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: +postcss-value-parser@^4.0.0, postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== @@ -8524,14 +8950,14 @@ postcss@^7.0.35: picocolors "^0.2.1" source-map "^0.6.1" -postcss@^8.2.15, postcss@^8.3.5, postcss@^8.4.4: - version "8.4.5" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.5.tgz#bae665764dfd4c6fcc24dc0fdf7e7aa00cc77f95" - integrity sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg== +postcss@^8.3.5, postcss@^8.4.23, postcss@^8.4.33, postcss@^8.4.4: + version "8.4.47" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.47.tgz#5bf6c9a010f3e724c503bf03ef7947dcb0fea365" + integrity sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ== dependencies: - nanoid "^3.1.30" - picocolors "^1.0.0" - source-map-js "^1.0.1" + nanoid "^3.3.7" + picocolors "^1.1.0" + source-map-js "^1.2.1" prelude-ls@^1.2.1: version "1.2.1" @@ -8541,7 +8967,7 @@ prelude-ls@^1.2.1: prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" - integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= + integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== pretty-bytes@^5.3.0, pretty-bytes@^5.4.1: version "5.6.0" @@ -8556,7 +8982,7 @@ pretty-error@^4.0.0: lodash "^4.17.20" renderkid "^3.0.0" -pretty-format@^27.0.0, pretty-format@^27.4.6: +pretty-format@^27.0.0: version "27.4.6" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.4.6.tgz#1b784d2f53c68db31797b2348fa39b49e31846b7" integrity sha512-NblstegA1y/RJW2VyML+3LlpFjzx62cUrtBIKIWDXEDkjNeleA7Od7nrzcs/VLQvAeV4CgSYhrN39DRN88Qi/g== @@ -8565,7 +8991,7 @@ pretty-format@^27.0.0, pretty-format@^27.4.6: ansi-styles "^5.0.0" react-is "^17.0.1" -pretty-format@^27.0.2: +pretty-format@^27.0.2, pretty-format@^27.4.6, pretty-format@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== @@ -8574,6 +9000,16 @@ pretty-format@^27.0.2: ansi-styles "^5.0.0" react-is "^17.0.1" +pretty-format@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-28.1.3.tgz#c9fba8cedf99ce50963a11b27d982a9ae90970d5" + integrity sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q== + dependencies: + "@jest/schemas" "^28.1.3" + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" + react-is "^18.0.0" + prismjs@^1.27.0: version "1.29.0" resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.29.0.tgz#f113555a8fa9b57c35e637bba27509dcf802dd12" @@ -8590,9 +9026,9 @@ process-nextick-args@~2.0.0: integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== promise@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/promise/-/promise-8.1.0.tgz#697c25c3dfe7435dd79fcd58c38a135888eaf05e" - integrity sha512-W04AqnILOL/sPRXziNicCjSNRruLAuIHEOVBazepu0545DDNGYHz7ar9ZgZ1fMU8/MA4mVxp5rkBWRi6OXIy3Q== + version "8.3.0" + resolved "https://registry.yarnpkg.com/promise/-/promise-8.3.0.tgz#8cb333d1edeb61ef23869fbb8a4ea0279ab60e0a" + integrity sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg== dependencies: asap "~2.0.6" @@ -8604,7 +9040,7 @@ prompts@^2.0.1, prompts@^2.4.2: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@^15.6.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -8620,17 +9056,17 @@ property-information@^5.0.0, property-information@^5.3.0: dependencies: xtend "^4.0.0" -protobufjs-cli@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/protobufjs-cli/-/protobufjs-cli-1.0.2.tgz#905fc49007cf4aaf3c45d5f250eb294eedeea062" - integrity sha512-cz9Pq9p/Zs7okc6avH20W7QuyjTclwJPgqXG11jNaulfS3nbVisID8rC+prfgq0gbZE0w9LBFd1OKFF03kgFzg== +protobufjs-cli@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/protobufjs-cli/-/protobufjs-cli-1.1.3.tgz#c58b8566784f0fa1aff11e8d875a31de999637fe" + integrity sha512-MqD10lqF+FMsOayFiNOdOGNlXc4iKDCf0ZQPkPR+gizYh9gqUeGTWulABUCdI+N67w5RfJ6xhgX4J8pa8qmMXQ== dependencies: chalk "^4.0.0" escodegen "^1.13.0" espree "^9.0.0" estraverse "^5.1.0" glob "^8.0.0" - jsdoc "^3.6.3" + jsdoc "^4.0.0" minimist "^1.2.0" semver "^7.1.2" tmp "^0.2.1" @@ -8663,26 +9099,31 @@ proxy-addr@~2.0.7: ipaddr.js "1.9.1" psl@^1.1.33: - version "1.8.0" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" - integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== + version "1.9.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" + integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== + +punycode.js@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7" + integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA== punycode@^2.1.0, punycode@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== q@^1.1.2: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" - integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= + integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw== -qs@6.11.0: - version "6.11.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" - integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== +qs@6.13.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== dependencies: - side-channel "^1.0.4" + side-channel "^1.0.6" query-string@^7.1.1: version "7.1.1" @@ -8704,11 +9145,6 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== -quick-lru@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" - integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== - raf-schd@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a" @@ -8772,10 +9208,10 @@ react-code-blocks@^0.1.6: styled-components "^6.1.0" tslib "^2.6.0" -react-dev-utils@^12.0.0: - version "12.0.0" - resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-12.0.0.tgz#4eab12cdb95692a077616770b5988f0adf806526" - integrity sha512-xBQkitdxozPxt1YZ9O1097EJiVpwHr9FoAuEVURCKV0Av8NBERovJauzP7bo1ThvuhZ4shsQ1AJiu4vQpoT1AQ== +react-dev-utils@^12.0.1: + version "12.0.1" + resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-12.0.1.tgz#ba92edb4a1f379bd46ccd6bcd4e7bc398df33e73" + integrity sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ== dependencies: "@babel/code-frame" "^7.16.0" address "^1.1.2" @@ -8796,7 +9232,7 @@ react-dev-utils@^12.0.0: open "^8.4.0" pkg-up "^3.1.0" prompts "^2.4.2" - react-error-overlay "^6.0.10" + react-error-overlay "^6.0.11" recursive-readdir "^2.2.2" shell-quote "^1.7.3" strip-ansi "^6.0.1" @@ -8828,10 +9264,10 @@ react-element-to-jsx-string@^15.0.0: is-plain-object "5.0.0" react-is "18.1.0" -react-error-overlay@^6.0.10: - version "6.0.10" - resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.10.tgz#0fe26db4fa85d9dbb8624729580e90e7159a59a6" - integrity sha512-mKR90fX7Pm5seCOfz8q9F+66VCc1PGsWSBxKbITjfKVQHMNF2zudxHnMdJiB1fRCb+XsbQV9sO9DCkgsMQgBIA== +react-error-overlay@^6.0.11: + version "6.0.11" + resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb" + integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg== react-focus-lock@^2.11.3: version "2.13.2" @@ -8937,10 +9373,10 @@ react-router@6.3.0: dependencies: history "^5.2.0" -react-scripts@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-5.0.0.tgz#6547a6d7f8b64364ef95273767466cc577cb4b60" - integrity sha512-3i0L2CyIlROz7mxETEdfif6Sfhh9Lfpzi10CtcGs1emDQStmZfWjJbAIMtRD0opVUjQuFWqHZyRZ9PPzKCFxWg== +react-scripts@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-5.0.1.tgz#6285dbd65a8ba6e49ca8d651ce30645a6d980003" + integrity sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ== dependencies: "@babel/core" "^7.16.0" "@pmmmwh/react-refresh-webpack-plugin" "^0.5.3" @@ -8958,7 +9394,7 @@ react-scripts@^5.0.0: dotenv "^10.0.0" dotenv-expand "^5.1.0" eslint "^8.3.0" - eslint-config-react-app "^7.0.0" + eslint-config-react-app "^7.0.1" eslint-webpack-plugin "^3.1.1" file-loader "^6.2.0" fs-extra "^10.0.0" @@ -8975,7 +9411,7 @@ react-scripts@^5.0.0: postcss-preset-env "^7.0.1" prompts "^2.4.2" react-app-polyfill "^3.0.0" - react-dev-utils "^12.0.0" + react-dev-utils "^12.0.1" react-refresh "^0.11.0" resolve "^1.20.0" resolve-url-loader "^4.0.0" @@ -9033,10 +9469,17 @@ react@^18.3.1: dependencies: loose-envify "^1.1.0" +read-cache@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" + integrity sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA== + dependencies: + pify "^2.3.0" + readable-stream@^2.0.1: - version "2.3.7" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== dependencies: core-util-is "~1.0.0" inherits "~2.0.3" @@ -9047,9 +9490,9 @@ readable-stream@^2.0.1: util-deprecate "~1.0.1" readable-stream@^3.0.6, readable-stream@^3.4.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== dependencies: inherits "^2.0.3" string_decoder "^1.1.1" @@ -9063,11 +9506,11 @@ readdirp@~3.6.0: picomatch "^2.2.1" recursive-readdir@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f" - integrity sha512-nRCcW9Sj7NuZwa2XvH9co8NPeXUBhZP7CRKJtU+cS6PW9FpCIFoI5ib0NT1ZrbNuPoRy0ylyCaUL8Gih4LSyFg== + version "2.2.3" + resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.3.tgz#e726f328c0d69153bcabd5c322d3195252379372" + integrity sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA== dependencies: - minimatch "3.0.4" + minimatch "^3.0.5" redent@^3.0.0: version "3.0.0" @@ -9084,6 +9527,19 @@ redux@^4.2.1: dependencies: "@babel/runtime" "^7.9.2" +reflect.getprototypeof@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz#3ab04c32a8390b770712b7a8633972702d278859" + integrity sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.1" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + globalthis "^1.0.3" + which-builtin-type "^1.1.3" + refractor@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/refractor/-/refractor-3.6.0.tgz#ac318f5a0715ead790fcfb0c71f4dd83d977935a" @@ -9093,10 +9549,10 @@ refractor@^3.6.0: parse-entities "^2.0.0" prismjs "~1.27.0" -regenerate-unicode-properties@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-9.0.0.tgz#54d09c7115e1f53dc2314a974b32c1c344efe326" - integrity sha512-3E12UeNSPfjrgwjkR81m5J7Aw/T55Tu7nUyZVQYCKEOs+2dkxEY+DpPtZzO4YruuiPb7NkYLVcyJC4+zCbk5pA== +regenerate-unicode-properties@^10.2.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz#626e39df8c372338ea9b8028d1f99dc3fd9c3db0" + integrity sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA== dependencies: regenerate "^1.4.2" @@ -9105,69 +9561,61 @@ regenerate@^1.4.2: resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== -regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.4: +regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.9: version "0.13.11" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== -regenerator-runtime@^0.13.9: - version "0.13.9" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" - integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== - regenerator-runtime@^0.14.0: version "0.14.1" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== -regenerator-transform@^0.14.2: - version "0.14.5" - resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.5.tgz#c98da154683671c9c4dcb16ece736517e1b7feb4" - integrity sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw== +regenerator-transform@^0.15.2: + version "0.15.2" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.2.tgz#5bbae58b522098ebdf09bca2f83838929001c7a4" + integrity sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg== dependencies: "@babel/runtime" "^7.8.4" regex-parser@^2.2.11: - version "2.2.11" - resolved "https://registry.yarnpkg.com/regex-parser/-/regex-parser-2.2.11.tgz#3b37ec9049e19479806e878cabe7c1ca83ccfe58" - integrity sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q== + version "2.3.0" + resolved "https://registry.yarnpkg.com/regex-parser/-/regex-parser-2.3.0.tgz#4bb61461b1a19b8b913f3960364bb57887f920ee" + integrity sha512-TVILVSz2jY5D47F4mA4MppkBrafEaiUWJO/TcZHEIuI13AqoZMkK1WMA4Om1YkYbTx+9Ki1/tSUXbceyr9saRg== -regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.3.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.1.tgz#b3f4c0059af9e47eca9f3f660e51d81307e72307" - integrity sha512-pMR7hBVUUGI7PMA37m2ofIdQCsomVnas+Jn5UPGAHQ+/LlwKm/aTLJHdasmHRzlfeZwHiAOaRSo2rbBDm3nNUQ== +regexp.prototype.flags@^1.5.1, regexp.prototype.flags@^1.5.2: + version "1.5.3" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz#b3ae40b1d2499b8350ab2c3fe6ef3845d3a96f42" + integrity sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ== dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - -regexpp@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" - integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== + call-bind "^1.0.7" + define-properties "^1.2.1" + es-errors "^1.3.0" + set-function-name "^2.0.2" -regexpu-core@^4.7.1: - version "4.8.0" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.8.0.tgz#e5605ba361b67b1718478501327502f4479a98f0" - integrity sha512-1F6bYsoYiz6is+oz70NWur2Vlh9KWtswuRuzJOfeYUrfPX2o8n74AnUVaOGDbUqVGO9fNHu48/pjJO4sNVwsOg== +regexpu-core@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-6.1.1.tgz#b469b245594cb2d088ceebc6369dceb8c00becac" + integrity sha512-k67Nb9jvwJcJmVpw0jPttR1/zVfnKf8Km0IPatrU/zJ5XeG3+Slx0xLXs9HByJSzXzrlz5EDvN6yLNMDc2qdnw== dependencies: regenerate "^1.4.2" - regenerate-unicode-properties "^9.0.0" - regjsgen "^0.5.2" - regjsparser "^0.7.0" + regenerate-unicode-properties "^10.2.0" + regjsgen "^0.8.0" + regjsparser "^0.11.0" unicode-match-property-ecmascript "^2.0.0" - unicode-match-property-value-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.1.0" -regjsgen@^0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.2.tgz#92ff295fb1deecbf6ecdab2543d207e91aa33733" - integrity sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A== +regjsgen@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.8.0.tgz#df23ff26e0c5b300a6470cad160a9d090c3a37ab" + integrity sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q== -regjsparser@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.7.0.tgz#a6b667b54c885e18b52554cb4960ef71187e9968" - integrity sha512-A4pcaORqmNMDVwUjWoTzuhwMGpP+NykpfqAsEgI1FSH/EzC7lrN5TMd+kN8YCovX+jMpu8eaqXgXPCa0g8FQNQ== +regjsparser@^0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.11.1.tgz#ae55c74f646db0c8fcb922d4da635e33da405149" + integrity sha512-1DHODs4B8p/mQHU9kr+jv8+wIC9mtG4eBHxWxIq5mhjE3D5oORhCc6deRKzTjs9DcfRFmj9BHSDguZklqCGFWQ== dependencies: - jsesc "~0.5.0" + jsesc "~3.0.2" rehype-raw@^5.1.0: version "5.1.0" @@ -9194,7 +9642,7 @@ rehype-stringify@^8.0.0: relateurl@^0.2.7: version "0.2.7" resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" - integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk= + integrity sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog== remark-breaks@^2.0.2: version "2.0.2" @@ -9264,7 +9712,7 @@ repeat-string@^1.5.4: require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== require-from-string@^2.0.2: version "2.0.2" @@ -9274,14 +9722,14 @@ require-from-string@^2.0.2: requires-port@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" - integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== requizzle@^0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/requizzle/-/requizzle-0.2.3.tgz#4675c90aacafb2c036bd39ba2daa4a1cb777fded" - integrity sha512-YanoyJjykPxGHii0fZP0uUPEXpvqfBDxWV7s6GKAiiOsiqhX6vHNyW3Qzdmqp/iq/ExbhaGbVrjB4ruEVSM4GQ== + version "0.2.4" + resolved "https://registry.yarnpkg.com/requizzle/-/requizzle-0.2.4.tgz#319eb658b28c370f0c20f968fa8ceab98c13d27c" + integrity sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw== dependencies: - lodash "^4.17.14" + lodash "^4.17.21" resolve-cwd@^3.0.0: version "3.0.0" @@ -9312,11 +9760,20 @@ resolve-url-loader@^4.0.0: source-map "0.6.1" resolve.exports@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.1.0.tgz#5ce842b94b05146c0e03076985d1d0e7e48c90c9" - integrity sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ== + version "1.1.1" + resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.1.1.tgz#05cfd5b3edf641571fd46fa608b610dda9ead999" + integrity sha512-/NtpHNDN7jWhAaQ9BvBUYZ6YTXsRBgfqWFWP7BZBaoMJO/I3G5OFzvTuWNlZC3aPjins1F+TNrLKsGbH4rfsRQ== + +resolve@^1.1.7, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.2, resolve@^1.22.4: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" -resolve@^1.14.2, resolve@^1.17.0, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.21.0: +resolve@^1.17.0: version "1.22.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198" integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw== @@ -9325,13 +9782,14 @@ resolve@^1.14.2, resolve@^1.17.0, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.2 path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -resolve@^2.0.0-next.3: - version "2.0.0-next.3" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.3.tgz#d41016293d4a8586a39ca5d9b5f15cbea1f55e46" - integrity sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q== +resolve@^2.0.0-next.5: + version "2.0.0-next.5" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.5.tgz#6b0ec3107e671e52b68cd068ef327173b90dc03c" + integrity sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA== dependencies: - is-core-module "^2.2.0" - path-parse "^1.0.6" + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" restore-cursor@^3.1.0: version "3.1.0" @@ -9358,10 +9816,10 @@ rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2: dependencies: glob "^7.1.3" -rollup-plugin-copy@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/rollup-plugin-copy/-/rollup-plugin-copy-3.4.0.tgz#f1228a3ffb66ffad8606e2f3fb7ff23141ed3286" - integrity sha512-rGUmYYsYsceRJRqLVlE9FivJMxJ7X6jDlP79fmFkL8sJs7VVMSVyA2yfyL+PGyO/vJs4A87hwhgVfz61njI+uQ== +rollup-plugin-copy@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-copy/-/rollup-plugin-copy-3.5.0.tgz#7ffa2a7a8303e143876fa64fb5eed9022d304eeb" + integrity sha512-wI8D5dvYovRMx/YYKtUNt3Yxaw4ORC9xo6Gt9t22kveWz1enG9QrhVlagzwrxSC455xD1dHMKhIJkbsQ7d48BA== dependencies: "@types/fs-extra" "^8.0.1" colorette "^1.1.0" @@ -9383,12 +9841,12 @@ rollup-plugin-svg@^2.0.0: dependencies: rollup-pluginutils "^1.3.1" -rollup-plugin-svgo@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/rollup-plugin-svgo/-/rollup-plugin-svgo-1.1.0.tgz#092faa52753aa0ede52f2405bc58286f945614ae" - integrity sha512-wcOKsBXBErjmCQJZmmnLlAjcVQAUApfzsp/k8fx7u/5vKm0sUFk+IiBT2ylGUX6jUap3PNASAhiew88TJrH6Qg== +rollup-plugin-svgo@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-svgo/-/rollup-plugin-svgo-2.0.0.tgz#d182c145fd11f3f8a43de804e3f5b4b70d580a7a" + integrity sha512-0ryWbGY3sP62brw5p8md5W+1WUMrLUE8d437nGh9gQ+fZFFjlYbyIkctBrTvCm3bIdqN4gxbxg1Xxen1tteD+A== dependencies: - svgo "1.3.0" + svgo "2.8.0" rollup-plugin-terser@^7.0.0, rollup-plugin-terser@^7.0.2: version "7.0.2" @@ -9427,23 +9885,42 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -rxjs@^7.2.0: - version "7.5.4" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.4.tgz#3d6bd407e6b7ce9a123e76b1e770dc5761aa368d" - integrity sha512-h5M3Hk78r6wAheJF0a5YahB1yRQKCsZ4MsGdZ5O9ETbVtjPcScGfrMmoOq7EBsCRzd4BDkvDJ7ogP8Sz5tTFiQ== +rxjs@^7.2.0, rxjs@^7.5.5: + version "7.8.1" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" + integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== dependencies: tslib "^2.1.0" +safe-array-concat@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.2.tgz#81d77ee0c4e8b863635227c721278dd524c20edb" + integrity sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q== + dependencies: + call-bind "^1.0.7" + get-intrinsic "^1.2.4" + has-symbols "^1.0.3" + isarray "^2.0.5" + safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +safe-regex-test@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377" + integrity sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-regex "^1.1.4" + "safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -9455,9 +9932,9 @@ sanitize.css@*: integrity sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA== sass-loader@^12.3.0: - version "12.4.0" - resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-12.4.0.tgz#260b0d51a8a373bb8e88efc11f6ba5583fea0bcf" - integrity sha512-7xN+8khDIzym1oL9XyS6zP6Ges+Bo2B2xbPrjdMHEYyV3AQYhd/wXeru++3ODHF0zMjYmVadblSKrPrjEkL8mg== + version "12.6.0" + resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-12.6.0.tgz#5148362c8e2cdd4b950f3c63ac5d16dbfed37bcb" + integrity sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA== dependencies: klona "^2.0.4" neo-async "^2.6.2" @@ -9499,16 +9976,7 @@ schema-utils@^2.6.5: ajv "^6.12.4" ajv-keywords "^3.5.2" -schema-utils@^3.0.0, schema-utils@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281" - integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== - dependencies: - "@types/json-schema" "^7.0.8" - ajv "^6.12.5" - ajv-keywords "^3.5.2" - -schema-utils@^3.2.0: +schema-utils@^3.0.0, schema-utils@^3.1.1, schema-utils@^3.2.0: version "3.3.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== @@ -9517,49 +9985,43 @@ schema-utils@^3.2.0: ajv "^6.12.5" ajv-keywords "^3.5.2" -schema-utils@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.0.0.tgz#60331e9e3ae78ec5d16353c467c34b3a0a1d3df7" - integrity sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg== +schema-utils@^4.0.0, schema-utils@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.2.0.tgz#70d7c93e153a273a805801882ebd3bff20d89c8b" + integrity sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw== dependencies: "@types/json-schema" "^7.0.9" - ajv "^8.8.0" + ajv "^8.9.0" ajv-formats "^2.1.1" - ajv-keywords "^5.0.0" + ajv-keywords "^5.1.0" select-hose@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" - integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= + integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg== -selfsigned@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.0.0.tgz#e927cd5377cbb0a1075302cff8df1042cc2bce5b" - integrity sha512-cUdFiCbKoa1mZ6osuJs2uDHrs0k0oprsKveFiiaBKCNq3SYyb5gs2HxhQyDNLCmL51ZZThqi4YNDpCK6GOP1iQ== +selfsigned@^2.1.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.4.1.tgz#560d90565442a3ed35b674034cec4e95dceb4ae0" + integrity sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q== dependencies: - node-forge "^1.2.0" - -semver@7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" - integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== + "@types/node-forge" "^1.3.0" + node-forge "^1" -semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: +semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.1.2, semver@^7.3.2, semver@^7.3.5: - version "7.5.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" - integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== - dependencies: - lru-cache "^6.0.0" +semver@^7.1.2, semver@^7.3.2, semver@^7.3.5, semver@^7.3.7, semver@^7.5.3, semver@^7.5.4: + version "7.6.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== -send@0.18.0: - version "0.18.0" - resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" - integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== +send@0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" + integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== dependencies: debug "2.6.9" depd "2.0.0" @@ -9582,14 +10044,7 @@ serialize-javascript@^4.0.0: dependencies: randombytes "^2.1.0" -serialize-javascript@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" - integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== - dependencies: - randombytes "^2.1.0" - -serialize-javascript@^6.0.1: +serialize-javascript@^6.0.0, serialize-javascript@^6.0.1: version "6.0.2" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== @@ -9604,7 +10059,7 @@ serialize-query-params@^1.3.5: serve-index@^1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" - integrity sha1-03aNabHn2C5c4FD/9bRTvqEqkjk= + integrity sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw== dependencies: accepts "~1.3.4" batch "0.6.1" @@ -9614,20 +10069,42 @@ serve-index@^1.9.1: mime-types "~2.1.17" parseurl "~1.3.2" -serve-static@1.15.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" - integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== +serve-static@1.16.2: + version "1.16.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296" + integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== dependencies: - encodeurl "~1.0.2" + encodeurl "~2.0.0" escape-html "~1.0.3" parseurl "~1.3.3" - send "0.18.0" + send "0.19.0" set-cookie-parser@^2.4.6: - version "2.4.8" - resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.4.8.tgz#d0da0ed388bc8f24e706a391f9c9e252a13c58b2" - integrity sha512-edRH8mBKEWNVIVMKejNnuJxleqYE/ZSdcT8/Nem9/mmosx12pctd80s2Oy00KNZzrogMZS5mauK2/ymL1bvlvg== + version "2.7.0" + resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.7.0.tgz#ef5552b56dc01baae102acb5fc9fb8cd060c30f9" + integrity sha512-lXLOiqpkUumhRdFF3k1osNXCy9akgx/dyPZ5p8qAg9seJzXr5ZrlqZuWIMuY6ejOsVLE6flJ5/h3lsn57fQ/PQ== + +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +set-function-name@^2.0.1, set-function-name@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" + integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.2" setprototypeof@1.1.0: version "1.1.0" @@ -9656,24 +10133,30 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shell-quote@^1.7.3: - version "1.7.3" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123" - integrity sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw== +shell-quote@^1.7.3, shell-quote@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" + integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== -side-channel@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" - integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== +side-channel@^1.0.4, side-channel@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== dependencies: - call-bind "^1.0.0" - get-intrinsic "^1.0.2" - object-inspect "^1.9.0" + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" signal-exit@^3.0.2, signal-exit@^3.0.3: - version "3.0.6" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.6.tgz#24e630c4b0f03fea446a2bd299e62b4a6ca8d0af" - integrity sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ== + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== sisteransi@^1.0.5: version "1.0.5" @@ -9690,7 +10173,7 @@ slash@^4.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7" integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== -sockjs@^0.3.21: +sockjs@^0.3.24: version "0.3.24" resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce" integrity sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ== @@ -9704,20 +10187,15 @@ source-list-map@^2.0.0, source-list-map@^2.0.1: resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== -source-map-js@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" - integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== - -source-map-js@^1.2.0: +source-map-js@^1.0.1, source-map-js@^1.2.0, source-map-js@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== source-map-loader@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-3.0.1.tgz#9ae5edc7c2d42570934be4c95d1ccc6352eba52d" - integrity sha512-Vp1UsfyPvgujKQzi4pyDiTOnE3E4H+yHvkVRN3c/9PJmQS4CQJExvcDvaX/D+RV+xQben9HJ56jMJS3CgUeWyA== + version "3.0.2" + resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-3.0.2.tgz#af23192f9b344daa729f6772933194cc5fa54fee" + integrity sha512-BokxPoLjyl3iOrgkWaakaxqnelAJSS+0V+De0kKIq6lyWrXuiPgYTGp6z3iHmqljKAaLXwZa+ctD8GccRJeVvg== dependencies: abab "^2.0.5" iconv-lite "^0.6.3" @@ -9731,25 +10209,20 @@ source-map-support@^0.5.6, source-map-support@~0.5.20: buffer-from "^1.0.0" source-map "^0.6.0" -source-map-url@^0.4.0: - version "0.4.1" - resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56" - integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw== - source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.7: +source-map@^0.5.7: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= source-map@^0.7.3: - version "0.7.3" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" - integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== + version "0.7.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" + integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== source-map@^0.8.0-beta.0: version "0.8.0-beta.0" @@ -9758,7 +10231,7 @@ source-map@^0.8.0-beta.0: dependencies: whatwg-url "^7.0.0" -sourcemap-codec@^1.4.4: +sourcemap-codec@^1.4.8: version "1.4.8" resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== @@ -9799,7 +10272,7 @@ split-on-first@^1.0.0: sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== stable@^0.1.8: version "0.1.8" @@ -9807,22 +10280,29 @@ stable@^0.1.8: integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== stack-utils@^2.0.3: - version "2.0.5" - resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.5.tgz#d25265fca995154659dbbfba3b49254778d2fdd5" - integrity sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA== + version "2.0.6" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" + integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ== dependencies: escape-string-regexp "^2.0.0" -stackframe@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.2.0.tgz#52429492d63c62eb989804c11552e3d22e779303" - integrity sha512-GrdeshiRmS1YLMYgzF16olf2jJ/IzxXY9lhKOskuVziubpTYcYqyOwYeJKzQkwy7uN0fYSsbsC4RQaXf9LCrYA== +stackframe@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.3.4.tgz#b881a004c8c149a5e8efef37d51b16e412943310" + integrity sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw== state-toggle@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/state-toggle/-/state-toggle-1.0.3.tgz#e123b16a88e143139b09c6852221bc9815917dfe" integrity sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ== +static-eval@2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/static-eval/-/static-eval-2.0.2.tgz#2d1759306b1befa688938454c546b7871f806a42" + integrity sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg== + dependencies: + escodegen "^1.8.1" + statuses@2.0.1, statuses@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" @@ -9831,12 +10311,19 @@ statuses@2.0.1, statuses@^2.0.0: "statuses@>= 1.4.0 < 2": version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" - integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== + +stop-iteration-iterator@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz#6a60be0b4ee757d1ed5254858ec66b10c49285e4" + integrity sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ== + dependencies: + internal-slot "^1.0.4" strict-event-emitter@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/strict-event-emitter/-/strict-event-emitter-0.2.0.tgz#78e2f75dc6ea502e5d8a877661065a1e2deedecd" - integrity sha512-zv7K2egoKwkQkZGEaH8m+i2D0XiKzx5jNsiSul6ja2IYFvil10A59Z9Y7PPAAe5OW53dQUf9CfsHKzjZzKkm1w== + version "0.2.8" + resolved "https://registry.yarnpkg.com/strict-event-emitter/-/strict-event-emitter-0.2.8.tgz#b4e768927c67273c14c13d20e19d5e6c934b47ca" + integrity sha512-KDf/ujU8Zud3YaLtMCcTI4xkZlZVIYxTLr+XIULexP+77EEVWixeXroLUXQXiVtH4XH2W7jr/3PT1v3zBuvc3A== dependencies: events "^3.3.0" @@ -9866,6 +10353,15 @@ string-natural-compare@^3.0.1: resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" @@ -9875,35 +10371,77 @@ string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -string.prototype.matchall@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.6.tgz#5abb5dabc94c7b0ea2380f65ba610b3a544b15fa" - integrity sha512-6WgDX8HmQqvEd7J+G6VtAahhsQIssiZ8zl7zKh1VDMFyL3hRTJP4FTNA3RbIp2TOQ9AYNDcc7e3fH0Qbup+DBg== +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.1" - get-intrinsic "^1.1.1" - has-symbols "^1.0.2" - internal-slot "^1.0.3" - regexp.prototype.flags "^1.3.1" - side-channel "^1.0.4" + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" -string.prototype.trimend@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80" - integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A== +string.prototype.includes@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz#eceef21283640761a81dbe16d6c7171a4edf7d92" + integrity sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.3" + +string.prototype.matchall@^4.0.11, string.prototype.matchall@^4.0.6: + version "4.0.11" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz#1092a72c59268d2abaad76582dccc687c0297e0a" + integrity sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-symbols "^1.0.3" + internal-slot "^1.0.7" + regexp.prototype.flags "^1.5.2" + set-function-name "^2.0.2" + side-channel "^1.0.6" + +string.prototype.repeat@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz#e90872ee0308b29435aa26275f6e1b762daee01a" + integrity sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w== dependencies: - call-bind "^1.0.2" define-properties "^1.1.3" + es-abstract "^1.17.5" -string.prototype.trimstart@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed" - integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw== +string.prototype.trim@^1.2.9: + version "1.2.9" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4" + integrity sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw== dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.0" + es-object-atoms "^1.0.0" + +string.prototype.trimend@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz#3651b8513719e8a9f48de7f2f77640b26652b229" + integrity sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +string.prototype.trimstart@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz#7ee834dda8c7c17eff3118472bb35bfedaa34dde" + integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" string_decoder@^1.1.1: version "1.3.0" @@ -9937,6 +10475,13 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" @@ -9944,17 +10489,17 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1: dependencies: ansi-regex "^5.0.1" -strip-ansi@^7.0.0, strip-ansi@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2" - integrity sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw== +strip-ansi@^7.0.1: + version "7.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== dependencies: ansi-regex "^6.0.1" strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" - integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= + integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== strip-bom@^4.0.0: version "4.0.0" @@ -9984,9 +10529,9 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== style-loader@^3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.1.tgz#057dfa6b3d4d7c7064462830f9113ed417d38575" - integrity sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ== + version "3.3.4" + resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.4.tgz#f30f786c36db03a45cbd55b6a70d930c479090e7" + integrity sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w== style-to-object@^0.3.0: version "0.3.0" @@ -10010,12 +10555,12 @@ styled-components@^6.1.0: stylis "4.3.2" tslib "2.6.2" -stylehacks@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-5.0.2.tgz#fa10e5181c6e8dc0bddb4a3fb372e9ac42bba2ad" - integrity sha512-114zeJdOpTrbQYRD4OU5UWJ99LKUaqCPJTU1HQ/n3q3BwmllFN8kHENaLnOeqVq6AhXrWfxHNZTl33iJ4oy3cQ== +stylehacks@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-5.1.1.tgz#7934a34eb59d7152149fa69d6e9e56f2fc34bcc9" + integrity sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw== dependencies: - browserslist "^4.16.6" + browserslist "^4.21.4" postcss-selector-parser "^6.0.4" stylis@4.2.0: @@ -10028,6 +10573,19 @@ stylis@4.3.2: resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.3.2.tgz#8f76b70777dd53eb669c6f58c997bf0a9972e444" integrity sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg== +sucrase@^3.32.0: + version "3.35.0" + resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.35.0.tgz#57f17a3d7e19b36d8995f06679d121be914ae263" + integrity sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA== + dependencies: + "@jridgewell/gen-mapping" "^0.3.2" + commander "^4.0.0" + glob "^10.3.10" + lines-and-columns "^1.1.6" + mz "^2.7.0" + pirates "^4.0.1" + ts-interface-checker "^0.1.9" + supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -10050,9 +10608,9 @@ supports-color@^8.0.0: has-flag "^4.0.0" supports-hyperlinks@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz#4f77b42488765891774b70c79babd87f9bd594bb" - integrity sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ== + version "2.3.0" + resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz#3943544347c1ff90b15effb03fc14ae45ec10624" + integrity sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA== dependencies: has-flag "^4.0.0" supports-color "^7.0.0" @@ -10067,24 +10625,18 @@ svg-parser@^2.0.2: resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5" integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ== -svgo@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.0.tgz#bae51ba95ded9a33a36b7c46ce9c359ae9154313" - integrity sha512-MLfUA6O+qauLDbym+mMZgtXCGRfIxyQoeH6IKVcFslyODEe/ElJNwr0FohQ3xG4C6HK6bk3KYPPXwHVJk3V5NQ== +svgo@2.8.0, svgo@^2.7.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24" + integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg== dependencies: - chalk "^2.4.1" - coa "^2.0.2" - css-select "^2.0.0" - css-select-base-adapter "^0.1.1" - css-tree "1.0.0-alpha.33" - csso "^3.5.1" - js-yaml "^3.13.1" - mkdirp "~0.5.1" - object.values "^1.1.0" - sax "~1.2.4" + "@trysound/sax" "0.2.0" + commander "^7.2.0" + css-select "^4.1.3" + css-tree "^1.1.3" + csso "^4.2.0" + picocolors "^1.0.0" stable "^0.1.8" - unquote "~1.1.1" - util.promisify "~1.0.0" svgo@^1.2.2: version "1.3.2" @@ -10105,19 +10657,6 @@ svgo@^1.2.2: unquote "~1.1.1" util.promisify "~1.0.0" -svgo@^2.7.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24" - integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg== - dependencies: - "@trysound/sax" "0.2.0" - commander "^7.2.0" - css-select "^4.1.3" - css-tree "^1.1.3" - csso "^4.2.0" - picocolors "^1.0.0" - stable "^0.1.8" - symbol-tree@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" @@ -10128,43 +10667,40 @@ tabbable@^5.3.3: resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-5.3.3.tgz#aac0ff88c73b22d6c3c5a50b1586310006b47fbf" integrity sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA== -taffydb@2.6.2: - version "2.6.2" - resolved "https://registry.yarnpkg.com/taffydb/-/taffydb-2.6.2.tgz#7cbcb64b5a141b6a2efc2c5d2c67b4e150b2a268" - integrity sha512-y3JaeRSplks6NYQuCOj3ZFMO3j60rTwbuKCvZxsAraGYH2epusatvZ0baZYA01WsGqJBq/Dl6vOrMUJqyMj8kA== - tailwindcss@^3.0.2: - version "3.0.18" - resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.0.18.tgz#ea4825e6496d77dc21877b6b61c7cc56cda3add5" - integrity sha512-ihPTpEyA5ANgZbwKlgrbfnzOp9R5vDHFWmqxB1PT8NwOGCOFVVMl+Ps1cQQ369acaqqf1BEF77roCwK0lvNmTw== + version "3.4.14" + resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.14.tgz#6dd23a7f54ec197b19159e91e3bb1e55e7aa73ac" + integrity sha512-IcSvOcTRcUtQQ7ILQL5quRDg7Xs93PdJEk1ZLbhhvJc7uj/OAhYOnruEiwnGgBvUtaUAJ8/mhSw1o8L2jCiENA== dependencies: - arg "^5.0.1" - chalk "^4.1.2" + "@alloc/quick-lru" "^5.2.0" + arg "^5.0.2" chokidar "^3.5.3" - color-name "^1.1.4" - cosmiconfig "^7.0.1" - detective "^5.2.0" didyoumean "^1.2.2" dlv "^1.1.3" - fast-glob "^3.2.11" + fast-glob "^3.3.0" glob-parent "^6.0.2" is-glob "^4.0.3" + jiti "^1.21.0" + lilconfig "^2.1.0" + micromatch "^4.0.5" normalize-path "^3.0.0" - object-hash "^2.2.0" - postcss-js "^4.0.0" - postcss-load-config "^3.1.0" - postcss-nested "5.0.6" - postcss-selector-parser "^6.0.9" - postcss-value-parser "^4.2.0" - quick-lru "^5.1.1" - resolve "^1.21.0" + object-hash "^3.0.0" + picocolors "^1.0.0" + postcss "^8.4.23" + postcss-import "^15.1.0" + postcss-js "^4.0.1" + postcss-load-config "^4.0.1" + postcss-nested "^6.0.1" + postcss-selector-parser "^6.0.11" + resolve "^1.22.2" + sucrase "^3.32.0" tapable@^1.0.0: version "1.1.3" resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== -tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0: +tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== @@ -10192,18 +10728,7 @@ terminal-link@^2.0.0: ansi-escapes "^4.2.1" supports-hyperlinks "^2.0.0" -terser-webpack-plugin@^5.2.5: - version "5.3.0" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.0.tgz#21641326486ecf91d8054161c816e464435bae9f" - integrity sha512-LPIisi3Ol4chwAaPP8toUJ3L4qCM1G0wao7L3qNv57Drezxj6+VEyySpPw4B1HSO2Eg/hDY/MNF5XihCAoqnsQ== - dependencies: - jest-worker "^27.4.1" - schema-utils "^3.1.1" - serialize-javascript "^6.0.0" - source-map "^0.6.1" - terser "^5.7.2" - -terser-webpack-plugin@^5.3.10: +terser-webpack-plugin@^5.2.5, terser-webpack-plugin@^5.3.10: version "5.3.10" resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz#904f4c9193c6fd2a03f693a2150c62a92f40d199" integrity sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w== @@ -10214,20 +10739,10 @@ terser-webpack-plugin@^5.3.10: serialize-javascript "^6.0.1" terser "^5.26.0" -terser@^5.0.0, terser@^5.10.0, terser@^5.7.2: - version "5.14.2" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.14.2.tgz#9ac9f22b06994d736174f4091aa368db896f1c10" - integrity sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA== - dependencies: - "@jridgewell/source-map" "^0.3.2" - acorn "^8.5.0" - commander "^2.20.0" - source-map-support "~0.5.20" - -terser@^5.26.0: - version "5.31.6" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.31.6.tgz#c63858a0f0703988d0266a82fcbf2d7ba76422b1" - integrity sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg== +terser@^5.0.0, terser@^5.10.0, terser@^5.26.0: + version "5.36.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.36.0.tgz#8b0dbed459ac40ff7b4c9fd5a3a2029de105180e" + integrity sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w== dependencies: "@jridgewell/source-map" "^0.3.3" acorn "^8.8.2" @@ -10251,28 +10766,37 @@ text-diff@^1.0.1: text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" - integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + +thenify-all@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" + integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA== + dependencies: + thenify ">= 3.1.0 < 4" + +"thenify@>= 3.1.0 < 4": + version "3.3.1" + resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f" + integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw== + dependencies: + any-promise "^1.0.0" throat@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/throat/-/throat-6.0.1.tgz#d514fedad95740c12c2d7fc70ea863eb51ade375" - integrity sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w== + version "6.0.2" + resolved "https://registry.yarnpkg.com/throat/-/throat-6.0.2.tgz#51a3fbb5e11ae72e2cf74861ed5c8020f89f29fe" + integrity sha512-WKexMoJj3vEuK0yFEapj8y64V0A6xcuPuK9Gt1d0R+dzCSJc0lHqQytAbSB4cDAK0dWh4T0E2ETkoLE2WZ41OQ== through@^2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== thunky@^1.0.2: version "1.1.0" resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== -timsort@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" - integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= - tiny-invariant@^1.0.6: version "1.2.0" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.2.0.tgz#a1141f86b672a9148c72e978a19a73b9b94a15a9" @@ -10286,11 +10810,9 @@ tmp@^0.0.33: os-tmpdir "~1.0.2" tmp@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" - integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== - dependencies: - rimraf "^3.0.0" + version "0.2.3" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.3.tgz#eb783cc22bc1e8bebd0671476d46ea4eb32a79ae" + integrity sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w== tmpl@1.0.5: version "1.0.5" @@ -10300,7 +10822,7 @@ tmpl@1.0.5: to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" - integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= + integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== to-regex-range@^5.0.1: version "5.0.1" @@ -10315,9 +10837,9 @@ toidentifier@1.0.1: integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== tough-cookie@^4.0.0: - version "4.1.3" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.3.tgz#97b9adb0728b42280aa3d814b6b999b2ff0318bf" - integrity sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw== + version "4.1.4" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.4.tgz#945f1461b45b5a8c76821c33ea49c3ac192c1b36" + integrity sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag== dependencies: psl "^1.1.33" punycode "^2.1.1" @@ -10327,7 +10849,7 @@ tough-cookie@^4.0.0: tr46@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" - integrity sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk= + integrity sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA== dependencies: punycode "^2.1.0" @@ -10341,7 +10863,7 @@ tr46@^2.1.0: tr46@~0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" - integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== trim-trailing-lines@^1.0.0: version "1.1.4" @@ -10358,14 +10880,19 @@ tryer@^1.0.1: resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8" integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA== -tsconfig-paths@^3.12.0: - version "3.12.0" - resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.12.0.tgz#19769aca6ee8f6a1a341e38c8fa45dd9fb18899b" - integrity sha512-e5adrnOYT6zqVnWqZu7i/BQ3BnhzvGbjEjejFXO20lKIKpwTaupkCPgEfv4GZK1IBciJUEhYs3J3p75FdaTFVg== +ts-interface-checker@^0.1.9: + version "0.1.13" + resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" + integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== + +tsconfig-paths@^3.15.0: + version "3.15.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" + integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== dependencies: "@types/json5" "^0.0.29" - json5 "^1.0.1" - minimist "^1.2.0" + json5 "^1.0.2" + minimist "^1.2.6" strip-bom "^3.0.0" tslib@2.6.2: @@ -10383,16 +10910,16 @@ tslib@^2.0.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== -tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" - integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== - -tslib@^2.6.0: +tslib@^2.0.3, tslib@^2.1.0, tslib@^2.6.0: version "2.8.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.0.tgz#d124c86c3c05a40a91e6fdea4021bd31d377971b" integrity sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA== +tslib@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" + integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" @@ -10410,7 +10937,7 @@ type-check@^0.4.0, type-check@~0.4.0: type-check@~0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" - integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= + integrity sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg== dependencies: prelude-ls "~1.1.2" @@ -10447,6 +10974,50 @@ type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" +typed-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz#1867c5d83b20fcb5ccf32649e5e2fc7424474ff3" + integrity sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + is-typed-array "^1.1.13" + +typed-array-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz#d92972d3cff99a3fa2e765a28fcdc0f1d89dec67" + integrity sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + +typed-array-byte-offset@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz#f9ec1acb9259f395093e4567eb3c28a580d02063" + integrity sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + +typed-array-length@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.6.tgz#57155207c76e64a3457482dfdc1c9d1d3c4c73a3" + integrity sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + possible-typed-array-names "^1.0.0" + typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" @@ -10459,30 +11030,40 @@ typescript@^4.9.5: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== -uc.micro@^1.0.1, uc.micro@^1.0.5: - version "1.0.6" - resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" - integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== +uc.micro@^2.0.0, uc.micro@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee" + integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A== uglify-js@^3.7.7: - version "3.17.0" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.0.tgz#55bd6e9d19ce5eef0d5ad17cd1f587d85b180a85" - integrity sha512-aTeNPVmgIMPpm1cxXr2Q/nEbvkmV8yq66F3om7X3P/cvOXQ0TMQ64Wk63iyT1gPlmdmGzjGpyLh1f3y8MZWXGg== + version "3.19.3" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.19.3.tgz#82315e9bbc6f2b25888858acd1fff8441035b77f" + integrity sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ== -unbox-primitive@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471" - integrity sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw== +unbox-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" + integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== dependencies: - function-bind "^1.1.1" - has-bigints "^1.0.1" - has-symbols "^1.0.2" + call-bind "^1.0.2" + has-bigints "^1.0.2" + has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +underscore@1.12.1: + version "1.12.1" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.12.1.tgz#7bb8cc9b3d397e201cf8553336d262544ead829e" + integrity sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw== + underscore@~1.13.2: - version "1.13.4" - resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.4.tgz#7886b46bbdf07f768e0052f1828e1dcab40c0dee" - integrity sha512-BQFnUDuAQ4Yf/cYY5LNrK9NCJFKriaRbD9uR1fTeXnBeoa97W0i41qkZfGO9pSo8I5KzjAcSY2XYtdf0oKd7KQ== + version "1.13.7" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.7.tgz#970e33963af9a7dda228f17ebe8399e5fbe63a10" + integrity sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g== + +undici-types@~6.19.2: + version "6.19.8" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" + integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== unherit@^1.0.4: version "1.1.3" @@ -10493,9 +11074,9 @@ unherit@^1.0.4: xtend "^4.0.0" unicode-canonical-property-names-ecmascript@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" - integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== + version "2.0.1" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz#cb3173fe47ca743e228216e4a3ddc4c84d628cc2" + integrity sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg== unicode-match-property-ecmascript@^2.0.0: version "2.0.0" @@ -10505,15 +11086,15 @@ unicode-match-property-ecmascript@^2.0.0: unicode-canonical-property-names-ecmascript "^2.0.0" unicode-property-aliases-ecmascript "^2.0.0" -unicode-match-property-value-ecmascript@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.0.0.tgz#1a01aa57247c14c568b89775a54938788189a714" - integrity sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw== +unicode-match-property-value-ecmascript@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz#a0401aee72714598f739b68b104e4fe3a0cb3c71" + integrity sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg== unicode-property-aliases-ecmascript@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz#0a36cb9a585c4f6abd51ad1deddb285c165297c8" - integrity sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ== + version "2.1.0" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" + integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== unified@^9.2.2: version "9.2.2" @@ -10615,9 +11196,9 @@ universalify@^0.2.0: integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== universalify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" - integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== unload@2.2.0: version "2.2.0" @@ -10635,7 +11216,7 @@ unpipe@1.0.0, unpipe@~1.0.0: unquote@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544" - integrity sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ= + integrity sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg== upath@^1.2.0: version "1.2.0" @@ -10643,12 +11224,12 @@ upath@^1.2.0: integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== update-browserslist-db@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz#7ca61c0d8650766090728046e416a8cde682859e" - integrity sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ== + version "1.1.1" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz#80846fba1d79e82547fb661f8d141e0945755fe5" + integrity sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A== dependencies: - escalade "^3.1.2" - picocolors "^1.0.1" + escalade "^3.2.0" + picocolors "^1.1.0" uri-js@^4.2.2: version "4.4.1" @@ -10707,7 +11288,7 @@ use-sync-external-store@^1.0.0: util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== util.promisify@~1.0.0: version "1.0.1" @@ -10722,7 +11303,7 @@ util.promisify@~1.0.0: utila@~0.4: version "0.4.0" resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" - integrity sha1-ihagXURWV6Oupe7MWxKk+lN5dyw= + integrity sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA== utils-merge@1.0.1: version "1.0.1" @@ -10734,11 +11315,6 @@ uuid@^8.3.0, uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== -v8-compile-cache@^2.0.3: - version "2.3.0" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" - integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== - v8-to-istanbul@^8.1.0: version "8.1.1" resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz#77b752fd3975e31bbcef938f85e9bd1c7a8d60ed" @@ -10815,7 +11391,7 @@ wbuf@^1.1.0, wbuf@^1.7.3: wcwidth@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" - integrity sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g= + integrity sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg== dependencies: defaults "^1.0.3" @@ -10827,7 +11403,7 @@ web-namespaces@^1.0.0: webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" - integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== webidl-conversions@^4.0.2: version "4.0.2" @@ -10844,7 +11420,7 @@ webidl-conversions@^6.1.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514" integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== -webpack-dev-middleware@^5.3.0: +webpack-dev-middleware@^5.3.4: version "5.3.4" resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz#eb7b39281cbce10e104eb2b8bf2b63fce49a3517" integrity sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q== @@ -10856,39 +11432,40 @@ webpack-dev-middleware@^5.3.0: schema-utils "^4.0.0" webpack-dev-server@^4.6.0: - version "4.7.3" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.7.3.tgz#4e995b141ff51fa499906eebc7906f6925d0beaa" - integrity sha512-mlxq2AsIw2ag016nixkzUkdyOE8ST2GTy34uKSABp1c4nhjZvH90D5ZRR+UOLSsG4Z3TFahAi72a3ymRtfRm+Q== + version "4.15.2" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz#9e0c70a42a012560860adb186986da1248333173" + integrity sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g== dependencies: "@types/bonjour" "^3.5.9" "@types/connect-history-api-fallback" "^1.3.5" + "@types/express" "^4.17.13" "@types/serve-index" "^1.9.1" + "@types/serve-static" "^1.13.10" "@types/sockjs" "^0.3.33" - "@types/ws" "^8.2.2" + "@types/ws" "^8.5.5" ansi-html-community "^0.0.8" - bonjour "^3.5.0" - chokidar "^3.5.2" + bonjour-service "^1.0.11" + chokidar "^3.5.3" colorette "^2.0.10" compression "^1.7.4" - connect-history-api-fallback "^1.6.0" + connect-history-api-fallback "^2.0.0" default-gateway "^6.0.3" - del "^6.0.0" - express "^4.17.1" + express "^4.17.3" graceful-fs "^4.2.6" html-entities "^2.3.2" - http-proxy-middleware "^2.0.0" + http-proxy-middleware "^2.0.3" ipaddr.js "^2.0.1" + launch-editor "^2.6.0" open "^8.0.9" p-retry "^4.5.0" - portfinder "^1.0.28" + rimraf "^3.0.2" schema-utils "^4.0.0" - selfsigned "^2.0.0" + selfsigned "^2.1.1" serve-index "^1.9.1" - sockjs "^0.3.21" + sockjs "^0.3.24" spdy "^4.0.2" - strip-ansi "^7.0.0" - webpack-dev-middleware "^5.3.0" - ws "^8.1.0" + webpack-dev-middleware "^5.3.4" + ws "^8.13.0" webpack-manifest-plugin@^4.0.2: version "4.1.1" @@ -10920,9 +11497,9 @@ webpack-sources@^3.2.3: integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== webpack@^5.64.4: - version "5.94.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.94.0.tgz#77a6089c716e7ab90c1c67574a28da518a20970f" - integrity sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg== + version "5.95.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.95.0.tgz#8fd8c454fa60dad186fbe36c400a55848307b4c0" + integrity sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q== dependencies: "@types/estree" "^1.0.5" "@webassemblyjs/ast" "^1.12.1" @@ -10970,9 +11547,9 @@ whatwg-encoding@^1.0.5: iconv-lite "0.4.24" whatwg-fetch@^3.6.2: - version "3.6.2" - resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c" - integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA== + version "3.6.20" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz#580ce6d791facec91d37c72890995a0b48d31c70" + integrity sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg== whatwg-mimetype@^2.3.0: version "2.3.0" @@ -10982,7 +11559,7 @@ whatwg-mimetype@^2.3.0: whatwg-url@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" - integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0= + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== dependencies: tr46 "~0.0.3" webidl-conversions "^3.0.0" @@ -11016,6 +11593,45 @@ which-boxed-primitive@^1.0.2: is-string "^1.0.5" is-symbol "^1.0.3" +which-builtin-type@^1.1.3: + version "1.1.4" + resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.1.4.tgz#592796260602fc3514a1b5ee7fa29319b72380c3" + integrity sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w== + dependencies: + function.prototype.name "^1.1.6" + has-tostringtag "^1.0.2" + is-async-function "^2.0.0" + is-date-object "^1.0.5" + is-finalizationregistry "^1.0.2" + is-generator-function "^1.0.10" + is-regex "^1.1.4" + is-weakref "^1.0.2" + isarray "^2.0.5" + which-boxed-primitive "^1.0.2" + which-collection "^1.0.2" + which-typed-array "^1.1.15" + +which-collection@^1.0.1, which-collection@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.2.tgz#627ef76243920a107e7ce8e96191debe4b16c2a0" + integrity sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw== + dependencies: + is-map "^2.0.3" + is-set "^2.0.3" + is-weakmap "^2.0.2" + is-weakset "^2.0.3" + +which-typed-array@^1.1.13, which-typed-array@^1.1.14, which-typed-array@^1.1.15: + version "1.1.15" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" + integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.2" + which@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" @@ -11030,30 +11646,30 @@ which@^2.0.1: dependencies: isexe "^2.0.0" -word-wrap@^1.2.3, word-wrap@~1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== +word-wrap@^1.2.5, word-wrap@~1.2.3: + version "1.2.5" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -workbox-background-sync@6.4.2: - version "6.4.2" - resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-6.4.2.tgz#bb31b95928d376abcb9bde0de3a0cef9bae46cf7" - integrity sha512-P7c8uG5X2k+DMICH9xeSA9eUlCOjHHYoB42Rq+RtUpuwBxUOflAXR1zdsMWj81LopE4gjKXlTw7BFd1BDAHo7g== +workbox-background-sync@6.6.1: + version "6.6.1" + resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-6.6.1.tgz#08d603a33717ce663e718c30cc336f74909aff2f" + integrity sha512-trJd3ovpWCvzu4sW0E8rV3FUyIcC0W8G+AZ+VcqzzA890AsWZlUGOTSxIMmIHVusUw/FDq1HFWfy/kC/WTRqSg== dependencies: - idb "^6.1.4" - workbox-core "6.4.2" + idb "^7.0.1" + workbox-core "6.6.1" -workbox-broadcast-update@6.4.2: - version "6.4.2" - resolved "https://registry.yarnpkg.com/workbox-broadcast-update/-/workbox-broadcast-update-6.4.2.tgz#5094c4767dfb590532ac03ee07e9e82b2ac206bc" - integrity sha512-qnBwQyE0+PWFFc/n4ISXINE49m44gbEreJUYt2ldGH3+CNrLmJ1egJOOyUqqu9R4Eb7QrXcmB34ClXG7S37LbA== +workbox-broadcast-update@6.6.1: + version "6.6.1" + resolved "https://registry.yarnpkg.com/workbox-broadcast-update/-/workbox-broadcast-update-6.6.1.tgz#0fad9454cf8e4ace0c293e5617c64c75d8a8c61e" + integrity sha512-fBhffRdaANdeQ1V8s692R9l/gzvjjRtydBOvR6WCSB0BNE2BacA29Z4r9/RHd9KaXCPl6JTdI9q0bR25YKP8TQ== dependencies: - workbox-core "6.4.2" + workbox-core "6.6.1" -workbox-build@6.4.2: - version "6.4.2" - resolved "https://registry.yarnpkg.com/workbox-build/-/workbox-build-6.4.2.tgz#47f9baa946c3491533cd5ccb1f194a7160e8a6e3" - integrity sha512-WMdYLhDIsuzViOTXDH+tJ1GijkFp5khSYolnxR/11zmfhNDtuo7jof72xPGFy+KRpsz6tug39RhivCj77qqO0w== +workbox-build@6.6.1: + version "6.6.1" + resolved "https://registry.yarnpkg.com/workbox-build/-/workbox-build-6.6.1.tgz#6010e9ce550910156761448f2dbea8cfcf759cb0" + integrity sha512-INPgDx6aRycAugUixbKgiEQBWD0MPZqU5r0jyr24CehvNuLPSXp/wGOpdRJmts656lNiXwqV7dC2nzyrzWEDnw== dependencies: "@apideck/better-ajv-errors" "^0.3.1" "@babel/core" "^7.11.1" @@ -11073,138 +11689,154 @@ workbox-build@6.4.2: rollup "^2.43.1" rollup-plugin-terser "^7.0.0" source-map "^0.8.0-beta.0" - source-map-url "^0.4.0" stringify-object "^3.3.0" strip-comments "^2.0.1" tempy "^0.6.0" upath "^1.2.0" - workbox-background-sync "6.4.2" - workbox-broadcast-update "6.4.2" - workbox-cacheable-response "6.4.2" - workbox-core "6.4.2" - workbox-expiration "6.4.2" - workbox-google-analytics "6.4.2" - workbox-navigation-preload "6.4.2" - workbox-precaching "6.4.2" - workbox-range-requests "6.4.2" - workbox-recipes "6.4.2" - workbox-routing "6.4.2" - workbox-strategies "6.4.2" - workbox-streams "6.4.2" - workbox-sw "6.4.2" - workbox-window "6.4.2" - -workbox-cacheable-response@6.4.2: - version "6.4.2" - resolved "https://registry.yarnpkg.com/workbox-cacheable-response/-/workbox-cacheable-response-6.4.2.tgz#ebcabb3667019da232e986a9927af97871e37ccb" - integrity sha512-9FE1W/cKffk1AJzImxgEN0ceWpyz1tqNjZVtA3/LAvYL3AC5SbIkhc7ZCO82WmO9IjTfu8Vut2X/C7ViMSF7TA== - dependencies: - workbox-core "6.4.2" - -workbox-core@6.4.2: - version "6.4.2" - resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-6.4.2.tgz#f99fd36a211cc01dce90aa7d5f2c255e8fe9d6bc" - integrity sha512-1U6cdEYPcajRXiboSlpJx6U7TvhIKbxRRerfepAJu2hniKwJ3DHILjpU/zx3yvzSBCWcNJDoFalf7Vgd7ey/rw== - -workbox-expiration@6.4.2: - version "6.4.2" - resolved "https://registry.yarnpkg.com/workbox-expiration/-/workbox-expiration-6.4.2.tgz#61613459fd6ddd1362730767618d444c6b9c9139" - integrity sha512-0hbpBj0tDnW+DZOUmwZqntB/8xrXOgO34i7s00Si/VlFJvvpRKg1leXdHHU8ykoSBd6+F2KDcMP3swoCi5guLw== - dependencies: - idb "^6.1.4" - workbox-core "6.4.2" - -workbox-google-analytics@6.4.2: - version "6.4.2" - resolved "https://registry.yarnpkg.com/workbox-google-analytics/-/workbox-google-analytics-6.4.2.tgz#eea7d511b3078665a726dc2ee9f11c6b7a897530" - integrity sha512-u+gxs3jXovPb1oul4CTBOb+T9fS1oZG+ZE6AzS7l40vnyfJV79DaLBvlpEZfXGv3CjMdV1sT/ltdOrKzo7HcGw== - dependencies: - workbox-background-sync "6.4.2" - workbox-core "6.4.2" - workbox-routing "6.4.2" - workbox-strategies "6.4.2" - -workbox-navigation-preload@6.4.2: - version "6.4.2" - resolved "https://registry.yarnpkg.com/workbox-navigation-preload/-/workbox-navigation-preload-6.4.2.tgz#35cd4ba416a530796af135410ca07db5bee11668" - integrity sha512-viyejlCtlKsbJCBHwhSBbWc57MwPXvUrc8P7d+87AxBGPU+JuWkT6nvBANgVgFz6FUhCvRC8aYt+B1helo166g== - dependencies: - workbox-core "6.4.2" - -workbox-precaching@6.4.2: - version "6.4.2" - resolved "https://registry.yarnpkg.com/workbox-precaching/-/workbox-precaching-6.4.2.tgz#8d87c05d54f32ac140f549faebf3b4d42d63621e" - integrity sha512-CZ6uwFN/2wb4noHVlALL7UqPFbLfez/9S2GAzGAb0Sk876ul9ukRKPJJ6gtsxfE2HSTwqwuyNVa6xWyeyJ1XSA== - dependencies: - workbox-core "6.4.2" - workbox-routing "6.4.2" - workbox-strategies "6.4.2" - -workbox-range-requests@6.4.2: - version "6.4.2" - resolved "https://registry.yarnpkg.com/workbox-range-requests/-/workbox-range-requests-6.4.2.tgz#050f0dfbb61cd1231e609ed91298b6c2442ae41b" - integrity sha512-SowF3z69hr3Po/w7+xarWfzxJX/3Fo0uSG72Zg4g5FWWnHpq2zPvgbWerBZIa81zpJVUdYpMa3akJJsv+LaO1Q== - dependencies: - workbox-core "6.4.2" - -workbox-recipes@6.4.2: - version "6.4.2" - resolved "https://registry.yarnpkg.com/workbox-recipes/-/workbox-recipes-6.4.2.tgz#68de41fa3a77b444b0f93c9c01a76ba1d41fd2bf" - integrity sha512-/oVxlZFpAjFVbY+3PoGEXe8qyvtmqMrTdWhbOfbwokNFtUZ/JCtanDKgwDv9x3AebqGAoJRvQNSru0F4nG+gWA== - dependencies: - workbox-cacheable-response "6.4.2" - workbox-core "6.4.2" - workbox-expiration "6.4.2" - workbox-precaching "6.4.2" - workbox-routing "6.4.2" - workbox-strategies "6.4.2" - -workbox-routing@6.4.2: - version "6.4.2" - resolved "https://registry.yarnpkg.com/workbox-routing/-/workbox-routing-6.4.2.tgz#65b1c61e8ca79bb9152f93263c26b1f248d09dcc" - integrity sha512-0ss/n9PAcHjTy4Ad7l2puuod4WtsnRYu9BrmHcu6Dk4PgWeJo1t5VnGufPxNtcuyPGQ3OdnMdlmhMJ57sSrrSw== - dependencies: - workbox-core "6.4.2" - -workbox-strategies@6.4.2: - version "6.4.2" - resolved "https://registry.yarnpkg.com/workbox-strategies/-/workbox-strategies-6.4.2.tgz#50c02bf2d116918e1a8052df5f2c1e4103c62d5d" - integrity sha512-YXh9E9dZGEO1EiPC3jPe2CbztO5WT8Ruj8wiYZM56XqEJp5YlGTtqRjghV+JovWOqkWdR+amJpV31KPWQUvn1Q== - dependencies: - workbox-core "6.4.2" - -workbox-streams@6.4.2: - version "6.4.2" - resolved "https://registry.yarnpkg.com/workbox-streams/-/workbox-streams-6.4.2.tgz#3bc615cccebfd62dedf28315afb7d9ee177912a5" - integrity sha512-ROEGlZHGVEgpa5bOZefiJEVsi5PsFjJG9Xd+wnDbApsCO9xq9rYFopF+IRq9tChyYzhBnyk2hJxbQVWphz3sog== - dependencies: - workbox-core "6.4.2" - workbox-routing "6.4.2" - -workbox-sw@6.4.2: - version "6.4.2" - resolved "https://registry.yarnpkg.com/workbox-sw/-/workbox-sw-6.4.2.tgz#9a6db5f74580915dc2f0dbd47d2ffe057c94a795" - integrity sha512-A2qdu9TLktfIM5NE/8+yYwfWu+JgDaCkbo5ikrky2c7r9v2X6DcJ+zSLphNHHLwM/0eVk5XVf1mC5HGhYpMhhg== + workbox-background-sync "6.6.1" + workbox-broadcast-update "6.6.1" + workbox-cacheable-response "6.6.1" + workbox-core "6.6.1" + workbox-expiration "6.6.1" + workbox-google-analytics "6.6.1" + workbox-navigation-preload "6.6.1" + workbox-precaching "6.6.1" + workbox-range-requests "6.6.1" + workbox-recipes "6.6.1" + workbox-routing "6.6.1" + workbox-strategies "6.6.1" + workbox-streams "6.6.1" + workbox-sw "6.6.1" + workbox-window "6.6.1" + +workbox-cacheable-response@6.6.1: + version "6.6.1" + resolved "https://registry.yarnpkg.com/workbox-cacheable-response/-/workbox-cacheable-response-6.6.1.tgz#284c2b86be3f4fd191970ace8c8e99797bcf58e9" + integrity sha512-85LY4veT2CnTCDxaVG7ft3NKaFbH6i4urZXgLiU4AiwvKqS2ChL6/eILiGRYXfZ6gAwDnh5RkuDbr/GMS4KSag== + dependencies: + workbox-core "6.6.1" + +workbox-core@6.6.1: + version "6.6.1" + resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-6.6.1.tgz#7184776d4134c5ed2f086878c882728fc9084265" + integrity sha512-ZrGBXjjaJLqzVothoE12qTbVnOAjFrHDXpZe7coCb6q65qI/59rDLwuFMO4PcZ7jcbxY+0+NhUVztzR/CbjEFw== + +workbox-expiration@6.6.1: + version "6.6.1" + resolved "https://registry.yarnpkg.com/workbox-expiration/-/workbox-expiration-6.6.1.tgz#a841fa36676104426dbfb9da1ef6a630b4f93739" + integrity sha512-qFiNeeINndiOxaCrd2DeL1Xh1RFug3JonzjxUHc5WkvkD2u5abY3gZL1xSUNt3vZKsFFGGORItSjVTVnWAZO4A== + dependencies: + idb "^7.0.1" + workbox-core "6.6.1" + +workbox-google-analytics@6.6.1: + version "6.6.1" + resolved "https://registry.yarnpkg.com/workbox-google-analytics/-/workbox-google-analytics-6.6.1.tgz#a07a6655ab33d89d1b0b0a935ffa5dea88618c5d" + integrity sha512-1TjSvbFSLmkpqLcBsF7FuGqqeDsf+uAXO/pjiINQKg3b1GN0nBngnxLcXDYo1n/XxK4N7RaRrpRlkwjY/3ocuA== + dependencies: + workbox-background-sync "6.6.1" + workbox-core "6.6.1" + workbox-routing "6.6.1" + workbox-strategies "6.6.1" + +workbox-navigation-preload@6.6.1: + version "6.6.1" + resolved "https://registry.yarnpkg.com/workbox-navigation-preload/-/workbox-navigation-preload-6.6.1.tgz#61a34fe125558dd88cf09237f11bd966504ea059" + integrity sha512-DQCZowCecO+wRoIxJI2V6bXWK6/53ff+hEXLGlQL4Rp9ZaPDLrgV/32nxwWIP7QpWDkVEtllTAK5h6cnhxNxDA== + dependencies: + workbox-core "6.6.1" + +workbox-precaching@6.6.1: + version "6.6.1" + resolved "https://registry.yarnpkg.com/workbox-precaching/-/workbox-precaching-6.6.1.tgz#dedeeba10a2d163d990bf99f1c2066ac0d1a19e2" + integrity sha512-K4znSJ7IKxCnCYEdhNkMr7X1kNh8cz+mFgx9v5jFdz1MfI84pq8C2zG+oAoeE5kFrUf7YkT5x4uLWBNg0DVZ5A== + dependencies: + workbox-core "6.6.1" + workbox-routing "6.6.1" + workbox-strategies "6.6.1" + +workbox-range-requests@6.6.1: + version "6.6.1" + resolved "https://registry.yarnpkg.com/workbox-range-requests/-/workbox-range-requests-6.6.1.tgz#ddaf7e73af11d362fbb2f136a9063a4c7f507a39" + integrity sha512-4BDzk28govqzg2ZpX0IFkthdRmCKgAKreontYRC5YsAPB2jDtPNxqx3WtTXgHw1NZalXpcH/E4LqUa9+2xbv1g== + dependencies: + workbox-core "6.6.1" + +workbox-recipes@6.6.1: + version "6.6.1" + resolved "https://registry.yarnpkg.com/workbox-recipes/-/workbox-recipes-6.6.1.tgz#ea70d2b2b0b0bce8de0a9d94f274d4a688e69fae" + integrity sha512-/oy8vCSzromXokDA+X+VgpeZJvtuf8SkQ8KL0xmRivMgJZrjwM3c2tpKTJn6PZA6TsbxGs3Sc7KwMoZVamcV2g== + dependencies: + workbox-cacheable-response "6.6.1" + workbox-core "6.6.1" + workbox-expiration "6.6.1" + workbox-precaching "6.6.1" + workbox-routing "6.6.1" + workbox-strategies "6.6.1" + +workbox-routing@6.6.1: + version "6.6.1" + resolved "https://registry.yarnpkg.com/workbox-routing/-/workbox-routing-6.6.1.tgz#cba9a1c7e0d1ea11e24b6f8c518840efdc94f581" + integrity sha512-j4ohlQvfpVdoR8vDYxTY9rA9VvxTHogkIDwGdJ+rb2VRZQ5vt1CWwUUZBeD/WGFAni12jD1HlMXvJ8JS7aBWTg== + dependencies: + workbox-core "6.6.1" + +workbox-strategies@6.6.1: + version "6.6.1" + resolved "https://registry.yarnpkg.com/workbox-strategies/-/workbox-strategies-6.6.1.tgz#38d0f0fbdddba97bd92e0c6418d0b1a2ccd5b8bf" + integrity sha512-WQLXkRnsk4L81fVPkkgon1rZNxnpdO5LsO+ws7tYBC6QQQFJVI6v98klrJEjFtZwzw/mB/HT5yVp7CcX0O+mrw== + dependencies: + workbox-core "6.6.1" + +workbox-streams@6.6.1: + version "6.6.1" + resolved "https://registry.yarnpkg.com/workbox-streams/-/workbox-streams-6.6.1.tgz#b2f7ba7b315c27a6e3a96a476593f99c5d227d26" + integrity sha512-maKG65FUq9e4BLotSKWSTzeF0sgctQdYyTMq529piEN24Dlu9b6WhrAfRpHdCncRS89Zi2QVpW5V33NX8PgH3Q== + dependencies: + workbox-core "6.6.1" + workbox-routing "6.6.1" + +workbox-sw@6.6.1: + version "6.6.1" + resolved "https://registry.yarnpkg.com/workbox-sw/-/workbox-sw-6.6.1.tgz#d4c4ca3125088e8b9fd7a748ed537fa0247bd72c" + integrity sha512-R7whwjvU2abHH/lR6kQTTXLHDFU2izht9kJOvBRYK65FbwutT4VvnUAJIgHvfWZ/fokrOPhfoWYoPCMpSgUKHQ== workbox-webpack-plugin@^6.4.1: - version "6.4.2" - resolved "https://registry.yarnpkg.com/workbox-webpack-plugin/-/workbox-webpack-plugin-6.4.2.tgz#aad9f11b028786d5b781420e68f4e8f570ea9936" - integrity sha512-CiEwM6kaJRkx1cP5xHksn13abTzUqMHiMMlp5Eh/v4wRcedgDTyv6Uo8+Hg9MurRbHDosO5suaPyF9uwVr4/CQ== + version "6.6.1" + resolved "https://registry.yarnpkg.com/workbox-webpack-plugin/-/workbox-webpack-plugin-6.6.1.tgz#4f81cc1ad4e5d2cd7477a86ba83c84ee2d187531" + integrity sha512-zpZ+ExFj9NmiI66cFEApyjk7hGsfJ1YMOaLXGXBoZf0v7Iu6hL0ZBe+83mnDq3YYWAfA3fnyFejritjOHkFcrA== dependencies: fast-json-stable-stringify "^2.1.0" pretty-bytes "^5.4.1" - source-map-url "^0.4.0" upath "^1.2.0" webpack-sources "^1.4.3" - workbox-build "6.4.2" + workbox-build "6.6.1" -workbox-window@6.4.2: - version "6.4.2" - resolved "https://registry.yarnpkg.com/workbox-window/-/workbox-window-6.4.2.tgz#5319a3e343fa1e4bd15a1f53a07b58999d064c8a" - integrity sha512-KVyRKmrJg7iB+uym/B/CnEUEFG9CvnTU1Bq5xpXHbtgD9l+ShDekSl1wYpqw/O0JfeeQVOFb8CiNfvnwWwqnWQ== +workbox-window@6.6.1: + version "6.6.1" + resolved "https://registry.yarnpkg.com/workbox-window/-/workbox-window-6.6.1.tgz#f22a394cbac36240d0dadcbdebc35f711bb7b89e" + integrity sha512-wil4nwOY58nTdCvif/KEZjQ2NP8uk3gGeRNy2jPBbzypU4BT4D9L8xiwbmDBpZlSgJd2xsT9FvSNU0gsxV51JQ== dependencies: "@types/trusted-types" "^2.0.2" - workbox-core "6.4.2" + workbox-core "6.6.1" + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^6.0.1: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" wrap-ansi@^7.0.0: version "7.0.0" @@ -11215,6 +11847,15 @@ wrap-ansi@^7.0.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -11235,10 +11876,10 @@ ws@^7.4.6: resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== -ws@^8.1.0: - version "8.17.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" - integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== +ws@^8.13.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== xml-name-validator@^3.0.0: version "3.0.0" @@ -11255,7 +11896,7 @@ xmlcreate@^2.0.4: resolved "https://registry.yarnpkg.com/xmlcreate/-/xmlcreate-2.0.4.tgz#0c5ab0f99cdd02a81065fa9cd8f8ae87624889be" integrity sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg== -xtend@^4.0.0, xtend@^4.0.1, xtend@^4.0.2: +xtend@^4.0.0, xtend@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== @@ -11265,25 +11906,30 @@ y18n@^5.0.5: resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2: version "1.10.2" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== +yaml@^2.3.4: + version "2.6.0" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.6.0.tgz#14059ad9d0b1680d0f04d3a60fe00f3a857303c3" + integrity sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ== + yargs-parser@^20.2.2: version "20.2.9" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== -yargs-parser@^21.0.0: - version "21.0.0" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.0.0.tgz#a485d3966be4317426dd56bdb6a30131b281dc55" - integrity sha512-z9kApYUOCwoeZ78rfRYYWdiU/iNL6mwwYlkkZfJoyMR1xps+NEBX5X7XmRpxkZHhXJ6+Ey00IwKxBBSW9FIjyA== +yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== yargs@^16.2.0: version "16.2.0" @@ -11299,17 +11945,17 @@ yargs@^16.2.0: yargs-parser "^20.2.2" yargs@^17.3.0: - version "17.3.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.3.1.tgz#da56b28f32e2fd45aefb402ed9c26f42be4c07b9" - integrity sha512-WUANQeVgjLbNsEmGk20f+nlHgOqzRFpiGWVaBrYGYIGANIIu3lWjoyi0fNlFmJkvfhCZ6BXINe7/W2O2bV4iaA== + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== dependencies: - cliui "^7.0.2" + cliui "^8.0.1" escalade "^3.1.1" get-caller-file "^2.0.5" require-directory "^2.1.1" string-width "^4.2.3" y18n "^5.0.5" - yargs-parser "^21.0.0" + yargs-parser "^21.1.1" yocto-queue@^0.1.0: version "0.1.0" From 651ef719ce9753ff60c20c845077bac122afa8c2 Mon Sep 17 00:00:00 2001 From: Harri Lehtola <1781172+peruukki@users.noreply.github.com> Date: Sun, 20 Oct 2024 22:55:15 +0300 Subject: [PATCH 157/185] chore: Define Node.js version with Node Version Manager in Feast UI (#4655) --- .github/workflows/build_wheels.yml | 4 ++-- .github/workflows/release.yml | 8 ++++---- .github/workflows/unit_tests.yml | 2 +- ui/.nvmrc | 1 + ui/CONTRIBUTING.md | 3 +++ ui/package.json | 2 +- ui/yarn.lock | 15 +++++++++++---- 7 files changed, 23 insertions(+), 12 deletions(-) create mode 100644 ui/.nvmrc diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index 14d0b7d5ae..8fca17cf63 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -64,7 +64,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v3 with: - node-version: '17.x' + node-version-file: './ui/.nvmrc' registry-url: 'https://registry.npmjs.org' - name: Build UI run: make build-ui @@ -91,7 +91,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v3 with: - node-version: '17.x' + node-version-file: './ui/.nvmrc' registry-url: 'https://registry.npmjs.org' - name: Build and install dependencies # There's a `git restore` in here because `make install-go-ci-dependencies` is actually messing up go.mod & go.sum. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5e2fcc1acb..aaa3f48b51 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,7 +36,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v3 with: - node-version: "lts/*" + node-version-file: './ui/.nvmrc' - name: Release (Dry Run) id: get_versions run: | @@ -62,7 +62,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v3 with: - node-version: "lts/*" + node-version-file: './ui/.nvmrc' - name: Bump file versions run: python ./infra/scripts/release/bump_file_versions.py ${CURRENT_VERSION} ${NEXT_VERSION} - name: Install yarn dependencies @@ -103,7 +103,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v3 with: - node-version: "lts/*" + node-version-file: './ui/.nvmrc' - name: Bump file versions (temporarily for Web UI publish) run: python ./infra/scripts/release/bump_file_versions.py ${CURRENT_VERSION} ${NEXT_VERSION} - name: Install yarn dependencies @@ -138,7 +138,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v3 with: - node-version: "lts/*" + node-version-file: './ui/.nvmrc' - name: Set up Homebrew id: set-up-homebrew uses: Homebrew/actions/setup-homebrew@master diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index efce61b7b2..af23c8d808 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -49,7 +49,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v3 with: - node-version: '20.x' + node-version-file: './ui/.nvmrc' registry-url: 'https://registry.npmjs.org' - name: Install yarn dependencies working-directory: ./ui diff --git a/ui/.nvmrc b/ui/.nvmrc new file mode 100644 index 0000000000..67e145bf0f --- /dev/null +++ b/ui/.nvmrc @@ -0,0 +1 @@ +v20.18.0 diff --git a/ui/CONTRIBUTING.md b/ui/CONTRIBUTING.md index 3c13759e26..f75c34c2a5 100644 --- a/ui/CONTRIBUTING.md +++ b/ui/CONTRIBUTING.md @@ -21,6 +21,9 @@ You can see the logic in [../sdk/python/feast/ui](../sdk/python/feast/ui/). This Under the hood, what happens is that the Feast SDK spins up a server which exposes an endpoint to the registry. It then mounts the UI on the server and points it to fetch data from that registry. +## Node.js version +[Node Version Manager](https://github.com/nvm-sh/nvm) is used for conveniently ensuring a consistent Node.js version. The version is defined in the [`.nvmrc`](.nvmrc) file, and you can install and use that version by running `nvm install` (or `nvm use` once it's installed). + ## NPM package project structure The Web UI is powered by a JSON registry dump from Feast (running `feast registry-dump`). Running `yarn start` launches a UI powered by test data. diff --git a/ui/package.json b/ui/package.json index 5de1537862..f793cd06e6 100644 --- a/ui/package.json +++ b/ui/package.json @@ -77,7 +77,7 @@ "@testing-library/react": "^16.0.1", "@testing-library/user-event": "^14.5.2", "@types/jest": "^27.0.1", - "@types/node": "^16.7.13", + "@types/node": "^20.16.13", "@types/react": "^18.3.11", "@types/react-dom": "^18.3.0", "msw": "^0.36.8", diff --git a/ui/yarn.lock b/ui/yarn.lock index 9c8c874e20..e3a9a5b7f5 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -2469,10 +2469,12 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.18.tgz#633184f55c322e4fb08612307c274ee6d5ed3154" integrity sha512-m+6nTEOadJZuTPkKR/SYK3A2d7FZrgElol9UP1Kae90VVU4a6mxnPuLiIW1m4Cq4gZ/nWb9GrdVXJCoCazDAbg== -"@types/node@^16.7.13": - version "16.11.21" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.21.tgz#474d7589a30afcf5291f59bd49cca9ad171ffde4" - integrity sha512-Pf8M1XD9i1ksZEcCP8vuSNwooJ/bZapNmIzpmsMaL+jMI+8mEYU3PKvs+xDNuQcJWF/x24WzY4qxLtB0zNow9A== +"@types/node@^20.16.13": + version "20.16.13" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.16.13.tgz#148c152d757dc73f8d65f0f6f078f39050b85b0c" + integrity sha512-GjQ7im10B0labo8ZGXDGROUl9k0BNyDgzfGpb4g/cl+4yYDWVKcozANF4FGr4/p0O/rAkQClM6Wiwkije++1Tg== + dependencies: + undici-types "~6.19.2" "@types/numeral@^2.0.5": version "2.0.5" @@ -11065,6 +11067,11 @@ undici-types@~6.19.2: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== +undici-types@~6.19.2: + version "6.19.8" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" + integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== + unherit@^1.0.4: version "1.1.3" resolved "https://registry.yarnpkg.com/unherit/-/unherit-1.1.3.tgz#6c9b503f2b41b262330c80e91c8614abdaa69c22" From 61abf894aca7aa52042c40e77f64b49835f4324e Mon Sep 17 00:00:00 2001 From: Rob Howley Date: Mon, 21 Oct 2024 09:42:02 -0400 Subject: [PATCH 158/185] perf: Make /push async (#4650) --- sdk/python/feast/feature_server.py | 13 +- sdk/python/feast/feature_store.py | 137 ++++++++++++++---- .../feast/infra/online_stores/online_store.py | 27 ++++ .../feast/infra/passthrough_provider.py | 56 +++++-- sdk/python/feast/infra/provider.py | 42 ++++++ sdk/python/tests/foo_provider.py | 29 ++++ sdk/python/tests/unit/test_feature_server.py | 47 ++++++ 7 files changed, 311 insertions(+), 40 deletions(-) create mode 100644 sdk/python/tests/unit/test_feature_server.py diff --git a/sdk/python/feast/feature_server.py b/sdk/python/feast/feature_server.py index ab88eca89f..a9f5c09404 100644 --- a/sdk/python/feast/feature_server.py +++ b/sdk/python/feast/feature_server.py @@ -165,7 +165,7 @@ async def get_online_features(body=Depends(get_body)): ) @app.post("/push", dependencies=[Depends(inject_user_details)]) - def push(body=Depends(get_body)): + async def push(body=Depends(get_body)): request = PushFeaturesRequest(**json.loads(body)) df = pd.DataFrame(request.df) actions = [] @@ -201,13 +201,22 @@ def push(body=Depends(get_body)): for feature_view in fvs_with_push_sources: assert_permissions(resource=feature_view, actions=actions) - store.push( + push_params = dict( push_source_name=request.push_source_name, df=df, allow_registry_cache=request.allow_registry_cache, to=to, ) + should_push_async = ( + store._get_provider().async_supported.online.write + and to in [PushMode.ONLINE, PushMode.ONLINE_AND_OFFLINE] + ) + if should_push_async: + await store.push_async(**push_params) + else: + store.push(**push_params) + @app.post("/write-to-online-store", dependencies=[Depends(inject_user_details)]) def write_to_online_store(body=Depends(get_body)): request = WriteToFeatureStoreRequest(**json.loads(body)) diff --git a/sdk/python/feast/feature_store.py b/sdk/python/feast/feature_store.py index 9b1a35303e..f9fa0a7881 100644 --- a/sdk/python/feast/feature_store.py +++ b/sdk/python/feast/feature_store.py @@ -11,6 +11,7 @@ # 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 asyncio import itertools import os import warnings @@ -33,6 +34,7 @@ import pandas as pd import pyarrow as pa from colorama import Fore, Style +from fastapi.concurrency import run_in_threadpool from google.protobuf.timestamp_pb2 import Timestamp from tqdm import tqdm @@ -1423,26 +1425,13 @@ def tqdm_builder(length): end_date, ) - def push( - self, - push_source_name: str, - df: pd.DataFrame, - allow_registry_cache: bool = True, - to: PushMode = PushMode.ONLINE, - ): - """ - Push features to a push source. This updates all the feature views that have the push source as stream source. - - Args: - push_source_name: The name of the push source we want to push data to. - df: The data being pushed. - allow_registry_cache: Whether to allow cached versions of the registry. - to: Whether to push to online or offline store. Defaults to online store only. - """ + def _fvs_for_push_source_or_raise( + self, push_source_name: str, allow_cache: bool + ) -> set[FeatureView]: from feast.data_source import PushSource - all_fvs = self.list_feature_views(allow_cache=allow_registry_cache) - all_fvs += self.list_stream_feature_views(allow_cache=allow_registry_cache) + all_fvs = self.list_feature_views(allow_cache=allow_cache) + all_fvs += self.list_stream_feature_views(allow_cache=allow_cache) fvs_with_push_sources = { fv @@ -1457,7 +1446,27 @@ def push( if not fvs_with_push_sources: raise PushSourceNotFoundException(push_source_name) - for fv in fvs_with_push_sources: + return fvs_with_push_sources + + def push( + self, + push_source_name: str, + df: pd.DataFrame, + allow_registry_cache: bool = True, + to: PushMode = PushMode.ONLINE, + ): + """ + Push features to a push source. This updates all the feature views that have the push source as stream source. + + Args: + push_source_name: The name of the push source we want to push data to. + df: The data being pushed. + allow_registry_cache: Whether to allow cached versions of the registry. + to: Whether to push to online or offline store. Defaults to online store only. + """ + for fv in self._fvs_for_push_source_or_raise( + push_source_name, allow_registry_cache + ): if to == PushMode.ONLINE or to == PushMode.ONLINE_AND_OFFLINE: self.write_to_online_store( fv.name, df, allow_registry_cache=allow_registry_cache @@ -1467,22 +1476,42 @@ def push( fv.name, df, allow_registry_cache=allow_registry_cache ) - def write_to_online_store( + async def push_async( + self, + push_source_name: str, + df: pd.DataFrame, + allow_registry_cache: bool = True, + to: PushMode = PushMode.ONLINE, + ): + fvs = self._fvs_for_push_source_or_raise(push_source_name, allow_registry_cache) + + if to == PushMode.ONLINE or to == PushMode.ONLINE_AND_OFFLINE: + _ = await asyncio.gather( + *[ + self.write_to_online_store_async( + fv.name, df, allow_registry_cache=allow_registry_cache + ) + for fv in fvs + ] + ) + + if to == PushMode.OFFLINE or to == PushMode.ONLINE_AND_OFFLINE: + + def _offline_write(): + for fv in fvs: + self.write_to_offline_store( + fv.name, df, allow_registry_cache=allow_registry_cache + ) + + await run_in_threadpool(_offline_write) + + def _get_feature_view_and_df_for_online_write( self, feature_view_name: str, df: Optional[pd.DataFrame] = None, inputs: Optional[Union[Dict[str, List[Any]], pd.DataFrame]] = None, allow_registry_cache: bool = True, ): - """ - Persists a dataframe to the online store. - - Args: - feature_view_name: The feature view to which the dataframe corresponds. - df: The dataframe to be persisted. - inputs: Optional the dictionary object to be written - allow_registry_cache (optional): Whether to allow retrieving feature views from a cached registry. - """ feature_view_dict = { fv_proto.name: fv_proto for fv_proto in self.list_all_feature_views(allow_registry_cache) @@ -1509,10 +1538,60 @@ def write_to_online_store( df = pd.DataFrame(df) except Exception as _: raise DataFrameSerializationError(df) + return feature_view, df + + def write_to_online_store( + self, + feature_view_name: str, + df: Optional[pd.DataFrame] = None, + inputs: Optional[Union[Dict[str, List[Any]], pd.DataFrame]] = None, + allow_registry_cache: bool = True, + ): + """ + Persists a dataframe to the online store. + Args: + feature_view_name: The feature view to which the dataframe corresponds. + df: The dataframe to be persisted. + inputs: Optional the dictionary object to be written + allow_registry_cache (optional): Whether to allow retrieving feature views from a cached registry. + """ + + feature_view, df = self._get_feature_view_and_df_for_online_write( + feature_view_name=feature_view_name, + df=df, + inputs=inputs, + allow_registry_cache=allow_registry_cache, + ) provider = self._get_provider() provider.ingest_df(feature_view, df) + async def write_to_online_store_async( + self, + feature_view_name: str, + df: Optional[pd.DataFrame] = None, + inputs: Optional[Union[Dict[str, List[Any]], pd.DataFrame]] = None, + allow_registry_cache: bool = True, + ): + """ + Persists a dataframe to the online store asynchronously. + + Args: + feature_view_name: The feature view to which the dataframe corresponds. + df: The dataframe to be persisted. + inputs: Optional the dictionary object to be written + allow_registry_cache (optional): Whether to allow retrieving feature views from a cached registry. + """ + + feature_view, df = self._get_feature_view_and_df_for_online_write( + feature_view_name=feature_view_name, + df=df, + inputs=inputs, + allow_registry_cache=allow_registry_cache, + ) + provider = self._get_provider() + await provider.ingest_df_async(feature_view, df) + def write_to_offline_store( self, feature_view_name: str, diff --git a/sdk/python/feast/infra/online_stores/online_store.py b/sdk/python/feast/infra/online_stores/online_store.py index be2065040b..15dd843ba8 100644 --- a/sdk/python/feast/infra/online_stores/online_store.py +++ b/sdk/python/feast/infra/online_stores/online_store.py @@ -67,6 +67,33 @@ def online_write_batch( """ pass + async def online_write_batch_async( + self, + config: RepoConfig, + table: FeatureView, + data: List[ + Tuple[EntityKeyProto, Dict[str, ValueProto], datetime, Optional[datetime]] + ], + progress: Optional[Callable[[int], Any]], + ) -> None: + """ + Writes a batch of feature rows to the online store asynchronously. + + If a tz-naive timestamp is passed to this method, it is assumed to be UTC. + + Args: + config: The config for the current feature store. + table: Feature view to which these feature rows correspond. + data: A list of quadruplets containing feature data. Each quadruplet contains an entity + key, a dict containing feature values, an event timestamp for the row, and the created + timestamp for the row if it exists. + progress: Function to be called once a batch of rows is written to the online store, used + to show progress. + """ + raise NotImplementedError( + f"Online store {self.__class__.__name__} does not support online write batch async" + ) + @abstractmethod def online_read( self, diff --git a/sdk/python/feast/infra/passthrough_provider.py b/sdk/python/feast/infra/passthrough_provider.py index ea75cf5ff2..9482b808a9 100644 --- a/sdk/python/feast/infra/passthrough_provider.py +++ b/sdk/python/feast/infra/passthrough_provider.py @@ -188,6 +188,20 @@ def online_write_batch( if self.online_store: self.online_store.online_write_batch(config, table, data, progress) + async def online_write_batch_async( + self, + config: RepoConfig, + table: Union[FeatureView, BaseFeatureView, OnDemandFeatureView], + data: List[ + Tuple[EntityKeyProto, Dict[str, ValueProto], datetime, Optional[datetime]] + ], + progress: Optional[Callable[[int], Any]], + ) -> None: + if self.online_store: + await self.online_store.online_write_batch_async( + config, table, data, progress + ) + def offline_write_batch( self, config: RepoConfig, @@ -291,8 +305,8 @@ def retrieve_online_documents( ) return result - def ingest_df( - self, + @staticmethod + def _prep_rows_to_write_for_ingestion( feature_view: Union[BaseFeatureView, FeatureView, OnDemandFeatureView], df: pd.DataFrame, field_mapping: Optional[Dict] = None, @@ -307,10 +321,6 @@ def ingest_df( for entity in feature_view.entity_columns } rows_to_write = _convert_arrow_to_proto(table, feature_view, join_keys) - - self.online_write_batch( - self.repo_config, feature_view, rows_to_write, progress=None - ) else: if hasattr(feature_view, "entity_columns"): join_keys = { @@ -336,9 +346,37 @@ def ingest_df( join_keys[entity.name] = entity.dtype.to_value_type() rows_to_write = _convert_arrow_to_proto(table, feature_view, join_keys) - self.online_write_batch( - self.repo_config, feature_view, rows_to_write, progress=None - ) + return rows_to_write + + def ingest_df( + self, + feature_view: Union[BaseFeatureView, FeatureView, OnDemandFeatureView], + df: pd.DataFrame, + field_mapping: Optional[Dict] = None, + ): + rows_to_write = self._prep_rows_to_write_for_ingestion( + feature_view=feature_view, + df=df, + field_mapping=field_mapping, + ) + self.online_write_batch( + self.repo_config, feature_view, rows_to_write, progress=None + ) + + async def ingest_df_async( + self, + feature_view: Union[BaseFeatureView, FeatureView, OnDemandFeatureView], + df: pd.DataFrame, + field_mapping: Optional[Dict] = None, + ): + rows_to_write = self._prep_rows_to_write_for_ingestion( + feature_view=feature_view, + df=df, + field_mapping=field_mapping, + ) + await self.online_write_batch_async( + self.repo_config, feature_view, rows_to_write, progress=None + ) def ingest_df_to_offline_store(self, feature_view: FeatureView, table: pa.Table): if feature_view.batch_source.field_mapping is not None: diff --git a/sdk/python/feast/infra/provider.py b/sdk/python/feast/infra/provider.py index fb483d194e..47b7c65ef0 100644 --- a/sdk/python/feast/infra/provider.py +++ b/sdk/python/feast/infra/provider.py @@ -141,6 +141,32 @@ def online_write_batch( """ pass + @abstractmethod + async def online_write_batch_async( + self, + config: RepoConfig, + table: FeatureView, + data: List[ + Tuple[EntityKeyProto, Dict[str, ValueProto], datetime, Optional[datetime]] + ], + progress: Optional[Callable[[int], Any]], + ) -> None: + """ + Writes a batch of feature rows to the online store asynchronously. + + If a tz-naive timestamp is passed to this method, it is assumed to be UTC. + + Args: + config: The config for the current feature store. + table: Feature view to which these feature rows correspond. + data: A list of quadruplets containing feature data. Each quadruplet contains an entity + key, a dict containing feature values, an event timestamp for the row, and the created + timestamp for the row if it exists. + progress: Function to be called once a batch of rows is written to the online store, used + to show progress. + """ + pass + def ingest_df( self, feature_view: Union[BaseFeatureView, FeatureView, OnDemandFeatureView], @@ -157,6 +183,22 @@ def ingest_df( """ pass + async def ingest_df_async( + self, + feature_view: Union[BaseFeatureView, FeatureView, OnDemandFeatureView], + df: pd.DataFrame, + field_mapping: Optional[Dict] = None, + ): + """ + Persists a dataframe to the online store asynchronously. + + Args: + feature_view: The feature view to which the dataframe corresponds. + df: The dataframe to be persisted. + field_mapping: A dictionary mapping dataframe column names to feature names. + """ + pass + def ingest_df_to_offline_store( self, feature_view: FeatureView, diff --git a/sdk/python/tests/foo_provider.py b/sdk/python/tests/foo_provider.py index 6fe6d15150..bc30d3ef88 100644 --- a/sdk/python/tests/foo_provider.py +++ b/sdk/python/tests/foo_provider.py @@ -22,6 +22,10 @@ from feast.infra.offline_stores.offline_store import RetrievalJob from feast.infra.provider import Provider from feast.infra.registry.base_registry import BaseRegistry +from feast.infra.supported_async_methods import ( + ProviderAsyncMethods, + SupportedAsyncMethods, +) from feast.online_response import OnlineResponse from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto from feast.protos.feast.types.Value_pb2 import RepeatedValue @@ -30,6 +34,20 @@ class FooProvider(Provider): + @staticmethod + def with_async_support(online_read=False, online_write=False): + class _FooProvider(FooProvider): + @property + def async_supported(self): + return ProviderAsyncMethods( + online=SupportedAsyncMethods( + read=online_read, + write=online_write, + ) + ) + + return _FooProvider(None) + def __init__(self, config: RepoConfig): pass @@ -184,3 +202,14 @@ async def get_online_features_async( full_feature_names: bool = False, ) -> OnlineResponse: pass + + async def online_write_batch_async( + self, + config: RepoConfig, + table: FeatureView, + data: List[ + Tuple[EntityKeyProto, Dict[str, ValueProto], datetime, Optional[datetime]] + ], + progress: Optional[Callable[[int], Any]], + ) -> None: + pass diff --git a/sdk/python/tests/unit/test_feature_server.py b/sdk/python/tests/unit/test_feature_server.py new file mode 100644 index 0000000000..34c3fc4068 --- /dev/null +++ b/sdk/python/tests/unit/test_feature_server.py @@ -0,0 +1,47 @@ +import json +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +from fastapi.testclient import TestClient + +from feast import FeatureStore +from feast.data_source import PushMode +from feast.feature_server import get_app +from feast.utils import _utc_now +from tests.foo_provider import FooProvider + + +@pytest.mark.parametrize( + "online_write,push_mode,async_count", + [ + (True, PushMode.ONLINE_AND_OFFLINE, 1), + (True, PushMode.OFFLINE, 0), + (True, PushMode.ONLINE, 1), + (False, PushMode.ONLINE_AND_OFFLINE, 0), + (False, PushMode.OFFLINE, 0), + (False, PushMode.ONLINE, 0), + ], +) +def test_push_online_async_supported(online_write, push_mode, async_count, environment): + push_payload = json.dumps( + { + "push_source_name": "location_stats_push_source", + "df": { + "location_id": [1], + "temperature": [100], + "event_timestamp": [str(_utc_now())], + "created": [str(_utc_now())], + }, + "to": push_mode.name.lower(), + } + ) + + provider = FooProvider.with_async_support(online_write=online_write) + with patch.object(FeatureStore, "_get_provider", return_value=provider): + fs = environment.feature_store + fs.push = MagicMock() + fs.push_async = AsyncMock() + client = TestClient(get_app(fs)) + client.post("/push", data=push_payload) + assert fs.push.call_count == 1 - async_count + assert fs.push_async.await_count == async_count From c7ddd4bb48290d6d9327fcb6349b84b4d14334af Mon Sep 17 00:00:00 2001 From: Bhargav Dodla <13788369+EXPEbdodla@users.noreply.github.com> Date: Mon, 21 Oct 2024 06:59:40 -0700 Subject: [PATCH 159/185] fix: Update min versions for pyarrow and protobuf (#4646) fix: Updated min versions for pyarrow and protobuf Signed-off-by: Bhargav Dodla Co-authored-by: Bhargav Dodla --- setup.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index c62fb8c50f..1cf51e3289 100644 --- a/setup.py +++ b/setup.py @@ -28,13 +28,13 @@ "click>=7.0.0,<9.0.0", "colorama>=0.3.9,<1", "dill~=0.3.0", - "protobuf<5", + "protobuf>=4.24.0,<5.0.0", "Jinja2>=2,<4", "jsonschema", "mmh3", "numpy>=1.22,<2", "pandas>=1.4.3,<3", - "pyarrow>=4", + "pyarrow>=9.0.0", "pydantic>=2.0.0", "pygments>=2.12.0,<3", "PyYAML>=5.4.0,<7", @@ -292,7 +292,7 @@ entry_points={"console_scripts": ["feast=feast.cli:cli"]}, use_scm_version=use_scm_version, setup_requires=[ - "pybindgen==0.22.0", #TODO do we need this? - "setuptools_scm>=6.2", #TODO do we need this? - ] + "pybindgen==0.22.0", # TODO do we need this? + "setuptools_scm>=6.2", # TODO do we need this? + ], ) From 7ac0908f3846cc0ab05082f748506814c84b2e9c Mon Sep 17 00:00:00 2001 From: Theodor Mihalache <84387487+tmihalac@users.noreply.github.com> Date: Mon, 21 Oct 2024 12:24:29 -0400 Subject: [PATCH 160/185] fix: Quickstart documentation changes (#4618) * fix: changes following issue 4593 Signed-off-by: Theodor Mihalache * fix: changes following issue 4593 - Reverted Fixed file path in templates to be relative path Signed-off-by: Theodor Mihalache * docs: updated following review Signed-off-by: Theodor Mihalache * Updated documents following review suggestions Added symlinks in all the templates pointing to the local template with the .gitignore file Signed-off-by: Theodor Mihalache --------- Signed-off-by: Theodor Mihalache --- docs/getting-started/concepts/README.md | 4 ++ docs/getting-started/concepts/feature-view.md | 2 +- docs/getting-started/concepts/overview.md | 5 +-- docs/getting-started/concepts/project.md | 19 ++++++++ docs/getting-started/quickstart.md | 43 ++++++++++++++---- sdk/python/feast/templates/athena/.gitignore | 1 + sdk/python/feast/templates/aws/.gitignore | 1 + .../feast/templates/cassandra/.gitignore | 1 + sdk/python/feast/templates/gcp/.gitignore | 1 + .../feast/templates/hazelcast/.gitignore | 1 + sdk/python/feast/templates/hbase/.gitignore | 1 + sdk/python/feast/templates/local/.gitignore | 45 +++++++++++++++++++ sdk/python/feast/templates/minimal/.gitignore | 1 + .../feast/templates/postgres/.gitignore | 1 + .../feast/templates/snowflake/.gitignore | 1 + sdk/python/feast/templates/spark/.gitignore | 1 + 16 files changed, 114 insertions(+), 14 deletions(-) create mode 100644 docs/getting-started/concepts/project.md create mode 120000 sdk/python/feast/templates/athena/.gitignore create mode 120000 sdk/python/feast/templates/aws/.gitignore create mode 120000 sdk/python/feast/templates/cassandra/.gitignore create mode 120000 sdk/python/feast/templates/gcp/.gitignore create mode 120000 sdk/python/feast/templates/hazelcast/.gitignore create mode 120000 sdk/python/feast/templates/hbase/.gitignore create mode 100644 sdk/python/feast/templates/local/.gitignore create mode 120000 sdk/python/feast/templates/minimal/.gitignore create mode 120000 sdk/python/feast/templates/postgres/.gitignore create mode 120000 sdk/python/feast/templates/snowflake/.gitignore create mode 120000 sdk/python/feast/templates/spark/.gitignore diff --git a/docs/getting-started/concepts/README.md b/docs/getting-started/concepts/README.md index a32c53b5f4..95e1a14bf1 100644 --- a/docs/getting-started/concepts/README.md +++ b/docs/getting-started/concepts/README.md @@ -4,6 +4,10 @@ [overview.md](overview.md) {% endcontent-ref %} +{% content-ref url="project.md" %} +[project.md](project.md) +{% endcontent-ref %} + {% content-ref url="data-ingestion.md" %} [data-ingestion.md](data-ingestion.md) {% endcontent-ref %} diff --git a/docs/getting-started/concepts/feature-view.md b/docs/getting-started/concepts/feature-view.md index ccb380497d..6ebe4feacf 100644 --- a/docs/getting-started/concepts/feature-view.md +++ b/docs/getting-started/concepts/feature-view.md @@ -14,7 +14,7 @@ Feature views consist of: * zero or more [entities](entity.md) * If the features are not related to a specific object, the feature view might not have entities; see [feature views without entities](feature-view.md#feature-views-without-entities) below. * a name to uniquely identify this feature view in the project. -* (optional, but recommended) a schema specifying one or more [features](feature-view.md#feature) (without this, Feast will infer the schema by reading from the data source) +* (optional, but recommended) a schema specifying one or more [features](feature-view.md#field) (without this, Feast will infer the schema by reading from the data source) * (optional, but recommended) metadata (for example, description, or other free-form metadata via `tags`) * (optional) a TTL, which limits how far back Feast will look when generating historical datasets diff --git a/docs/getting-started/concepts/overview.md b/docs/getting-started/concepts/overview.md index ffbad86c03..033a80ad12 100644 --- a/docs/getting-started/concepts/overview.md +++ b/docs/getting-started/concepts/overview.md @@ -3,10 +3,7 @@ ### Feast project structure The top-level namespace within Feast is a **project**. Users define one or more [feature views](feature-view.md) within a project. Each feature view contains one or more [features](feature-view.md#feature). These features typically relate to one or more [entities](entity.md). A feature view must always have a [data source](data-ingestion.md), which in turn is used during the generation of training [datasets](feature-retrieval.md#dataset) and when materializing feature values into the online store. - -![](<../../.gitbook/assets/image (7).png>) - -**Projects** provide complete isolation of feature stores at the infrastructure level. This is accomplished through resource namespacing, e.g., prefixing table names with the associated project. Each project should be considered a completely separate universe of entities and features. It is not possible to retrieve features from multiple projects in a single request. We recommend having a single feature store and a single project per environment (`dev`, `staging`, `prod`). +You can read more about Feast projects in the [project page](project.md). ### Data ingestion diff --git a/docs/getting-started/concepts/project.md b/docs/getting-started/concepts/project.md new file mode 100644 index 0000000000..8a82db32b2 --- /dev/null +++ b/docs/getting-started/concepts/project.md @@ -0,0 +1,19 @@ +# Project + +Projects provide complete isolation of feature stores at the infrastructure level. This is accomplished through resource namespacing, e.g., prefixing table names with the associated project. Each project should be considered a completely separate universe of entities and features. It is not possible to retrieve features from multiple projects in a single request. We recommend having a single feature store and a single project per environment (`dev`, `staging`, `prod`). + +![](<../../.gitbook/assets/image (7).png>) + +Users define one or more [feature views](feature-view.md) within a project. Each feature view contains one or more [features](feature-view.md#field). These features typically relate to one or more [entities](entity.md). A feature view must always have a [data source](data-ingestion.md), which in turn is used during the generation of training [datasets](feature-retrieval.md#dataset) and when materializing feature values into the online store. + +The concept of a "project" provide the following benefits: + +**Logical Grouping**: Projects group related features together, making it easier to manage and track them. + +**Feature Definitions**: Within a project, you can define features, including their metadata, types, and sources. This helps standardize how features are created and consumed. + +**Isolation**: Projects provide a way to isolate different environments, such as development, testing, and production, ensuring that changes in one project do not affect others. + +**Collaboration**: By organizing features within projects, teams can collaborate more effectively, with clear boundaries around the features they are responsible for. + +**Access Control**: Projects can implement permissions, allowing different users or teams to access only the features relevant to their work. \ No newline at end of file diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md index 4afd0086d9..ec101e5e81 100644 --- a/docs/getting-started/quickstart.md +++ b/docs/getting-started/quickstart.md @@ -1,6 +1,29 @@ # Quickstart -In this tutorial we will +## What is Feast? + +Feast (Feature Store) is an open-source feature store designed to facilitate the management and serving of machine learning features in a way that supports both batch and real-time applications. + +* *For Data Scientists*: Feast is a a tool where you can easily define, store, and retrieve your features for both model development and model deployment. By using Feast, you can focus on what you do best: build features that power your AI/ML models and maximize the value of your data. + +* *For MLOps Engineers*: Feast is a library that allows you to connect your existing infrastructure (e.g., online database, application server, microservice, analytical database, and orchestration tooling) that enables your Data Scientists to ship features for their models to production using a friendly SDK without having to be concerned with software engineering challenges that occur from serving real-time production systems. By using Feast, you can focus on maintaining a resilient system, instead of implementing features for Data Scientists. + +* *For Data Engineers*: Feast provides a centralized catalog for storing feature definitions allowing one to maintain a single source of truth for feature data. It provides the abstraction for reading and writing to many different types of offline and online data stores. Using either the provided python SDK or the feature server service, users can write data to the online and/or offline stores and then read that data out again in either low-latency online scenarios for model inference, or in batch scenarios for model training. + +For more info refer to [Introduction to feast](../README.md) + +## Prerequisites +* Ensure that you have Python (3.9 or above) installed. +* It is recommended to create and work in a virtual environment: + ```sh + # create & activate a virtual environment + python -m venv venv/ + source venv/bin/activate + ``` + +## Overview + +In this tutorial we will: 1. Deploy a local feature store with a **Parquet file offline store** and **Sqlite online store**. 2. Build a training dataset using our time series features from our **Parquet files**. @@ -9,7 +32,9 @@ In this tutorial we will 5. Read the latest features from the online store for real-time inference. 6. Explore the (experimental) Feast UI -## Overview +***Note*** - Feast provides a python SDK as well as an optional [hosted service](../reference/feature-servers/python-feature-server.md) for reading and writing feature data to the online and offline data stores. The latter might be useful when non-python languages are required. + +For this tutorial, we will be using the python SDK. In this tutorial, we'll use Feast to generate training data and power online model inference for a ride-sharing driver satisfaction prediction model. Feast solves several common issues in this flow: @@ -279,7 +304,7 @@ There's an included `test_workflow.py` file which runs through a full sample wor 7. Verify online features are updated / fresher We'll walk through some snippets of code below and explain -### Step 3a: Register feature definitions and deploy your feature store +### Step 4: Register feature definitions and deploy your feature store The `apply` command scans python files in the current directory for feature view/entity definitions, registers the objects, and deploys infrastructure. In this example, it reads `example_repo.py` and sets up SQLite online store tables. Note that we had specified SQLite as the default online store by @@ -311,7 +336,7 @@ Created sqlite table my_project_driver_hourly_stats {% endtab %} {% endtabs %} -### Step 3b: Generating training data or powering batch scoring models +### Step 5: Generating training data or powering batch scoring models To train a model, we need features and labels. Often, this label data is stored separately (e.g. you have one table storing user survey results and another set of tables with feature values). Feast can help generate the features that map to these labels. @@ -466,7 +491,7 @@ print(training_df.head()) ``` {% endtab %} {% endtabs %} -### Step 3c: Ingest batch features into your online store +### Step 6: Ingest batch features into your online store We now serialize the latest values of features since the beginning of time to prepare for serving (note: `materialize-incremental` serializes all new features since the last `materialize` call). @@ -499,7 +524,7 @@ Materializing 2 feature views to 2024-04-19 10:59:58-04:00 into the sqlite onlin {% endtab %} {% endtabs %} -### Step 3d: Fetching feature vectors for inference +### Step 7: Fetching feature vectors for inference At inference time, we need to quickly read the latest feature values for different drivers (which otherwise might have existed only in batch sources) from the online feature store using `get_online_features()`. These feature @@ -544,7 +569,7 @@ pprint(feature_vector) {% endtab %} {% endtabs %} -### Step 3e: Using a feature service to fetch online features instead. +### Step 8: Using a feature service to fetch online features instead. You can also use feature services to manage multiple features, and decouple feature view definitions and the features needed by end applications. The feature store can also be used to fetch either online or historical @@ -594,7 +619,7 @@ pprint(feature_vector) {% endtab %} {% endtabs %} -## Step 4: Browse your features with the Web UI (experimental) +## Step 9: Browse your features with the Web UI (experimental) View all registered features, data sources, entities, and feature services with the Web UI. @@ -626,7 +651,7 @@ INFO: Uvicorn running on http://0.0.0.0:8888 (Press CTRL+C to quit) ![](../reference/ui.png) -## Step 5: Re-examine `test_workflow.py` +## Step 10: Re-examine `test_workflow.py` Take a look at `test_workflow.py` again. It showcases many sample flows on how to interact with Feast. You'll see these show up in the upcoming concepts + architecture + tutorial pages as well. diff --git a/sdk/python/feast/templates/athena/.gitignore b/sdk/python/feast/templates/athena/.gitignore new file mode 120000 index 0000000000..4401ed4150 --- /dev/null +++ b/sdk/python/feast/templates/athena/.gitignore @@ -0,0 +1 @@ +../local/.gitignore \ No newline at end of file diff --git a/sdk/python/feast/templates/aws/.gitignore b/sdk/python/feast/templates/aws/.gitignore new file mode 120000 index 0000000000..4401ed4150 --- /dev/null +++ b/sdk/python/feast/templates/aws/.gitignore @@ -0,0 +1 @@ +../local/.gitignore \ No newline at end of file diff --git a/sdk/python/feast/templates/cassandra/.gitignore b/sdk/python/feast/templates/cassandra/.gitignore new file mode 120000 index 0000000000..4401ed4150 --- /dev/null +++ b/sdk/python/feast/templates/cassandra/.gitignore @@ -0,0 +1 @@ +../local/.gitignore \ No newline at end of file diff --git a/sdk/python/feast/templates/gcp/.gitignore b/sdk/python/feast/templates/gcp/.gitignore new file mode 120000 index 0000000000..4401ed4150 --- /dev/null +++ b/sdk/python/feast/templates/gcp/.gitignore @@ -0,0 +1 @@ +../local/.gitignore \ No newline at end of file diff --git a/sdk/python/feast/templates/hazelcast/.gitignore b/sdk/python/feast/templates/hazelcast/.gitignore new file mode 120000 index 0000000000..4401ed4150 --- /dev/null +++ b/sdk/python/feast/templates/hazelcast/.gitignore @@ -0,0 +1 @@ +../local/.gitignore \ No newline at end of file diff --git a/sdk/python/feast/templates/hbase/.gitignore b/sdk/python/feast/templates/hbase/.gitignore new file mode 120000 index 0000000000..4401ed4150 --- /dev/null +++ b/sdk/python/feast/templates/hbase/.gitignore @@ -0,0 +1 @@ +../local/.gitignore \ No newline at end of file diff --git a/sdk/python/feast/templates/local/.gitignore b/sdk/python/feast/templates/local/.gitignore new file mode 100644 index 0000000000..e86277f60f --- /dev/null +++ b/sdk/python/feast/templates/local/.gitignore @@ -0,0 +1,45 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*.pyo +*.pyd + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +*.egg-info/ +dist/ +build/ +.venv + +# Pytest +.cache +*.cover +*.log +.coverage +nosetests.xml +coverage.xml +*.hypothesis/ +*.pytest_cache/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IDEs and Editors +.vscode/ +.idea/ +*.swp +*.swo +*.sublime-workspace +*.sublime-project + +# OS generated files +.DS_Store +Thumbs.db diff --git a/sdk/python/feast/templates/minimal/.gitignore b/sdk/python/feast/templates/minimal/.gitignore new file mode 120000 index 0000000000..4401ed4150 --- /dev/null +++ b/sdk/python/feast/templates/minimal/.gitignore @@ -0,0 +1 @@ +../local/.gitignore \ No newline at end of file diff --git a/sdk/python/feast/templates/postgres/.gitignore b/sdk/python/feast/templates/postgres/.gitignore new file mode 120000 index 0000000000..4401ed4150 --- /dev/null +++ b/sdk/python/feast/templates/postgres/.gitignore @@ -0,0 +1 @@ +../local/.gitignore \ No newline at end of file diff --git a/sdk/python/feast/templates/snowflake/.gitignore b/sdk/python/feast/templates/snowflake/.gitignore new file mode 120000 index 0000000000..4401ed4150 --- /dev/null +++ b/sdk/python/feast/templates/snowflake/.gitignore @@ -0,0 +1 @@ +../local/.gitignore \ No newline at end of file diff --git a/sdk/python/feast/templates/spark/.gitignore b/sdk/python/feast/templates/spark/.gitignore new file mode 120000 index 0000000000..4401ed4150 --- /dev/null +++ b/sdk/python/feast/templates/spark/.gitignore @@ -0,0 +1 @@ +../local/.gitignore \ No newline at end of file From 58e03d17448a883ef57bcd6d8926d1e54ddcdece Mon Sep 17 00:00:00 2001 From: Dan Baron <84331438+danbaron63@users.noreply.github.com> Date: Mon, 21 Oct 2024 18:10:14 +0100 Subject: [PATCH 161/185] fix: Bigquery dataset create table disposition (#4649) * fix: adding self to a method which is failing linting in file.py integration tests, added self param to a method that was failing linting and ignoring other issues Signed-off-by: Dan Baron * fix: added create_table_disposition check when creating a dataset when get_historical_features is called Signed-off-by: Dan Baron * fix: ignoring some mypy linting errors caused by expanding a dict into kwargs in the repo_configuration integration tests Signed-off-by: Dan Baron * ignoring typing in auth_permissions_util that would be unreasonable to fix due to length of type required and imports Signed-off-by: Dan Baron * fixing method declaration that has no self parameter Signed-off-by: Dan Baron * made xdist_group methods static Signed-off-by: Dan Baron --------- Signed-off-by: Dan Baron --- sdk/python/feast/infra/offline_stores/bigquery.py | 9 ++++++++- .../integration/feature_repos/repo_configuration.py | 4 ++-- .../feature_repos/universal/data_source_creator.py | 1 + .../feature_repos/universal/data_sources/file.py | 9 +++++---- sdk/python/tests/utils/auth_permissions_util.py | 2 +- 5 files changed, 17 insertions(+), 8 deletions(-) diff --git a/sdk/python/feast/infra/offline_stores/bigquery.py b/sdk/python/feast/infra/offline_stores/bigquery.py index 3ee1717461..ed635ae214 100644 --- a/sdk/python/feast/infra/offline_stores/bigquery.py +++ b/sdk/python/feast/infra/offline_stores/bigquery.py @@ -242,6 +242,7 @@ def get_historical_features( dataset_project, config.offline_store.dataset, config.offline_store.location, + config.offline_store.table_create_disposition, ) entity_schema = _get_entity_schema( @@ -670,6 +671,7 @@ def _get_table_reference_for_new_entity( dataset_project: str, dataset_name: str, dataset_location: Optional[str], + table_create_disposition: str, ) -> str: """Gets the table_id for the new entity to be uploaded.""" @@ -679,8 +681,13 @@ def _get_table_reference_for_new_entity( try: client.get_dataset(dataset.reference) - except NotFound: + except NotFound as nfe: # Only create the dataset if it does not exist + if table_create_disposition == "CREATE_NEVER": + raise ValueError( + f"Dataset {dataset_project}.{dataset_name} does not exist " + f"and table_create_disposition is set to {table_create_disposition}." + ) from nfe client.create_dataset(dataset, exists_ok=True) table_name = offline_utils.get_temp_entity_table_name() diff --git a/sdk/python/tests/integration/feature_repos/repo_configuration.py b/sdk/python/tests/integration/feature_repos/repo_configuration.py index 73f99fb7c2..c688a84836 100644 --- a/sdk/python/tests/integration/feature_repos/repo_configuration.py +++ b/sdk/python/tests/integration/feature_repos/repo_configuration.py @@ -575,9 +575,9 @@ def construct_test_environment( } if not isinstance(offline_creator, RemoteOfflineOidcAuthStoreDataSourceCreator): - environment = Environment(**environment_params) + environment = Environment(**environment_params) # type: ignore else: - environment = OfflineServerPermissionsEnvironment(**environment_params) + environment = OfflineServerPermissionsEnvironment(**environment_params) # type: ignore return environment diff --git a/sdk/python/tests/integration/feature_repos/universal/data_source_creator.py b/sdk/python/tests/integration/feature_repos/universal/data_source_creator.py index aa46160358..513a94ee21 100644 --- a/sdk/python/tests/integration/feature_repos/universal/data_source_creator.py +++ b/sdk/python/tests/integration/feature_repos/universal/data_source_creator.py @@ -61,5 +61,6 @@ def create_logged_features_destination(self) -> LoggingDestination: def teardown(self): raise NotImplementedError + @staticmethod def xdist_groups() -> list[str]: return [] diff --git a/sdk/python/tests/integration/feature_repos/universal/data_sources/file.py b/sdk/python/tests/integration/feature_repos/universal/data_sources/file.py index d8b75aca24..35325c2737 100644 --- a/sdk/python/tests/integration/feature_repos/universal/data_sources/file.py +++ b/sdk/python/tests/integration/feature_repos/universal/data_sources/file.py @@ -451,6 +451,7 @@ def __init__(self, project_name: str, *args, **kwargs): self.server_port: int = 0 self.proc = None + @staticmethod def xdist_groups() -> list[str]: return ["keycloak"] @@ -464,10 +465,10 @@ def setup(self, registry: RegistryConfig): entity_key_serialization_version=2, ) - repo_path = Path(tempfile.mkdtemp()) - with open(repo_path / "feature_store.yaml", "w") as outfile: + repo_base_path = Path(tempfile.mkdtemp()) + with open(repo_base_path / "feature_store.yaml", "w") as outfile: yaml.dump(config.model_dump(by_alias=True), outfile) - repo_path = str(repo_path.resolve()) + repo_path = str(repo_base_path.resolve()) include_auth_config( file_path=f"{repo_path}/feature_store.yaml", auth_config=self.auth_config @@ -486,7 +487,7 @@ def setup(self, registry: RegistryConfig): ] self.proc = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL - ) + ) # type: ignore _time_out_sec: int = 60 # Wait for server to start diff --git a/sdk/python/tests/utils/auth_permissions_util.py b/sdk/python/tests/utils/auth_permissions_util.py index b8ca7355e9..49ddd1b530 100644 --- a/sdk/python/tests/utils/auth_permissions_util.py +++ b/sdk/python/tests/utils/auth_permissions_util.py @@ -49,7 +49,7 @@ def default_store( fs = FeatureStore(repo_path=repo_path) - fs.apply(permissions) + fs.apply(permissions) # type: ignore return fs From bde3b4ea9db2ce183800877da86c6ceb3078a3e2 Mon Sep 17 00:00:00 2001 From: Harri Lehtola <1781172+peruukki@users.noreply.github.com> Date: Tue, 22 Oct 2024 12:56:44 +0300 Subject: [PATCH 162/185] chore: Add packageManager to ui/package.json (#4663) --- ui/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/package.json b/ui/package.json index f793cd06e6..9a1876809b 100644 --- a/ui/package.json +++ b/ui/package.json @@ -107,5 +107,6 @@ "license": "Apache-2.0", "bugs": { "url": "https://github.com/feast-dev/feast/issues" - } + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } From 9ca1452e1e613257296ec59fcb7757fe4bec5c19 Mon Sep 17 00:00:00 2001 From: Theodor Mihalache <84387487+tmihalac@users.noreply.github.com> Date: Tue, 22 Oct 2024 09:30:51 -0400 Subject: [PATCH 163/185] chore: Added uvicorn-worker dependency (#4659) Added uvicorn-worker dependency Signed-off-by: Theodor Mihalache --- sdk/python/feast/feature_server.py | 2 +- sdk/python/requirements/py3.10-ci-requirements.txt | 3 ++- sdk/python/requirements/py3.10-requirements.txt | 3 ++- sdk/python/requirements/py3.11-ci-requirements.txt | 3 ++- sdk/python/requirements/py3.11-requirements.txt | 3 ++- sdk/python/requirements/py3.9-ci-requirements.txt | 3 ++- sdk/python/requirements/py3.9-requirements.txt | 3 ++- setup.py | 1 + 8 files changed, 14 insertions(+), 7 deletions(-) diff --git a/sdk/python/feast/feature_server.py b/sdk/python/feast/feature_server.py index a9f5c09404..b4ed591b04 100644 --- a/sdk/python/feast/feature_server.py +++ b/sdk/python/feast/feature_server.py @@ -307,7 +307,7 @@ def load_config(self): if key.lower() in self.cfg.settings and value is not None: self.cfg.set(key.lower(), value) - self.cfg.set("worker_class", "uvicorn.workers.UvicornWorker") + self.cfg.set("worker_class", "uvicorn_worker.UvicornWorker") def load(self): return self._app diff --git a/sdk/python/requirements/py3.10-ci-requirements.txt b/sdk/python/requirements/py3.10-ci-requirements.txt index be13d71b82..57a21cd6d9 100644 --- a/sdk/python/requirements/py3.10-ci-requirements.txt +++ b/sdk/python/requirements/py3.10-ci-requirements.txt @@ -167,7 +167,7 @@ execnet==2.1.1 # via pytest-xdist executing==2.1.0 # via stack-data -fastapi==0.115.0 +fastapi==0.115.2 fastjsonschema==2.20.0 # via nbformat filelock==3.16.1 @@ -889,6 +889,7 @@ urllib3==2.2.3 # responses # testcontainers uvicorn[standard]==0.30.6 +uvicorn-worker uvloop==0.20.0 # via uvicorn virtualenv==20.23.0 diff --git a/sdk/python/requirements/py3.10-requirements.txt b/sdk/python/requirements/py3.10-requirements.txt index 8d34dcdcf3..c5549401ea 100644 --- a/sdk/python/requirements/py3.10-requirements.txt +++ b/sdk/python/requirements/py3.10-requirements.txt @@ -29,7 +29,7 @@ dask-expr==1.1.14 dill==0.3.8 exceptiongroup==1.2.2 # via anyio -fastapi==0.115.0 +fastapi==0.115.2 fsspec==2024.9.0 # via dask greenlet==3.1.0 @@ -136,6 +136,7 @@ tzdata==2024.1 urllib3==2.2.3 # via requests uvicorn[standard]==0.30.6 +uvicorn-worker uvloop==0.20.0 # via uvicorn watchfiles==0.24.0 diff --git a/sdk/python/requirements/py3.11-ci-requirements.txt b/sdk/python/requirements/py3.11-ci-requirements.txt index 1c0d09139a..ed6dc239d3 100644 --- a/sdk/python/requirements/py3.11-ci-requirements.txt +++ b/sdk/python/requirements/py3.11-ci-requirements.txt @@ -160,7 +160,7 @@ execnet==2.1.1 # via pytest-xdist executing==2.1.0 # via stack-data -fastapi==0.115.0 +fastapi==0.115.2 fastjsonschema==2.20.0 # via nbformat filelock==3.16.1 @@ -866,6 +866,7 @@ urllib3==2.2.3 # responses # testcontainers uvicorn[standard]==0.30.6 +uvicorn-worker uvloop==0.20.0 # via uvicorn virtualenv==20.23.0 diff --git a/sdk/python/requirements/py3.11-requirements.txt b/sdk/python/requirements/py3.11-requirements.txt index 649b08f492..d7ed97723f 100644 --- a/sdk/python/requirements/py3.11-requirements.txt +++ b/sdk/python/requirements/py3.11-requirements.txt @@ -27,7 +27,7 @@ dask[dataframe]==2024.9.0 dask-expr==1.1.14 # via dask dill==0.3.8 -fastapi==0.115.0 +fastapi==0.115.2 fsspec==2024.9.0 # via dask greenlet==3.1.0 @@ -130,6 +130,7 @@ tzdata==2024.1 urllib3==2.2.3 # via requests uvicorn[standard]==0.30.6 +uvicorn-worker uvloop==0.20.0 # via uvicorn watchfiles==0.24.0 diff --git a/sdk/python/requirements/py3.9-ci-requirements.txt b/sdk/python/requirements/py3.9-ci-requirements.txt index 3dba480af6..e7d6686b4d 100644 --- a/sdk/python/requirements/py3.9-ci-requirements.txt +++ b/sdk/python/requirements/py3.9-ci-requirements.txt @@ -169,7 +169,7 @@ execnet==2.1.1 # via pytest-xdist executing==2.1.0 # via stack-data -fastapi==0.115.0 +fastapi==0.115.2 fastjsonschema==2.20.0 # via nbformat filelock==3.16.1 @@ -900,6 +900,7 @@ urllib3==1.26.20 # snowflake-connector-python # testcontainers uvicorn[standard]==0.30.6 +uvicorn-worker uvloop==0.20.0 # via uvicorn virtualenv==20.23.0 diff --git a/sdk/python/requirements/py3.9-requirements.txt b/sdk/python/requirements/py3.9-requirements.txt index ba30a4ecf5..16afecdfb5 100644 --- a/sdk/python/requirements/py3.9-requirements.txt +++ b/sdk/python/requirements/py3.9-requirements.txt @@ -29,7 +29,7 @@ dask-expr==1.1.10 dill==0.3.8 exceptiongroup==1.2.2 # via anyio -fastapi==0.115.0 +fastapi==0.115.2 fsspec==2024.9.0 # via dask greenlet==3.1.0 @@ -139,6 +139,7 @@ tzdata==2024.1 urllib3==2.2.3 # via requests uvicorn[standard]==0.30.6 +uvicorn-worker uvloop==0.20.0 # via uvicorn watchfiles==0.24.0 diff --git a/setup.py b/setup.py index 1cf51e3289..156d7c8d0a 100644 --- a/setup.py +++ b/setup.py @@ -47,6 +47,7 @@ "typeguard>=4.0.0", "fastapi>=0.68.0", "uvicorn[standard]>=0.14.0,<1", + "uvicorn-worker", "gunicorn; platform_system != 'Windows'", "dask[dataframe]>=2024.2.1", "prometheus_client", From a1ff1290002376b4c9ac8ad14e60df4622bb47a4 Mon Sep 17 00:00:00 2001 From: Hao Xu Date: Tue, 22 Oct 2024 22:55:23 -0700 Subject: [PATCH 164/185] feat: Faiss and In memory store (#4464) * add faiss & in memory online store Signed-off-by: cmuhao * add faiss & in memory online store Signed-off-by: cmuhao * add faiss & in memory online store Signed-off-by: cmuhao * add faiss & in memory online store Signed-off-by: cmuhao * add faiss & in memory online store Signed-off-by: cmuhao * add faiss & in memory online store Signed-off-by: cmuhao * add faiss & in memory online store Signed-off-by: cmuhao * add faiss & in memory online store Signed-off-by: cmuhao * add faiss & in memory online store Signed-off-by: cmuhao * add faiss & in memory online store Signed-off-by: cmuhao * add faiss & in memory online store Signed-off-by: cmuhao * add faiss & in memory online store Signed-off-by: cmuhao * add dependency Signed-off-by: cmuhao * update package name Signed-off-by: cmuhao --------- Signed-off-by: cmuhao --- .../contrib/faiss_online_store.py | 236 ++++++++++++++++++ setup.py | 4 + 2 files changed, 240 insertions(+) create mode 100644 sdk/python/feast/infra/online_stores/contrib/faiss_online_store.py diff --git a/sdk/python/feast/infra/online_stores/contrib/faiss_online_store.py b/sdk/python/feast/infra/online_stores/contrib/faiss_online_store.py new file mode 100644 index 0000000000..f69ca899d6 --- /dev/null +++ b/sdk/python/feast/infra/online_stores/contrib/faiss_online_store.py @@ -0,0 +1,236 @@ +import logging +from datetime import datetime +from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple + +import faiss +import numpy as np +from google.protobuf.timestamp_pb2 import Timestamp + +from feast import Entity, FeatureView, RepoConfig +from feast.infra.key_encoding_utils import serialize_entity_key +from feast.infra.online_stores.online_store import OnlineStore +from feast.protos.feast.types.EntityKey_pb2 import EntityKey +from feast.protos.feast.types.Value_pb2 import Value +from feast.repo_config import FeastConfigBaseModel + + +class FaissOnlineStoreConfig(FeastConfigBaseModel): + dimension: int + index_path: str + index_type: str = "IVFFlat" + nlist: int = 100 + + +class InMemoryStore: + def __init__(self): + self.feature_names: List[str] = [] + self.entity_keys: Dict[str, int] = {} + + def update(self, feature_names: List[str], entity_keys: Dict[str, int]): + self.feature_names = feature_names + self.entity_keys = entity_keys + + def delete(self, entity_keys: List[str]): + for entity_key in entity_keys: + if entity_key in self.entity_keys: + del self.entity_keys[entity_key] + + def read(self, entity_keys: List[str]) -> List[Optional[int]]: + return [self.entity_keys.get(entity_key) for entity_key in entity_keys] + + def teardown(self): + self.feature_names = [] + self.entity_keys = {} + + +class FaissOnlineStore(OnlineStore): + _index: Optional[faiss.IndexIVFFlat] = None + _in_memory_store: InMemoryStore = InMemoryStore() + _config: Optional[FaissOnlineStoreConfig] = None + _logger: logging.Logger = logging.getLogger(__name__) + + def _get_index(self, config: RepoConfig) -> faiss.IndexIVFFlat: + if self._index is None or self._config is None: + raise ValueError("Index is not initialized") + return self._index + + def update( + self, + config: RepoConfig, + tables_to_delete: Sequence[FeatureView], + tables_to_keep: Sequence[FeatureView], + entities_to_delete: Sequence[Entity], + entities_to_keep: Sequence[Entity], + partial: bool, + ): + feature_views = tables_to_keep + if not feature_views: + return + + feature_names = [f.name for f in feature_views[0].features] + dimension = len(feature_names) + + self._config = FaissOnlineStoreConfig(**config.online_store.dict()) + if self._index is None or not partial: + quantizer = faiss.IndexFlatL2(dimension) + self._index = faiss.IndexIVFFlat(quantizer, dimension, self._config.nlist) + self._index.train( + np.random.rand(self._config.nlist * 100, dimension).astype(np.float32) + ) + self._in_memory_store = InMemoryStore() + + self._in_memory_store.update(feature_names, {}) + + def teardown( + self, + config: RepoConfig, + tables: Sequence[FeatureView], + entities: Sequence[Entity], + ): + self._index = None + self._in_memory_store.teardown() + + def online_read( + self, + config: RepoConfig, + table: FeatureView, + entity_keys: List[EntityKey], + requested_features: Optional[List[str]] = None, + ) -> List[Tuple[Optional[datetime], Optional[Dict[str, Value]]]]: + if self._index is None: + return [(None, None)] * len(entity_keys) + + results: List[Tuple[Optional[datetime], Optional[Dict[str, Any]]]] = [] + for entity_key in entity_keys: + serialized_key = serialize_entity_key( + entity_key, config.entity_key_serialization_version + ).hex() + idx = self._in_memory_store.entity_keys.get(serialized_key, -1) + if idx == -1: + results.append((None, None)) + else: + feature_vector = self._index.reconstruct(int(idx)) + feature_dict = { + name: Value(double_val=value) + for name, value in zip( + self._in_memory_store.feature_names, feature_vector + ) + } + results.append((None, feature_dict)) + return results + + def online_write_batch( + self, + config: RepoConfig, + table: FeatureView, + data: List[Tuple[EntityKey, Dict[str, Value], datetime, Optional[datetime]]], + progress: Optional[Callable[[int], Any]], + ) -> None: + if self._index is None: + self._logger.warning("Index is not initialized. Skipping write operation.") + return + + feature_vectors = [] + serialized_keys = [] + + for entity_key, feature_dict, _, _ in data: + serialized_key = serialize_entity_key( + entity_key, config.entity_key_serialization_version + ).hex() + feature_vector = np.array( + [ + feature_dict[name].double_val + for name in self._in_memory_store.feature_names + ], + dtype=np.float32, + ) + + feature_vectors.append(feature_vector) + serialized_keys.append(serialized_key) + + feature_vectors_array = np.array(feature_vectors) + + existing_indices = [ + self._in_memory_store.entity_keys.get(sk, -1) for sk in serialized_keys + ] + mask = np.array(existing_indices) != -1 + if np.any(mask): + self._index.remove_ids( + np.array([idx for idx in existing_indices if idx != -1]) + ) + + new_indices = np.arange( + self._index.ntotal, self._index.ntotal + len(feature_vectors_array) + ) + self._index.add(feature_vectors_array) + + for sk, idx in zip(serialized_keys, new_indices): + self._in_memory_store.entity_keys[sk] = idx + + if progress: + progress(len(data)) + + def retrieve_online_documents( + self, + config: RepoConfig, + table: FeatureView, + requested_feature: str, + embedding: List[float], + top_k: int, + distance_metric: Optional[str] = None, + ) -> List[ + Tuple[ + Optional[datetime], + Optional[Value], + Optional[Value], + Optional[Value], + ] + ]: + if self._index is None: + self._logger.warning("Index is not initialized. Returning empty result.") + return [] + + query_vector = np.array(embedding, dtype=np.float32).reshape(1, -1) + distances, indices = self._index.search(query_vector, top_k) + + results: List[ + Tuple[ + Optional[datetime], + Optional[Value], + Optional[Value], + Optional[Value], + ] + ] = [] + for i, idx in enumerate(indices[0]): + if idx == -1: + continue + + feature_vector = self._index.reconstruct(int(idx)) + + timestamp = Timestamp() + timestamp.GetCurrentTime() + + feature_value = Value(string_val=",".join(map(str, feature_vector))) + vector_value = Value(string_val=",".join(map(str, feature_vector))) + distance_value = Value(float_val=distances[0][i]) + + results.append( + ( + timestamp.ToDatetime(), + feature_value, + vector_value, + distance_value, + ) + ) + + return results + + async def online_read_async( + self, + config: RepoConfig, + table: FeatureView, + entity_keys: List[EntityKey], + requested_features: Optional[List[str]] = None, + ) -> List[Tuple[Optional[datetime], Optional[Dict[str, Value]]]]: + # Implement async read if needed + raise NotImplementedError("Async read is not implemented for FaissOnlineStore") diff --git a/setup.py b/setup.py index 156d7c8d0a..7c75625d30 100644 --- a/setup.py +++ b/setup.py @@ -144,6 +144,8 @@ MSSQL_REQUIRED = ["ibis-framework[mssql]>=9.0.0,<10"] +FAISS_REQUIRED = ["faiss-cpu>=1.7.0,<2"] + CI_REQUIRED = ( [ "build", @@ -210,6 +212,7 @@ + SQLITE_VEC_REQUIRED + SINGLESTORE_REQUIRED + OPENTELEMETRY + + FAISS_REQUIRED ) DOCS_REQUIRED = CI_REQUIRED @@ -279,6 +282,7 @@ "sqlite_vec": SQLITE_VEC_REQUIRED, "singlestore": SINGLESTORE_REQUIRED, "opentelemetry": OPENTELEMETRY, + "faiss": FAISS_REQUIRED, }, include_package_data=True, license="Apache", From 41aaeebaa5908f44cda28a4410e2fca412f53e92 Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Wed, 23 Oct 2024 04:28:38 -0400 Subject: [PATCH 165/185] fix: Fixing failure of protos during ODFV transformations for missing entities (#4667) --- sdk/python/feast/utils.py | 24 +++++++++++++---- .../test_on_demand_python_transformation.py | 26 +++++++++---------- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/sdk/python/feast/utils.py b/sdk/python/feast/utils.py index ec2da79782..32cd2f606c 100644 --- a/sdk/python/feast/utils.py +++ b/sdk/python/feast/utils.py @@ -42,7 +42,7 @@ from feast.protos.feast.types.Value_pb2 import RepeatedValue as RepeatedValueProto from feast.protos.feast.types.Value_pb2 import Value as ValueProto from feast.type_map import python_values_to_proto_values -from feast.types import from_feast_to_pyarrow_type +from feast.types import ComplexFeastType, PrimitiveFeastType, from_feast_to_pyarrow_type from feast.value_type import ValueType from feast.version import get_version @@ -552,13 +552,27 @@ def _augment_response_with_on_demand_transforms( selected_subset = [f for f in transformed_columns if f in _feature_refs] proto_values = [] + schema_dict = {k.name: k.dtype for k in odfv.schema} for selected_feature in selected_subset: feature_vector = transformed_features[selected_feature] + selected_feature_type = schema_dict.get(selected_feature, None) + feature_type: ValueType = ValueType.UNKNOWN + if selected_feature_type is not None: + if isinstance( + selected_feature_type, (ComplexFeastType, PrimitiveFeastType) + ): + feature_type = selected_feature_type.to_value_type() + elif not isinstance(selected_feature_type, ValueType): + raise TypeError( + f"Unexpected type for feature_type: {type(feature_type)}" + ) + proto_values.append( - python_values_to_proto_values(feature_vector, ValueType.UNKNOWN) - if odfv.mode == "python" - else python_values_to_proto_values( - feature_vector.to_numpy(), ValueType.UNKNOWN + python_values_to_proto_values( + feature_vector + if odfv.mode == "python" + else feature_vector.to_numpy(), + feature_type, ) ) diff --git a/sdk/python/tests/unit/test_on_demand_python_transformation.py b/sdk/python/tests/unit/test_on_demand_python_transformation.py index b7ddfb9e75..530bf1fa0a 100644 --- a/sdk/python/tests/unit/test_on_demand_python_transformation.py +++ b/sdk/python/tests/unit/test_on_demand_python_transformation.py @@ -609,19 +609,19 @@ def pandas_view(features_df: pd.DataFrame) -> pd.DataFrame: "val_to_add", "val_to_add_2", ] - with pytest.raises(TypeError): - _ = self.store.get_online_features( - entity_rows=[ - {"driver_id": 1234567890, "val_to_add": 0, "val_to_add_2": 1} - ], - features=[ - "driver_hourly_stats:conv_rate", - "driver_hourly_stats:acc_rate", - "driver_hourly_stats:avg_daily_trips", - "pandas_view:conv_rate_plus_val1", - "pandas_view:conv_rate_plus_val2", - ], - ) + resp_online_missing_entity = self.store.get_online_features( + entity_rows=[ + {"driver_id": 1234567890, "val_to_add": 0, "val_to_add_2": 1} + ], + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "driver_hourly_stats:avg_daily_trips", + "pandas_view:conv_rate_plus_val1", + "pandas_view:conv_rate_plus_val2", + ], + ) + assert resp_online_missing_entity is not None resp_online = self.store.get_online_features( entity_rows=[{"driver_id": 1001, "val_to_add": 0, "val_to_add_2": 1}], features=[ From 0d45e95767773846861d53feb4af4c6bc1451b5e Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Wed, 23 Oct 2024 12:19:16 -0400 Subject: [PATCH 166/185] fix: Patch FAISS online return signature (#4671) fix: Path faiss online return signature Signed-off-by: Francisco Javier Arceo --- .../contrib/faiss_online_store.py | 41 +++++++++++-------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/sdk/python/feast/infra/online_stores/contrib/faiss_online_store.py b/sdk/python/feast/infra/online_stores/contrib/faiss_online_store.py index f69ca899d6..cc2e75800e 100644 --- a/sdk/python/feast/infra/online_stores/contrib/faiss_online_store.py +++ b/sdk/python/feast/infra/online_stores/contrib/faiss_online_store.py @@ -9,8 +9,8 @@ from feast import Entity, FeatureView, RepoConfig from feast.infra.key_encoding_utils import serialize_entity_key from feast.infra.online_stores.online_store import OnlineStore -from feast.protos.feast.types.EntityKey_pb2 import EntityKey -from feast.protos.feast.types.Value_pb2 import Value +from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto +from feast.protos.feast.types.Value_pb2 import Value as ValueProto from feast.repo_config import FeastConfigBaseModel @@ -94,9 +94,9 @@ def online_read( self, config: RepoConfig, table: FeatureView, - entity_keys: List[EntityKey], + entity_keys: List[EntityKeyProto], requested_features: Optional[List[str]] = None, - ) -> List[Tuple[Optional[datetime], Optional[Dict[str, Value]]]]: + ) -> List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]]: if self._index is None: return [(None, None)] * len(entity_keys) @@ -111,7 +111,7 @@ def online_read( else: feature_vector = self._index.reconstruct(int(idx)) feature_dict = { - name: Value(double_val=value) + name: ValueProto(double_val=value) for name, value in zip( self._in_memory_store.feature_names, feature_vector ) @@ -123,7 +123,9 @@ def online_write_batch( self, config: RepoConfig, table: FeatureView, - data: List[Tuple[EntityKey, Dict[str, Value], datetime, Optional[datetime]]], + data: List[ + Tuple[EntityKeyProto, Dict[str, ValueProto], datetime, Optional[datetime]] + ], progress: Optional[Callable[[int], Any]], ) -> None: if self._index is None: @@ -181,9 +183,10 @@ def retrieve_online_documents( ) -> List[ Tuple[ Optional[datetime], - Optional[Value], - Optional[Value], - Optional[Value], + Optional[EntityKeyProto], + Optional[ValueProto], + Optional[ValueProto], + Optional[ValueProto], ] ]: if self._index is None: @@ -196,9 +199,10 @@ def retrieve_online_documents( results: List[ Tuple[ Optional[datetime], - Optional[Value], - Optional[Value], - Optional[Value], + Optional[EntityKeyProto], + Optional[ValueProto], + Optional[ValueProto], + Optional[ValueProto], ] ] = [] for i, idx in enumerate(indices[0]): @@ -209,14 +213,15 @@ def retrieve_online_documents( timestamp = Timestamp() timestamp.GetCurrentTime() - - feature_value = Value(string_val=",".join(map(str, feature_vector))) - vector_value = Value(string_val=",".join(map(str, feature_vector))) - distance_value = Value(float_val=distances[0][i]) + entity_value = EntityKeyProto() + feature_value = ValueProto(string_val=",".join(map(str, feature_vector))) + vector_value = ValueProto(string_val=",".join(map(str, feature_vector))) + distance_value = ValueProto(float_val=distances[0][i]) results.append( ( timestamp.ToDatetime(), + entity_value, feature_value, vector_value, distance_value, @@ -229,8 +234,8 @@ async def online_read_async( self, config: RepoConfig, table: FeatureView, - entity_keys: List[EntityKey], + entity_keys: List[EntityKeyProto], requested_features: Optional[List[str]] = None, - ) -> List[Tuple[Optional[datetime], Optional[Dict[str, Value]]]]: + ) -> List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]]: # Implement async read if needed raise NotImplementedError("Async read is not implemented for FaissOnlineStore") From 2807dfaf46d3d9e79f84b0ff22dbaeede377c89b Mon Sep 17 00:00:00 2001 From: Matt Green Date: Wed, 23 Oct 2024 10:11:16 -0700 Subject: [PATCH 167/185] fix: Fix gitignore issue (#4674) fix: gitignore issue Signed-off-by: Matt Green --- sdk/python/feast/repo_operations.py | 4 ++++ sdk/python/feast/templates/athena/{.gitignore => gitignore} | 0 sdk/python/feast/templates/aws/{.gitignore => gitignore} | 0 .../feast/templates/cassandra/{.gitignore => gitignore} | 0 sdk/python/feast/templates/gcp/{.gitignore => gitignore} | 0 .../feast/templates/hazelcast/{.gitignore => gitignore} | 0 sdk/python/feast/templates/hbase/{.gitignore => gitignore} | 0 sdk/python/feast/templates/minimal/{.gitignore => gitignore} | 0 sdk/python/feast/templates/postgres/{.gitignore => gitignore} | 0 .../feast/templates/snowflake/{.gitignore => gitignore} | 0 sdk/python/feast/templates/spark/{.gitignore => gitignore} | 0 11 files changed, 4 insertions(+) rename sdk/python/feast/templates/athena/{.gitignore => gitignore} (100%) rename sdk/python/feast/templates/aws/{.gitignore => gitignore} (100%) rename sdk/python/feast/templates/cassandra/{.gitignore => gitignore} (100%) rename sdk/python/feast/templates/gcp/{.gitignore => gitignore} (100%) rename sdk/python/feast/templates/hazelcast/{.gitignore => gitignore} (100%) rename sdk/python/feast/templates/hbase/{.gitignore => gitignore} (100%) rename sdk/python/feast/templates/minimal/{.gitignore => gitignore} (100%) rename sdk/python/feast/templates/postgres/{.gitignore => gitignore} (100%) rename sdk/python/feast/templates/snowflake/{.gitignore => gitignore} (100%) rename sdk/python/feast/templates/spark/{.gitignore => gitignore} (100%) diff --git a/sdk/python/feast/repo_operations.py b/sdk/python/feast/repo_operations.py index 6629768375..4db0bbc6fd 100644 --- a/sdk/python/feast/repo_operations.py +++ b/sdk/python/feast/repo_operations.py @@ -468,6 +468,10 @@ def init_repo(repo_name: str, template: str): raise IOError(f"Could not find template {template}") copytree(template_path, str(repo_path), dirs_exist_ok=True) + # Rename gitignore files back to .gitignore + for gitignore_path in repo_path.rglob("gitignore"): + gitignore_path.rename(gitignore_path.with_name(".gitignore")) + # Seed the repository bootstrap_path = repo_path / "bootstrap.py" if os.path.exists(bootstrap_path): diff --git a/sdk/python/feast/templates/athena/.gitignore b/sdk/python/feast/templates/athena/gitignore similarity index 100% rename from sdk/python/feast/templates/athena/.gitignore rename to sdk/python/feast/templates/athena/gitignore diff --git a/sdk/python/feast/templates/aws/.gitignore b/sdk/python/feast/templates/aws/gitignore similarity index 100% rename from sdk/python/feast/templates/aws/.gitignore rename to sdk/python/feast/templates/aws/gitignore diff --git a/sdk/python/feast/templates/cassandra/.gitignore b/sdk/python/feast/templates/cassandra/gitignore similarity index 100% rename from sdk/python/feast/templates/cassandra/.gitignore rename to sdk/python/feast/templates/cassandra/gitignore diff --git a/sdk/python/feast/templates/gcp/.gitignore b/sdk/python/feast/templates/gcp/gitignore similarity index 100% rename from sdk/python/feast/templates/gcp/.gitignore rename to sdk/python/feast/templates/gcp/gitignore diff --git a/sdk/python/feast/templates/hazelcast/.gitignore b/sdk/python/feast/templates/hazelcast/gitignore similarity index 100% rename from sdk/python/feast/templates/hazelcast/.gitignore rename to sdk/python/feast/templates/hazelcast/gitignore diff --git a/sdk/python/feast/templates/hbase/.gitignore b/sdk/python/feast/templates/hbase/gitignore similarity index 100% rename from sdk/python/feast/templates/hbase/.gitignore rename to sdk/python/feast/templates/hbase/gitignore diff --git a/sdk/python/feast/templates/minimal/.gitignore b/sdk/python/feast/templates/minimal/gitignore similarity index 100% rename from sdk/python/feast/templates/minimal/.gitignore rename to sdk/python/feast/templates/minimal/gitignore diff --git a/sdk/python/feast/templates/postgres/.gitignore b/sdk/python/feast/templates/postgres/gitignore similarity index 100% rename from sdk/python/feast/templates/postgres/.gitignore rename to sdk/python/feast/templates/postgres/gitignore diff --git a/sdk/python/feast/templates/snowflake/.gitignore b/sdk/python/feast/templates/snowflake/gitignore similarity index 100% rename from sdk/python/feast/templates/snowflake/.gitignore rename to sdk/python/feast/templates/snowflake/gitignore diff --git a/sdk/python/feast/templates/spark/.gitignore b/sdk/python/feast/templates/spark/gitignore similarity index 100% rename from sdk/python/feast/templates/spark/.gitignore rename to sdk/python/feast/templates/spark/gitignore From 5291289e06cdb4e36b3a591686fcda077cc4f89d Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Wed, 23 Oct 2024 13:31:12 -0400 Subject: [PATCH 168/185] chore: Add Daniele and Lokesh as Approvers and Reviewers (#4672) * chore: Add Daniele and Lokesh as Approvers and Reviewers Add Daniele and Lokesh as Approvers and Reviewers * Delete OWNERS * Update CODEOWNERS --- CODEOWNERS | 9 +++++++++ OWNERS | 47 ----------------------------------------------- 2 files changed, 9 insertions(+), 47 deletions(-) delete mode 100644 OWNERS diff --git a/CODEOWNERS b/CODEOWNERS index 75ede8b6aa..e4eb72958c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,6 +1,15 @@ # See https://help.github.com/articles/about-codeowners/ # for more info about CODEOWNERS file +/docs/ @feast-dev/reviewers-and-approvers +/examples/ @feast-dev/reviewers-and-approvers +/go/ @feast-dev/reviewers-and-approvers +/infra/ @feast-dev/reviewers-and-approvers +/java/ @feast-dev/reviewers-and-approvers +/protos/ @feast-dev/reviewers-and-approvers +/sdk/ @feast-dev/reviewers-and-approvers +/ui/ @feast-dev/reviewers-and-approvers + # Core Interfaces /sdk/python/feast/infra/offline_stores/offline_store.py @feast-dev/maintainers /sdk/python/feast/infra/online_stores/online_store.py @feast-dev/maintainers diff --git a/OWNERS b/OWNERS deleted file mode 100644 index 1072fc2187..0000000000 --- a/OWNERS +++ /dev/null @@ -1,47 +0,0 @@ -# This file is different from the CODEOWNERS file. -# OWNERS is used by feast-ci-bot to accept commands like `/ok-to-test` and `/lgtm` -# More info at https://www.kubernetes.dev/docs/guide/owners/ -approvers: - - woop - - achals - - adchia - - felixwang9817 - - MattDelac - - kevjumba - - chhabrakadabra - - gbmarc1 - - sfc-gh-madkins - - zhilingc - - whoahbot - - niklasvm - - toping4445 - - DvirDukhan - - hemidactylus - - franciscojavierarceo - - haoxuai - - jeremyary - - shuchu - - tokoko - -reviewers: - - woop - - achals - - tedhtchang - - adchia - - felixwang9817 - - MattDelac - - kevjumba - - chhabrakadabra - - gbmarc1 - - sfc-gh-madkins - - zhilingc - - whoahbot - - niklasvm - - toping4445 - - DvirDukhan - - hemidactylus - - franciscojavierarceo - - haoxuai - - jeremyary - - shuchu - - tokoko From e726c096f2de93d6dc0a807c97c47476cc79dd61 Mon Sep 17 00:00:00 2001 From: Matt Green Date: Wed, 23 Oct 2024 18:17:25 -0700 Subject: [PATCH 169/185] feat: Add more __repr__ methods (#4676) --- sdk/python/feast/entity.py | 14 ++++++++++++++ sdk/python/feast/feature.py | 12 +++++++++--- sdk/python/feast/feature_store.py | 10 ++++++++++ sdk/python/feast/field.py | 9 ++++++++- 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/sdk/python/feast/entity.py b/sdk/python/feast/entity.py index a988c200d7..290e6307a4 100644 --- a/sdk/python/feast/entity.py +++ b/sdk/python/feast/entity.py @@ -99,6 +99,20 @@ def __init__( self.created_timestamp = None self.last_updated_timestamp = None + def __repr__(self): + return ( + f"Entity(\n" + f" name={self.name!r},\n" + f" value_type={self.value_type!r},\n" + f" join_key={self.join_key!r},\n" + f" description={self.description!r},\n" + f" tags={self.tags!r},\n" + f" owner={self.owner!r},\n" + f" created_timestamp={self.created_timestamp!r},\n" + f" last_updated_timestamp={self.last_updated_timestamp!r}\n" + f")" + ) + def __hash__(self) -> int: return hash((self.name, self.join_key)) diff --git a/sdk/python/feast/feature.py b/sdk/python/feast/feature.py index b919706544..db629d677a 100644 --- a/sdk/python/feast/feature.py +++ b/sdk/python/feast/feature.py @@ -58,12 +58,18 @@ def __lt__(self, other): return self.name < other.name def __repr__(self): - # return string representation of the reference - return f"{self.name}-{self.dtype}" + return ( + f"Feature(\n" + f" name={self._name!r},\n" + f" dtype={self._dtype!r},\n" + f" description={self._description!r},\n" + f" labels={self._labels!r}\n" + f")" + ) def __str__(self): # readable string of the reference - return f"Feature<{self.__repr__()}>" + return f"Feature<{self.name}: {self.dtype}>" @property def name(self): diff --git a/sdk/python/feast/feature_store.py b/sdk/python/feast/feature_store.py index f9fa0a7881..217303df91 100644 --- a/sdk/python/feast/feature_store.py +++ b/sdk/python/feast/feature_store.py @@ -175,6 +175,16 @@ def version(self) -> str: """Returns the version of the current Feast SDK/CLI.""" return get_version() + def __repr__(self) -> str: + return ( + f"FeatureStore(\n" + f" repo_path={self.repo_path!r},\n" + f" config={self.config!r},\n" + f" registry={self._registry!r},\n" + f" provider={self._provider!r}\n" + f")" + ) + @property def registry(self) -> BaseRegistry: """Gets the registry of this feature store.""" diff --git a/sdk/python/feast/field.py b/sdk/python/feast/field.py index 7c9f14decf..a41dcf5d5e 100644 --- a/sdk/python/feast/field.py +++ b/sdk/python/feast/field.py @@ -81,7 +81,14 @@ def __lt__(self, other): return self.name < other.name def __repr__(self): - return f"Field(name='{self.name}', dtype={self.dtype}, description='{self.description}' tags={self.tags})" + return ( + f"Field(\n" + f" name={self.name!r},\n" + f" dtype={self.dtype!r},\n" + f" description={self.description!r},\n" + f" tags={self.tags!r}\n" + f")" + ) def __str__(self): return f"Field(name={self.name}, dtype={self.dtype}, tags={self.tags})" From 47dc04d43f483944f97248abaccd15dba319281f Mon Sep 17 00:00:00 2001 From: Rob Howley Date: Thu, 24 Oct 2024 08:06:34 -0400 Subject: [PATCH 170/185] perf: Add init and cleanup of long lived resources (#4642) * rebase Signed-off-by: Rob Howley * offline store init doesnt make sense Signed-off-by: Rob Howley * dont init or close Signed-off-by: Rob Howley * update test to handle event loop for dynamo case Signed-off-by: Rob Howley * use run util complete Signed-off-by: Rob Howley * fix: spelling sigh Signed-off-by: Rob Howley * run integration test as async since that is default for read Signed-off-by: Rob Howley * add pytest async to ci reqs Signed-off-by: Rob Howley * be safe w cleanup in test fixture Signed-off-by: Rob Howley * be safe w cleanup in test fixture Signed-off-by: Rob Howley * update pytest ini Signed-off-by: Rob Howley * not in a finally Signed-off-by: Rob Howley * remove close Signed-off-by: Rob Howley * test client is a lifespan aware context manager Signed-off-by: Rob Howley --------- Signed-off-by: Rob Howley --- sdk/python/feast/feature_server.py | 2 + sdk/python/feast/feature_store.py | 8 + .../feast/infra/online_stores/dynamodb.py | 74 +++++-- .../feast/infra/online_stores/online_store.py | 6 + .../feast/infra/passthrough_provider.py | 6 + sdk/python/feast/infra/provider.py | 8 + sdk/python/pytest.ini | 4 + .../requirements/py3.10-ci-requirements.txt | 175 +++++++++++++--- .../requirements/py3.11-ci-requirements.txt | 194 ++++++++++++++++-- .../requirements/py3.9-ci-requirements.txt | 186 +++++++++++++---- sdk/python/tests/foo_provider.py | 6 + .../test_python_feature_server.py | 26 ++- setup.py | 1 + 13 files changed, 583 insertions(+), 113 deletions(-) diff --git a/sdk/python/feast/feature_server.py b/sdk/python/feast/feature_server.py index b4ed591b04..f485d874e1 100644 --- a/sdk/python/feast/feature_server.py +++ b/sdk/python/feast/feature_server.py @@ -100,9 +100,11 @@ def async_refresh(): @asynccontextmanager async def lifespan(app: FastAPI): + await store.initialize() async_refresh() yield stop_refresh() + await store.close() app = FastAPI(lifespan=lifespan) diff --git a/sdk/python/feast/feature_store.py b/sdk/python/feast/feature_store.py index 217303df91..033f39e1f2 100644 --- a/sdk/python/feast/feature_store.py +++ b/sdk/python/feast/feature_store.py @@ -2167,6 +2167,14 @@ def list_saved_datasets( self.project, allow_cache=allow_cache, tags=tags ) + async def initialize(self) -> None: + """Initialize long-lived clients and/or resources needed for accessing datastores""" + await self._get_provider().initialize(self.config) + + async def close(self) -> None: + """Cleanup any long-lived clients and/or resources""" + await self._get_provider().close() + def _print_materialization_log( start_date, end_date, num_feature_views: int, online_store: str diff --git a/sdk/python/feast/infra/online_stores/dynamodb.py b/sdk/python/feast/infra/online_stores/dynamodb.py index a915d2ee34..a97e81bc44 100644 --- a/sdk/python/feast/infra/online_stores/dynamodb.py +++ b/sdk/python/feast/infra/online_stores/dynamodb.py @@ -12,11 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. import asyncio +import contextlib import itertools import logging from datetime import datetime from typing import Any, Callable, Dict, List, Literal, Optional, Sequence, Tuple, Union +from aiobotocore.config import AioConfig from pydantic import StrictBool, StrictStr from feast import Entity, FeatureView, utils @@ -75,6 +77,9 @@ class DynamoDBOnlineStoreConfig(FeastConfigBaseModel): session_based_auth: bool = False """AWS session based client authentication""" + max_pool_connections: int = 10 + """Max number of connections for async Dynamodb operations""" + class DynamoDBOnlineStore(OnlineStore): """ @@ -87,7 +92,14 @@ class DynamoDBOnlineStore(OnlineStore): _dynamodb_client = None _dynamodb_resource = None - _aioboto_session = None + + async def initialize(self, config: RepoConfig): + await _get_aiodynamodb_client( + config.online_store.region, config.online_store.max_pool_connections + ) + + async def close(self): + await _aiodynamodb_close() @property def async_supported(self) -> SupportedAsyncMethods: @@ -326,15 +338,17 @@ def to_tbl_resp(raw_client_response): batches.append(batch) entity_id_batches.append(entity_id_batch) - async with self._get_aiodynamodb_client(online_config.region) as client: - response_batches = await asyncio.gather( - *[ - client.batch_get_item( - RequestItems=entity_id_batch, - ) - for entity_id_batch in entity_id_batches - ] - ) + client = await _get_aiodynamodb_client( + online_config.region, online_config.max_pool_connections + ) + response_batches = await asyncio.gather( + *[ + client.batch_get_item( + RequestItems=entity_id_batch, + ) + for entity_id_batch in entity_id_batches + ] + ) result_batches = [] for batch, response in zip(batches, response_batches): @@ -349,14 +363,6 @@ def to_tbl_resp(raw_client_response): return list(itertools.chain(*result_batches)) - def _get_aioboto_session(self): - if self._aioboto_session is None: - self._aioboto_session = session.get_session() - return self._aioboto_session - - def _get_aiodynamodb_client(self, region: str): - return self._get_aioboto_session().create_client("dynamodb", region_name=region) - def _get_dynamodb_client( self, region: str, @@ -489,6 +495,38 @@ def _to_client_batch_get_payload(online_config, table_name, batch): } +_aioboto_session = None +_aioboto_client = None + + +def _get_aioboto_session(): + global _aioboto_session + if _aioboto_session is None: + logger.debug("initializing the aiobotocore session") + _aioboto_session = session.get_session() + return _aioboto_session + + +async def _get_aiodynamodb_client(region: str, max_pool_connections: int): + global _aioboto_client + if _aioboto_client is None: + logger.debug("initializing the aiobotocore dynamodb client") + client_context = _get_aioboto_session().create_client( + "dynamodb", + region_name=region, + config=AioConfig(max_pool_connections=max_pool_connections), + ) + context_stack = contextlib.AsyncExitStack() + _aioboto_client = await context_stack.enter_async_context(client_context) + return _aioboto_client + + +async def _aiodynamodb_close(): + global _aioboto_client + if _aioboto_client: + await _aioboto_client.close() + + def _initialize_dynamodb_client( region: str, endpoint_url: Optional[str] = None, diff --git a/sdk/python/feast/infra/online_stores/online_store.py b/sdk/python/feast/infra/online_stores/online_store.py index 15dd843ba8..789885f82b 100644 --- a/sdk/python/feast/infra/online_stores/online_store.py +++ b/sdk/python/feast/infra/online_stores/online_store.py @@ -422,3 +422,9 @@ def retrieve_online_documents( raise NotImplementedError( f"Online store {self.__class__.__name__} does not support online retrieval" ) + + async def initialize(self, config: RepoConfig) -> None: + pass + + async def close(self) -> None: + pass diff --git a/sdk/python/feast/infra/passthrough_provider.py b/sdk/python/feast/infra/passthrough_provider.py index 9482b808a9..a1e9ef82ad 100644 --- a/sdk/python/feast/infra/passthrough_provider.py +++ b/sdk/python/feast/infra/passthrough_provider.py @@ -518,3 +518,9 @@ def get_table_column_names_and_types_from_data_source( return self.offline_store.get_table_column_names_and_types_from_data_source( config=config, data_source=data_source ) + + async def initialize(self, config: RepoConfig) -> None: + await self.online_store.initialize(config) + + async def close(self) -> None: + await self.online_store.close() diff --git a/sdk/python/feast/infra/provider.py b/sdk/python/feast/infra/provider.py index 47b7c65ef0..8351f389ad 100644 --- a/sdk/python/feast/infra/provider.py +++ b/sdk/python/feast/infra/provider.py @@ -476,6 +476,14 @@ def get_table_column_names_and_types_from_data_source( """ pass + @abstractmethod + async def initialize(self, config: RepoConfig) -> None: + pass + + @abstractmethod + async def close(self) -> None: + pass + def get_provider(config: RepoConfig) -> Provider: if "." not in config.provider: diff --git a/sdk/python/pytest.ini b/sdk/python/pytest.ini index d87e4c07cb..a073676760 100644 --- a/sdk/python/pytest.ini +++ b/sdk/python/pytest.ini @@ -1,4 +1,6 @@ [pytest] +asyncio_mode = auto + markers = universal_offline_stores: mark a test as using all offline stores. universal_online_stores: mark a test as using all online stores. @@ -7,6 +9,8 @@ env = IS_TEST=True filterwarnings = + error::_pytest.warning_types.PytestConfigWarning + error::_pytest.warning_types.PytestUnhandledCoroutineWarning ignore::DeprecationWarning:pyspark.sql.pandas.*: ignore::DeprecationWarning:pyspark.sql.connect.*: ignore::DeprecationWarning:httpx.*: diff --git a/sdk/python/requirements/py3.10-ci-requirements.txt b/sdk/python/requirements/py3.10-ci-requirements.txt index 57a21cd6d9..7109a6feae 100644 --- a/sdk/python/requirements/py3.10-ci-requirements.txt +++ b/sdk/python/requirements/py3.10-ci-requirements.txt @@ -1,6 +1,7 @@ # This file was autogenerated by uv via the following command: -# uv pip compile -p 3.10 --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.10-ci-requirements.txt +# uv pip compile --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.10-ci-requirements.txt aiobotocore==2.15.1 + # via feast (setup.py) aiohappyeyeballs==2.4.0 # via aiohttp aiohttp==3.10.5 @@ -21,6 +22,8 @@ anyio==4.5.0 # jupyter-server # starlette # watchfiles +appnope==0.1.4 + # via ipykernel argon2-cffi==23.1.0 # via jupyter-server argon2-cffi-bindings==21.2.0 @@ -30,6 +33,7 @@ arrow==1.3.0 asn1crypto==1.5.1 # via snowflake-connector-python assertpy==1.1 + # via feast (setup.py) asttokens==2.4.1 # via stack-data async-lru==2.0.4 @@ -52,7 +56,9 @@ azure-core==1.31.0 # azure-identity # azure-storage-blob azure-identity==1.18.0 + # via feast (setup.py) azure-storage-blob==12.23.0 + # via feast (setup.py) babel==2.16.0 # via # jupyterlab-server @@ -60,10 +66,13 @@ babel==2.16.0 beautifulsoup4==4.12.3 # via nbconvert bigtree==0.21.1 + # via feast (setup.py) bleach==6.1.0 # via nbconvert boto3==1.35.23 - # via moto + # via + # feast (setup.py) + # moto botocore==1.35.23 # via # aiobotocore @@ -72,11 +81,13 @@ botocore==1.35.23 # s3transfer build==1.2.2 # via + # feast (setup.py) # pip-tools # singlestoredb cachetools==5.5.0 # via google-auth cassandra-driver==3.29.2 + # via feast (setup.py) certifi==2024.8.30 # via # elastic-transport @@ -99,6 +110,7 @@ charset-normalizer==3.3.2 # snowflake-connector-python click==8.1.7 # via + # feast (setup.py) # dask # geomet # great-expectations @@ -107,15 +119,18 @@ click==8.1.7 cloudpickle==3.0.0 # via dask colorama==0.4.6 - # via great-expectations + # via + # feast (setup.py) + # great-expectations comm==0.2.2 # via # ipykernel # ipywidgets coverage[toml]==7.6.1 # via pytest-cov -cryptography==43.0.1 +cryptography==42.0.8 # via + # feast (setup.py) # azure-identity # azure-storage-blob # great-expectations @@ -130,7 +145,9 @@ cryptography==43.0.1 cython==3.0.11 # via thriftpy2 dask[dataframe]==2024.9.0 - # via dask-expr + # via + # feast (setup.py) + # dask-expr dask-expr==1.1.14 # via dask db-dtypes==1.3.0 @@ -142,9 +159,11 @@ decorator==5.1.1 defusedxml==0.7.1 # via nbconvert deltalake==0.20.0 + # via feast (setup.py) deprecation==2.1.0 # via python-keycloak dill==0.3.8 + # via feast (setup.py) distlib==0.3.8 # via virtualenv docker==7.1.0 @@ -156,6 +175,7 @@ duckdb==1.1.0 elastic-transport==8.15.0 # via elasticsearch elasticsearch==8.15.1 + # via feast (setup.py) entrypoints==0.4 # via altair exceptiongroup==1.2.2 @@ -167,7 +187,10 @@ execnet==2.1.1 # via pytest-xdist executing==2.1.0 # via stack-data +faiss-cpu==1.9.0 + # via feast (setup.py) fastapi==0.115.2 + # via feast (setup.py) fastjsonschema==2.20.0 # via nbformat filelock==3.16.1 @@ -181,11 +204,14 @@ frozenlist==1.4.1 # aiohttp # aiosignal fsspec==2024.9.0 - # via dask + # via + # feast (setup.py) + # dask geomet==0.2.1.post1 # via cassandra-driver google-api-core[grpc]==2.20.0 # via + # feast (setup.py) # google-cloud-bigquery # google-cloud-bigquery-storage # google-cloud-bigtable @@ -203,8 +229,11 @@ google-auth==2.35.0 # google-cloud-storage # kubernetes google-cloud-bigquery[pandas]==3.25.0 + # via feast (setup.py) google-cloud-bigquery-storage==2.26.0 + # via feast (setup.py) google-cloud-bigtable==2.26.0 + # via feast (setup.py) google-cloud-core==2.4.1 # via # google-cloud-bigquery @@ -212,7 +241,9 @@ google-cloud-core==2.4.1 # google-cloud-datastore # google-cloud-storage google-cloud-datastore==2.20.1 + # via feast (setup.py) google-cloud-storage==2.18.2 + # via feast (setup.py) google-crc32c==1.6.0 # via # google-cloud-storage @@ -223,16 +254,17 @@ google-resumable-media==2.7.2 # google-cloud-storage googleapis-common-protos[grpc]==1.65.0 # via + # feast (setup.py) # google-api-core # grpc-google-iam-v1 # grpcio-status great-expectations==0.18.21 -greenlet==3.1.0 - # via sqlalchemy + # via feast (setup.py) grpc-google-iam-v1==0.13.1 # via google-cloud-bigtable grpcio==1.66.1 # via + # feast (setup.py) # google-api-core # googleapis-common-protos # grpc-google-iam-v1 @@ -242,30 +274,44 @@ grpcio==1.66.1 # grpcio-testing # grpcio-tools grpcio-health-checking==1.62.3 + # via feast (setup.py) grpcio-reflection==1.62.3 + # via feast (setup.py) grpcio-status==1.62.3 # via google-api-core grpcio-testing==1.62.3 + # via feast (setup.py) grpcio-tools==1.62.3 + # via feast (setup.py) gunicorn==23.0.0 + # via + # feast (setup.py) + # uvicorn-worker h11==0.14.0 # via # httpcore # uvicorn happybase==1.2.0 + # via feast (setup.py) hazelcast-python-client==5.5.0 + # via feast (setup.py) hiredis==2.4.0 + # via feast (setup.py) httpcore==1.0.5 # via httpx httptools==0.6.1 # via uvicorn httpx==0.27.2 # via + # feast (setup.py) # jupyterlab # python-keycloak ibis-framework[duckdb]==9.5.0 - # via ibis-substrait + # via + # feast (setup.py) + # ibis-substrait ibis-substrait==4.0.1 + # via feast (setup.py) identify==2.6.1 # via pre-commit idna==3.10 @@ -279,9 +325,7 @@ idna==3.10 imagesize==1.4.1 # via sphinx importlib-metadata==8.5.0 - # via - # build - # dask + # via dask iniconfig==2.0.0 # via pytest ipykernel==6.29.5 @@ -301,6 +345,7 @@ jedi==0.19.1 # via ipython jinja2==3.1.4 # via + # feast (setup.py) # altair # great-expectations # jupyter-server @@ -323,6 +368,7 @@ jsonpointer==3.0.0 # jsonschema jsonschema[format-nongpl]==4.23.0 # via + # feast (setup.py) # altair # great-expectations # jupyter-events @@ -370,6 +416,7 @@ jupyterlab-widgets==3.0.13 jwcrypto==1.5.6 # via python-keycloak kubernetes==20.13.0 + # via feast (setup.py) locket==1.0.0 # via partd makefun==1.15.4 @@ -390,13 +437,17 @@ matplotlib-inline==0.1.7 mdurl==0.1.2 # via markdown-it-py minio==7.1.0 + # via feast (setup.py) mistune==3.0.2 # via # great-expectations # nbconvert mmh3==5.0.0 + # via feast (setup.py) mock==2.0.0 + # via feast (setup.py) moto==4.2.14 + # via feast (setup.py) msal==1.31.0 # via # azure-identity @@ -408,10 +459,13 @@ multidict==6.1.0 # aiohttp # yarl mypy==1.11.2 - # via sqlalchemy + # via + # feast (setup.py) + # sqlalchemy mypy-extensions==1.0.0 # via mypy mypy-protobuf==3.3.0 + # via feast (setup.py) nbclient==0.10.0 # via nbconvert nbconvert==7.16.4 @@ -434,9 +488,11 @@ notebook-shim==0.2.4 # notebook numpy==1.26.4 # via + # feast (setup.py) # altair # dask # db-dtypes + # faiss-cpu # great-expectations # ibis-framework # pandas @@ -452,6 +508,7 @@ packaging==24.1 # dask # db-dtypes # deprecation + # faiss-cpu # google-cloud-bigquery # great-expectations # gunicorn @@ -468,6 +525,7 @@ packaging==24.1 # sphinx pandas==2.2.2 # via + # feast (setup.py) # altair # dask # dask-expr @@ -493,6 +551,7 @@ pexpect==4.9.0 pip==24.2 # via pip-tools pip-tools==7.4.1 + # via feast (setup.py) platformdirs==3.11.0 # via # jupyter-core @@ -505,8 +564,11 @@ ply==3.11 portalocker==2.10.1 # via msal-extensions pre-commit==3.3.1 + # via feast (setup.py) prometheus-client==0.20.0 - # via jupyter-server + # via + # feast (setup.py) + # jupyter-server prompt-toolkit==3.0.47 # via ipython proto-plus==1.24.0 @@ -517,6 +579,7 @@ proto-plus==1.24.0 # google-cloud-datastore protobuf==4.25.5 # via + # feast (setup.py) # google-api-core # google-cloud-bigquery-storage # google-cloud-bigtable @@ -532,9 +595,12 @@ protobuf==4.25.5 # proto-plus # substrait psutil==5.9.0 - # via ipykernel -psycopg[binary, pool]==3.2.2 -psycopg-binary==3.2.2 + # via + # feast (setup.py) + # ipykernel +psycopg[binary, pool]==3.1.19 + # via feast (setup.py) +psycopg-binary==3.1.19 # via psycopg psycopg-pool==3.2.3 # via psycopg @@ -545,12 +611,14 @@ ptyprocess==0.7.0 pure-eval==0.2.3 # via stack-data py==1.11.0 + # via feast (setup.py) py-cpuinfo==9.0.0 # via pytest-benchmark py4j==0.10.9.7 # via pyspark pyarrow==17.0.0 # via + # feast (setup.py) # dask-expr # db-dtypes # deltalake @@ -566,28 +634,35 @@ pyasn1==0.6.1 pyasn1-modules==0.4.1 # via google-auth pybindgen==0.22.1 + # via feast (setup.py) pycparser==2.22 # via cffi pydantic==2.9.2 # via + # feast (setup.py) # fastapi # great-expectations pydantic-core==2.23.4 # via pydantic pygments==2.18.0 # via + # feast (setup.py) # ipython # nbconvert # rich # sphinx pyjwt[crypto]==2.9.0 # via + # feast (setup.py) # msal # singlestoredb # snowflake-connector-python pymssql==2.3.1 + # via feast (setup.py) pymysql==1.1.1 + # via feast (setup.py) pyodbc==5.1.0 + # via feast (setup.py) pyopenssl==24.2.1 # via snowflake-connector-python pyparsing==3.1.4 @@ -597,8 +672,11 @@ pyproject-hooks==1.1.0 # build # pip-tools pyspark==3.5.2 + # via feast (setup.py) pytest==7.4.4 # via + # feast (setup.py) + # pytest-asyncio # pytest-benchmark # pytest-cov # pytest-env @@ -607,14 +685,24 @@ pytest==7.4.4 # pytest-ordering # pytest-timeout # pytest-xdist +pytest-asyncio==0.23.8 + # via feast (setup.py) pytest-benchmark==3.4.1 + # via feast (setup.py) pytest-cov==5.0.0 + # via feast (setup.py) pytest-env==1.1.3 + # via feast (setup.py) pytest-lazy-fixture==0.6.3 + # via feast (setup.py) pytest-mock==1.10.4 + # via feast (setup.py) pytest-ordering==0.6 + # via feast (setup.py) pytest-timeout==1.4.2 + # via feast (setup.py) pytest-xdist==3.6.1 + # via feast (setup.py) python-dateutil==2.9.0.post0 # via # arrow @@ -632,6 +720,7 @@ python-dotenv==1.0.1 python-json-logger==2.0.7 # via jupyter-events python-keycloak==4.2.2 + # via feast (setup.py) pytz==2024.2 # via # great-expectations @@ -641,6 +730,7 @@ pytz==2024.2 # trino pyyaml==6.0.2 # via + # feast (setup.py) # dask # ibis-substrait # jupyter-events @@ -654,15 +744,19 @@ pyzmq==26.2.0 # jupyter-client # jupyter-server redis==4.6.0 + # via feast (setup.py) referencing==0.35.1 # via # jsonschema # jsonschema-specifications # jupyter-events regex==2024.9.11 - # via parsimonious + # via + # feast (setup.py) + # parsimonious requests==2.32.3 # via + # feast (setup.py) # azure-core # docker # google-api-core @@ -708,6 +802,7 @@ ruamel-yaml==0.17.40 ruamel-yaml-clib==0.2.8 # via ruamel-yaml ruff==0.6.6 + # via feast (setup.py) s3transfer==0.10.2 # via boto3 scipy==1.14.1 @@ -722,6 +817,7 @@ setuptools==75.1.0 # pip-tools # singlestoredb singlestoredb==1.6.3 + # via feast (setup.py) six==1.16.0 # via # asttokens @@ -742,11 +838,13 @@ sniffio==1.3.1 snowballstemmer==2.2.0 # via sphinx snowflake-connector-python[pandas]==3.12.2 + # via feast (setup.py) sortedcontainers==2.4.0 # via snowflake-connector-python soupsieve==2.6 # via beautifulsoup4 sphinx==6.2.1 + # via feast (setup.py) sphinxcontrib-applehelp==2.0.0 # via sphinx sphinxcontrib-devhelp==2.0.0 @@ -760,9 +858,11 @@ sphinxcontrib-qthelp==2.0.0 sphinxcontrib-serializinghtml==2.0.0 # via sphinx sqlalchemy[mypy]==2.0.35 + # via feast (setup.py) sqlglot==25.20.1 # via ibis-framework sqlite-vec==0.1.1 + # via feast (setup.py) sqlparams==6.1.0 # via singlestoredb stack-data==0.6.3 @@ -772,17 +872,21 @@ starlette==0.40.0 substrait==0.23.0 # via ibis-substrait tabulate==0.9.0 + # via feast (setup.py) tenacity==8.5.0 + # via feast (setup.py) terminado==0.18.1 # via # jupyter-server # jupyter-server-terminals testcontainers==4.4.0 + # via feast (setup.py) thriftpy2==0.5.2 # via happybase tinycss2==1.3.0 # via nbconvert toml==0.10.2 + # via feast (setup.py) tomli==2.0.1 # via # build @@ -810,7 +914,9 @@ tornado==6.4.1 # notebook # terminado tqdm==4.66.5 - # via great-expectations + # via + # feast (setup.py) + # great-expectations traitlets==5.14.3 # via # comm @@ -827,23 +933,37 @@ traitlets==5.14.3 # nbconvert # nbformat trino==0.329.0 + # via feast (setup.py) typeguard==4.3.0 + # via feast (setup.py) types-cffi==1.16.0.20240331 # via types-pyopenssl types-protobuf==3.19.22 - # via mypy-protobuf + # via + # feast (setup.py) + # mypy-protobuf types-pymysql==1.1.0.20240524 + # via feast (setup.py) types-pyopenssl==24.1.0.20240722 # via types-redis types-python-dateutil==2.9.0.20240906 - # via arrow + # via + # feast (setup.py) + # arrow types-pytz==2024.2.0.20240913 + # via feast (setup.py) types-pyyaml==6.0.12.20240917 + # via feast (setup.py) types-redis==4.6.0.20240903 + # via feast (setup.py) types-requests==2.30.0.0 + # via feast (setup.py) types-setuptools==75.1.0.20240917 - # via types-cffi + # via + # feast (setup.py) + # types-cffi types-tabulate==0.9.0.20240106 + # via feast (setup.py) types-urllib3==1.26.25.14 # via types-requests typing-extensions==4.12.2 @@ -879,6 +999,7 @@ uri-template==1.3.0 # via jsonschema urllib3==2.2.3 # via + # feast (setup.py) # botocore # docker # elastic-transport @@ -889,11 +1010,17 @@ urllib3==2.2.3 # responses # testcontainers uvicorn[standard]==0.30.6 -uvicorn-worker + # via + # feast (setup.py) + # uvicorn-worker +uvicorn-worker==0.2.0 + # via feast (setup.py) uvloop==0.20.0 # via uvicorn virtualenv==20.23.0 - # via pre-commit + # via + # feast (setup.py) + # pre-commit watchfiles==0.24.0 # via uvicorn wcwidth==0.2.13 diff --git a/sdk/python/requirements/py3.11-ci-requirements.txt b/sdk/python/requirements/py3.11-ci-requirements.txt index ed6dc239d3..eb8bbc280b 100644 --- a/sdk/python/requirements/py3.11-ci-requirements.txt +++ b/sdk/python/requirements/py3.11-ci-requirements.txt @@ -1,6 +1,7 @@ # This file was autogenerated by uv via the following command: -# uv pip compile -p 3.11 --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.11-ci-requirements.txt +# uv pip compile --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.11-ci-requirements.txt aiobotocore==2.15.1 + # via feast (setup.py) aiohappyeyeballs==2.4.0 # via aiohttp aiohttp==3.10.5 @@ -21,6 +22,8 @@ anyio==4.5.0 # jupyter-server # starlette # watchfiles +appnope==0.1.4 + # via ipykernel argon2-cffi==23.1.0 # via jupyter-server argon2-cffi-bindings==21.2.0 @@ -30,6 +33,7 @@ arrow==1.3.0 asn1crypto==1.5.1 # via snowflake-connector-python assertpy==1.1 + # via feast (setup.py) asttokens==2.4.1 # via stack-data async-lru==2.0.4 @@ -37,7 +41,9 @@ async-lru==2.0.4 async-property==0.2.2 # via python-keycloak async-timeout==4.0.3 - # via redis + # via + # aiohttp + # redis atpublic==5.0 # via ibis-framework attrs==24.2.0 @@ -50,7 +56,9 @@ azure-core==1.31.0 # azure-identity # azure-storage-blob azure-identity==1.18.0 + # via feast (setup.py) azure-storage-blob==12.23.0 + # via feast (setup.py) babel==2.16.0 # via # jupyterlab-server @@ -58,10 +66,13 @@ babel==2.16.0 beautifulsoup4==4.12.3 # via nbconvert bigtree==0.21.1 + # via feast (setup.py) bleach==6.1.0 # via nbconvert boto3==1.35.23 - # via moto + # via + # feast (setup.py) + # moto botocore==1.35.23 # via # aiobotocore @@ -70,11 +81,13 @@ botocore==1.35.23 # s3transfer build==1.2.2 # via + # feast (setup.py) # pip-tools # singlestoredb cachetools==5.5.0 # via google-auth cassandra-driver==3.29.2 + # via feast (setup.py) certifi==2024.8.30 # via # elastic-transport @@ -97,6 +110,7 @@ charset-normalizer==3.3.2 # snowflake-connector-python click==8.1.7 # via + # feast (setup.py) # dask # geomet # great-expectations @@ -105,15 +119,18 @@ click==8.1.7 cloudpickle==3.0.0 # via dask colorama==0.4.6 - # via great-expectations + # via + # feast (setup.py) + # great-expectations comm==0.2.2 # via # ipykernel # ipywidgets coverage[toml]==7.6.1 # via pytest-cov -cryptography==43.0.1 +cryptography==42.0.8 # via + # feast (setup.py) # azure-identity # azure-storage-blob # great-expectations @@ -128,7 +145,9 @@ cryptography==43.0.1 cython==3.0.11 # via thriftpy2 dask[dataframe]==2024.9.0 - # via dask-expr + # via + # feast (setup.py) + # dask-expr dask-expr==1.1.14 # via dask db-dtypes==1.3.0 @@ -140,9 +159,11 @@ decorator==5.1.1 defusedxml==0.7.1 # via nbconvert deltalake==0.20.0 + # via feast (setup.py) deprecation==2.1.0 # via python-keycloak dill==0.3.8 + # via feast (setup.py) distlib==0.3.8 # via virtualenv docker==7.1.0 @@ -154,13 +175,22 @@ duckdb==1.1.0 elastic-transport==8.15.0 # via elasticsearch elasticsearch==8.15.1 + # via feast (setup.py) entrypoints==0.4 # via altair +exceptiongroup==1.2.2 + # via + # anyio + # ipython + # pytest execnet==2.1.1 # via pytest-xdist executing==2.1.0 # via stack-data +faiss-cpu==1.9.0 + # via feast (setup.py) fastapi==0.115.2 + # via feast (setup.py) fastjsonschema==2.20.0 # via nbformat filelock==3.16.1 @@ -174,11 +204,14 @@ frozenlist==1.4.1 # aiohttp # aiosignal fsspec==2024.9.0 - # via dask + # via + # feast (setup.py) + # dask geomet==0.2.1.post1 # via cassandra-driver google-api-core[grpc]==2.20.0 # via + # feast (setup.py) # google-cloud-bigquery # google-cloud-bigquery-storage # google-cloud-bigtable @@ -196,8 +229,11 @@ google-auth==2.35.0 # google-cloud-storage # kubernetes google-cloud-bigquery[pandas]==3.25.0 + # via feast (setup.py) google-cloud-bigquery-storage==2.26.0 + # via feast (setup.py) google-cloud-bigtable==2.26.0 + # via feast (setup.py) google-cloud-core==2.4.1 # via # google-cloud-bigquery @@ -205,7 +241,9 @@ google-cloud-core==2.4.1 # google-cloud-datastore # google-cloud-storage google-cloud-datastore==2.20.1 + # via feast (setup.py) google-cloud-storage==2.18.2 + # via feast (setup.py) google-crc32c==1.6.0 # via # google-cloud-storage @@ -216,16 +254,17 @@ google-resumable-media==2.7.2 # google-cloud-storage googleapis-common-protos[grpc]==1.65.0 # via + # feast (setup.py) # google-api-core # grpc-google-iam-v1 # grpcio-status great-expectations==0.18.21 -greenlet==3.1.0 - # via sqlalchemy + # via feast (setup.py) grpc-google-iam-v1==0.13.1 # via google-cloud-bigtable grpcio==1.66.1 # via + # feast (setup.py) # google-api-core # googleapis-common-protos # grpc-google-iam-v1 @@ -235,30 +274,44 @@ grpcio==1.66.1 # grpcio-testing # grpcio-tools grpcio-health-checking==1.62.3 + # via feast (setup.py) grpcio-reflection==1.62.3 + # via feast (setup.py) grpcio-status==1.62.3 # via google-api-core grpcio-testing==1.62.3 + # via feast (setup.py) grpcio-tools==1.62.3 + # via feast (setup.py) gunicorn==23.0.0 + # via + # feast (setup.py) + # uvicorn-worker h11==0.14.0 # via # httpcore # uvicorn happybase==1.2.0 + # via feast (setup.py) hazelcast-python-client==5.5.0 + # via feast (setup.py) hiredis==2.4.0 + # via feast (setup.py) httpcore==1.0.5 # via httpx httptools==0.6.1 # via uvicorn httpx==0.27.2 # via + # feast (setup.py) # jupyterlab # python-keycloak ibis-framework[duckdb]==9.5.0 - # via ibis-substrait + # via + # feast (setup.py) + # ibis-substrait ibis-substrait==4.0.1 + # via feast (setup.py) identify==2.6.1 # via pre-commit idna==3.10 @@ -292,6 +345,7 @@ jedi==0.19.1 # via ipython jinja2==3.1.4 # via + # feast (setup.py) # altair # great-expectations # jupyter-server @@ -314,6 +368,7 @@ jsonpointer==3.0.0 # jsonschema jsonschema[format-nongpl]==4.23.0 # via + # feast (setup.py) # altair # great-expectations # jupyter-events @@ -361,6 +416,7 @@ jupyterlab-widgets==3.0.13 jwcrypto==1.5.6 # via python-keycloak kubernetes==20.13.0 + # via feast (setup.py) locket==1.0.0 # via partd makefun==1.15.4 @@ -381,13 +437,17 @@ matplotlib-inline==0.1.7 mdurl==0.1.2 # via markdown-it-py minio==7.1.0 + # via feast (setup.py) mistune==3.0.2 # via # great-expectations # nbconvert mmh3==5.0.0 + # via feast (setup.py) mock==2.0.0 + # via feast (setup.py) moto==4.2.14 + # via feast (setup.py) msal==1.31.0 # via # azure-identity @@ -399,10 +459,13 @@ multidict==6.1.0 # aiohttp # yarl mypy==1.11.2 - # via sqlalchemy + # via + # feast (setup.py) + # sqlalchemy mypy-extensions==1.0.0 # via mypy mypy-protobuf==3.3.0 + # via feast (setup.py) nbclient==0.10.0 # via nbconvert nbconvert==7.16.4 @@ -425,9 +488,11 @@ notebook-shim==0.2.4 # notebook numpy==1.26.4 # via + # feast (setup.py) # altair # dask # db-dtypes + # faiss-cpu # great-expectations # ibis-framework # pandas @@ -443,6 +508,7 @@ packaging==24.1 # dask # db-dtypes # deprecation + # faiss-cpu # google-cloud-bigquery # great-expectations # gunicorn @@ -459,6 +525,7 @@ packaging==24.1 # sphinx pandas==2.2.2 # via + # feast (setup.py) # altair # dask # dask-expr @@ -484,6 +551,7 @@ pexpect==4.9.0 pip==24.2 # via pip-tools pip-tools==7.4.1 + # via feast (setup.py) platformdirs==3.11.0 # via # jupyter-core @@ -496,8 +564,11 @@ ply==3.11 portalocker==2.10.1 # via msal-extensions pre-commit==3.3.1 + # via feast (setup.py) prometheus-client==0.20.0 - # via jupyter-server + # via + # feast (setup.py) + # jupyter-server prompt-toolkit==3.0.47 # via ipython proto-plus==1.24.0 @@ -508,6 +579,7 @@ proto-plus==1.24.0 # google-cloud-datastore protobuf==4.25.5 # via + # feast (setup.py) # google-api-core # google-cloud-bigquery-storage # google-cloud-bigtable @@ -523,9 +595,12 @@ protobuf==4.25.5 # proto-plus # substrait psutil==5.9.0 - # via ipykernel -psycopg[binary, pool]==3.2.2 -psycopg-binary==3.2.2 + # via + # feast (setup.py) + # ipykernel +psycopg[binary, pool]==3.1.19 + # via feast (setup.py) +psycopg-binary==3.1.19 # via psycopg psycopg-pool==3.2.3 # via psycopg @@ -536,12 +611,14 @@ ptyprocess==0.7.0 pure-eval==0.2.3 # via stack-data py==1.11.0 + # via feast (setup.py) py-cpuinfo==9.0.0 # via pytest-benchmark py4j==0.10.9.7 # via pyspark pyarrow==17.0.0 # via + # feast (setup.py) # dask-expr # db-dtypes # deltalake @@ -557,28 +634,35 @@ pyasn1==0.6.1 pyasn1-modules==0.4.1 # via google-auth pybindgen==0.22.1 + # via feast (setup.py) pycparser==2.22 # via cffi pydantic==2.9.2 # via + # feast (setup.py) # fastapi # great-expectations pydantic-core==2.23.4 # via pydantic pygments==2.18.0 # via + # feast (setup.py) # ipython # nbconvert # rich # sphinx pyjwt[crypto]==2.9.0 # via + # feast (setup.py) # msal # singlestoredb # snowflake-connector-python pymssql==2.3.1 + # via feast (setup.py) pymysql==1.1.1 + # via feast (setup.py) pyodbc==5.1.0 + # via feast (setup.py) pyopenssl==24.2.1 # via snowflake-connector-python pyparsing==3.1.4 @@ -588,8 +672,11 @@ pyproject-hooks==1.1.0 # build # pip-tools pyspark==3.5.2 + # via feast (setup.py) pytest==7.4.4 # via + # feast (setup.py) + # pytest-asyncio # pytest-benchmark # pytest-cov # pytest-env @@ -598,14 +685,24 @@ pytest==7.4.4 # pytest-ordering # pytest-timeout # pytest-xdist +pytest-asyncio==0.23.8 + # via feast (setup.py) pytest-benchmark==3.4.1 + # via feast (setup.py) pytest-cov==5.0.0 + # via feast (setup.py) pytest-env==1.1.3 + # via feast (setup.py) pytest-lazy-fixture==0.6.3 + # via feast (setup.py) pytest-mock==1.10.4 + # via feast (setup.py) pytest-ordering==0.6 + # via feast (setup.py) pytest-timeout==1.4.2 + # via feast (setup.py) pytest-xdist==3.6.1 + # via feast (setup.py) python-dateutil==2.9.0.post0 # via # arrow @@ -623,6 +720,7 @@ python-dotenv==1.0.1 python-json-logger==2.0.7 # via jupyter-events python-keycloak==4.2.2 + # via feast (setup.py) pytz==2024.2 # via # great-expectations @@ -632,6 +730,7 @@ pytz==2024.2 # trino pyyaml==6.0.2 # via + # feast (setup.py) # dask # ibis-substrait # jupyter-events @@ -645,15 +744,19 @@ pyzmq==26.2.0 # jupyter-client # jupyter-server redis==4.6.0 + # via feast (setup.py) referencing==0.35.1 # via # jsonschema # jsonschema-specifications # jupyter-events regex==2024.9.11 - # via parsimonious + # via + # feast (setup.py) + # parsimonious requests==2.32.3 # via + # feast (setup.py) # azure-core # docker # google-api-core @@ -699,6 +802,7 @@ ruamel-yaml==0.17.40 ruamel-yaml-clib==0.2.8 # via ruamel-yaml ruff==0.6.6 + # via feast (setup.py) s3transfer==0.10.2 # via boto3 scipy==1.14.1 @@ -713,6 +817,7 @@ setuptools==75.1.0 # pip-tools # singlestoredb singlestoredb==1.6.3 + # via feast (setup.py) six==1.16.0 # via # asttokens @@ -733,11 +838,13 @@ sniffio==1.3.1 snowballstemmer==2.2.0 # via sphinx snowflake-connector-python[pandas]==3.12.2 + # via feast (setup.py) sortedcontainers==2.4.0 # via snowflake-connector-python soupsieve==2.6 # via beautifulsoup4 sphinx==6.2.1 + # via feast (setup.py) sphinxcontrib-applehelp==2.0.0 # via sphinx sphinxcontrib-devhelp==2.0.0 @@ -751,9 +858,11 @@ sphinxcontrib-qthelp==2.0.0 sphinxcontrib-serializinghtml==2.0.0 # via sphinx sqlalchemy[mypy]==2.0.35 + # via feast (setup.py) sqlglot==25.20.1 # via ibis-framework sqlite-vec==0.1.1 + # via feast (setup.py) sqlparams==6.1.0 # via singlestoredb stack-data==0.6.3 @@ -763,17 +872,31 @@ starlette==0.40.0 substrait==0.23.0 # via ibis-substrait tabulate==0.9.0 + # via feast (setup.py) tenacity==8.5.0 + # via feast (setup.py) terminado==0.18.1 # via # jupyter-server # jupyter-server-terminals testcontainers==4.4.0 + # via feast (setup.py) thriftpy2==0.5.2 # via happybase tinycss2==1.3.0 # via nbconvert toml==0.10.2 + # via feast (setup.py) +tomli==2.0.2 + # via + # build + # coverage + # jupyterlab + # mypy + # pip-tools + # pytest + # pytest-env + # singlestoredb tomlkit==0.13.2 # via snowflake-connector-python toolz==0.12.1 @@ -791,7 +914,9 @@ tornado==6.4.1 # notebook # terminado tqdm==4.66.5 - # via great-expectations + # via + # feast (setup.py) + # great-expectations traitlets==5.14.3 # via # comm @@ -808,27 +933,43 @@ traitlets==5.14.3 # nbconvert # nbformat trino==0.329.0 + # via feast (setup.py) typeguard==4.3.0 + # via feast (setup.py) types-cffi==1.16.0.20240331 # via types-pyopenssl types-protobuf==3.19.22 - # via mypy-protobuf + # via + # feast (setup.py) + # mypy-protobuf types-pymysql==1.1.0.20240524 + # via feast (setup.py) types-pyopenssl==24.1.0.20240722 # via types-redis types-python-dateutil==2.9.0.20240906 - # via arrow + # via + # feast (setup.py) + # arrow types-pytz==2024.2.0.20240913 + # via feast (setup.py) types-pyyaml==6.0.12.20240917 + # via feast (setup.py) types-redis==4.6.0.20240903 + # via feast (setup.py) types-requests==2.30.0.0 + # via feast (setup.py) types-setuptools==75.1.0.20240917 - # via types-cffi + # via + # feast (setup.py) + # types-cffi types-tabulate==0.9.0.20240106 + # via feast (setup.py) types-urllib3==1.26.25.14 # via types-requests typing-extensions==4.12.2 # via + # anyio + # async-lru # azure-core # azure-identity # azure-storage-blob @@ -837,6 +978,7 @@ typing-extensions==4.12.2 # ibis-framework # ipython # jwcrypto + # multidict # mypy # psycopg # psycopg-pool @@ -846,6 +988,7 @@ typing-extensions==4.12.2 # sqlalchemy # testcontainers # typeguard + # uvicorn tzdata==2024.1 # via pandas tzlocal==5.2 @@ -856,6 +999,7 @@ uri-template==1.3.0 # via jsonschema urllib3==2.2.3 # via + # feast (setup.py) # botocore # docker # elastic-transport @@ -866,11 +1010,17 @@ urllib3==2.2.3 # responses # testcontainers uvicorn[standard]==0.30.6 -uvicorn-worker + # via + # feast (setup.py) + # uvicorn-worker +uvicorn-worker==0.2.0 + # via feast (setup.py) uvloop==0.20.0 # via uvicorn virtualenv==20.23.0 - # via pre-commit + # via + # feast (setup.py) + # pre-commit watchfiles==0.24.0 # via uvicorn wcwidth==0.2.13 diff --git a/sdk/python/requirements/py3.9-ci-requirements.txt b/sdk/python/requirements/py3.9-ci-requirements.txt index e7d6686b4d..2a380d6fc3 100644 --- a/sdk/python/requirements/py3.9-ci-requirements.txt +++ b/sdk/python/requirements/py3.9-ci-requirements.txt @@ -1,6 +1,7 @@ # This file was autogenerated by uv via the following command: -# uv pip compile -p 3.9 --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.9-ci-requirements.txt +# uv pip compile --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.9-ci-requirements.txt aiobotocore==2.15.1 + # via feast (setup.py) aiohappyeyeballs==2.4.0 # via aiohttp aiohttp==3.10.5 @@ -21,6 +22,8 @@ anyio==4.5.0 # jupyter-server # starlette # watchfiles +appnope==0.1.4 + # via ipykernel argon2-cffi==23.1.0 # via jupyter-server argon2-cffi-bindings==21.2.0 @@ -30,6 +33,7 @@ arrow==1.3.0 asn1crypto==1.5.1 # via snowflake-connector-python assertpy==1.1 + # via feast (setup.py) asttokens==2.4.1 # via stack-data async-lru==2.0.4 @@ -52,7 +56,9 @@ azure-core==1.31.0 # azure-identity # azure-storage-blob azure-identity==1.18.0 + # via feast (setup.py) azure-storage-blob==12.23.0 + # via feast (setup.py) babel==2.16.0 # via # jupyterlab-server @@ -62,10 +68,13 @@ beautifulsoup4==4.12.3 bidict==0.23.1 # via ibis-framework bigtree==0.21.1 + # via feast (setup.py) bleach==6.1.0 # via nbconvert boto3==1.35.23 - # via moto + # via + # feast (setup.py) + # moto botocore==1.35.23 # via # aiobotocore @@ -74,11 +83,13 @@ botocore==1.35.23 # s3transfer build==1.2.2 # via + # feast (setup.py) # pip-tools # singlestoredb cachetools==5.5.0 # via google-auth cassandra-driver==3.29.2 + # via feast (setup.py) certifi==2024.8.30 # via # elastic-transport @@ -101,6 +112,7 @@ charset-normalizer==3.3.2 # snowflake-connector-python click==8.1.7 # via + # feast (setup.py) # dask # geomet # great-expectations @@ -109,15 +121,18 @@ click==8.1.7 cloudpickle==3.0.0 # via dask colorama==0.4.6 - # via great-expectations + # via + # feast (setup.py) + # great-expectations comm==0.2.2 # via # ipykernel # ipywidgets coverage[toml]==7.6.1 # via pytest-cov -cryptography==43.0.1 +cryptography==42.0.8 # via + # feast (setup.py) # azure-identity # azure-storage-blob # great-expectations @@ -132,7 +147,9 @@ cryptography==43.0.1 cython==3.0.11 # via thriftpy2 dask[dataframe]==2024.8.0 - # via dask-expr + # via + # feast (setup.py) + # dask-expr dask-expr==1.1.10 # via dask db-dtypes==1.3.0 @@ -144,9 +161,11 @@ decorator==5.1.1 defusedxml==0.7.1 # via nbconvert deltalake==0.20.0 + # via feast (setup.py) deprecation==2.1.0 # via python-keycloak dill==0.3.8 + # via feast (setup.py) distlib==0.3.8 # via virtualenv docker==7.1.0 @@ -158,6 +177,7 @@ duckdb==0.10.3 elastic-transport==8.15.0 # via elasticsearch elasticsearch==8.15.1 + # via feast (setup.py) entrypoints==0.4 # via altair exceptiongroup==1.2.2 @@ -169,7 +189,10 @@ execnet==2.1.1 # via pytest-xdist executing==2.1.0 # via stack-data +faiss-cpu==1.9.0 + # via feast (setup.py) fastapi==0.115.2 + # via feast (setup.py) fastjsonschema==2.20.0 # via nbformat filelock==3.16.1 @@ -183,11 +206,14 @@ frozenlist==1.4.1 # aiohttp # aiosignal fsspec==2024.9.0 - # via dask + # via + # feast (setup.py) + # dask geomet==0.2.1.post1 # via cassandra-driver google-api-core[grpc]==2.20.0 # via + # feast (setup.py) # google-cloud-bigquery # google-cloud-bigquery-storage # google-cloud-bigtable @@ -205,8 +231,11 @@ google-auth==2.35.0 # google-cloud-storage # kubernetes google-cloud-bigquery[pandas]==3.25.0 + # via feast (setup.py) google-cloud-bigquery-storage==2.26.0 + # via feast (setup.py) google-cloud-bigtable==2.26.0 + # via feast (setup.py) google-cloud-core==2.4.1 # via # google-cloud-bigquery @@ -214,7 +243,9 @@ google-cloud-core==2.4.1 # google-cloud-datastore # google-cloud-storage google-cloud-datastore==2.20.1 + # via feast (setup.py) google-cloud-storage==2.18.2 + # via feast (setup.py) google-crc32c==1.6.0 # via # google-cloud-storage @@ -225,16 +256,17 @@ google-resumable-media==2.7.2 # google-cloud-storage googleapis-common-protos[grpc]==1.65.0 # via + # feast (setup.py) # google-api-core # grpc-google-iam-v1 # grpcio-status great-expectations==0.18.21 -greenlet==3.1.0 - # via sqlalchemy + # via feast (setup.py) grpc-google-iam-v1==0.13.1 # via google-cloud-bigtable grpcio==1.66.1 # via + # feast (setup.py) # google-api-core # googleapis-common-protos # grpc-google-iam-v1 @@ -244,30 +276,44 @@ grpcio==1.66.1 # grpcio-testing # grpcio-tools grpcio-health-checking==1.62.3 + # via feast (setup.py) grpcio-reflection==1.62.3 + # via feast (setup.py) grpcio-status==1.62.3 # via google-api-core grpcio-testing==1.62.3 + # via feast (setup.py) grpcio-tools==1.62.3 + # via feast (setup.py) gunicorn==23.0.0 + # via + # feast (setup.py) + # uvicorn-worker h11==0.14.0 # via # httpcore # uvicorn happybase==1.2.0 + # via feast (setup.py) hazelcast-python-client==5.5.0 + # via feast (setup.py) hiredis==2.4.0 + # via feast (setup.py) httpcore==1.0.5 # via httpx httptools==0.6.1 # via uvicorn httpx==0.27.2 # via + # feast (setup.py) # jupyterlab # python-keycloak ibis-framework[duckdb]==9.0.0 - # via ibis-substrait + # via + # feast (setup.py) + # ibis-substrait ibis-substrait==4.0.1 + # via feast (setup.py) identify==2.6.1 # via pre-commit idna==3.10 @@ -281,16 +327,7 @@ idna==3.10 imagesize==1.4.1 # via sphinx importlib-metadata==8.5.0 - # via - # build - # dask - # jupyter-client - # jupyter-lsp - # jupyterlab - # jupyterlab-server - # nbconvert - # sphinx - # typeguard + # via dask iniconfig==2.0.0 # via pytest ipykernel==6.29.5 @@ -310,6 +347,7 @@ jedi==0.19.1 # via ipython jinja2==3.1.4 # via + # feast (setup.py) # altair # great-expectations # jupyter-server @@ -332,6 +370,7 @@ jsonpointer==3.0.0 # jsonschema jsonschema[format-nongpl]==4.23.0 # via + # feast (setup.py) # altair # great-expectations # jupyter-events @@ -379,6 +418,7 @@ jupyterlab-widgets==3.0.13 jwcrypto==1.5.6 # via python-keycloak kubernetes==20.13.0 + # via feast (setup.py) locket==1.0.0 # via partd makefun==1.15.4 @@ -399,13 +439,17 @@ matplotlib-inline==0.1.7 mdurl==0.1.2 # via markdown-it-py minio==7.1.0 + # via feast (setup.py) mistune==3.0.2 # via # great-expectations # nbconvert mmh3==5.0.0 + # via feast (setup.py) mock==2.0.0 + # via feast (setup.py) moto==4.2.14 + # via feast (setup.py) msal==1.31.0 # via # azure-identity @@ -417,10 +461,13 @@ multidict==6.1.0 # aiohttp # yarl mypy==1.11.2 - # via sqlalchemy + # via + # feast (setup.py) + # sqlalchemy mypy-extensions==1.0.0 # via mypy mypy-protobuf==3.3.0 + # via feast (setup.py) nbclient==0.10.0 # via nbconvert nbconvert==7.16.4 @@ -443,9 +490,11 @@ notebook-shim==0.2.4 # notebook numpy==1.26.4 # via + # feast (setup.py) # altair # dask # db-dtypes + # faiss-cpu # great-expectations # ibis-framework # pandas @@ -461,6 +510,7 @@ packaging==24.1 # dask # db-dtypes # deprecation + # faiss-cpu # google-cloud-bigquery # great-expectations # gunicorn @@ -476,6 +526,7 @@ packaging==24.1 # sphinx pandas==2.2.2 # via + # feast (setup.py) # altair # dask # dask-expr @@ -501,6 +552,7 @@ pexpect==4.9.0 pip==24.2 # via pip-tools pip-tools==7.4.1 + # via feast (setup.py) platformdirs==3.11.0 # via # jupyter-core @@ -513,8 +565,11 @@ ply==3.11 portalocker==2.10.1 # via msal-extensions pre-commit==3.3.1 + # via feast (setup.py) prometheus-client==0.20.0 - # via jupyter-server + # via + # feast (setup.py) + # jupyter-server prompt-toolkit==3.0.47 # via ipython proto-plus==1.24.0 @@ -525,6 +580,7 @@ proto-plus==1.24.0 # google-cloud-datastore protobuf==4.25.5 # via + # feast (setup.py) # google-api-core # google-cloud-bigquery-storage # google-cloud-bigtable @@ -540,9 +596,12 @@ protobuf==4.25.5 # proto-plus # substrait psutil==5.9.0 - # via ipykernel -psycopg[binary, pool]==3.2.2 -psycopg-binary==3.2.2 + # via + # feast (setup.py) + # ipykernel +psycopg[binary, pool]==3.1.19 + # via feast (setup.py) +psycopg-binary==3.1.19 # via psycopg psycopg-pool==3.2.3 # via psycopg @@ -553,12 +612,14 @@ ptyprocess==0.7.0 pure-eval==0.2.3 # via stack-data py==1.11.0 + # via feast (setup.py) py-cpuinfo==9.0.0 # via pytest-benchmark py4j==0.10.9.7 # via pyspark pyarrow==16.1.0 # via + # feast (setup.py) # dask-expr # db-dtypes # deltalake @@ -574,28 +635,35 @@ pyasn1==0.6.1 pyasn1-modules==0.4.1 # via google-auth pybindgen==0.22.1 + # via feast (setup.py) pycparser==2.22 # via cffi pydantic==2.9.2 # via + # feast (setup.py) # fastapi # great-expectations pydantic-core==2.23.4 # via pydantic pygments==2.18.0 # via + # feast (setup.py) # ipython # nbconvert # rich # sphinx pyjwt[crypto]==2.9.0 # via + # feast (setup.py) # msal # singlestoredb # snowflake-connector-python pymssql==2.3.1 + # via feast (setup.py) pymysql==1.1.1 + # via feast (setup.py) pyodbc==5.1.0 + # via feast (setup.py) pyopenssl==24.2.1 # via snowflake-connector-python pyparsing==3.1.4 @@ -605,8 +673,11 @@ pyproject-hooks==1.1.0 # build # pip-tools pyspark==3.5.2 + # via feast (setup.py) pytest==7.4.4 # via + # feast (setup.py) + # pytest-asyncio # pytest-benchmark # pytest-cov # pytest-env @@ -615,14 +686,24 @@ pytest==7.4.4 # pytest-ordering # pytest-timeout # pytest-xdist +pytest-asyncio==0.23.8 + # via feast (setup.py) pytest-benchmark==3.4.1 + # via feast (setup.py) pytest-cov==5.0.0 + # via feast (setup.py) pytest-env==1.1.3 + # via feast (setup.py) pytest-lazy-fixture==0.6.3 + # via feast (setup.py) pytest-mock==1.10.4 + # via feast (setup.py) pytest-ordering==0.6 + # via feast (setup.py) pytest-timeout==1.4.2 + # via feast (setup.py) pytest-xdist==3.6.1 + # via feast (setup.py) python-dateutil==2.9.0.post0 # via # arrow @@ -640,6 +721,7 @@ python-dotenv==1.0.1 python-json-logger==2.0.7 # via jupyter-events python-keycloak==4.2.2 + # via feast (setup.py) pytz==2024.2 # via # great-expectations @@ -649,6 +731,7 @@ pytz==2024.2 # trino pyyaml==6.0.2 # via + # feast (setup.py) # dask # ibis-substrait # jupyter-events @@ -662,15 +745,19 @@ pyzmq==26.2.0 # jupyter-client # jupyter-server redis==4.6.0 + # via feast (setup.py) referencing==0.35.1 # via # jsonschema # jsonschema-specifications # jupyter-events regex==2024.9.11 - # via parsimonious + # via + # feast (setup.py) + # parsimonious requests==2.32.3 # via + # feast (setup.py) # azure-core # docker # google-api-core @@ -716,6 +803,7 @@ ruamel-yaml==0.17.40 ruamel-yaml-clib==0.2.8 # via ruamel-yaml ruff==0.6.6 + # via feast (setup.py) s3transfer==0.10.2 # via boto3 scipy==1.13.1 @@ -730,6 +818,7 @@ setuptools==75.1.0 # pip-tools # singlestoredb singlestoredb==1.6.3 + # via feast (setup.py) six==1.16.0 # via # asttokens @@ -750,11 +839,13 @@ sniffio==1.3.1 snowballstemmer==2.2.0 # via sphinx snowflake-connector-python[pandas]==3.12.2 + # via feast (setup.py) sortedcontainers==2.4.0 # via snowflake-connector-python soupsieve==2.6 # via beautifulsoup4 sphinx==6.2.1 + # via feast (setup.py) sphinxcontrib-applehelp==2.0.0 # via sphinx sphinxcontrib-devhelp==2.0.0 @@ -768,9 +859,11 @@ sphinxcontrib-qthelp==2.0.0 sphinxcontrib-serializinghtml==2.0.0 # via sphinx sqlalchemy[mypy]==2.0.35 + # via feast (setup.py) sqlglot==23.12.2 # via ibis-framework sqlite-vec==0.1.1 + # via feast (setup.py) sqlparams==6.1.0 # via singlestoredb stack-data==0.6.3 @@ -780,17 +873,21 @@ starlette==0.40.0 substrait==0.23.0 # via ibis-substrait tabulate==0.9.0 + # via feast (setup.py) tenacity==8.5.0 + # via feast (setup.py) terminado==0.18.1 # via # jupyter-server # jupyter-server-terminals testcontainers==4.4.0 + # via feast (setup.py) thriftpy2==0.5.2 # via happybase tinycss2==1.3.0 # via nbconvert toml==0.10.2 + # via feast (setup.py) tomli==2.0.1 # via # build @@ -818,7 +915,9 @@ tornado==6.4.1 # notebook # terminado tqdm==4.66.5 - # via great-expectations + # via + # feast (setup.py) + # great-expectations traitlets==5.14.3 # via # comm @@ -835,28 +934,41 @@ traitlets==5.14.3 # nbconvert # nbformat trino==0.329.0 + # via feast (setup.py) typeguard==4.3.0 + # via feast (setup.py) types-cffi==1.16.0.20240331 # via types-pyopenssl types-protobuf==3.19.22 - # via mypy-protobuf + # via + # feast (setup.py) + # mypy-protobuf types-pymysql==1.1.0.20240524 + # via feast (setup.py) types-pyopenssl==24.1.0.20240722 # via types-redis types-python-dateutil==2.9.0.20240906 - # via arrow + # via + # feast (setup.py) + # arrow types-pytz==2024.2.0.20240913 + # via feast (setup.py) types-pyyaml==6.0.12.20240917 + # via feast (setup.py) types-redis==4.6.0.20240903 + # via feast (setup.py) types-requests==2.30.0.0 + # via feast (setup.py) types-setuptools==75.1.0.20240917 - # via types-cffi + # via + # feast (setup.py) + # types-cffi types-tabulate==0.9.0.20240106 + # via feast (setup.py) types-urllib3==1.26.25.14 # via types-requests typing-extensions==4.12.2 # via - # aioitertools # anyio # async-lru # azure-core @@ -865,7 +977,6 @@ typing-extensions==4.12.2 # fastapi # great-expectations # ibis-framework - # ipython # jwcrypto # multidict # mypy @@ -875,7 +986,6 @@ typing-extensions==4.12.2 # pydantic-core # snowflake-connector-python # sqlalchemy - # starlette # testcontainers # typeguard # uvicorn @@ -889,6 +999,7 @@ uri-template==1.3.0 # via jsonschema urllib3==1.26.20 # via + # feast (setup.py) # botocore # docker # elastic-transport @@ -897,14 +1008,19 @@ urllib3==1.26.20 # minio # requests # responses - # snowflake-connector-python # testcontainers uvicorn[standard]==0.30.6 -uvicorn-worker + # via + # feast (setup.py) + # uvicorn-worker +uvicorn-worker==0.2.0 + # via feast (setup.py) uvloop==0.20.0 # via uvicorn virtualenv==20.23.0 - # via pre-commit + # via + # feast (setup.py) + # pre-commit watchfiles==0.24.0 # via uvicorn wcwidth==0.2.13 diff --git a/sdk/python/tests/foo_provider.py b/sdk/python/tests/foo_provider.py index bc30d3ef88..570a6d4f8d 100644 --- a/sdk/python/tests/foo_provider.py +++ b/sdk/python/tests/foo_provider.py @@ -213,3 +213,9 @@ async def online_write_batch_async( progress: Optional[Callable[[int], Any]], ) -> None: pass + + async def initialize(self, config: RepoConfig) -> None: + pass + + async def close(self) -> None: + pass diff --git a/sdk/python/tests/integration/online_store/test_python_feature_server.py b/sdk/python/tests/integration/online_store/test_python_feature_server.py index d08e1104eb..8e69f719bc 100644 --- a/sdk/python/tests/integration/online_store/test_python_feature_server.py +++ b/sdk/python/tests/integration/online_store/test_python_feature_server.py @@ -20,7 +20,7 @@ @pytest.mark.integration @pytest.mark.universal_online_stores -def test_get_online_features(python_fs_client): +async def test_get_online_features(python_fs_client): request_data_dict = { "features": [ "driver_stats:conv_rate", @@ -58,16 +58,16 @@ def test_get_online_features(python_fs_client): @pytest.mark.integration @pytest.mark.universal_online_stores -def test_push(python_fs_client): - initial_temp = _get_temperatures_from_feature_server( +async def test_push(python_fs_client): + initial_temp = await _get_temperatures_from_feature_server( python_fs_client, location_ids=[1] - )[0] + ) json_data = json.dumps( { "push_source_name": "location_stats_push_source", "df": { "location_id": [1], - "temperature": [initial_temp * 100], + "temperature": [initial_temp[0] * 100], "event_timestamp": [str(_utc_now())], "created": [str(_utc_now())], }, @@ -80,17 +80,15 @@ def test_push(python_fs_client): # Check new pushed temperature is fetched assert response.status_code == 200 - assert _get_temperatures_from_feature_server( + actual = await _get_temperatures_from_feature_server( python_fs_client, location_ids=[1] - ) == [initial_temp * 100] + ) + assert actual == [initial_temp[0] * 100] @pytest.mark.integration @pytest.mark.universal_online_stores def test_push_source_does_not_exist(python_fs_client): - initial_temp = _get_temperatures_from_feature_server( - python_fs_client, location_ids=[1] - )[0] with pytest.raises( PushSourceNotFoundException, match="Unable to find push source 'push_source_does_not_exist'", @@ -102,7 +100,7 @@ def test_push_source_does_not_exist(python_fs_client): "push_source_name": "push_source_does_not_exist", "df": { "location_id": [1], - "temperature": [initial_temp * 100], + "temperature": [100], "event_timestamp": [str(_utc_now())], "created": [str(_utc_now())], }, @@ -111,7 +109,7 @@ def test_push_source_does_not_exist(python_fs_client): ) -def _get_temperatures_from_feature_server(client, location_ids: List[int]): +async def _get_temperatures_from_feature_server(client, location_ids: List[int]): get_request_data = { "features": ["pushable_location_stats:temperature"], "entities": {"location_id": location_ids}, @@ -137,5 +135,5 @@ def python_fs_client(environment, universal_data_sources, request): feast_objects.extend([driver(), customer(), location()]) fs.apply(feast_objects) fs.materialize(environment.start_date, environment.end_date) - client = TestClient(get_app(fs)) - yield client + with TestClient(get_app(fs)) as client: + yield client diff --git a/setup.py b/setup.py index 7c75625d30..5ab57c906c 100644 --- a/setup.py +++ b/setup.py @@ -165,6 +165,7 @@ "psutil==5.9.0", "py>=1.11.0", # https://github.com/pytest-dev/pytest/issues/10420 "pytest>=6.0.0,<8", + "pytest-asyncio<=0.24.0", "pytest-cov", "pytest-xdist", "pytest-benchmark>=3.4.1,<4", From d95ed18debe3ad419fdede890276f4361506ab23 Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Thu, 24 Oct 2024 14:27:04 -0400 Subject: [PATCH 171/185] chore: Updating latest requirements to fix broken CI (#4683) * chore: Updating requirements Signed-off-by: Francisco Javier Arceo * adding mypy constraint Signed-off-by: Francisco Javier Arceo * downgrading mypy filter Signed-off-by: Francisco Javier Arceo --------- Signed-off-by: Francisco Javier Arceo --- .../requirements/py3.10-ci-requirements.txt | 146 ++++++++-------- .../requirements/py3.10-requirements.txt | 96 +++++++---- .../requirements/py3.11-ci-requirements.txt | 162 ++++++++---------- .../requirements/py3.11-requirements.txt | 94 ++++++---- .../requirements/py3.9-ci-requirements.txt | 147 +++++++++------- .../requirements/py3.9-requirements.txt | 92 ++++++---- setup.py | 2 +- 7 files changed, 420 insertions(+), 319 deletions(-) diff --git a/sdk/python/requirements/py3.10-ci-requirements.txt b/sdk/python/requirements/py3.10-ci-requirements.txt index 7109a6feae..1aede896ae 100644 --- a/sdk/python/requirements/py3.10-ci-requirements.txt +++ b/sdk/python/requirements/py3.10-ci-requirements.txt @@ -1,10 +1,10 @@ # This file was autogenerated by uv via the following command: -# uv pip compile --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.10-ci-requirements.txt -aiobotocore==2.15.1 +# uv pip compile -p 3.10 --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.10-ci-requirements.txt +aiobotocore==2.15.2 # via feast (setup.py) -aiohappyeyeballs==2.4.0 +aiohappyeyeballs==2.4.3 # via aiohttp -aiohttp==3.10.5 +aiohttp==3.10.10 # via aiobotocore aioitertools==0.12.0 # via aiobotocore @@ -16,7 +16,7 @@ altair==4.2.2 # via great-expectations annotated-types==0.7.0 # via pydantic -anyio==4.5.0 +anyio==4.6.2.post1 # via # httpx # jupyter-server @@ -55,9 +55,9 @@ azure-core==1.31.0 # via # azure-identity # azure-storage-blob -azure-identity==1.18.0 +azure-identity==1.19.0 # via feast (setup.py) -azure-storage-blob==12.23.0 +azure-storage-blob==12.23.1 # via feast (setup.py) babel==2.16.0 # via @@ -65,21 +65,21 @@ babel==2.16.0 # sphinx beautifulsoup4==4.12.3 # via nbconvert -bigtree==0.21.1 +bigtree==0.21.3 # via feast (setup.py) bleach==6.1.0 # via nbconvert -boto3==1.35.23 +boto3==1.35.36 # via # feast (setup.py) # moto -botocore==1.35.23 +botocore==1.35.36 # via # aiobotocore # boto3 # moto # s3transfer -build==1.2.2 +build==1.2.2.post1 # via # feast (setup.py) # pip-tools @@ -104,7 +104,7 @@ cffi==1.17.1 # snowflake-connector-python cfgv==3.4.0 # via pre-commit -charset-normalizer==3.3.2 +charset-normalizer==3.4.0 # via # requests # snowflake-connector-python @@ -116,7 +116,7 @@ click==8.1.7 # great-expectations # pip-tools # uvicorn -cloudpickle==3.0.0 +cloudpickle==3.1.0 # via dask colorama==0.4.6 # via @@ -126,7 +126,7 @@ comm==0.2.2 # via # ipykernel # ipywidgets -coverage[toml]==7.6.1 +coverage[toml]==7.6.4 # via pytest-cov cryptography==42.0.8 # via @@ -144,35 +144,35 @@ cryptography==42.0.8 # types-redis cython==3.0.11 # via thriftpy2 -dask[dataframe]==2024.9.0 +dask[dataframe]==2024.10.0 # via # feast (setup.py) # dask-expr -dask-expr==1.1.14 +dask-expr==1.1.16 # via dask db-dtypes==1.3.0 # via google-cloud-bigquery -debugpy==1.8.5 +debugpy==1.8.7 # via ipykernel decorator==5.1.1 # via ipython defusedxml==0.7.1 # via nbconvert -deltalake==0.20.0 +deltalake==0.20.2 # via feast (setup.py) deprecation==2.1.0 # via python-keycloak -dill==0.3.8 +dill==0.3.9 # via feast (setup.py) -distlib==0.3.8 +distlib==0.3.9 # via virtualenv docker==7.1.0 # via testcontainers docutils==0.19 # via sphinx -duckdb==1.1.0 +duckdb==1.1.2 # via ibis-framework -elastic-transport==8.15.0 +elastic-transport==8.15.1 # via elasticsearch elasticsearch==8.15.1 # via feast (setup.py) @@ -189,7 +189,7 @@ executing==2.1.0 # via stack-data faiss-cpu==1.9.0 # via feast (setup.py) -fastapi==0.115.2 +fastapi==0.115.3 # via feast (setup.py) fastjsonschema==2.20.0 # via nbformat @@ -199,7 +199,7 @@ filelock==3.16.1 # virtualenv fqdn==1.5.1 # via jsonschema -frozenlist==1.4.1 +frozenlist==1.5.0 # via # aiohttp # aiosignal @@ -209,7 +209,7 @@ fsspec==2024.9.0 # dask geomet==0.2.1.post1 # via cassandra-driver -google-api-core[grpc]==2.20.0 +google-api-core[grpc]==2.21.0 # via # feast (setup.py) # google-cloud-bigquery @@ -228,9 +228,9 @@ google-auth==2.35.0 # google-cloud-datastore # google-cloud-storage # kubernetes -google-cloud-bigquery[pandas]==3.25.0 +google-cloud-bigquery[pandas]==3.26.0 # via feast (setup.py) -google-cloud-bigquery-storage==2.26.0 +google-cloud-bigquery-storage==2.27.0 # via feast (setup.py) google-cloud-bigtable==2.26.0 # via feast (setup.py) @@ -262,7 +262,7 @@ great-expectations==0.18.21 # via feast (setup.py) grpc-google-iam-v1==0.13.1 # via google-cloud-bigtable -grpcio==1.66.1 +grpcio==1.67.0 # via # feast (setup.py) # google-api-core @@ -297,9 +297,9 @@ hazelcast-python-client==5.5.0 # via feast (setup.py) hiredis==2.4.0 # via feast (setup.py) -httpcore==1.0.5 +httpcore==1.0.6 # via httpx -httptools==0.6.1 +httptools==0.6.4 # via uvicorn httpx==0.27.2 # via @@ -325,19 +325,21 @@ idna==3.10 imagesize==1.4.1 # via sphinx importlib-metadata==8.5.0 - # via dask + # via + # build + # dask iniconfig==2.0.0 # via pytest ipykernel==6.29.5 # via jupyterlab -ipython==8.27.0 +ipython==8.28.0 # via # great-expectations # ipykernel # ipywidgets ipywidgets==8.1.5 # via great-expectations -isodate==0.6.1 +isodate==0.7.2 # via azure-storage-blob isoduration==20.11.0 # via jsonschema @@ -374,7 +376,7 @@ jsonschema[format-nongpl]==4.23.0 # jupyter-events # jupyterlab-server # nbformat -jsonschema-specifications==2023.12.1 +jsonschema-specifications==2024.10.1 # via jsonschema jupyter-client==8.6.3 # via @@ -419,16 +421,16 @@ kubernetes==20.13.0 # via feast (setup.py) locket==1.0.0 # via partd -makefun==1.15.4 +makefun==1.15.6 # via great-expectations markdown-it-py==3.0.0 # via rich -markupsafe==2.1.5 +markupsafe==3.0.2 # via # jinja2 # nbconvert # werkzeug -marshmallow==3.22.0 +marshmallow==3.23.0 # via great-expectations matplotlib-inline==0.1.7 # via @@ -442,7 +444,7 @@ mistune==3.0.2 # via # great-expectations # nbconvert -mmh3==5.0.0 +mmh3==5.0.1 # via feast (setup.py) mock==2.0.0 # via feast (setup.py) @@ -523,7 +525,7 @@ packaging==24.1 # pytest # snowflake-connector-python # sphinx -pandas==2.2.2 +pandas==2.2.3 # via # feast (setup.py) # altair @@ -565,13 +567,15 @@ portalocker==2.10.1 # via msal-extensions pre-commit==3.3.1 # via feast (setup.py) -prometheus-client==0.20.0 +prometheus-client==0.21.0 # via # feast (setup.py) # jupyter-server -prompt-toolkit==3.0.47 +prompt-toolkit==3.0.48 # via ipython -proto-plus==1.24.0 +propcache==0.2.0 + # via yarl +proto-plus==1.25.0 # via # google-api-core # google-cloud-bigquery-storage @@ -598,9 +602,9 @@ psutil==5.9.0 # via # feast (setup.py) # ipykernel -psycopg[binary, pool]==3.1.19 +psycopg[binary, pool]==3.2.3 # via feast (setup.py) -psycopg-binary==3.1.19 +psycopg-binary==3.2.3 # via psycopg psycopg-pool==3.2.3 # via psycopg @@ -661,17 +665,17 @@ pymssql==2.3.1 # via feast (setup.py) pymysql==1.1.1 # via feast (setup.py) -pyodbc==5.1.0 +pyodbc==5.2.0 # via feast (setup.py) pyopenssl==24.2.1 # via snowflake-connector-python -pyparsing==3.1.4 +pyparsing==3.2.0 # via great-expectations -pyproject-hooks==1.1.0 +pyproject-hooks==1.2.0 # via # build # pip-tools -pyspark==3.5.2 +pyspark==3.5.3 # via feast (setup.py) pytest==7.4.4 # via @@ -789,7 +793,7 @@ rfc3986-validator==0.1.1 # via # jsonschema # jupyter-events -rich==13.8.1 +rich==13.9.3 # via ibis-framework rpds-py==0.20.0 # via @@ -799,24 +803,24 @@ rsa==4.9 # via google-auth ruamel-yaml==0.17.40 # via great-expectations -ruamel-yaml-clib==0.2.8 +ruamel-yaml-clib==0.2.12 # via ruamel-yaml -ruff==0.6.6 +ruff==0.7.1 # via feast (setup.py) -s3transfer==0.10.2 +s3transfer==0.10.3 # via boto3 scipy==1.14.1 # via great-expectations send2trash==1.8.3 # via jupyter-server -setuptools==75.1.0 +setuptools==75.2.0 # via # grpcio-tools # jupyterlab # kubernetes # pip-tools # singlestoredb -singlestoredb==1.6.3 +singlestoredb==1.7.2 # via feast (setup.py) six==1.16.0 # via @@ -825,7 +829,6 @@ six==1.16.0 # bleach # geomet # happybase - # isodate # kubernetes # mock # python-dateutil @@ -857,9 +860,9 @@ sphinxcontrib-qthelp==2.0.0 # via sphinx sphinxcontrib-serializinghtml==2.0.0 # via sphinx -sqlalchemy[mypy]==2.0.35 +sqlalchemy[mypy]==2.0.36 # via feast (setup.py) -sqlglot==25.20.1 +sqlglot==25.20.2 # via ibis-framework sqlite-vec==0.1.1 # via feast (setup.py) @@ -867,7 +870,7 @@ sqlparams==6.1.0 # via singlestoredb stack-data==0.6.3 # via ipython -starlette==0.40.0 +starlette==0.41.0 # via fastapi substrait==0.23.0 # via ibis-substrait @@ -883,11 +886,11 @@ testcontainers==4.4.0 # via feast (setup.py) thriftpy2==0.5.2 # via happybase -tinycss2==1.3.0 +tinycss2==1.4.0 # via nbconvert toml==0.10.2 # via feast (setup.py) -tomli==2.0.1 +tomli==2.0.2 # via # build # coverage @@ -932,7 +935,7 @@ traitlets==5.14.3 # nbclient # nbconvert # nbformat -trino==0.329.0 +trino==0.330.0 # via feast (setup.py) typeguard==4.3.0 # via feast (setup.py) @@ -946,19 +949,19 @@ types-pymysql==1.1.0.20240524 # via feast (setup.py) types-pyopenssl==24.1.0.20240722 # via types-redis -types-python-dateutil==2.9.0.20240906 +types-python-dateutil==2.9.0.20241003 # via # feast (setup.py) # arrow -types-pytz==2024.2.0.20240913 +types-pytz==2024.2.0.20241003 # via feast (setup.py) types-pyyaml==6.0.12.20240917 # via feast (setup.py) -types-redis==4.6.0.20240903 +types-redis==4.6.0.20241004 # via feast (setup.py) types-requests==2.30.0.0 # via feast (setup.py) -types-setuptools==75.1.0.20240917 +types-setuptools==75.2.0.20241019 # via # feast (setup.py) # types-cffi @@ -984,12 +987,13 @@ typing-extensions==4.12.2 # psycopg-pool # pydantic # pydantic-core + # rich # snowflake-connector-python # sqlalchemy # testcontainers # typeguard # uvicorn -tzdata==2024.1 +tzdata==2024.2 # via pandas tzlocal==5.2 # via @@ -1009,13 +1013,13 @@ urllib3==2.2.3 # requests # responses # testcontainers -uvicorn[standard]==0.30.6 +uvicorn[standard]==0.32.0 # via # feast (setup.py) # uvicorn-worker uvicorn-worker==0.2.0 # via feast (setup.py) -uvloop==0.20.0 +uvloop==0.21.0 # via uvicorn virtualenv==20.23.0 # via @@ -1035,7 +1039,7 @@ websocket-client==1.8.0 # via # jupyter-server # kubernetes -websockets==13.0.1 +websockets==13.1 # via uvicorn werkzeug==3.0.4 # via moto @@ -1049,9 +1053,9 @@ wrapt==1.16.0 # via # aiobotocore # testcontainers -xmltodict==0.13.0 +xmltodict==0.14.2 # via moto -yarl==1.11.1 +yarl==1.16.0 # via aiohttp zipp==3.20.2 # via importlib-metadata diff --git a/sdk/python/requirements/py3.10-requirements.txt b/sdk/python/requirements/py3.10-requirements.txt index c5549401ea..94c5d3945a 100644 --- a/sdk/python/requirements/py3.10-requirements.txt +++ b/sdk/python/requirements/py3.10-requirements.txt @@ -2,7 +2,7 @@ # uv pip compile -p 3.10 --system --no-strip-extras setup.py --output-file sdk/python/requirements/py3.10-requirements.txt annotated-types==0.7.0 # via pydantic -anyio==4.5.0 +anyio==4.6.2.post1 # via # starlette # watchfiles @@ -10,34 +10,42 @@ attrs==24.2.0 # via # jsonschema # referencing -bigtree==0.21.1 +bigtree==0.21.3 + # via feast (setup.py) certifi==2024.8.30 # via requests -charset-normalizer==3.3.2 +charset-normalizer==3.4.0 # via requests click==8.1.7 # via + # feast (setup.py) # dask # uvicorn -cloudpickle==3.0.0 +cloudpickle==3.1.0 # via dask colorama==0.4.6 -dask[dataframe]==2024.9.0 - # via dask-expr -dask-expr==1.1.14 + # via feast (setup.py) +dask[dataframe]==2024.10.0 + # via + # feast (setup.py) + # dask-expr +dask-expr==1.1.16 # via dask -dill==0.3.8 +dill==0.3.9 + # via feast (setup.py) exceptiongroup==1.2.2 # via anyio -fastapi==0.115.2 -fsspec==2024.9.0 +fastapi==0.115.3 + # via feast (setup.py) +fsspec==2024.10.0 # via dask -greenlet==3.1.0 - # via sqlalchemy gunicorn==23.0.0 + # via + # feast (setup.py) + # uvicorn-worker h11==0.14.0 # via uvicorn -httptools==0.6.1 +httptools==0.6.4 # via uvicorn idna==3.10 # via @@ -46,20 +54,24 @@ idna==3.10 importlib-metadata==8.5.0 # via dask jinja2==3.1.4 + # via feast (setup.py) jsonschema==4.23.0 -jsonschema-specifications==2023.12.1 + # via feast (setup.py) +jsonschema-specifications==2024.10.1 # via jsonschema locket==1.0.0 # via partd -markupsafe==2.1.5 +markupsafe==3.0.2 # via jinja2 -mmh3==5.0.0 -mypy==1.11.2 +mmh3==5.0.1 + # via feast (setup.py) +mypy==1.13.0 # via sqlalchemy mypy-extensions==1.0.0 # via mypy numpy==1.26.4 # via + # feast (setup.py) # dask # pandas # pyarrow @@ -67,23 +79,33 @@ packaging==24.1 # via # dask # gunicorn -pandas==2.2.2 +pandas==2.2.3 # via + # feast (setup.py) # dask # dask-expr partd==1.4.2 # via dask -prometheus-client==0.20.0 +prometheus-client==0.21.0 + # via feast (setup.py) protobuf==4.25.5 -psutil==6.0.0 + # via feast (setup.py) +psutil==6.1.0 + # via feast (setup.py) pyarrow==17.0.0 - # via dask-expr + # via + # feast (setup.py) + # dask-expr pydantic==2.9.2 - # via fastapi + # via + # feast (setup.py) + # fastapi pydantic-core==2.23.4 # via pydantic pygments==2.18.0 + # via feast (setup.py) pyjwt==2.9.0 + # via feast (setup.py) python-dateutil==2.9.0.post0 # via pandas python-dotenv==1.0.1 @@ -92,6 +114,7 @@ pytz==2024.2 # via pandas pyyaml==6.0.2 # via + # feast (setup.py) # dask # uvicorn referencing==0.35.1 @@ -99,6 +122,7 @@ referencing==0.35.1 # jsonschema # jsonschema-specifications requests==2.32.3 + # via feast (setup.py) rpds-py==0.20.0 # via # jsonschema @@ -107,20 +131,26 @@ six==1.16.0 # via python-dateutil sniffio==1.3.1 # via anyio -sqlalchemy[mypy]==2.0.35 -starlette==0.40.0 +sqlalchemy[mypy]==2.0.36 + # via feast (setup.py) +starlette==0.41.0 # via fastapi tabulate==0.9.0 + # via feast (setup.py) tenacity==8.5.0 + # via feast (setup.py) toml==0.10.2 -tomli==2.0.1 + # via feast (setup.py) +tomli==2.0.2 # via mypy -toolz==0.12.1 +toolz==1.0.0 # via # dask # partd tqdm==4.66.5 + # via feast (setup.py) typeguard==4.3.0 + # via feast (setup.py) typing-extensions==4.12.2 # via # anyio @@ -131,17 +161,21 @@ typing-extensions==4.12.2 # sqlalchemy # typeguard # uvicorn -tzdata==2024.1 +tzdata==2024.2 # via pandas urllib3==2.2.3 # via requests -uvicorn[standard]==0.30.6 -uvicorn-worker -uvloop==0.20.0 +uvicorn[standard]==0.32.0 + # via + # feast (setup.py) + # uvicorn-worker +uvicorn-worker==0.2.0 + # via feast (setup.py) +uvloop==0.21.0 # via uvicorn watchfiles==0.24.0 # via uvicorn -websockets==13.0.1 +websockets==13.1 # via uvicorn zipp==3.20.2 # via importlib-metadata diff --git a/sdk/python/requirements/py3.11-ci-requirements.txt b/sdk/python/requirements/py3.11-ci-requirements.txt index eb8bbc280b..7a91c95c6c 100644 --- a/sdk/python/requirements/py3.11-ci-requirements.txt +++ b/sdk/python/requirements/py3.11-ci-requirements.txt @@ -1,10 +1,10 @@ # This file was autogenerated by uv via the following command: -# uv pip compile --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.11-ci-requirements.txt -aiobotocore==2.15.1 +# uv pip compile -p 3.11 --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.11-ci-requirements.txt +aiobotocore==2.15.2 # via feast (setup.py) -aiohappyeyeballs==2.4.0 +aiohappyeyeballs==2.4.3 # via aiohttp -aiohttp==3.10.5 +aiohttp==3.10.10 # via aiobotocore aioitertools==0.12.0 # via aiobotocore @@ -16,7 +16,7 @@ altair==4.2.2 # via great-expectations annotated-types==0.7.0 # via pydantic -anyio==4.5.0 +anyio==4.6.2.post1 # via # httpx # jupyter-server @@ -41,9 +41,7 @@ async-lru==2.0.4 async-property==0.2.2 # via python-keycloak async-timeout==4.0.3 - # via - # aiohttp - # redis + # via redis atpublic==5.0 # via ibis-framework attrs==24.2.0 @@ -55,9 +53,9 @@ azure-core==1.31.0 # via # azure-identity # azure-storage-blob -azure-identity==1.18.0 +azure-identity==1.19.0 # via feast (setup.py) -azure-storage-blob==12.23.0 +azure-storage-blob==12.23.1 # via feast (setup.py) babel==2.16.0 # via @@ -65,21 +63,21 @@ babel==2.16.0 # sphinx beautifulsoup4==4.12.3 # via nbconvert -bigtree==0.21.1 +bigtree==0.21.3 # via feast (setup.py) bleach==6.1.0 # via nbconvert -boto3==1.35.23 +boto3==1.35.36 # via # feast (setup.py) # moto -botocore==1.35.23 +botocore==1.35.36 # via # aiobotocore # boto3 # moto # s3transfer -build==1.2.2 +build==1.2.2.post1 # via # feast (setup.py) # pip-tools @@ -104,7 +102,7 @@ cffi==1.17.1 # snowflake-connector-python cfgv==3.4.0 # via pre-commit -charset-normalizer==3.3.2 +charset-normalizer==3.4.0 # via # requests # snowflake-connector-python @@ -116,7 +114,7 @@ click==8.1.7 # great-expectations # pip-tools # uvicorn -cloudpickle==3.0.0 +cloudpickle==3.1.0 # via dask colorama==0.4.6 # via @@ -126,7 +124,7 @@ comm==0.2.2 # via # ipykernel # ipywidgets -coverage[toml]==7.6.1 +coverage[toml]==7.6.4 # via pytest-cov cryptography==42.0.8 # via @@ -144,52 +142,47 @@ cryptography==42.0.8 # types-redis cython==3.0.11 # via thriftpy2 -dask[dataframe]==2024.9.0 +dask[dataframe]==2024.10.0 # via # feast (setup.py) # dask-expr -dask-expr==1.1.14 +dask-expr==1.1.16 # via dask db-dtypes==1.3.0 # via google-cloud-bigquery -debugpy==1.8.5 +debugpy==1.8.7 # via ipykernel decorator==5.1.1 # via ipython defusedxml==0.7.1 # via nbconvert -deltalake==0.20.0 +deltalake==0.20.2 # via feast (setup.py) deprecation==2.1.0 # via python-keycloak -dill==0.3.8 +dill==0.3.9 # via feast (setup.py) -distlib==0.3.8 +distlib==0.3.9 # via virtualenv docker==7.1.0 # via testcontainers docutils==0.19 # via sphinx -duckdb==1.1.0 +duckdb==1.1.2 # via ibis-framework -elastic-transport==8.15.0 +elastic-transport==8.15.1 # via elasticsearch elasticsearch==8.15.1 # via feast (setup.py) entrypoints==0.4 # via altair -exceptiongroup==1.2.2 - # via - # anyio - # ipython - # pytest execnet==2.1.1 # via pytest-xdist executing==2.1.0 # via stack-data faiss-cpu==1.9.0 # via feast (setup.py) -fastapi==0.115.2 +fastapi==0.115.3 # via feast (setup.py) fastjsonschema==2.20.0 # via nbformat @@ -199,7 +192,7 @@ filelock==3.16.1 # virtualenv fqdn==1.5.1 # via jsonschema -frozenlist==1.4.1 +frozenlist==1.5.0 # via # aiohttp # aiosignal @@ -209,7 +202,7 @@ fsspec==2024.9.0 # dask geomet==0.2.1.post1 # via cassandra-driver -google-api-core[grpc]==2.20.0 +google-api-core[grpc]==2.21.0 # via # feast (setup.py) # google-cloud-bigquery @@ -228,9 +221,9 @@ google-auth==2.35.0 # google-cloud-datastore # google-cloud-storage # kubernetes -google-cloud-bigquery[pandas]==3.25.0 +google-cloud-bigquery[pandas]==3.26.0 # via feast (setup.py) -google-cloud-bigquery-storage==2.26.0 +google-cloud-bigquery-storage==2.27.0 # via feast (setup.py) google-cloud-bigtable==2.26.0 # via feast (setup.py) @@ -262,7 +255,7 @@ great-expectations==0.18.21 # via feast (setup.py) grpc-google-iam-v1==0.13.1 # via google-cloud-bigtable -grpcio==1.66.1 +grpcio==1.67.0 # via # feast (setup.py) # google-api-core @@ -297,9 +290,9 @@ hazelcast-python-client==5.5.0 # via feast (setup.py) hiredis==2.4.0 # via feast (setup.py) -httpcore==1.0.5 +httpcore==1.0.6 # via httpx -httptools==0.6.1 +httptools==0.6.4 # via uvicorn httpx==0.27.2 # via @@ -330,14 +323,14 @@ iniconfig==2.0.0 # via pytest ipykernel==6.29.5 # via jupyterlab -ipython==8.27.0 +ipython==8.28.0 # via # great-expectations # ipykernel # ipywidgets ipywidgets==8.1.5 # via great-expectations -isodate==0.6.1 +isodate==0.7.2 # via azure-storage-blob isoduration==20.11.0 # via jsonschema @@ -374,7 +367,7 @@ jsonschema[format-nongpl]==4.23.0 # jupyter-events # jupyterlab-server # nbformat -jsonschema-specifications==2023.12.1 +jsonschema-specifications==2024.10.1 # via jsonschema jupyter-client==8.6.3 # via @@ -419,16 +412,16 @@ kubernetes==20.13.0 # via feast (setup.py) locket==1.0.0 # via partd -makefun==1.15.4 +makefun==1.15.6 # via great-expectations markdown-it-py==3.0.0 # via rich -markupsafe==2.1.5 +markupsafe==3.0.2 # via # jinja2 # nbconvert # werkzeug -marshmallow==3.22.0 +marshmallow==3.23.0 # via great-expectations matplotlib-inline==0.1.7 # via @@ -442,7 +435,7 @@ mistune==3.0.2 # via # great-expectations # nbconvert -mmh3==5.0.0 +mmh3==5.0.1 # via feast (setup.py) mock==2.0.0 # via feast (setup.py) @@ -523,7 +516,7 @@ packaging==24.1 # pytest # snowflake-connector-python # sphinx -pandas==2.2.2 +pandas==2.2.3 # via # feast (setup.py) # altair @@ -565,13 +558,15 @@ portalocker==2.10.1 # via msal-extensions pre-commit==3.3.1 # via feast (setup.py) -prometheus-client==0.20.0 +prometheus-client==0.21.0 # via # feast (setup.py) # jupyter-server -prompt-toolkit==3.0.47 +prompt-toolkit==3.0.48 # via ipython -proto-plus==1.24.0 +propcache==0.2.0 + # via yarl +proto-plus==1.25.0 # via # google-api-core # google-cloud-bigquery-storage @@ -598,9 +593,9 @@ psutil==5.9.0 # via # feast (setup.py) # ipykernel -psycopg[binary, pool]==3.1.19 +psycopg[binary, pool]==3.2.3 # via feast (setup.py) -psycopg-binary==3.1.19 +psycopg-binary==3.2.3 # via psycopg psycopg-pool==3.2.3 # via psycopg @@ -661,17 +656,17 @@ pymssql==2.3.1 # via feast (setup.py) pymysql==1.1.1 # via feast (setup.py) -pyodbc==5.1.0 +pyodbc==5.2.0 # via feast (setup.py) pyopenssl==24.2.1 # via snowflake-connector-python -pyparsing==3.1.4 +pyparsing==3.2.0 # via great-expectations -pyproject-hooks==1.1.0 +pyproject-hooks==1.2.0 # via # build # pip-tools -pyspark==3.5.2 +pyspark==3.5.3 # via feast (setup.py) pytest==7.4.4 # via @@ -789,7 +784,7 @@ rfc3986-validator==0.1.1 # via # jsonschema # jupyter-events -rich==13.8.1 +rich==13.9.3 # via ibis-framework rpds-py==0.20.0 # via @@ -799,24 +794,24 @@ rsa==4.9 # via google-auth ruamel-yaml==0.17.40 # via great-expectations -ruamel-yaml-clib==0.2.8 +ruamel-yaml-clib==0.2.12 # via ruamel-yaml -ruff==0.6.6 +ruff==0.7.1 # via feast (setup.py) -s3transfer==0.10.2 +s3transfer==0.10.3 # via boto3 scipy==1.14.1 # via great-expectations send2trash==1.8.3 # via jupyter-server -setuptools==75.1.0 +setuptools==75.2.0 # via # grpcio-tools # jupyterlab # kubernetes # pip-tools # singlestoredb -singlestoredb==1.6.3 +singlestoredb==1.7.2 # via feast (setup.py) six==1.16.0 # via @@ -825,7 +820,6 @@ six==1.16.0 # bleach # geomet # happybase - # isodate # kubernetes # mock # python-dateutil @@ -857,9 +851,9 @@ sphinxcontrib-qthelp==2.0.0 # via sphinx sphinxcontrib-serializinghtml==2.0.0 # via sphinx -sqlalchemy[mypy]==2.0.35 +sqlalchemy[mypy]==2.0.36 # via feast (setup.py) -sqlglot==25.20.1 +sqlglot==25.20.2 # via ibis-framework sqlite-vec==0.1.1 # via feast (setup.py) @@ -867,7 +861,7 @@ sqlparams==6.1.0 # via singlestoredb stack-data==0.6.3 # via ipython -starlette==0.40.0 +starlette==0.41.0 # via fastapi substrait==0.23.0 # via ibis-substrait @@ -883,20 +877,10 @@ testcontainers==4.4.0 # via feast (setup.py) thriftpy2==0.5.2 # via happybase -tinycss2==1.3.0 +tinycss2==1.4.0 # via nbconvert toml==0.10.2 # via feast (setup.py) -tomli==2.0.2 - # via - # build - # coverage - # jupyterlab - # mypy - # pip-tools - # pytest - # pytest-env - # singlestoredb tomlkit==0.13.2 # via snowflake-connector-python toolz==0.12.1 @@ -932,7 +916,7 @@ traitlets==5.14.3 # nbclient # nbconvert # nbformat -trino==0.329.0 +trino==0.330.0 # via feast (setup.py) typeguard==4.3.0 # via feast (setup.py) @@ -946,19 +930,19 @@ types-pymysql==1.1.0.20240524 # via feast (setup.py) types-pyopenssl==24.1.0.20240722 # via types-redis -types-python-dateutil==2.9.0.20240906 +types-python-dateutil==2.9.0.20241003 # via # feast (setup.py) # arrow -types-pytz==2024.2.0.20240913 +types-pytz==2024.2.0.20241003 # via feast (setup.py) types-pyyaml==6.0.12.20240917 # via feast (setup.py) -types-redis==4.6.0.20240903 +types-redis==4.6.0.20241004 # via feast (setup.py) types-requests==2.30.0.0 # via feast (setup.py) -types-setuptools==75.1.0.20240917 +types-setuptools==75.2.0.20241019 # via # feast (setup.py) # types-cffi @@ -968,8 +952,6 @@ types-urllib3==1.26.25.14 # via types-requests typing-extensions==4.12.2 # via - # anyio - # async-lru # azure-core # azure-identity # azure-storage-blob @@ -978,7 +960,6 @@ typing-extensions==4.12.2 # ibis-framework # ipython # jwcrypto - # multidict # mypy # psycopg # psycopg-pool @@ -988,8 +969,7 @@ typing-extensions==4.12.2 # sqlalchemy # testcontainers # typeguard - # uvicorn -tzdata==2024.1 +tzdata==2024.2 # via pandas tzlocal==5.2 # via @@ -1009,13 +989,13 @@ urllib3==2.2.3 # requests # responses # testcontainers -uvicorn[standard]==0.30.6 +uvicorn[standard]==0.32.0 # via # feast (setup.py) # uvicorn-worker uvicorn-worker==0.2.0 # via feast (setup.py) -uvloop==0.20.0 +uvloop==0.21.0 # via uvicorn virtualenv==20.23.0 # via @@ -1035,7 +1015,7 @@ websocket-client==1.8.0 # via # jupyter-server # kubernetes -websockets==13.0.1 +websockets==13.1 # via uvicorn werkzeug==3.0.4 # via moto @@ -1049,9 +1029,9 @@ wrapt==1.16.0 # via # aiobotocore # testcontainers -xmltodict==0.13.0 +xmltodict==0.14.2 # via moto -yarl==1.11.1 +yarl==1.16.0 # via aiohttp zipp==3.20.2 # via importlib-metadata diff --git a/sdk/python/requirements/py3.11-requirements.txt b/sdk/python/requirements/py3.11-requirements.txt index d7ed97723f..e2a8589e77 100644 --- a/sdk/python/requirements/py3.11-requirements.txt +++ b/sdk/python/requirements/py3.11-requirements.txt @@ -2,7 +2,7 @@ # uv pip compile -p 3.11 --system --no-strip-extras setup.py --output-file sdk/python/requirements/py3.11-requirements.txt annotated-types==0.7.0 # via pydantic -anyio==4.5.0 +anyio==4.6.2.post1 # via # starlette # watchfiles @@ -10,32 +10,40 @@ attrs==24.2.0 # via # jsonschema # referencing -bigtree==0.21.1 +bigtree==0.21.3 + # via feast (setup.py) certifi==2024.8.30 # via requests -charset-normalizer==3.3.2 +charset-normalizer==3.4.0 # via requests click==8.1.7 # via + # feast (setup.py) # dask # uvicorn -cloudpickle==3.0.0 +cloudpickle==3.1.0 # via dask colorama==0.4.6 -dask[dataframe]==2024.9.0 - # via dask-expr -dask-expr==1.1.14 + # via feast (setup.py) +dask[dataframe]==2024.10.0 + # via + # feast (setup.py) + # dask-expr +dask-expr==1.1.16 # via dask -dill==0.3.8 -fastapi==0.115.2 -fsspec==2024.9.0 +dill==0.3.9 + # via feast (setup.py) +fastapi==0.115.3 + # via feast (setup.py) +fsspec==2024.10.0 # via dask -greenlet==3.1.0 - # via sqlalchemy gunicorn==23.0.0 + # via + # feast (setup.py) + # uvicorn-worker h11==0.14.0 # via uvicorn -httptools==0.6.1 +httptools==0.6.4 # via uvicorn idna==3.10 # via @@ -44,20 +52,24 @@ idna==3.10 importlib-metadata==8.5.0 # via dask jinja2==3.1.4 + # via feast (setup.py) jsonschema==4.23.0 -jsonschema-specifications==2023.12.1 + # via feast (setup.py) +jsonschema-specifications==2024.10.1 # via jsonschema locket==1.0.0 # via partd -markupsafe==2.1.5 +markupsafe==3.0.2 # via jinja2 -mmh3==5.0.0 -mypy==1.11.2 +mmh3==5.0.1 + # via feast (setup.py) +mypy==1.13.0 # via sqlalchemy mypy-extensions==1.0.0 # via mypy numpy==1.26.4 # via + # feast (setup.py) # dask # pandas # pyarrow @@ -65,23 +77,33 @@ packaging==24.1 # via # dask # gunicorn -pandas==2.2.2 +pandas==2.2.3 # via + # feast (setup.py) # dask # dask-expr partd==1.4.2 # via dask -prometheus-client==0.20.0 +prometheus-client==0.21.0 + # via feast (setup.py) protobuf==4.25.5 -psutil==6.0.0 + # via feast (setup.py) +psutil==6.1.0 + # via feast (setup.py) pyarrow==17.0.0 - # via dask-expr + # via + # feast (setup.py) + # dask-expr pydantic==2.9.2 - # via fastapi + # via + # feast (setup.py) + # fastapi pydantic-core==2.23.4 # via pydantic pygments==2.18.0 + # via feast (setup.py) pyjwt==2.9.0 + # via feast (setup.py) python-dateutil==2.9.0.post0 # via pandas python-dotenv==1.0.1 @@ -90,6 +112,7 @@ pytz==2024.2 # via pandas pyyaml==6.0.2 # via + # feast (setup.py) # dask # uvicorn referencing==0.35.1 @@ -97,6 +120,7 @@ referencing==0.35.1 # jsonschema # jsonschema-specifications requests==2.32.3 + # via feast (setup.py) rpds-py==0.20.0 # via # jsonschema @@ -105,18 +129,24 @@ six==1.16.0 # via python-dateutil sniffio==1.3.1 # via anyio -sqlalchemy[mypy]==2.0.35 -starlette==0.40.0 +sqlalchemy[mypy]==2.0.36 + # via feast (setup.py) +starlette==0.41.0 # via fastapi tabulate==0.9.0 + # via feast (setup.py) tenacity==8.5.0 + # via feast (setup.py) toml==0.10.2 -toolz==0.12.1 + # via feast (setup.py) +toolz==1.0.0 # via # dask # partd tqdm==4.66.5 + # via feast (setup.py) typeguard==4.3.0 + # via feast (setup.py) typing-extensions==4.12.2 # via # fastapi @@ -125,17 +155,21 @@ typing-extensions==4.12.2 # pydantic-core # sqlalchemy # typeguard -tzdata==2024.1 +tzdata==2024.2 # via pandas urllib3==2.2.3 # via requests -uvicorn[standard]==0.30.6 -uvicorn-worker -uvloop==0.20.0 +uvicorn[standard]==0.32.0 + # via + # feast (setup.py) + # uvicorn-worker +uvicorn-worker==0.2.0 + # via feast (setup.py) +uvloop==0.21.0 # via uvicorn watchfiles==0.24.0 # via uvicorn -websockets==13.0.1 +websockets==13.1 # via uvicorn zipp==3.20.2 # via importlib-metadata diff --git a/sdk/python/requirements/py3.9-ci-requirements.txt b/sdk/python/requirements/py3.9-ci-requirements.txt index 2a380d6fc3..ed77acb4aa 100644 --- a/sdk/python/requirements/py3.9-ci-requirements.txt +++ b/sdk/python/requirements/py3.9-ci-requirements.txt @@ -1,10 +1,10 @@ # This file was autogenerated by uv via the following command: -# uv pip compile --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.9-ci-requirements.txt -aiobotocore==2.15.1 +# uv pip compile -p 3.9 --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.9-ci-requirements.txt +aiobotocore==2.15.2 # via feast (setup.py) -aiohappyeyeballs==2.4.0 +aiohappyeyeballs==2.4.3 # via aiohttp -aiohttp==3.10.5 +aiohttp==3.10.10 # via aiobotocore aioitertools==0.12.0 # via aiobotocore @@ -16,7 +16,7 @@ altair==4.2.2 # via great-expectations annotated-types==0.7.0 # via pydantic -anyio==4.5.0 +anyio==4.6.2.post1 # via # httpx # jupyter-server @@ -55,9 +55,9 @@ azure-core==1.31.0 # via # azure-identity # azure-storage-blob -azure-identity==1.18.0 +azure-identity==1.19.0 # via feast (setup.py) -azure-storage-blob==12.23.0 +azure-storage-blob==12.23.1 # via feast (setup.py) babel==2.16.0 # via @@ -67,21 +67,21 @@ beautifulsoup4==4.12.3 # via nbconvert bidict==0.23.1 # via ibis-framework -bigtree==0.21.1 +bigtree==0.21.3 # via feast (setup.py) bleach==6.1.0 # via nbconvert -boto3==1.35.23 +boto3==1.35.36 # via # feast (setup.py) # moto -botocore==1.35.23 +botocore==1.35.36 # via # aiobotocore # boto3 # moto # s3transfer -build==1.2.2 +build==1.2.2.post1 # via # feast (setup.py) # pip-tools @@ -106,7 +106,7 @@ cffi==1.17.1 # snowflake-connector-python cfgv==3.4.0 # via pre-commit -charset-normalizer==3.3.2 +charset-normalizer==3.4.0 # via # requests # snowflake-connector-python @@ -118,7 +118,7 @@ click==8.1.7 # great-expectations # pip-tools # uvicorn -cloudpickle==3.0.0 +cloudpickle==3.1.0 # via dask colorama==0.4.6 # via @@ -128,7 +128,7 @@ comm==0.2.2 # via # ipykernel # ipywidgets -coverage[toml]==7.6.1 +coverage[toml]==7.6.4 # via pytest-cov cryptography==42.0.8 # via @@ -154,19 +154,19 @@ dask-expr==1.1.10 # via dask db-dtypes==1.3.0 # via google-cloud-bigquery -debugpy==1.8.5 +debugpy==1.8.7 # via ipykernel decorator==5.1.1 # via ipython defusedxml==0.7.1 # via nbconvert -deltalake==0.20.0 +deltalake==0.20.2 # via feast (setup.py) deprecation==2.1.0 # via python-keycloak -dill==0.3.8 +dill==0.3.9 # via feast (setup.py) -distlib==0.3.8 +distlib==0.3.9 # via virtualenv docker==7.1.0 # via testcontainers @@ -174,7 +174,7 @@ docutils==0.19 # via sphinx duckdb==0.10.3 # via ibis-framework -elastic-transport==8.15.0 +elastic-transport==8.15.1 # via elasticsearch elasticsearch==8.15.1 # via feast (setup.py) @@ -191,7 +191,7 @@ executing==2.1.0 # via stack-data faiss-cpu==1.9.0 # via feast (setup.py) -fastapi==0.115.2 +fastapi==0.115.3 # via feast (setup.py) fastjsonschema==2.20.0 # via nbformat @@ -201,7 +201,7 @@ filelock==3.16.1 # virtualenv fqdn==1.5.1 # via jsonschema -frozenlist==1.4.1 +frozenlist==1.5.0 # via # aiohttp # aiosignal @@ -211,7 +211,7 @@ fsspec==2024.9.0 # dask geomet==0.2.1.post1 # via cassandra-driver -google-api-core[grpc]==2.20.0 +google-api-core[grpc]==2.21.0 # via # feast (setup.py) # google-cloud-bigquery @@ -230,9 +230,9 @@ google-auth==2.35.0 # google-cloud-datastore # google-cloud-storage # kubernetes -google-cloud-bigquery[pandas]==3.25.0 +google-cloud-bigquery[pandas]==3.26.0 # via feast (setup.py) -google-cloud-bigquery-storage==2.26.0 +google-cloud-bigquery-storage==2.27.0 # via feast (setup.py) google-cloud-bigtable==2.26.0 # via feast (setup.py) @@ -264,7 +264,7 @@ great-expectations==0.18.21 # via feast (setup.py) grpc-google-iam-v1==0.13.1 # via google-cloud-bigtable -grpcio==1.66.1 +grpcio==1.67.0 # via # feast (setup.py) # google-api-core @@ -299,9 +299,9 @@ hazelcast-python-client==5.5.0 # via feast (setup.py) hiredis==2.4.0 # via feast (setup.py) -httpcore==1.0.5 +httpcore==1.0.6 # via httpx -httptools==0.6.1 +httptools==0.6.4 # via uvicorn httpx==0.27.2 # via @@ -327,7 +327,16 @@ idna==3.10 imagesize==1.4.1 # via sphinx importlib-metadata==8.5.0 - # via dask + # via + # build + # dask + # jupyter-client + # jupyter-lsp + # jupyterlab + # jupyterlab-server + # nbconvert + # sphinx + # typeguard iniconfig==2.0.0 # via pytest ipykernel==6.29.5 @@ -339,7 +348,7 @@ ipython==8.18.1 # ipywidgets ipywidgets==8.1.5 # via great-expectations -isodate==0.6.1 +isodate==0.7.2 # via azure-storage-blob isoduration==20.11.0 # via jsonschema @@ -376,7 +385,7 @@ jsonschema[format-nongpl]==4.23.0 # jupyter-events # jupyterlab-server # nbformat -jsonschema-specifications==2023.12.1 +jsonschema-specifications==2024.10.1 # via jsonschema jupyter-client==8.6.3 # via @@ -421,16 +430,16 @@ kubernetes==20.13.0 # via feast (setup.py) locket==1.0.0 # via partd -makefun==1.15.4 +makefun==1.15.6 # via great-expectations markdown-it-py==3.0.0 # via rich -markupsafe==2.1.5 +markupsafe==3.0.2 # via # jinja2 # nbconvert # werkzeug -marshmallow==3.22.0 +marshmallow==3.23.0 # via great-expectations matplotlib-inline==0.1.7 # via @@ -444,7 +453,7 @@ mistune==3.0.2 # via # great-expectations # nbconvert -mmh3==5.0.0 +mmh3==5.0.1 # via feast (setup.py) mock==2.0.0 # via feast (setup.py) @@ -524,7 +533,7 @@ packaging==24.1 # pytest # snowflake-connector-python # sphinx -pandas==2.2.2 +pandas==2.2.3 # via # feast (setup.py) # altair @@ -566,13 +575,15 @@ portalocker==2.10.1 # via msal-extensions pre-commit==3.3.1 # via feast (setup.py) -prometheus-client==0.20.0 +prometheus-client==0.21.0 # via # feast (setup.py) # jupyter-server -prompt-toolkit==3.0.47 +prompt-toolkit==3.0.48 # via ipython -proto-plus==1.24.0 +propcache==0.2.0 + # via yarl +proto-plus==1.25.0 # via # google-api-core # google-cloud-bigquery-storage @@ -599,9 +610,9 @@ psutil==5.9.0 # via # feast (setup.py) # ipykernel -psycopg[binary, pool]==3.1.19 +psycopg[binary, pool]==3.1.18 # via feast (setup.py) -psycopg-binary==3.1.19 +psycopg-binary==3.1.18 # via psycopg psycopg-pool==3.2.3 # via psycopg @@ -662,17 +673,17 @@ pymssql==2.3.1 # via feast (setup.py) pymysql==1.1.1 # via feast (setup.py) -pyodbc==5.1.0 +pyodbc==5.2.0 # via feast (setup.py) pyopenssl==24.2.1 # via snowflake-connector-python -pyparsing==3.1.4 +pyparsing==3.2.0 # via great-expectations -pyproject-hooks==1.1.0 +pyproject-hooks==1.2.0 # via # build # pip-tools -pyspark==3.5.2 +pyspark==3.5.3 # via feast (setup.py) pytest==7.4.4 # via @@ -790,7 +801,7 @@ rfc3986-validator==0.1.1 # via # jsonschema # jupyter-events -rich==13.8.1 +rich==13.9.3 # via ibis-framework rpds-py==0.20.0 # via @@ -800,24 +811,24 @@ rsa==4.9 # via google-auth ruamel-yaml==0.17.40 # via great-expectations -ruamel-yaml-clib==0.2.8 +ruamel-yaml-clib==0.2.12 # via ruamel-yaml -ruff==0.6.6 +ruff==0.7.1 # via feast (setup.py) -s3transfer==0.10.2 +s3transfer==0.10.3 # via boto3 scipy==1.13.1 # via great-expectations send2trash==1.8.3 # via jupyter-server -setuptools==75.1.0 +setuptools==75.2.0 # via # grpcio-tools # jupyterlab # kubernetes # pip-tools # singlestoredb -singlestoredb==1.6.3 +singlestoredb==1.7.2 # via feast (setup.py) six==1.16.0 # via @@ -826,7 +837,6 @@ six==1.16.0 # bleach # geomet # happybase - # isodate # kubernetes # mock # python-dateutil @@ -858,7 +868,7 @@ sphinxcontrib-qthelp==2.0.0 # via sphinx sphinxcontrib-serializinghtml==2.0.0 # via sphinx -sqlalchemy[mypy]==2.0.35 +sqlalchemy[mypy]==2.0.36 # via feast (setup.py) sqlglot==23.12.2 # via ibis-framework @@ -868,7 +878,7 @@ sqlparams==6.1.0 # via singlestoredb stack-data==0.6.3 # via ipython -starlette==0.40.0 +starlette==0.41.0 # via fastapi substrait==0.23.0 # via ibis-substrait @@ -884,11 +894,11 @@ testcontainers==4.4.0 # via feast (setup.py) thriftpy2==0.5.2 # via happybase -tinycss2==1.3.0 +tinycss2==1.4.0 # via nbconvert toml==0.10.2 # via feast (setup.py) -tomli==2.0.1 +tomli==2.0.2 # via # build # coverage @@ -933,7 +943,7 @@ traitlets==5.14.3 # nbclient # nbconvert # nbformat -trino==0.329.0 +trino==0.330.0 # via feast (setup.py) typeguard==4.3.0 # via feast (setup.py) @@ -947,19 +957,19 @@ types-pymysql==1.1.0.20240524 # via feast (setup.py) types-pyopenssl==24.1.0.20240722 # via types-redis -types-python-dateutil==2.9.0.20240906 +types-python-dateutil==2.9.0.20241003 # via # feast (setup.py) # arrow -types-pytz==2024.2.0.20240913 +types-pytz==2024.2.0.20241003 # via feast (setup.py) types-pyyaml==6.0.12.20240917 # via feast (setup.py) -types-redis==4.6.0.20240903 +types-redis==4.6.0.20241004 # via feast (setup.py) types-requests==2.30.0.0 # via feast (setup.py) -types-setuptools==75.1.0.20240917 +types-setuptools==75.2.0.20241019 # via # feast (setup.py) # types-cffi @@ -969,6 +979,7 @@ types-urllib3==1.26.25.14 # via types-requests typing-extensions==4.12.2 # via + # aioitertools # anyio # async-lru # azure-core @@ -977,6 +988,7 @@ typing-extensions==4.12.2 # fastapi # great-expectations # ibis-framework + # ipython # jwcrypto # multidict # mypy @@ -984,12 +996,14 @@ typing-extensions==4.12.2 # psycopg-pool # pydantic # pydantic-core + # rich # snowflake-connector-python # sqlalchemy + # starlette # testcontainers # typeguard # uvicorn -tzdata==2024.1 +tzdata==2024.2 # via pandas tzlocal==5.2 # via @@ -1008,14 +1022,15 @@ urllib3==1.26.20 # minio # requests # responses + # snowflake-connector-python # testcontainers -uvicorn[standard]==0.30.6 +uvicorn[standard]==0.32.0 # via # feast (setup.py) # uvicorn-worker uvicorn-worker==0.2.0 # via feast (setup.py) -uvloop==0.20.0 +uvloop==0.21.0 # via uvicorn virtualenv==20.23.0 # via @@ -1035,7 +1050,7 @@ websocket-client==1.8.0 # via # jupyter-server # kubernetes -websockets==13.0.1 +websockets==13.1 # via uvicorn werkzeug==3.0.4 # via moto @@ -1049,9 +1064,9 @@ wrapt==1.16.0 # via # aiobotocore # testcontainers -xmltodict==0.13.0 +xmltodict==0.14.2 # via moto -yarl==1.11.1 +yarl==1.16.0 # via aiohttp zipp==3.20.2 # via importlib-metadata diff --git a/sdk/python/requirements/py3.9-requirements.txt b/sdk/python/requirements/py3.9-requirements.txt index 16afecdfb5..7f8eecd6f8 100644 --- a/sdk/python/requirements/py3.9-requirements.txt +++ b/sdk/python/requirements/py3.9-requirements.txt @@ -2,7 +2,7 @@ # uv pip compile -p 3.9 --system --no-strip-extras setup.py --output-file sdk/python/requirements/py3.9-requirements.txt annotated-types==0.7.0 # via pydantic -anyio==4.5.0 +anyio==4.6.2.post1 # via # starlette # watchfiles @@ -10,34 +10,42 @@ attrs==24.2.0 # via # jsonschema # referencing -bigtree==0.21.1 +bigtree==0.21.3 + # via feast (setup.py) certifi==2024.8.30 # via requests -charset-normalizer==3.3.2 +charset-normalizer==3.4.0 # via requests click==8.1.7 # via + # feast (setup.py) # dask # uvicorn -cloudpickle==3.0.0 +cloudpickle==3.1.0 # via dask colorama==0.4.6 + # via feast (setup.py) dask[dataframe]==2024.8.0 - # via dask-expr + # via + # feast (setup.py) + # dask-expr dask-expr==1.1.10 # via dask -dill==0.3.8 +dill==0.3.9 + # via feast (setup.py) exceptiongroup==1.2.2 # via anyio -fastapi==0.115.2 -fsspec==2024.9.0 +fastapi==0.115.3 + # via feast (setup.py) +fsspec==2024.10.0 # via dask -greenlet==3.1.0 - # via sqlalchemy gunicorn==23.0.0 + # via + # feast (setup.py) + # uvicorn-worker h11==0.14.0 # via uvicorn -httptools==0.6.1 +httptools==0.6.4 # via uvicorn idna==3.10 # via @@ -48,20 +56,24 @@ importlib-metadata==8.5.0 # dask # typeguard jinja2==3.1.4 + # via feast (setup.py) jsonschema==4.23.0 -jsonschema-specifications==2023.12.1 + # via feast (setup.py) +jsonschema-specifications==2024.10.1 # via jsonschema locket==1.0.0 # via partd -markupsafe==2.1.5 +markupsafe==3.0.2 # via jinja2 -mmh3==5.0.0 -mypy==1.11.2 +mmh3==5.0.1 + # via feast (setup.py) +mypy==1.13.0 # via sqlalchemy mypy-extensions==1.0.0 # via mypy numpy==1.26.4 # via + # feast (setup.py) # dask # pandas # pyarrow @@ -69,23 +81,33 @@ packaging==24.1 # via # dask # gunicorn -pandas==2.2.2 +pandas==2.2.3 # via + # feast (setup.py) # dask # dask-expr partd==1.4.2 # via dask -prometheus-client==0.20.0 +prometheus-client==0.21.0 + # via feast (setup.py) protobuf==4.25.5 -psutil==6.0.0 + # via feast (setup.py) +psutil==6.1.0 + # via feast (setup.py) pyarrow==17.0.0 - # via dask-expr + # via + # feast (setup.py) + # dask-expr pydantic==2.9.2 - # via fastapi + # via + # feast (setup.py) + # fastapi pydantic-core==2.23.4 # via pydantic pygments==2.18.0 + # via feast (setup.py) pyjwt==2.9.0 + # via feast (setup.py) python-dateutil==2.9.0.post0 # via pandas python-dotenv==1.0.1 @@ -94,6 +116,7 @@ pytz==2024.2 # via pandas pyyaml==6.0.2 # via + # feast (setup.py) # dask # uvicorn referencing==0.35.1 @@ -101,6 +124,7 @@ referencing==0.35.1 # jsonschema # jsonschema-specifications requests==2.32.3 + # via feast (setup.py) rpds-py==0.20.0 # via # jsonschema @@ -109,20 +133,26 @@ six==1.16.0 # via python-dateutil sniffio==1.3.1 # via anyio -sqlalchemy[mypy]==2.0.35 -starlette==0.40.0 +sqlalchemy[mypy]==2.0.36 + # via feast (setup.py) +starlette==0.41.0 # via fastapi tabulate==0.9.0 + # via feast (setup.py) tenacity==8.5.0 + # via feast (setup.py) toml==0.10.2 -tomli==2.0.1 + # via feast (setup.py) +tomli==2.0.2 # via mypy -toolz==0.12.1 +toolz==1.0.0 # via # dask # partd tqdm==4.66.5 + # via feast (setup.py) typeguard==4.3.0 + # via feast (setup.py) typing-extensions==4.12.2 # via # anyio @@ -134,17 +164,21 @@ typing-extensions==4.12.2 # starlette # typeguard # uvicorn -tzdata==2024.1 +tzdata==2024.2 # via pandas urllib3==2.2.3 # via requests -uvicorn[standard]==0.30.6 -uvicorn-worker -uvloop==0.20.0 +uvicorn[standard]==0.32.0 + # via + # feast (setup.py) + # uvicorn-worker +uvicorn-worker==0.2.0 + # via feast (setup.py) +uvloop==0.21.0 # via uvicorn watchfiles==0.24.0 # via uvicorn -websockets==13.0.1 +websockets==13.1 # via uvicorn zipp==3.20.2 # via importlib-metadata diff --git a/setup.py b/setup.py index 5ab57c906c..96a6f311e5 100644 --- a/setup.py +++ b/setup.py @@ -160,7 +160,7 @@ "minio==7.1.0", "mock==2.0.0", "moto<5", - "mypy>=1.4.1", + "mypy>=1.4.1,<1.11.3", "urllib3>=1.25.4,<3", "psutil==5.9.0", "py>=1.11.0", # https://github.com/pytest-dev/pytest/issues/10420 From ba4404cecfb196c4084f9bf892cd3528184d42c1 Mon Sep 17 00:00:00 2001 From: Rob Howley Date: Thu, 24 Oct 2024 14:52:52 -0400 Subject: [PATCH 172/185] perf: Implement dynamo write_batch_async (#4675) * rebase Signed-off-by: Rob Howley * offline store init doesnt make sense Signed-off-by: Rob Howley * dont init or close Signed-off-by: Rob Howley * update test to handle event loop for dynamo case Signed-off-by: Rob Howley * use run util complete Signed-off-by: Rob Howley * fix: spelling sigh Signed-off-by: Rob Howley * run integration test as async since that is default for read Signed-off-by: Rob Howley * add pytest async to ci reqs Signed-off-by: Rob Howley * be safe w cleanup in test fixture Signed-off-by: Rob Howley * be safe w cleanup in test fixture Signed-off-by: Rob Howley * update pytest ini Signed-off-by: Rob Howley * not in a finally Signed-off-by: Rob Howley * remove close Signed-off-by: Rob Howley * test client is a lifespan aware context manager Signed-off-by: Rob Howley * add async writer for dynamo Signed-off-by: Rob Howley * fix dynamo client put item format Signed-off-by: Rob Howley * clarify documentation Signed-off-by: Rob Howley * add deduplication to async dynamo write Signed-off-by: Rob Howley --------- Signed-off-by: Rob Howley --- .../feast/infra/online_stores/dynamodb.py | 97 ++++++++++++++++--- sdk/python/feast/infra/utils/aws_utils.py | 64 ++++++++++++ .../test_push_features_to_online_store.py | 38 ++++++-- .../test_dynamodb_online_store.py | 23 +++++ 4 files changed, 201 insertions(+), 21 deletions(-) diff --git a/sdk/python/feast/infra/online_stores/dynamodb.py b/sdk/python/feast/infra/online_stores/dynamodb.py index a97e81bc44..15e8357754 100644 --- a/sdk/python/feast/infra/online_stores/dynamodb.py +++ b/sdk/python/feast/infra/online_stores/dynamodb.py @@ -15,6 +15,7 @@ import contextlib import itertools import logging +from collections import OrderedDict from datetime import datetime from typing import Any, Callable, Dict, List, Literal, Optional, Sequence, Tuple, Union @@ -26,6 +27,7 @@ from feast.infra.online_stores.helpers import compute_entity_id from feast.infra.online_stores.online_store import OnlineStore from feast.infra.supported_async_methods import SupportedAsyncMethods +from feast.infra.utils.aws_utils import dynamo_write_items_async from feast.protos.feast.core.DynamoDBTable_pb2 import ( DynamoDBTable as DynamoDBTableProto, ) @@ -103,7 +105,7 @@ async def close(self): @property def async_supported(self) -> SupportedAsyncMethods: - return SupportedAsyncMethods(read=True) + return SupportedAsyncMethods(read=True, write=True) def update( self, @@ -238,6 +240,42 @@ def online_write_batch( ) self._write_batch_non_duplicates(table_instance, data, progress, config) + async def online_write_batch_async( + self, + config: RepoConfig, + table: FeatureView, + data: List[ + Tuple[EntityKeyProto, Dict[str, ValueProto], datetime, Optional[datetime]] + ], + progress: Optional[Callable[[int], Any]], + ) -> None: + """ + Writes a batch of feature rows to the online store asynchronously. + + If a tz-naive timestamp is passed to this method, it is assumed to be UTC. + + Args: + config: The config for the current feature store. + table: Feature view to which these feature rows correspond. + data: A list of quadruplets containing feature data. Each quadruplet contains an entity + key, a dict containing feature values, an event timestamp for the row, and the created + timestamp for the row if it exists. + progress: Function to be called once a batch of rows is written to the online store, used + to show progress. + """ + online_config = config.online_store + assert isinstance(online_config, DynamoDBOnlineStoreConfig) + + table_name = _get_table_name(online_config, config, table) + items = [ + _to_client_write_item(config, entity_key, features, timestamp) + for entity_key, features, timestamp, _ in _latest_data_to_write(data) + ] + client = await _get_aiodynamodb_client( + online_config.region, config.online_store.max_pool_connections + ) + await dynamo_write_items_async(client, table_name, items) + def online_read( self, config: RepoConfig, @@ -419,19 +457,10 @@ def _write_batch_non_duplicates( """Deduplicate write batch request items on ``entity_id`` primary key.""" with table_instance.batch_writer(overwrite_by_pkeys=["entity_id"]) as batch: for entity_key, features, timestamp, created_ts in data: - entity_id = compute_entity_id( - entity_key, - entity_key_serialization_version=config.entity_key_serialization_version, - ) batch.put_item( - Item={ - "entity_id": entity_id, # PartitionKey - "event_ts": str(utils.make_tzaware(timestamp)), - "values": { - k: v.SerializeToString() - for k, v in features.items() # Serialized Features - }, - } + Item=_to_resource_write_item( + config, entity_key, features, timestamp + ) ) if progress: progress(1) @@ -675,3 +704,45 @@ def _get_dynamodb_resource(self, region: str, endpoint_url: Optional[str] = None region, endpoint_url ) return self._dynamodb_resource + + +def _to_resource_write_item(config, entity_key, features, timestamp): + entity_id = compute_entity_id( + entity_key, + entity_key_serialization_version=config.entity_key_serialization_version, + ) + return { + "entity_id": entity_id, # PartitionKey + "event_ts": str(utils.make_tzaware(timestamp)), + "values": { + k: v.SerializeToString() + for k, v in features.items() # Serialized Features + }, + } + + +def _to_client_write_item(config, entity_key, features, timestamp): + entity_id = compute_entity_id( + entity_key, + entity_key_serialization_version=config.entity_key_serialization_version, + ) + return { + "entity_id": {"S": entity_id}, # PartitionKey + "event_ts": {"S": str(utils.make_tzaware(timestamp))}, + "values": { + "M": { + k: {"B": v.SerializeToString()} + for k, v in features.items() # Serialized Features + } + }, + } + + +def _latest_data_to_write( + data: List[ + Tuple[EntityKeyProto, Dict[str, ValueProto], datetime, Optional[datetime]] + ], +): + as_hashable = ((d[0].SerializeToString(), d) for d in data) + sorted_data = sorted(as_hashable, key=lambda ah: (ah[0], ah[1][2])) + return (v for _, v in OrderedDict((ah[0], ah[1]) for ah in sorted_data).items()) diff --git a/sdk/python/feast/infra/utils/aws_utils.py b/sdk/python/feast/infra/utils/aws_utils.py index 8e1b182249..0526cf8b65 100644 --- a/sdk/python/feast/infra/utils/aws_utils.py +++ b/sdk/python/feast/infra/utils/aws_utils.py @@ -1,4 +1,6 @@ +import asyncio import contextlib +import itertools import os import tempfile import uuid @@ -10,6 +12,7 @@ import pyarrow as pa import pyarrow.parquet as pq from tenacity import ( + AsyncRetrying, retry, retry_if_exception_type, stop_after_attempt, @@ -1076,3 +1079,64 @@ def upload_arrow_table_to_athena( # Clean up S3 temporary data # for file_path in uploaded_files: # s3_resource.Object(bucket, file_path).delete() + + +class DynamoUnprocessedWriteItems(Exception): + pass + + +async def dynamo_write_items_async( + dynamo_client, table_name: str, items: list[dict] +) -> None: + """ + Writes in batches to a dynamo table asynchronously. Max size of each + attempted batch is 25. + Raises DynamoUnprocessedWriteItems if not all items can be written. + + Args: + dynamo_client: async dynamodb client + table_name: name of table being written to + items: list of items to be written. see boto3 docs on format of the items. + """ + DYNAMO_MAX_WRITE_BATCH_SIZE = 25 + + async def _do_write(items): + item_iter = iter(items) + item_batches = [] + while True: + item_batch = [ + item + for item in itertools.islice(item_iter, DYNAMO_MAX_WRITE_BATCH_SIZE) + ] + if not item_batch: + break + + item_batches.append(item_batch) + + return await asyncio.gather( + *[ + dynamo_client.batch_write_item( + RequestItems={table_name: item_batch}, + ) + for item_batch in item_batches + ] + ) + + put_items = [{"PutRequest": {"Item": item}} for item in items] + + retries = AsyncRetrying( + retry=retry_if_exception_type(DynamoUnprocessedWriteItems), + wait=wait_exponential(multiplier=1, max=4), + reraise=True, + ) + + async for attempt in retries: + with attempt: + response_batches = await _do_write(put_items) + + put_items = [] + for response in response_batches: + put_items.extend(response["UnprocessedItems"]) + + if put_items: + raise DynamoUnprocessedWriteItems() diff --git a/sdk/python/tests/integration/online_store/test_push_features_to_online_store.py b/sdk/python/tests/integration/online_store/test_push_features_to_online_store.py index 98fe3ab1ec..8986e21c57 100644 --- a/sdk/python/tests/integration/online_store/test_push_features_to_online_store.py +++ b/sdk/python/tests/integration/online_store/test_push_features_to_online_store.py @@ -8,29 +8,51 @@ from tests.integration.feature_repos.universal.entities import location -@pytest.mark.integration -@pytest.mark.universal_online_stores -def test_push_features_and_read(environment, universal_data_sources): +@pytest.fixture +def store(environment, universal_data_sources): store = environment.feature_store _, _, data_sources = universal_data_sources feature_views = construct_universal_feature_views(data_sources) location_fv = feature_views.pushed_locations store.apply([location(), location_fv]) + return store + +def _ingest_df(): data = { "location_id": [1], "temperature": [4], "event_timestamp": [pd.Timestamp(_utc_now()).round("ms")], "created": [pd.Timestamp(_utc_now()).round("ms")], } - df_ingest = pd.DataFrame(data) + return pd.DataFrame(data) - store.push("location_stats_push_source", df_ingest) + +def assert_response(online_resp): + online_resp_dict = online_resp.to_dict() + assert online_resp_dict["location_id"] == [1] + assert online_resp_dict["temperature"] == [4] + + +@pytest.mark.integration +@pytest.mark.universal_online_stores +def test_push_features_and_read(store): + store.push("location_stats_push_source", _ingest_df()) online_resp = store.get_online_features( features=["pushable_location_stats:temperature"], entity_rows=[{"location_id": 1}], ) - online_resp_dict = online_resp.to_dict() - assert online_resp_dict["location_id"] == [1] - assert online_resp_dict["temperature"] == [4] + assert_response(online_resp) + + +@pytest.mark.integration +@pytest.mark.universal_online_stores(only=["dynamodb"]) +async def test_push_features_and_read_async(store): + await store.push_async("location_stats_push_source", _ingest_df()) + + online_resp = await store.get_online_features_async( + features=["pushable_location_stats:temperature"], + entity_rows=[{"location_id": 1}], + ) + assert_response(online_resp) diff --git a/sdk/python/tests/unit/infra/online_store/test_dynamodb_online_store.py b/sdk/python/tests/unit/infra/online_store/test_dynamodb_online_store.py index 6ff7b3c360..cb1c15ee6e 100644 --- a/sdk/python/tests/unit/infra/online_store/test_dynamodb_online_store.py +++ b/sdk/python/tests/unit/infra/online_store/test_dynamodb_online_store.py @@ -1,5 +1,6 @@ from copy import deepcopy from dataclasses import dataclass +from datetime import datetime import boto3 import pytest @@ -10,6 +11,7 @@ DynamoDBOnlineStore, DynamoDBOnlineStoreConfig, DynamoDBTable, + _latest_data_to_write, ) from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto from feast.protos.feast.types.Value_pb2 import Value as ValueProto @@ -358,3 +360,24 @@ def test_dynamodb_online_store_online_read_unknown_entity_end_of_batch( # ensure the entity is not dropped assert len(returned_items) == len(entity_keys) assert returned_items[-1] == (None, None) + + +def test_batch_write_deduplication(): + def to_ek_proto(val): + return EntityKeyProto( + join_keys=["customer"], entity_values=[ValueProto(string_val=val)] + ) + + # is out of order and has duplicate keys + data = [ + (to_ek_proto("key-1"), {}, datetime(2024, 1, 1), None), + (to_ek_proto("key-2"), {}, datetime(2024, 1, 1), None), + (to_ek_proto("key-1"), {}, datetime(2024, 1, 3), None), + (to_ek_proto("key-1"), {}, datetime(2024, 1, 2), None), + (to_ek_proto("key-3"), {}, datetime(2024, 1, 2), None), + ] + + # assert we only keep the most recent record per key + actual = list(_latest_data_to_write(data)) + expected = [data[2], data[1], data[4]] + assert expected == actual From 8892235d029e12dec4f55ef2c9395e1a8d142730 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Oct 2024 14:52:59 -0400 Subject: [PATCH 173/185] chore: Bump sqlite-vec from 0.1.1 to 0.1.3 in /sdk/python/requirements (#4578) Bumps [sqlite-vec](https://TODO.com) from 0.1.1 to 0.1.3. --- updated-dependencies: - dependency-name: sqlite-vec dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdk/python/requirements/py3.10-ci-requirements.txt | 2 +- sdk/python/requirements/py3.11-ci-requirements.txt | 2 +- sdk/python/requirements/py3.9-ci-requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdk/python/requirements/py3.10-ci-requirements.txt b/sdk/python/requirements/py3.10-ci-requirements.txt index 1aede896ae..bc29f69671 100644 --- a/sdk/python/requirements/py3.10-ci-requirements.txt +++ b/sdk/python/requirements/py3.10-ci-requirements.txt @@ -864,7 +864,7 @@ sqlalchemy[mypy]==2.0.36 # via feast (setup.py) sqlglot==25.20.2 # via ibis-framework -sqlite-vec==0.1.1 +sqlite-vec==0.1.3 # via feast (setup.py) sqlparams==6.1.0 # via singlestoredb diff --git a/sdk/python/requirements/py3.11-ci-requirements.txt b/sdk/python/requirements/py3.11-ci-requirements.txt index 7a91c95c6c..a75b57f48e 100644 --- a/sdk/python/requirements/py3.11-ci-requirements.txt +++ b/sdk/python/requirements/py3.11-ci-requirements.txt @@ -855,7 +855,7 @@ sqlalchemy[mypy]==2.0.36 # via feast (setup.py) sqlglot==25.20.2 # via ibis-framework -sqlite-vec==0.1.1 +sqlite-vec==0.1.3 # via feast (setup.py) sqlparams==6.1.0 # via singlestoredb diff --git a/sdk/python/requirements/py3.9-ci-requirements.txt b/sdk/python/requirements/py3.9-ci-requirements.txt index ed77acb4aa..5b8086099c 100644 --- a/sdk/python/requirements/py3.9-ci-requirements.txt +++ b/sdk/python/requirements/py3.9-ci-requirements.txt @@ -872,7 +872,7 @@ sqlalchemy[mypy]==2.0.36 # via feast (setup.py) sqlglot==23.12.2 # via ibis-framework -sqlite-vec==0.1.1 +sqlite-vec==0.1.3 # via feast (setup.py) sqlparams==6.1.0 # via singlestoredb From 9f41fd6673b9576c802c2378b56e04b9a090d99d Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Thu, 24 Oct 2024 16:45:38 -0400 Subject: [PATCH 174/185] fix: Fix Feast project name test (#4685) * fix: Fix missing __init__.py file for repo ops project name test Signed-off-by: Francisco Javier Arceo * adjusting import Signed-off-by: Francisco Javier Arceo --------- Signed-off-by: Francisco Javier Arceo --- .../test_repo_operations_validate_feast_project_name.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename sdk/python/tests/unit/{sdk/python/feast => }/test_repo_operations_validate_feast_project_name.py (92%) diff --git a/sdk/python/tests/unit/sdk/python/feast/test_repo_operations_validate_feast_project_name.py b/sdk/python/tests/unit/test_repo_operations_validate_feast_project_name.py similarity index 92% rename from sdk/python/tests/unit/sdk/python/feast/test_repo_operations_validate_feast_project_name.py rename to sdk/python/tests/unit/test_repo_operations_validate_feast_project_name.py index ba4a60ddc0..0dc4b2651b 100644 --- a/sdk/python/tests/unit/sdk/python/feast/test_repo_operations_validate_feast_project_name.py +++ b/sdk/python/tests/unit/test_repo_operations_validate_feast_project_name.py @@ -1,4 +1,4 @@ -from sdk.python.feast.repo_operations import is_valid_name +from feast.repo_operations import is_valid_name def test_is_valid_name(): From 35fbdc91cc8a327be2a8cd162f7ccff983b16932 Mon Sep 17 00:00:00 2001 From: Daniele Martinoli <86618610+dmartinol@users.noreply.github.com> Date: Fri, 25 Oct 2024 01:41:27 +0200 Subject: [PATCH 175/185] fix: Updated README link (#4669) --- docs/reference/offline-stores/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/offline-stores/README.md b/docs/reference/offline-stores/README.md index 87c92bfcf8..2b62c4e1f1 100644 --- a/docs/reference/offline-stores/README.md +++ b/docs/reference/offline-stores/README.md @@ -6,8 +6,8 @@ Please see [Offline Store](../../getting-started/components/offline-store.md) fo [overview.md](overview.md) {% endcontent-ref %} -{% content-ref url="file.md" %} -[file.md](file.md) +{% content-ref url="dask.md" %} +[dask.md](dask.md) {% endcontent-ref %} {% content-ref url="snowflake.md" %} From 1443da447db157979aae324d709316a24bca58d8 Mon Sep 17 00:00:00 2001 From: Rob Howley Date: Fri, 25 Oct 2024 05:39:07 -0400 Subject: [PATCH 176/185] chore: Fix async get/push test setup (#4687) --- .../test_python_feature_server.py | 21 +++++++++++++----- .../online_store/test_universal_online.py | 22 +++++++++---------- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/sdk/python/tests/integration/online_store/test_python_feature_server.py b/sdk/python/tests/integration/online_store/test_python_feature_server.py index 8e69f719bc..3427a5f218 100644 --- a/sdk/python/tests/integration/online_store/test_python_feature_server.py +++ b/sdk/python/tests/integration/online_store/test_python_feature_server.py @@ -20,7 +20,10 @@ @pytest.mark.integration @pytest.mark.universal_online_stores -async def test_get_online_features(python_fs_client): +async def test_get_online_features(python_fs_client_w_fs): + python_fs_client, fs = python_fs_client_w_fs + await fs.initialize() + request_data_dict = { "features": [ "driver_stats:conv_rate", @@ -55,10 +58,15 @@ async def test_get_online_features(python_fs_client): == request_data_dict["entities"]["driver_id"] ) + await fs.close() + @pytest.mark.integration @pytest.mark.universal_online_stores -async def test_push(python_fs_client): +async def test_push(python_fs_client_w_fs): + python_fs_client, fs = python_fs_client_w_fs + + await fs.initialize() initial_temp = await _get_temperatures_from_feature_server( python_fs_client, location_ids=[1] ) @@ -84,11 +92,14 @@ async def test_push(python_fs_client): python_fs_client, location_ids=[1] ) assert actual == [initial_temp[0] * 100] + await fs.close() @pytest.mark.integration @pytest.mark.universal_online_stores -def test_push_source_does_not_exist(python_fs_client): +def test_push_source_does_not_exist(python_fs_client_w_fs): + python_fs_client, _ = python_fs_client_w_fs + with pytest.raises( PushSourceNotFoundException, match="Unable to find push source 'push_source_does_not_exist'", @@ -126,7 +137,7 @@ async def _get_temperatures_from_feature_server(client, location_ids: List[int]) @pytest.fixture -def python_fs_client(environment, universal_data_sources, request): +def python_fs_client_w_fs(environment, universal_data_sources, request): fs = environment.feature_store entities, datasets, data_sources = universal_data_sources feature_views = construct_universal_feature_views(data_sources) @@ -136,4 +147,4 @@ def python_fs_client(environment, universal_data_sources, request): fs.apply(feast_objects) fs.materialize(environment.start_date, environment.end_date) with TestClient(get_app(fs)) as client: - yield client + yield client, fs diff --git a/sdk/python/tests/integration/online_store/test_universal_online.py b/sdk/python/tests/integration/online_store/test_universal_online.py index 1a0803acff..c543645753 100644 --- a/sdk/python/tests/integration/online_store/test_universal_online.py +++ b/sdk/python/tests/integration/online_store/test_universal_online.py @@ -1,4 +1,3 @@ -import asyncio import datetime import os import time @@ -488,27 +487,28 @@ def test_online_retrieval_with_event_timestamps(environment, universal_data_sour @pytest.mark.integration @pytest.mark.universal_online_stores(only=["redis", "dynamodb", "postgres"]) -def test_async_online_retrieval_with_event_timestamps( +async def test_async_online_retrieval_with_event_timestamps( environment, universal_data_sources ): fs = setup_feature_store_universal_feature_views( environment, universal_data_sources ) + await fs.initialize() - response = asyncio.run( - fs.get_online_features_async( - features=[ - "driver_stats:avg_daily_trips", - "driver_stats:acc_rate", - "driver_stats:conv_rate", - ], - entity_rows=[{"driver_id": 1}, {"driver_id": 2}], - ) + response = await fs.get_online_features_async( + features=[ + "driver_stats:avg_daily_trips", + "driver_stats:acc_rate", + "driver_stats:conv_rate", + ], + entity_rows=[{"driver_id": 1}, {"driver_id": 2}], ) df = response.to_df(True) assert_feature_store_universal_feature_views_response(df) + await fs.close() + @pytest.mark.integration @pytest.mark.universal_online_stores From 658b18f31f537b4a747aa94ed92798807b32ba7a Mon Sep 17 00:00:00 2001 From: Rob Howley Date: Fri, 25 Oct 2024 16:21:37 -0400 Subject: [PATCH 177/185] chore: Make feature server test a unit test (#4694) * make feature server tests unit tests, remove integration Signed-off-by: Rob Howley * use local example repo for test Signed-off-by: Rob Howley * use example repo 1, switch to drive loc Signed-off-by: Rob Howley * missing timestamp in payload Signed-off-by: Rob Howley * combine tests Signed-off-by: Rob Howley * simplify the request bodies Signed-off-by: Rob Howley * use diff feature view Signed-off-by: Rob Howley * fix feature name Signed-off-by: Rob Howley * approx equal Signed-off-by: Rob Howley * approx equal Signed-off-by: Rob Howley * fix response format assertion Signed-off-by: Rob Howley * dont need lazy fixture, simplify Signed-off-by: Rob Howley --------- Signed-off-by: Rob Howley --- .../test_python_feature_server.py | 150 ------------------ sdk/python/tests/unit/test_feature_server.py | 147 ++++++++++++++--- 2 files changed, 124 insertions(+), 173 deletions(-) delete mode 100644 sdk/python/tests/integration/online_store/test_python_feature_server.py diff --git a/sdk/python/tests/integration/online_store/test_python_feature_server.py b/sdk/python/tests/integration/online_store/test_python_feature_server.py deleted file mode 100644 index 3427a5f218..0000000000 --- a/sdk/python/tests/integration/online_store/test_python_feature_server.py +++ /dev/null @@ -1,150 +0,0 @@ -import json -from typing import List - -import pytest -from fastapi.testclient import TestClient - -from feast.errors import PushSourceNotFoundException -from feast.feast_object import FeastObject -from feast.feature_server import get_app -from feast.utils import _utc_now -from tests.integration.feature_repos.repo_configuration import ( - construct_universal_feature_views, -) -from tests.integration.feature_repos.universal.entities import ( - customer, - driver, - location, -) - - -@pytest.mark.integration -@pytest.mark.universal_online_stores -async def test_get_online_features(python_fs_client_w_fs): - python_fs_client, fs = python_fs_client_w_fs - await fs.initialize() - - request_data_dict = { - "features": [ - "driver_stats:conv_rate", - "driver_stats:acc_rate", - "driver_stats:avg_daily_trips", - ], - "entities": {"driver_id": [5001, 5002]}, - } - response = python_fs_client.post( - "/get-online-features", data=json.dumps(request_data_dict) - ) - - # Check entities and features are present - parsed_response = json.loads(response.text) - assert "metadata" in parsed_response - metadata = parsed_response["metadata"] - expected_features = ["driver_id", "conv_rate", "acc_rate", "avg_daily_trips"] - response_feature_names = metadata["feature_names"] - assert len(response_feature_names) == len(expected_features) - for expected_feature in expected_features: - assert expected_feature in response_feature_names - assert "results" in parsed_response - results = parsed_response["results"] - for result in results: - # Same order as in metadata - assert len(result["statuses"]) == 2 # Requested two entities - for status in result["statuses"]: - assert status == "PRESENT" - results_driver_id_index = response_feature_names.index("driver_id") - assert ( - results[results_driver_id_index]["values"] - == request_data_dict["entities"]["driver_id"] - ) - - await fs.close() - - -@pytest.mark.integration -@pytest.mark.universal_online_stores -async def test_push(python_fs_client_w_fs): - python_fs_client, fs = python_fs_client_w_fs - - await fs.initialize() - initial_temp = await _get_temperatures_from_feature_server( - python_fs_client, location_ids=[1] - ) - json_data = json.dumps( - { - "push_source_name": "location_stats_push_source", - "df": { - "location_id": [1], - "temperature": [initial_temp[0] * 100], - "event_timestamp": [str(_utc_now())], - "created": [str(_utc_now())], - }, - } - ) - response = python_fs_client.post( - "/push", - data=json_data, - ) - - # Check new pushed temperature is fetched - assert response.status_code == 200 - actual = await _get_temperatures_from_feature_server( - python_fs_client, location_ids=[1] - ) - assert actual == [initial_temp[0] * 100] - await fs.close() - - -@pytest.mark.integration -@pytest.mark.universal_online_stores -def test_push_source_does_not_exist(python_fs_client_w_fs): - python_fs_client, _ = python_fs_client_w_fs - - with pytest.raises( - PushSourceNotFoundException, - match="Unable to find push source 'push_source_does_not_exist'", - ): - python_fs_client.post( - "/push", - data=json.dumps( - { - "push_source_name": "push_source_does_not_exist", - "df": { - "location_id": [1], - "temperature": [100], - "event_timestamp": [str(_utc_now())], - "created": [str(_utc_now())], - }, - } - ), - ) - - -async def _get_temperatures_from_feature_server(client, location_ids: List[int]): - get_request_data = { - "features": ["pushable_location_stats:temperature"], - "entities": {"location_id": location_ids}, - } - response = client.post("/get-online-features", data=json.dumps(get_request_data)) - parsed_response = json.loads(response.text) - assert "metadata" in parsed_response - metadata = parsed_response["metadata"] - response_feature_names = metadata["feature_names"] - assert "results" in parsed_response - results = parsed_response["results"] - results_temperature_index = response_feature_names.index("temperature") - return results[results_temperature_index]["values"] - - -@pytest.fixture -def python_fs_client_w_fs(environment, universal_data_sources, request): - fs = environment.feature_store - entities, datasets, data_sources = universal_data_sources - feature_views = construct_universal_feature_views(data_sources) - feast_objects: List[FeastObject] = [] - feast_objects.extend(feature_views.values()) - feast_objects.extend([driver(), customer(), location()]) - fs.apply(feast_objects) - fs.materialize(environment.start_date, environment.end_date) - with TestClient(get_app(fs)) as client: - yield client, fs diff --git a/sdk/python/tests/unit/test_feature_server.py b/sdk/python/tests/unit/test_feature_server.py index 34c3fc4068..e07cdc8655 100644 --- a/sdk/python/tests/unit/test_feature_server.py +++ b/sdk/python/tests/unit/test_feature_server.py @@ -1,14 +1,75 @@ import json -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock, MagicMock import pytest from fastapi.testclient import TestClient -from feast import FeatureStore from feast.data_source import PushMode +from feast.errors import PushSourceNotFoundException from feast.feature_server import get_app +from feast.online_response import OnlineResponse +from feast.protos.feast.serving.ServingService_pb2 import GetOnlineFeaturesResponse from feast.utils import _utc_now from tests.foo_provider import FooProvider +from tests.utils.cli_repo_creator import CliRunner, get_example_repo + + +@pytest.fixture +def mock_fs_factory(): + def builder(**async_support): + provider = FooProvider.with_async_support(**async_support) + fs = MagicMock() + fs._get_provider.return_value = provider + empty_response = OnlineResponse(GetOnlineFeaturesResponse(results=[])) + fs.get_online_features = MagicMock(return_value=empty_response) + fs.push = MagicMock() + fs.get_online_features_async = AsyncMock(return_value=empty_response) + fs.push_async = AsyncMock() + return fs + + return builder + + +@pytest.fixture +def test_client(): + runner = CliRunner() + with runner.local_repo( + get_example_repo("example_feature_repo_1.py"), "file" + ) as store: + yield TestClient(get_app(store)) + + +def get_online_features_body(): + return { + "features": [ + "pushed_driver_locations:driver_lat", + "pushed_driver_locations:driver_long", + ], + "entities": {"driver_id": [123]}, + } + + +def push_body(push_mode=PushMode.ONLINE, lat=42.0): + return { + "push_source_name": "driver_locations_push", + "df": { + "driver_lat": [lat], + "driver_long": ["42.0"], + "driver_id": [123], + "event_timestamp": [str(_utc_now())], + "created_timestamp": [str(_utc_now())], + }, + "to": push_mode.name.lower(), + } + + +@pytest.mark.parametrize("async_online_read", [True, False]) +def test_get_online_features_async_supported(async_online_read, mock_fs_factory): + fs = mock_fs_factory(online_read=async_online_read) + client = TestClient(get_app(fs)) + client.post("/get-online-features", json=get_online_features_body()) + assert fs.get_online_features.call_count == int(not async_online_read) + assert fs.get_online_features_async.await_count == int(async_online_read) @pytest.mark.parametrize( @@ -22,26 +83,66 @@ (False, PushMode.ONLINE, 0), ], ) -def test_push_online_async_supported(online_write, push_mode, async_count, environment): - push_payload = json.dumps( - { - "push_source_name": "location_stats_push_source", - "df": { - "location_id": [1], - "temperature": [100], - "event_timestamp": [str(_utc_now())], - "created": [str(_utc_now())], - }, - "to": push_mode.name.lower(), - } +def test_push_online_async_supported( + online_write, push_mode, async_count, mock_fs_factory +): + fs = mock_fs_factory(online_write=online_write) + client = TestClient(get_app(fs)) + client.post("/push", json=push_body(push_mode)) + assert fs.push.call_count == 1 - async_count + assert fs.push_async.await_count == async_count + + +async def test_push_and_get(test_client): + driver_lat = 55.1 + push_payload = push_body(lat=driver_lat) + response = test_client.post("/push", json=push_payload) + assert response.status_code == 200 + + # Check new pushed temperature is fetched + request_payload = get_online_features_body() + actual_resp = test_client.post("/get-online-features", json=request_payload) + actual = json.loads(actual_resp.text) + + ix = actual["metadata"]["feature_names"].index("driver_lat") + assert actual["results"][ix]["values"][0] == pytest.approx(driver_lat, 0.0001) + + assert_get_online_features_response_format( + actual, request_payload["entities"]["driver_id"][0] ) - provider = FooProvider.with_async_support(online_write=online_write) - with patch.object(FeatureStore, "_get_provider", return_value=provider): - fs = environment.feature_store - fs.push = MagicMock() - fs.push_async = AsyncMock() - client = TestClient(get_app(fs)) - client.post("/push", data=push_payload) - assert fs.push.call_count == 1 - async_count - assert fs.push_async.await_count == async_count + +def assert_get_online_features_response_format(parsed_response, expected_entity_id): + assert "metadata" in parsed_response + metadata = parsed_response["metadata"] + expected_features = ["driver_id", "driver_lat", "driver_long"] + response_feature_names = metadata["feature_names"] + assert len(response_feature_names) == len(expected_features) + for expected_feature in expected_features: + assert expected_feature in response_feature_names + assert "results" in parsed_response + results = parsed_response["results"] + for result in results: + # Same order as in metadata + assert len(result["statuses"]) == 1 # Requested one entity + for status in result["statuses"]: + assert status == "PRESENT" + results_driver_id_index = response_feature_names.index("driver_id") + assert results[results_driver_id_index]["values"][0] == expected_entity_id + + +def test_push_source_does_not_exist(test_client): + with pytest.raises( + PushSourceNotFoundException, + match="Unable to find push source 'push_source_does_not_exist'", + ): + test_client.post( + "/push", + json={ + "push_source_name": "push_source_does_not_exist", + "df": { + "any_data": [1], + "event_timestamp": [str(_utc_now())], + }, + }, + ) From 80a5b3c499faca8625d60267a34dcbddfe0c042a Mon Sep 17 00:00:00 2001 From: lokeshrangineni <19699092+lokeshrangineni@users.noreply.github.com> Date: Fri, 25 Oct 2024 16:58:41 -0400 Subject: [PATCH 178/185] feat: Adding SSL support for online server (#4677) * * Adding the SSL support for online_server. * Adding the SSL support for remote online client. * Adding the integration test to run the remote online server in SSL and non SSL mode. * Incorporated code review comments Signed-off-by: lrangine <19699092+lokeshrangineni@users.noreply.github.com> incorporating code review comments. Signed-off-by: lrangine <19699092+lokeshrangineni@users.noreply.github.com> * Incorporating code review comment. Signed-off-by: lrangine <19699092+lokeshrangineni@users.noreply.github.com> incorporating code review comments. Signed-off-by: lrangine <19699092+lokeshrangineni@users.noreply.github.com> * Update docs/reference/feature-servers/python-feature-server.md Co-authored-by: Francisco Arceo Signed-off-by: lrangine <19699092+lokeshrangineni@users.noreply.github.com> * * Update docs/reference/feature-servers/python-feature-server.md * fixing the integration test failure. Signed-off-by: lrangine <19699092+lokeshrangineni@users.noreply.github.com> --------- Signed-off-by: lrangine <19699092+lokeshrangineni@users.noreply.github.com> Co-authored-by: Francisco Arceo --- .../feature-servers/python-feature-server.md | 22 +++++ docs/reference/online-stores/remote.md | 3 + sdk/python/feast/cli.py | 25 ++++++ sdk/python/feast/feature_server.py | 35 ++++++-- sdk/python/feast/feature_store.py | 4 + .../feast/infra/online_stores/remote.py | 17 +++- .../online_store/test_remote_online_store.py | 83 ++++++++++++++----- .../tests/utils/auth_permissions_util.py | 23 ++++- .../generate_self_signed_certifcate_util.py | 73 ++++++++++++++++ 9 files changed, 248 insertions(+), 37 deletions(-) create mode 100644 sdk/python/tests/utils/generate_self_signed_certifcate_util.py diff --git a/docs/reference/feature-servers/python-feature-server.md b/docs/reference/feature-servers/python-feature-server.md index 255b85e606..bdba367833 100644 --- a/docs/reference/feature-servers/python-feature-server.md +++ b/docs/reference/feature-servers/python-feature-server.md @@ -200,6 +200,28 @@ requests.post( data=json.dumps(push_data)) ``` +## Starting the feature server in SSL mode + +Enabling SSL mode ensures that data between the Feast client and server is transmitted securely. For an ideal production environment, it is recommended to start the feature server in SSL mode. + +### Obtaining a self-signed SSL certificate and key +In development mode we can generate a self-signed certificate for testing. In an actual production environment it is always recommended to get it from a trusted SSL certificate provider. + +```shell +openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes +``` + +The above command will generate two files +* `key.pem` : certificate private key +* `cert.pem`: certificate public key + +### Starting the Online Server in SSL Mode +To start the feature server in SSL mode, you need to provide the private and public keys using the `--ssl-key-path` and `--ssl-cert-path` arguments with the `feast serve` command. + +```shell +feast serve --ssl-key-path key.pem --ssl-cert-path cert.pem +``` + # Online Feature Server Permissions and Access Control ## API Endpoints and Permissions diff --git a/docs/reference/online-stores/remote.md b/docs/reference/online-stores/remote.md index 4dd4fb65b5..61bb50793d 100644 --- a/docs/reference/online-stores/remote.md +++ b/docs/reference/online-stores/remote.md @@ -16,12 +16,15 @@ provider: local online_store: path: http://localhost:6566 type: remote + ssl_cert_path: /path/to/cert.pem entity_key_serialization_version: 2 auth: type: no_auth ``` {% endcode %} +`ssl_cert_path` is an optional configuration to the public certificate path when the online server starts in SSL mode. This may be needed if the online server is started with a self-signed certificate, typically this file ends with `*.crt`, `*.cer`, or `*.pem`. + ## How to configure Authentication and Authorization Please refer the [page](./../../../docs/getting-started/concepts/permission.md) for more details on how to configure authentication and authorization. diff --git a/sdk/python/feast/cli.py b/sdk/python/feast/cli.py index 010493f01c..ecb307b606 100644 --- a/sdk/python/feast/cli.py +++ b/sdk/python/feast/cli.py @@ -911,6 +911,22 @@ def init_command(project_directory, minimal: bool, template: str): default=5, show_default=True, ) +@click.option( + "--ssl-key-path", + "-k", + type=click.STRING, + default="", + show_default=False, + help="path to SSL certificate private key. You need to pass ssl-cert-path as well to start server in SSL mode", +) +@click.option( + "--ssl-cert-path", + "-c", + type=click.STRING, + default="", + show_default=False, + help="path to SSL certificate public key. You need to pass ssl-key-path as well to start server in SSL mode", +) @click.option( "--metrics", "-m", @@ -928,9 +944,16 @@ def serve_command( workers: int, metrics: bool, keep_alive_timeout: int, + ssl_key_path: str, + ssl_cert_path: str, registry_ttl_sec: int = 5, ): """Start a feature server locally on a given port.""" + if (ssl_key_path and not ssl_cert_path) or (not ssl_key_path and ssl_cert_path): + raise click.BadParameter( + "Please configure ssl-cert-path and ssl-key-path args to start the feature server in SSL mode." + ) + store = create_feature_store(ctx) store.serve( @@ -941,6 +964,8 @@ def serve_command( workers=workers, metrics=metrics, keep_alive_timeout=keep_alive_timeout, + ssl_key_path=ssl_key_path, + ssl_cert_path=ssl_cert_path, registry_ttl_sec=registry_ttl_sec, ) diff --git a/sdk/python/feast/feature_server.py b/sdk/python/feast/feature_server.py index f485d874e1..0502c2a85d 100644 --- a/sdk/python/feast/feature_server.py +++ b/sdk/python/feast/feature_server.py @@ -339,6 +339,8 @@ def start_server( workers: int, keep_alive_timeout: int, registry_ttl_sec: int, + ssl_key_path: str, + ssl_cert_path: str, metrics: bool, ): if metrics: @@ -364,16 +366,31 @@ def start_server( logger.debug("Auth manager initialized successfully") if sys.platform != "win32": - FeastServeApplication( - store=store, - bind=f"{host}:{port}", - accesslog=None if no_access_log else "-", - workers=workers, - keepalive=keep_alive_timeout, - registry_ttl_sec=registry_ttl_sec, - ).run() + options = { + "bind": f"{host}:{port}", + "accesslog": None if no_access_log else "-", + "workers": workers, + "keepalive": keep_alive_timeout, + "registry_ttl_sec": registry_ttl_sec, + } + + # Add SSL options if the paths exist + if ssl_key_path and ssl_cert_path: + options["keyfile"] = ssl_key_path + options["certfile"] = ssl_cert_path + FeastServeApplication(store=store, **options).run() else: import uvicorn app = get_app(store, registry_ttl_sec) - uvicorn.run(app, host=host, port=port, access_log=(not no_access_log)) + if ssl_key_path and ssl_cert_path: + uvicorn.run( + app, + host=host, + port=port, + access_log=(not no_access_log), + ssl_keyfile=ssl_key_path, + ssl_certfile=ssl_cert_path, + ) + else: + uvicorn.run(app, host=host, port=port, access_log=(not no_access_log)) diff --git a/sdk/python/feast/feature_store.py b/sdk/python/feast/feature_store.py index 033f39e1f2..876345c8bb 100644 --- a/sdk/python/feast/feature_store.py +++ b/sdk/python/feast/feature_store.py @@ -1896,6 +1896,8 @@ def serve( workers: int = 1, metrics: bool = False, keep_alive_timeout: int = 30, + ssl_key_path: str = "", + ssl_cert_path: str = "", registry_ttl_sec: int = 2, ) -> None: """Start the feature consumption server locally on a given port.""" @@ -1913,6 +1915,8 @@ def serve( workers=workers, metrics=metrics, keep_alive_timeout=keep_alive_timeout, + ssl_key_path=ssl_key_path, + ssl_cert_path=ssl_cert_path, registry_ttl_sec=registry_ttl_sec, ) diff --git a/sdk/python/feast/infra/online_stores/remote.py b/sdk/python/feast/infra/online_stores/remote.py index 8a7e299516..70edf93eb3 100644 --- a/sdk/python/feast/infra/online_stores/remote.py +++ b/sdk/python/feast/infra/online_stores/remote.py @@ -41,6 +41,10 @@ class RemoteOnlineStoreConfig(FeastConfigBaseModel): """ str: Path to metadata store. If type is 'remote', then this is a URL for registry server """ + ssl_cert_path: StrictStr = "" + """ str: Path to the public certificate when the online server starts in SSL mode. This may be needed if the online server started with a self-signed certificate, typically this file ends with `*.crt`, `*.cer`, or `*.pem`. + If type is 'remote', then this configuration is needed to connect to remote online server in SSL mode. """ + class RemoteOnlineStore(OnlineStore): """ @@ -170,6 +174,13 @@ def teardown( def get_remote_online_features( session: requests.Session, config: RepoConfig, req_body: str ) -> requests.Response: - return session.post( - f"{config.online_store.path}/get-online-features", data=req_body - ) + if config.online_store.ssl_cert_path: + return session.post( + f"{config.online_store.path}/get-online-features", + data=req_body, + verify=config.online_store.ssl_cert_path, + ) + else: + return session.post( + f"{config.online_store.path}/get-online-features", data=req_body + ) diff --git a/sdk/python/tests/integration/online_store/test_remote_online_store.py b/sdk/python/tests/integration/online_store/test_remote_online_store.py index d8c92077db..0c7894d112 100644 --- a/sdk/python/tests/integration/online_store/test_remote_online_store.py +++ b/sdk/python/tests/integration/online_store/test_remote_online_store.py @@ -15,11 +15,13 @@ start_feature_server, ) from tests.utils.cli_repo_creator import CliRunner +from tests.utils.generate_self_signed_certifcate_util import generate_self_signed_cert from tests.utils.http_server import free_port +@pytest.mark.parametrize("ssl_mode", [True, False]) @pytest.mark.integration -def test_remote_online_store_read(auth_config): +def test_remote_online_store_read(auth_config, ssl_mode): with tempfile.TemporaryDirectory() as remote_server_tmp_dir, tempfile.TemporaryDirectory() as remote_client_tmp_dir: permissions_list = [ Permission( @@ -41,11 +43,12 @@ def test_remote_online_store_read(auth_config): actions=[AuthzedAction.READ_ONLINE], ), ] - server_store, server_url, registry_path = ( + server_store, server_url, registry_path, ssl_cert_path = ( _create_server_store_spin_feature_server( temp_dir=remote_server_tmp_dir, auth_config=auth_config, permissions_list=permissions_list, + ssl_mode=ssl_mode, ) ) assert None not in (server_store, server_url, registry_path) @@ -54,6 +57,7 @@ def test_remote_online_store_read(auth_config): server_registry_path=str(registry_path), feature_server_url=server_url, auth_config=auth_config, + ssl_cert_path=ssl_cert_path, ) assert client_store is not None _assert_non_existing_entity_feature_views_entity( @@ -159,21 +163,46 @@ def _assert_client_server_online_stores_are_matching( def _create_server_store_spin_feature_server( - temp_dir, auth_config: str, permissions_list + temp_dir, auth_config: str, permissions_list, ssl_mode: bool ): store = default_store(str(temp_dir), auth_config, permissions_list) feast_server_port = free_port() + if ssl_mode: + certificates_path = tempfile.mkdtemp() + ssl_key_path = os.path.join(certificates_path, "key.pem") + ssl_cert_path = os.path.join(certificates_path, "cert.pem") + generate_self_signed_cert(cert_path=ssl_cert_path, key_path=ssl_key_path) + else: + ssl_key_path = "" + ssl_cert_path = "" + server_url = next( start_feature_server( - repo_path=str(store.repo_path), server_port=feast_server_port + repo_path=str(store.repo_path), + server_port=feast_server_port, + ssl_key_path=ssl_key_path, + ssl_cert_path=ssl_cert_path, ) ) - print(f"Server started successfully, {server_url}") - return store, server_url, os.path.join(store.repo_path, "data", "registry.db") + if ssl_cert_path and ssl_key_path: + print(f"Online Server started successfully in SSL mode, {server_url}") + else: + print(f"Server started successfully, {server_url}") + + return ( + store, + server_url, + os.path.join(store.repo_path, "data", "registry.db"), + ssl_cert_path, + ) def _create_remote_client_feature_store( - temp_dir, server_registry_path: str, feature_server_url: str, auth_config: str + temp_dir, + server_registry_path: str, + feature_server_url: str, + auth_config: str, + ssl_cert_path: str = "", ) -> FeatureStore: project_name = "REMOTE_ONLINE_CLIENT_PROJECT" runner = CliRunner() @@ -185,27 +214,35 @@ def _create_remote_client_feature_store( registry_path=server_registry_path, feature_server_url=feature_server_url, auth_config=auth_config, + ssl_cert_path=ssl_cert_path, ) return FeatureStore(repo_path=repo_path) def _overwrite_remote_client_feature_store_yaml( - repo_path: str, registry_path: str, feature_server_url: str, auth_config: str + repo_path: str, + registry_path: str, + feature_server_url: str, + auth_config: str, + ssl_cert_path: str = "", ): repo_config = os.path.join(repo_path, "feature_store.yaml") - with open(repo_config, "w") as repo_config: - repo_config.write( - dedent( - f""" - project: {PROJECT_NAME} - registry: {registry_path} - provider: local - online_store: - path: {feature_server_url} - type: remote - entity_key_serialization_version: 2 - """ - ) - + auth_config - ) + + config_content = "entity_key_serialization_version: 2\n" + auth_config + config_content += dedent( + f""" + project: {PROJECT_NAME} + registry: {registry_path} + provider: local + online_store: + path: {feature_server_url} + type: remote + """ + ) + + if ssl_cert_path: + config_content += f" ssl_cert_path: {ssl_cert_path}\n" + + with open(repo_config, "w") as repo_config_file: + repo_config_file.write(config_content) diff --git a/sdk/python/tests/utils/auth_permissions_util.py b/sdk/python/tests/utils/auth_permissions_util.py index 49ddd1b530..1147e66a0d 100644 --- a/sdk/python/tests/utils/auth_permissions_util.py +++ b/sdk/python/tests/utils/auth_permissions_util.py @@ -54,7 +54,13 @@ def default_store( return fs -def start_feature_server(repo_path: str, server_port: int, metrics: bool = False): +def start_feature_server( + repo_path: str, + server_port: int, + metrics: bool = False, + ssl_key_path: str = "", + ssl_cert_path: str = "", +): host = "0.0.0.0" cmd = [ "feast", @@ -65,6 +71,13 @@ def start_feature_server(repo_path: str, server_port: int, metrics: bool = False "--port", str(server_port), ] + + if ssl_cert_path and ssl_cert_path: + cmd.append("--ssl-key-path") + cmd.append(ssl_key_path) + cmd.append("--ssl-cert-path") + cmd.append(ssl_cert_path) + feast_server_process = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) @@ -91,7 +104,13 @@ def start_feature_server(repo_path: str, server_port: int, metrics: bool = False "localhost", 8000 ), "Prometheus server is running when it should be disabled." - yield f"http://localhost:{server_port}" + online_server_url = ( + f"https://localhost:{server_port}" + if ssl_key_path and ssl_cert_path + else f"http://localhost:{server_port}" + ) + + yield (online_server_url) if feast_server_process is not None: feast_server_process.kill() diff --git a/sdk/python/tests/utils/generate_self_signed_certifcate_util.py b/sdk/python/tests/utils/generate_self_signed_certifcate_util.py new file mode 100644 index 0000000000..1b0b212818 --- /dev/null +++ b/sdk/python/tests/utils/generate_self_signed_certifcate_util.py @@ -0,0 +1,73 @@ +import logging +from datetime import datetime, timedelta + +from cryptography import x509 +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.x509.oid import NameOID + +logger = logging.getLogger(__name__) + + +def generate_self_signed_cert( + cert_path="cert.pem", key_path="key.pem", common_name="localhost" +): + """ + Generate a self-signed certificate and save it to the specified paths. + + :param cert_path: Path to save the certificate (PEM format) + :param key_path: Path to save the private key (PEM format) + :param common_name: Common name (CN) for the certificate, defaults to 'localhost' + """ + # Generate private key + key = rsa.generate_private_key( + public_exponent=65537, key_size=2048, backend=default_backend() + ) + + # Create a self-signed certificate + subject = issuer = x509.Name( + [ + x509.NameAttribute(NameOID.COUNTRY_NAME, "US"), + x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "California"), + x509.NameAttribute(NameOID.LOCALITY_NAME, "San Francisco"), + x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Feast"), + x509.NameAttribute(NameOID.COMMON_NAME, common_name), + ] + ) + + certificate = ( + x509.CertificateBuilder() + .subject_name(subject) + .issuer_name(issuer) + .public_key(key.public_key()) + .serial_number(x509.random_serial_number()) + .not_valid_before(datetime.utcnow()) + .not_valid_after( + # Certificate valid for 1 year + datetime.utcnow() + timedelta(days=365) + ) + .add_extension( + x509.SubjectAlternativeName([x509.DNSName(common_name)]), + critical=False, + ) + .sign(key, hashes.SHA256(), default_backend()) + ) + + # Write the private key to a file + with open(key_path, "wb") as f: + f.write( + key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ) + ) + + # Write the certificate to a file + with open(cert_path, "wb") as f: + f.write(certificate.public_bytes(serialization.Encoding.PEM)) + + logger.info( + f"Self-signed certificate and private key have been generated at {cert_path} and {key_path}." + ) From 063dbb7cece7216de66030fb4a53ae7e07420df5 Mon Sep 17 00:00:00 2001 From: Rob Howley Date: Fri, 25 Oct 2024 17:37:53 -0400 Subject: [PATCH 179/185] chore: Address flakey online retrieval test (#4698) --- .../online_store/test_universal_online.py | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/sdk/python/tests/integration/online_store/test_universal_online.py b/sdk/python/tests/integration/online_store/test_universal_online.py index c543645753..a5493fbdb1 100644 --- a/sdk/python/tests/integration/online_store/test_universal_online.py +++ b/sdk/python/tests/integration/online_store/test_universal_online.py @@ -485,11 +485,7 @@ def test_online_retrieval_with_event_timestamps(environment, universal_data_sour assert_feature_store_universal_feature_views_response(df) -@pytest.mark.integration -@pytest.mark.universal_online_stores(only=["redis", "dynamodb", "postgres"]) -async def test_async_online_retrieval_with_event_timestamps( - environment, universal_data_sources -): +async def _do_async_retrieval_test(environment, universal_data_sources): fs = setup_feature_store_universal_feature_views( environment, universal_data_sources ) @@ -510,6 +506,22 @@ async def test_async_online_retrieval_with_event_timestamps( await fs.close() +@pytest.mark.integration +@pytest.mark.universal_online_stores(only=["redis", "postgres"]) +async def test_async_online_retrieval_with_event_timestamps( + environment, universal_data_sources +): + await _do_async_retrieval_test(environment, universal_data_sources) + + +@pytest.mark.integration +@pytest.mark.universal_online_stores(only=["dynamodb"]) +async def test_async_online_retrieval_with_event_timestamps_dynamo( + environment, universal_data_sources +): + await _do_async_retrieval_test(environment, universal_data_sources) + + @pytest.mark.integration @pytest.mark.universal_online_stores def test_online_list_retrieval(environment, universal_data_sources): From 7da0601b174b078721159e2cc6c5d11713bb6d48 Mon Sep 17 00:00:00 2001 From: Matt Green Date: Fri, 25 Oct 2024 16:41:25 -0700 Subject: [PATCH 180/185] docs: Add section on using denormalized for generating realtime features (#4697) --- docs/SUMMARY.md | 1 + docs/assets/feast-denormalized.png | Bin 0 -> 49406 bytes docs/reference/denormalized.md | 112 +++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+) create mode 100644 docs/assets/feast-denormalized.png create mode 100644 docs/reference/denormalized.md diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 1c4cece799..b7faf526c2 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -137,6 +137,7 @@ * [\[Beta\] On demand feature view](reference/beta-on-demand-feature-view.md) * [\[Alpha\] Vector Database](reference/alpha-vector-database.md) * [\[Alpha\] Data quality monitoring](reference/dqm.md) +* [\[Alpha\] Streaming feature computation with Denormalized](reference/denormalized.md) * [Feast CLI reference](reference/feast-cli-commands.md) * [Python API reference](http://rtd.feast.dev) * [Usage](reference/usage.md) diff --git a/docs/assets/feast-denormalized.png b/docs/assets/feast-denormalized.png new file mode 100644 index 0000000000000000000000000000000000000000..4cb36e73339aeec6eb6b1f9e5d4839d733dfbb34 GIT binary patch literal 49406 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 Date: Sat, 26 Oct 2024 00:41:25 +0000 Subject: [PATCH 181/185] chore(release): release 0.41.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # [0.41.0](https://github.com/feast-dev/feast/compare/v0.40.0...v0.41.0) (2024-10-26) * chore!: Update @elastic/eui and @emotion/react in Feast UI ([#4597](https://github.com/feast-dev/feast/issues/4597)) ([b9ddbf9](https://github.com/feast-dev/feast/commit/b9ddbf9351a55b1a5c102102b06ad3b2f28b3d1f)) ### Bug Fixes * Add --chdir to test_workflow.py ([#4453](https://github.com/feast-dev/feast/issues/4453)) ([6b2f026](https://github.com/feast-dev/feast/commit/6b2f026747b8adebe659aed3d4d2f95d551d5d1e)) * Add feast-operator files to semantic-release script ([#4382](https://github.com/feast-dev/feast/issues/4382)) ([8eceff2](https://github.com/feast-dev/feast/commit/8eceff26ba00fd446d27ad5ce2ee9d039c57fd9a)) * Add feast-operator Makefile to semantic-release script ([#4424](https://github.com/feast-dev/feast/issues/4424)) ([d18d01d](https://github.com/feast-dev/feast/commit/d18d01de8356ffdc29b35c93978f36e7541d415e)) * Added Offline Store Arrow client errors handler ([#4524](https://github.com/feast-dev/feast/issues/4524)) ([7535b40](https://github.com/feast-dev/feast/commit/7535b4036ce980c9a05bc33a9e61a7938ea1303e)) * Added Online Store REST client errors handler ([#4488](https://github.com/feast-dev/feast/issues/4488)) ([2118719](https://github.com/feast-dev/feast/commit/21187199173f4c4f5417205d99535af6be492a9a)) * Added Permission API docs ([#4485](https://github.com/feast-dev/feast/issues/4485)) ([2bd03fa](https://github.com/feast-dev/feast/commit/2bd03fa4da5e76f6b29b0b54b455d5552d256838)) * Added support for multiple name patterns to Permissions ([#4633](https://github.com/feast-dev/feast/issues/4633)) ([f05e928](https://github.com/feast-dev/feast/commit/f05e92861da64d1c5e9cfe3c6307b3422d0d83b8)) * Adding protobuf<5 as a required dependency due to snowflake limitations ([#4537](https://github.com/feast-dev/feast/issues/4537)) ([cecca83](https://github.com/feast-dev/feast/commit/cecca8360bed62ab2f4fddc5d3a888247ea0a87a)) * Avoid the python 3.9+ threadpool cleanup bug ([#4627](https://github.com/feast-dev/feast/issues/4627)) ([ba05893](https://github.com/feast-dev/feast/commit/ba05893ba6db2d8d1e7bcc8cf8162f4fb72c9563)) * Bigquery dataset create table disposition ([#4649](https://github.com/feast-dev/feast/issues/4649)) ([58e03d1](https://github.com/feast-dev/feast/commit/58e03d17448a883ef57bcd6d8926d1e54ddcdece)) * Changes template file path to relative path ([#4624](https://github.com/feast-dev/feast/issues/4624)) ([3e313b1](https://github.com/feast-dev/feast/commit/3e313b15efc7fc72d35d70315fc8b7c172fc7993)) * Check for snowflake functions when setting up materialization engine ([#4456](https://github.com/feast-dev/feast/issues/4456)) ([c365b4e](https://github.com/feast-dev/feast/commit/c365b4e71a16fb69883608c5f781c6d55502bb8e)) * Correctly handle list values in _python_value_to_proto_value ([#4608](https://github.com/feast-dev/feast/issues/4608)) ([c0a1026](https://github.com/feast-dev/feast/commit/c0a10269914c2ca01fe1cf6b24b120bfa58d04e7)) * Default to pandas mode if not specified in ODFV proto in database ([#4420](https://github.com/feast-dev/feast/issues/4420)) ([d235832](https://github.com/feast-dev/feast/commit/d235832b78027b98df8e8a9e434a51a0c78b3092)) * Deleting data from feast_metadata when we delete project ([#4550](https://github.com/feast-dev/feast/issues/4550)) ([351a2d0](https://github.com/feast-dev/feast/commit/351a2d0a7f9808178ab9d201083eb2894ce7384f)) * Disable active_timer When registry_ttl_sec is 0 ([#4499](https://github.com/feast-dev/feast/issues/4499)) ([c94f32f](https://github.com/feast-dev/feast/commit/c94f32f2b637c7b7d917d2456432180af7569cf5)) * Escape special characters in the Postgres password ([#4394](https://github.com/feast-dev/feast/issues/4394)) ([419ca5e](https://github.com/feast-dev/feast/commit/419ca5e9523ff38f27141b79ae12ebb0646c6617)) * FeastExtrasDependencyImportError when using SparkOfflineStore without S3 ([#4594](https://github.com/feast-dev/feast/issues/4594)) ([1ba94f7](https://github.com/feast-dev/feast/commit/1ba94f7e2018fea0114f1703dd3942d589071825)) * Fix Feast project name test ([#4685](https://github.com/feast-dev/feast/issues/4685)) ([9f41fd6](https://github.com/feast-dev/feast/commit/9f41fd6673b9576c802c2378b56e04b9a090d99d)) * Fix for SQL registry initialization fails [#4543](https://github.com/feast-dev/feast/issues/4543) ([#4544](https://github.com/feast-dev/feast/issues/4544)) ([4e2eacc](https://github.com/feast-dev/feast/commit/4e2eacc1beea8f8866b78968abadfd42eee63d6a)) * Fix gitignore issue ([#4674](https://github.com/feast-dev/feast/issues/4674)) ([2807dfa](https://github.com/feast-dev/feast/commit/2807dfaf46d3d9e79f84b0ff22dbaeede377c89b)) * Fix online pg import ([#4581](https://github.com/feast-dev/feast/issues/4581)) ([1f17caa](https://github.com/feast-dev/feast/commit/1f17caacdaa573d08dbf8dc68b20e73a187ed8a4)) * Fix the mypy type check issue. ([#4498](https://github.com/feast-dev/feast/issues/4498)) ([7ecc615](https://github.com/feast-dev/feast/commit/7ecc615945b7bb48e103ca6eb278b39759d71c5a)) * Fix vector store config ([#4583](https://github.com/feast-dev/feast/issues/4583)) ([11c00d4](https://github.com/feast-dev/feast/commit/11c00d43fd1b2c5caf4d49f705bd55c704edae8a)) * Fixes validator field access for 'project_id' in BigQuery offline Store ([#4509](https://github.com/feast-dev/feast/issues/4509)) ([9a0398e](https://github.com/feast-dev/feast/commit/9a0398e2e18585172d857cf3202a81551d31609b)) * Fixing failure of protos during ODFV transformations for missing entities ([#4667](https://github.com/feast-dev/feast/issues/4667)) ([41aaeeb](https://github.com/feast-dev/feast/commit/41aaeebaa5908f44cda28a4410e2fca412f53e92)) * Fixing the master branch build failure. ([#4563](https://github.com/feast-dev/feast/issues/4563)) ([0192b2e](https://github.com/feast-dev/feast/commit/0192b2eb245c8e0ea9a913195ddf28382dc23982)) * Hao xu request source timestamp_field ([#4495](https://github.com/feast-dev/feast/issues/4495)) ([96344b2](https://github.com/feast-dev/feast/commit/96344b2b6830dcc280567542d111d1b0f39879e0)) * Ignore the type check as both functions calls are not belonging to Feast code. ([#4500](https://github.com/feast-dev/feast/issues/4500)) ([867f532](https://github.com/feast-dev/feast/commit/867f532154977790e3bb11f2a94baa4f2289de99)) * Import grpc only for type checking in errors.py ([#4533](https://github.com/feast-dev/feast/issues/4533)) ([f308572](https://github.com/feast-dev/feast/commit/f308572715d0593951f71bb3da5c5be6de29a2f9)) * Initial commit targetting grpc registry server ([#4458](https://github.com/feast-dev/feast/issues/4458)) ([484240c](https://github.com/feast-dev/feast/commit/484240c4e783d68bc521b62b723c2dcbd00fab5e)), closes [#4465](https://github.com/feast-dev/feast/issues/4465) * Links to the RBAC documentation under Concepts and Components ([#4430](https://github.com/feast-dev/feast/issues/4430)) ([0a48f7b](https://github.com/feast-dev/feast/commit/0a48f7bb436febb0171c78a559a577eedeff421f)) * Locate feature_store.yaml from __file__ ([#4443](https://github.com/feast-dev/feast/issues/4443)) ([20290ce](https://github.com/feast-dev/feast/commit/20290ce28c513f705db1dbb6b0f719ba1846217f)) * Logger settings for feature servers and updated logger for permission flow ([#4531](https://github.com/feast-dev/feast/issues/4531)) ([50b8f23](https://github.com/feast-dev/feast/commit/50b8f238b6f9adbc9ff0b20e18b78b2948c2f440)) * Move tslib from devDependencies to dependencies in Feast UI ([#4525](https://github.com/feast-dev/feast/issues/4525)) ([c5a4d90](https://github.com/feast-dev/feast/commit/c5a4d907bf34f4cf7477b212cd2820b0e7d24b7b)) * Null value compatibility for unit timestamp list value type ([#4378](https://github.com/feast-dev/feast/issues/4378)) ([8f264b6](https://github.com/feast-dev/feast/commit/8f264b6807a07874dc01207c655baeef7dfaa7b2)) * Patch FAISS online return signature ([#4671](https://github.com/feast-dev/feast/issues/4671)) ([0d45e95](https://github.com/feast-dev/feast/commit/0d45e95767773846861d53feb4af4c6bc1451b5e)) * Quickstart documentation changes ([#4618](https://github.com/feast-dev/feast/issues/4618)) ([7ac0908](https://github.com/feast-dev/feast/commit/7ac0908f3846cc0ab05082f748506814c84b2e9c)) * Refactor auth_client_manager_factory.py in function get_auth_client_m… ([#4505](https://github.com/feast-dev/feast/issues/4505)) ([def8633](https://github.com/feast-dev/feast/commit/def863360bf0e553d242900ee915e953c6c3f9b6)) * Remote apply using offline store ([#4559](https://github.com/feast-dev/feast/issues/4559)) ([ac62a32](https://github.com/feast-dev/feast/commit/ac62a323c86fba8096eefde85775dd7a857e9e25)) * Remove Feast UI TypeScript dependencies from `peerDependencies` and `dependencies` ([#4554](https://github.com/feast-dev/feast/issues/4554)) ([e781e16](https://github.com/feast-dev/feast/commit/e781e1652cadc6576dbab369248d6e4afdb5f158)) * Remove unnecessary peer dependencies from Feast UI ([#4577](https://github.com/feast-dev/feast/issues/4577)) ([9ac7f4e](https://github.com/feast-dev/feast/commit/9ac7f4ea77357219fc8420f05e493767bc5357c2)) * Removed protobuf as a required dependency ([#4535](https://github.com/feast-dev/feast/issues/4535)) ([0fb76e9](https://github.com/feast-dev/feast/commit/0fb76e9041885659c68e294b0c033c62050bd374)) * Removed the k8s dependency from required dependencies ([#4519](https://github.com/feast-dev/feast/issues/4519)) ([3073ea5](https://github.com/feast-dev/feast/commit/3073ea5911339a5744be45512a9a2ee8b250292b)) * Removed usage of pull_request_target as much as possible to prevent security concerns ([#4549](https://github.com/feast-dev/feast/issues/4549)) ([3198371](https://github.com/feast-dev/feast/commit/3198371fc0e07f6b51b62c7e3abbc48729078bb9)) * Replaced ClusterRoles with local RoleBindings ([#4625](https://github.com/feast-dev/feast/issues/4625)) ([ca9fb9b](https://github.com/feast-dev/feast/commit/ca9fb9bc8c3f3b06b4ba5fce362e26633144715c)) * Retire pytz library ([#4406](https://github.com/feast-dev/feast/issues/4406)) ([23c6c86](https://github.com/feast-dev/feast/commit/23c6c862e1da4e9523530eb48c7ce79319dc442d)) * Typos related to k8s ([#4442](https://github.com/feast-dev/feast/issues/4442)) ([dda0088](https://github.com/feast-dev/feast/commit/dda0088f25eab5828613bd6d080aeddf681641f0)) * Update java testcontainers to use Compose V2 ([#4381](https://github.com/feast-dev/feast/issues/4381)) ([9a33fce](https://github.com/feast-dev/feast/commit/9a33fce695c54226b3afd7b998e284f358bab141)) * Update min versions for pyarrow and protobuf ([#4646](https://github.com/feast-dev/feast/issues/4646)) ([c7ddd4b](https://github.com/feast-dev/feast/commit/c7ddd4bb48290d6d9327fcb6349b84b4d14334af)) * Update react-router-dom to 6.3.0 and restrict its version in Feast UI ([#4556](https://github.com/feast-dev/feast/issues/4556)) ([4293608](https://github.com/feast-dev/feast/commit/42936084a7d214d65faea5359ae70eefda8d23ad)), closes [#3794](https://github.com/feast-dev/feast/issues/3794) [/github.com/remix-run/react-router/blob/main/CHANGELOG.md#v630](https://github.com//github.com/remix-run/react-router/blob/main/CHANGELOG.md/issues/v630) * Update the base image for feature-server. ([#4576](https://github.com/feast-dev/feast/issues/4576)) ([0390d8a](https://github.com/feast-dev/feast/commit/0390d8a86f50360a4df89165db62972328d22ca4)) * Update the base image of materilization engine. ([#4580](https://github.com/feast-dev/feast/issues/4580)) ([f8592d8](https://github.com/feast-dev/feast/commit/f8592d86b2903ebfebc505bbf9392927aae5609c)) * Updated README link ([#4669](https://github.com/feast-dev/feast/issues/4669)) ([35fbdc9](https://github.com/feast-dev/feast/commit/35fbdc91cc8a327be2a8cd162f7ccff983b16932)) * Updating the documentation and adding tests for project length ([#4628](https://github.com/feast-dev/feast/issues/4628)) ([945b0fa](https://github.com/feast-dev/feast/commit/945b0faadd40c8dc76d104bce14ee902bd513127)) * Using get_type_hints instead of inspect signature for udf return annotation ([#4391](https://github.com/feast-dev/feast/issues/4391)) ([3a32e8a](https://github.com/feast-dev/feast/commit/3a32e8ae28110db0934fc26ec6992eb606fed012)) * Using repo_config parameter in teardown to allow for feature-store-yaml overrides ([#4413](https://github.com/feast-dev/feast/issues/4413)) ([0baeeb5](https://github.com/feast-dev/feast/commit/0baeeb5ec524c1e6209edab9605ca8a098a2ec88)) * Validating permission to update an existing request on both the new and the old instance ([#4449](https://github.com/feast-dev/feast/issues/4449)) ([635a01b](https://github.com/feast-dev/feast/commit/635a01b4c77db781d67f9f5ebb1067806b1e2a13)) ### Features * Add boto3 session based auth for dynamodb online store for cross account access ([#4606](https://github.com/feast-dev/feast/issues/4606)) ([00eaf74](https://github.com/feast-dev/feast/commit/00eaf744f50f56f449c7ad8f6ceb92a09304ee0b)) * Add cli list/describe for SavedDatasets, StreamFeatureViews, & … ([#4487](https://github.com/feast-dev/feast/issues/4487)) ([7b250e5](https://github.com/feast-dev/feast/commit/7b250e5eff5de56f5c5da103e91051276940298a)) * Add connection_name field to Snowflake config ([#4600](https://github.com/feast-dev/feast/issues/4600)) ([10ce2aa](https://github.com/feast-dev/feast/commit/10ce2aa0050e419acfae27971a6fff87bade3ba4)) * Add health check service to registry server ([#4421](https://github.com/feast-dev/feast/issues/4421)) ([46655f0](https://github.com/feast-dev/feast/commit/46655f06de339ee09245de3e1a648eb4e3bcd729)) * Add more __repr__ methods ([#4676](https://github.com/feast-dev/feast/issues/4676)) ([e726c09](https://github.com/feast-dev/feast/commit/e726c096f2de93d6dc0a807c97c47476cc79dd61)) * Add registry methods for dealing with all FV types ([#4435](https://github.com/feast-dev/feast/issues/4435)) ([ac381b2](https://github.com/feast-dev/feast/commit/ac381b292cfa29804ee5f0822f876d227a0989d9)) * Added Project object to Feast Objects ([#4475](https://github.com/feast-dev/feast/issues/4475)) ([4a6b663](https://github.com/feast-dev/feast/commit/4a6b663f80bc91d6de35ed2ec428d34811d17a18)) * Added support for reading from Reader Endpoints for AWS Aurora use cases ([#4494](https://github.com/feast-dev/feast/issues/4494)) ([d793c77](https://github.com/feast-dev/feast/commit/d793c77d923df95a186b9d4829b167f1a5a304e6)) * Adding documentation for On Demand Feature Transformations with writes ([#4607](https://github.com/feast-dev/feast/issues/4607)) ([8e0c1b5](https://github.com/feast-dev/feast/commit/8e0c1b51665357d44229239401a04b76396b9047)) * Adding mode='python' for get_historical_features on ODFVs ([#4653](https://github.com/feast-dev/feast/issues/4653)) ([c40d539](https://github.com/feast-dev/feast/commit/c40d539b85fe537077dd26904d78624da3d33951)) * Adding registry cache support for get_on_demand_feature_view ([#4572](https://github.com/feast-dev/feast/issues/4572)) ([354c059](https://github.com/feast-dev/feast/commit/354c059e5475f9c3927d9180a421118507a22cf0)) * Adding SSL support for online server ([#4677](https://github.com/feast-dev/feast/issues/4677)) ([80a5b3c](https://github.com/feast-dev/feast/commit/80a5b3c499faca8625d60267a34dcbddfe0c042a)) * Adding write capability to online store to on demand feature views ([#4585](https://github.com/feast-dev/feast/issues/4585)) ([ef9e0bb](https://github.com/feast-dev/feast/commit/ef9e0bbdb2a80250786b87972a53c4cf5890bb76)), closes [#4603](https://github.com/feast-dev/feast/issues/4603) * Allow feast snowflake to read in byte string for private-key authentication ([#4384](https://github.com/feast-dev/feast/issues/4384)) ([5215a21](https://github.com/feast-dev/feast/commit/5215a2139a9d824dc2d8f45181bd177a1e8e9561)) * An action to test operator at PR time ([#4635](https://github.com/feast-dev/feast/issues/4635)) ([14c1000](https://github.com/feast-dev/feast/commit/14c1000554c590cb89ecb5ef44c57aa1b5dd1387)) * Create ADOPTERS.md ([#4410](https://github.com/feast-dev/feast/issues/4410)) ([721ec74](https://github.com/feast-dev/feast/commit/721ec74f17ee95e375054f21135e54e0687104a7)) * Create initial structure of Feast Go Operator ([#4596](https://github.com/feast-dev/feast/issues/4596)) ([b5ab6c7](https://github.com/feast-dev/feast/commit/b5ab6c799d529aaea19a196cedca7bb2c93cbbe9)) * Faiss and In memory store ([#4464](https://github.com/feast-dev/feast/issues/4464)) ([a1ff129](https://github.com/feast-dev/feast/commit/a1ff1290002376b4c9ac8ad14e60df4622bb47a4)) * Feast Security Model (aka RBAC) ([#4380](https://github.com/feast-dev/feast/issues/4380)) ([1771f66](https://github.com/feast-dev/feast/commit/1771f668247ef3b46ea7dac634e557e249bc1ba9)), closes [#36](https://github.com/feast-dev/feast/issues/36) * Instrument Feast using Prometheus and OpenTelemetry ([#4366](https://github.com/feast-dev/feast/issues/4366)) ([a571e08](https://github.com/feast-dev/feast/commit/a571e08b97a95f8543d7dea27902c135ab3a4378)) * Intra server to server communication ([#4433](https://github.com/feast-dev/feast/issues/4433)) ([729c874](https://github.com/feast-dev/feast/commit/729c874e8c30719f23ad287d3cb84f1d654274ec)) * Publish TypeScript types in Feast UI package ([#4551](https://github.com/feast-dev/feast/issues/4551)) ([334e5d7](https://github.com/feast-dev/feast/commit/334e5d78855709d4ca56619f16eecb414f88ce2d)) * Refactoring code to get oidc end points from discovery URL. ([#4429](https://github.com/feast-dev/feast/issues/4429)) ([896360a](https://github.com/feast-dev/feast/commit/896360af19a37c9a2a4634ec88021c4f69bdb141)) * Return entity key in the retrieval document api ([#4511](https://github.com/feast-dev/feast/issues/4511)) ([5f5caf0](https://github.com/feast-dev/feast/commit/5f5caf0cac539ed779692e0ec819659cf5a33a0d)) * Update roadmap.md ([#4445](https://github.com/feast-dev/feast/issues/4445)) ([34238d2](https://github.com/feast-dev/feast/commit/34238d2a0bfe9dbad753fec9613c83d848b1a520)) * Update sqlite-vec package ([#4389](https://github.com/feast-dev/feast/issues/4389)) ([b734cb1](https://github.com/feast-dev/feast/commit/b734cb147a4afd28407ec57d95f70ff604f82954)) * Updated Feast model Inference Architecture ([#4570](https://github.com/feast-dev/feast/issues/4570)) ([8cd0dcf](https://github.com/feast-dev/feast/commit/8cd0dcff0cc3b1387c7ca65018ebb09e03538242)) * Updating docs to include model inference guidelines ([#4416](https://github.com/feast-dev/feast/issues/4416)) ([cebbe04](https://github.com/feast-dev/feast/commit/cebbe045597b85e1ae4394a8c14741e88347a6b8)) * Updating FeatureViewProjection and OnDemandFeatureView to add batch_source and entities ([#4530](https://github.com/feast-dev/feast/issues/4530)) ([0795496](https://github.com/feast-dev/feast/commit/07954960c5501e2ecc1f1285ddf4aa68f9ac880b)) * Upgrade React from 17.0.2 to 18.3.1 in Feast UI ([#4620](https://github.com/feast-dev/feast/issues/4620)) ([d6f3cb8](https://github.com/feast-dev/feast/commit/d6f3cb81a4c2a51bae2d29a185753cf6b1d2b16d)) ### Performance Improvements * Add init and cleanup of long lived resources ([#4642](https://github.com/feast-dev/feast/issues/4642)) ([47dc04d](https://github.com/feast-dev/feast/commit/47dc04d43f483944f97248abaccd15dba319281f)) * Added indexes to sql tables to optimize query execution ([#4538](https://github.com/feast-dev/feast/issues/4538)) ([9688790](https://github.com/feast-dev/feast/commit/9688790a5e7a70f628a46021bde0201922c7e04d)) * Default to async endpoints, use threadpool for sync ([#4647](https://github.com/feast-dev/feast/issues/4647)) ([c1f1912](https://github.com/feast-dev/feast/commit/c1f19127ab5c22dfe67869990480e7e9d8183ab1)) * Implement dynamo write_batch_async ([#4675](https://github.com/feast-dev/feast/issues/4675)) ([ba4404c](https://github.com/feast-dev/feast/commit/ba4404cecfb196c4084f9bf892cd3528184d42c1)) * Make /push async ([#4650](https://github.com/feast-dev/feast/issues/4650)) ([61abf89](https://github.com/feast-dev/feast/commit/61abf894aca7aa52042c40e77f64b49835f4324e)) * Parallelize read calls by table and batch ([#4619](https://github.com/feast-dev/feast/issues/4619)) ([043eff1](https://github.com/feast-dev/feast/commit/043eff1a87bdf775a437503395acda87cbecf875)) ### BREAKING CHANGES * Consuming apps that use @elastic/eui should update it to a compatible version. If you use @elastic/eui components that have been renamed or replaced with others, you'll need to update your code accordingly. Signed-off-by: Harri Lehtola * chore: Update Node version from 17 to 20 in UI unit tests Node 17 is not an LTS (long-term support) version and apparently rejected by the latest versions of Elastic UI: > error @elastic/eui@95.12.0: The engine "node" is incompatible with > this module. Expected version "16.x || 18.x || >=20.x". Got "17.9.1" Let's try with the latest LTS version. Signed-off-by: Harri Lehtola --- CHANGELOG.md | 133 ++++ infra/charts/feast-feature-server/Chart.yaml | 2 +- infra/charts/feast-feature-server/README.md | 7 +- infra/charts/feast-feature-server/values.yaml | 2 +- infra/charts/feast/Chart.yaml | 2 +- infra/charts/feast/README.md | 6 +- .../feast/charts/feature-server/Chart.yaml | 4 +- .../feast/charts/feature-server/README.md | 4 +- .../feast/charts/feature-server/values.yaml | 2 +- .../charts/transformation-service/Chart.yaml | 4 +- .../charts/transformation-service/README.md | 4 +- .../charts/transformation-service/values.yaml | 2 +- infra/charts/feast/requirements.yaml | 4 +- infra/feast-helm-operator/Makefile | 2 +- .../config/manager/kustomization.yaml | 2 +- infra/feast-operator/Makefile | 2 +- .../config/manager/kustomization.yaml | 2 +- java/pom.xml | 2 +- sdk/python/feast/ui/package.json | 2 +- sdk/python/feast/ui/yarn.lock | 730 ++++++++++++------ ui/package.json | 2 +- 21 files changed, 675 insertions(+), 245 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b7c8be4b7..8368cf6718 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,138 @@ # Changelog +# [0.41.0](https://github.com/feast-dev/feast/compare/v0.40.0...v0.41.0) (2024-10-26) + + +* chore!: Update @elastic/eui and @emotion/react in Feast UI ([#4597](https://github.com/feast-dev/feast/issues/4597)) ([b9ddbf9](https://github.com/feast-dev/feast/commit/b9ddbf9351a55b1a5c102102b06ad3b2f28b3d1f)) + + +### Bug Fixes + +* Add --chdir to test_workflow.py ([#4453](https://github.com/feast-dev/feast/issues/4453)) ([6b2f026](https://github.com/feast-dev/feast/commit/6b2f026747b8adebe659aed3d4d2f95d551d5d1e)) +* Add feast-operator files to semantic-release script ([#4382](https://github.com/feast-dev/feast/issues/4382)) ([8eceff2](https://github.com/feast-dev/feast/commit/8eceff26ba00fd446d27ad5ce2ee9d039c57fd9a)) +* Add feast-operator Makefile to semantic-release script ([#4424](https://github.com/feast-dev/feast/issues/4424)) ([d18d01d](https://github.com/feast-dev/feast/commit/d18d01de8356ffdc29b35c93978f36e7541d415e)) +* Added Offline Store Arrow client errors handler ([#4524](https://github.com/feast-dev/feast/issues/4524)) ([7535b40](https://github.com/feast-dev/feast/commit/7535b4036ce980c9a05bc33a9e61a7938ea1303e)) +* Added Online Store REST client errors handler ([#4488](https://github.com/feast-dev/feast/issues/4488)) ([2118719](https://github.com/feast-dev/feast/commit/21187199173f4c4f5417205d99535af6be492a9a)) +* Added Permission API docs ([#4485](https://github.com/feast-dev/feast/issues/4485)) ([2bd03fa](https://github.com/feast-dev/feast/commit/2bd03fa4da5e76f6b29b0b54b455d5552d256838)) +* Added support for multiple name patterns to Permissions ([#4633](https://github.com/feast-dev/feast/issues/4633)) ([f05e928](https://github.com/feast-dev/feast/commit/f05e92861da64d1c5e9cfe3c6307b3422d0d83b8)) +* Adding protobuf<5 as a required dependency due to snowflake limitations ([#4537](https://github.com/feast-dev/feast/issues/4537)) ([cecca83](https://github.com/feast-dev/feast/commit/cecca8360bed62ab2f4fddc5d3a888247ea0a87a)) +* Avoid the python 3.9+ threadpool cleanup bug ([#4627](https://github.com/feast-dev/feast/issues/4627)) ([ba05893](https://github.com/feast-dev/feast/commit/ba05893ba6db2d8d1e7bcc8cf8162f4fb72c9563)) +* Bigquery dataset create table disposition ([#4649](https://github.com/feast-dev/feast/issues/4649)) ([58e03d1](https://github.com/feast-dev/feast/commit/58e03d17448a883ef57bcd6d8926d1e54ddcdece)) +* Changes template file path to relative path ([#4624](https://github.com/feast-dev/feast/issues/4624)) ([3e313b1](https://github.com/feast-dev/feast/commit/3e313b15efc7fc72d35d70315fc8b7c172fc7993)) +* Check for snowflake functions when setting up materialization engine ([#4456](https://github.com/feast-dev/feast/issues/4456)) ([c365b4e](https://github.com/feast-dev/feast/commit/c365b4e71a16fb69883608c5f781c6d55502bb8e)) +* Correctly handle list values in _python_value_to_proto_value ([#4608](https://github.com/feast-dev/feast/issues/4608)) ([c0a1026](https://github.com/feast-dev/feast/commit/c0a10269914c2ca01fe1cf6b24b120bfa58d04e7)) +* Default to pandas mode if not specified in ODFV proto in database ([#4420](https://github.com/feast-dev/feast/issues/4420)) ([d235832](https://github.com/feast-dev/feast/commit/d235832b78027b98df8e8a9e434a51a0c78b3092)) +* Deleting data from feast_metadata when we delete project ([#4550](https://github.com/feast-dev/feast/issues/4550)) ([351a2d0](https://github.com/feast-dev/feast/commit/351a2d0a7f9808178ab9d201083eb2894ce7384f)) +* Disable active_timer When registry_ttl_sec is 0 ([#4499](https://github.com/feast-dev/feast/issues/4499)) ([c94f32f](https://github.com/feast-dev/feast/commit/c94f32f2b637c7b7d917d2456432180af7569cf5)) +* Escape special characters in the Postgres password ([#4394](https://github.com/feast-dev/feast/issues/4394)) ([419ca5e](https://github.com/feast-dev/feast/commit/419ca5e9523ff38f27141b79ae12ebb0646c6617)) +* FeastExtrasDependencyImportError when using SparkOfflineStore without S3 ([#4594](https://github.com/feast-dev/feast/issues/4594)) ([1ba94f7](https://github.com/feast-dev/feast/commit/1ba94f7e2018fea0114f1703dd3942d589071825)) +* Fix Feast project name test ([#4685](https://github.com/feast-dev/feast/issues/4685)) ([9f41fd6](https://github.com/feast-dev/feast/commit/9f41fd6673b9576c802c2378b56e04b9a090d99d)) +* Fix for SQL registry initialization fails [#4543](https://github.com/feast-dev/feast/issues/4543) ([#4544](https://github.com/feast-dev/feast/issues/4544)) ([4e2eacc](https://github.com/feast-dev/feast/commit/4e2eacc1beea8f8866b78968abadfd42eee63d6a)) +* Fix gitignore issue ([#4674](https://github.com/feast-dev/feast/issues/4674)) ([2807dfa](https://github.com/feast-dev/feast/commit/2807dfaf46d3d9e79f84b0ff22dbaeede377c89b)) +* Fix online pg import ([#4581](https://github.com/feast-dev/feast/issues/4581)) ([1f17caa](https://github.com/feast-dev/feast/commit/1f17caacdaa573d08dbf8dc68b20e73a187ed8a4)) +* Fix the mypy type check issue. ([#4498](https://github.com/feast-dev/feast/issues/4498)) ([7ecc615](https://github.com/feast-dev/feast/commit/7ecc615945b7bb48e103ca6eb278b39759d71c5a)) +* Fix vector store config ([#4583](https://github.com/feast-dev/feast/issues/4583)) ([11c00d4](https://github.com/feast-dev/feast/commit/11c00d43fd1b2c5caf4d49f705bd55c704edae8a)) +* Fixes validator field access for 'project_id' in BigQuery offline Store ([#4509](https://github.com/feast-dev/feast/issues/4509)) ([9a0398e](https://github.com/feast-dev/feast/commit/9a0398e2e18585172d857cf3202a81551d31609b)) +* Fixing failure of protos during ODFV transformations for missing entities ([#4667](https://github.com/feast-dev/feast/issues/4667)) ([41aaeeb](https://github.com/feast-dev/feast/commit/41aaeebaa5908f44cda28a4410e2fca412f53e92)) +* Fixing the master branch build failure. ([#4563](https://github.com/feast-dev/feast/issues/4563)) ([0192b2e](https://github.com/feast-dev/feast/commit/0192b2eb245c8e0ea9a913195ddf28382dc23982)) +* Hao xu request source timestamp_field ([#4495](https://github.com/feast-dev/feast/issues/4495)) ([96344b2](https://github.com/feast-dev/feast/commit/96344b2b6830dcc280567542d111d1b0f39879e0)) +* Ignore the type check as both functions calls are not belonging to Feast code. ([#4500](https://github.com/feast-dev/feast/issues/4500)) ([867f532](https://github.com/feast-dev/feast/commit/867f532154977790e3bb11f2a94baa4f2289de99)) +* Import grpc only for type checking in errors.py ([#4533](https://github.com/feast-dev/feast/issues/4533)) ([f308572](https://github.com/feast-dev/feast/commit/f308572715d0593951f71bb3da5c5be6de29a2f9)) +* Initial commit targetting grpc registry server ([#4458](https://github.com/feast-dev/feast/issues/4458)) ([484240c](https://github.com/feast-dev/feast/commit/484240c4e783d68bc521b62b723c2dcbd00fab5e)), closes [#4465](https://github.com/feast-dev/feast/issues/4465) +* Links to the RBAC documentation under Concepts and Components ([#4430](https://github.com/feast-dev/feast/issues/4430)) ([0a48f7b](https://github.com/feast-dev/feast/commit/0a48f7bb436febb0171c78a559a577eedeff421f)) +* Locate feature_store.yaml from __file__ ([#4443](https://github.com/feast-dev/feast/issues/4443)) ([20290ce](https://github.com/feast-dev/feast/commit/20290ce28c513f705db1dbb6b0f719ba1846217f)) +* Logger settings for feature servers and updated logger for permission flow ([#4531](https://github.com/feast-dev/feast/issues/4531)) ([50b8f23](https://github.com/feast-dev/feast/commit/50b8f238b6f9adbc9ff0b20e18b78b2948c2f440)) +* Move tslib from devDependencies to dependencies in Feast UI ([#4525](https://github.com/feast-dev/feast/issues/4525)) ([c5a4d90](https://github.com/feast-dev/feast/commit/c5a4d907bf34f4cf7477b212cd2820b0e7d24b7b)) +* Null value compatibility for unit timestamp list value type ([#4378](https://github.com/feast-dev/feast/issues/4378)) ([8f264b6](https://github.com/feast-dev/feast/commit/8f264b6807a07874dc01207c655baeef7dfaa7b2)) +* Patch FAISS online return signature ([#4671](https://github.com/feast-dev/feast/issues/4671)) ([0d45e95](https://github.com/feast-dev/feast/commit/0d45e95767773846861d53feb4af4c6bc1451b5e)) +* Quickstart documentation changes ([#4618](https://github.com/feast-dev/feast/issues/4618)) ([7ac0908](https://github.com/feast-dev/feast/commit/7ac0908f3846cc0ab05082f748506814c84b2e9c)) +* Refactor auth_client_manager_factory.py in function get_auth_client_m… ([#4505](https://github.com/feast-dev/feast/issues/4505)) ([def8633](https://github.com/feast-dev/feast/commit/def863360bf0e553d242900ee915e953c6c3f9b6)) +* Remote apply using offline store ([#4559](https://github.com/feast-dev/feast/issues/4559)) ([ac62a32](https://github.com/feast-dev/feast/commit/ac62a323c86fba8096eefde85775dd7a857e9e25)) +* Remove Feast UI TypeScript dependencies from `peerDependencies` and `dependencies` ([#4554](https://github.com/feast-dev/feast/issues/4554)) ([e781e16](https://github.com/feast-dev/feast/commit/e781e1652cadc6576dbab369248d6e4afdb5f158)) +* Remove unnecessary peer dependencies from Feast UI ([#4577](https://github.com/feast-dev/feast/issues/4577)) ([9ac7f4e](https://github.com/feast-dev/feast/commit/9ac7f4ea77357219fc8420f05e493767bc5357c2)) +* Removed protobuf as a required dependency ([#4535](https://github.com/feast-dev/feast/issues/4535)) ([0fb76e9](https://github.com/feast-dev/feast/commit/0fb76e9041885659c68e294b0c033c62050bd374)) +* Removed the k8s dependency from required dependencies ([#4519](https://github.com/feast-dev/feast/issues/4519)) ([3073ea5](https://github.com/feast-dev/feast/commit/3073ea5911339a5744be45512a9a2ee8b250292b)) +* Removed usage of pull_request_target as much as possible to prevent security concerns ([#4549](https://github.com/feast-dev/feast/issues/4549)) ([3198371](https://github.com/feast-dev/feast/commit/3198371fc0e07f6b51b62c7e3abbc48729078bb9)) +* Replaced ClusterRoles with local RoleBindings ([#4625](https://github.com/feast-dev/feast/issues/4625)) ([ca9fb9b](https://github.com/feast-dev/feast/commit/ca9fb9bc8c3f3b06b4ba5fce362e26633144715c)) +* Retire pytz library ([#4406](https://github.com/feast-dev/feast/issues/4406)) ([23c6c86](https://github.com/feast-dev/feast/commit/23c6c862e1da4e9523530eb48c7ce79319dc442d)) +* Typos related to k8s ([#4442](https://github.com/feast-dev/feast/issues/4442)) ([dda0088](https://github.com/feast-dev/feast/commit/dda0088f25eab5828613bd6d080aeddf681641f0)) +* Update java testcontainers to use Compose V2 ([#4381](https://github.com/feast-dev/feast/issues/4381)) ([9a33fce](https://github.com/feast-dev/feast/commit/9a33fce695c54226b3afd7b998e284f358bab141)) +* Update min versions for pyarrow and protobuf ([#4646](https://github.com/feast-dev/feast/issues/4646)) ([c7ddd4b](https://github.com/feast-dev/feast/commit/c7ddd4bb48290d6d9327fcb6349b84b4d14334af)) +* Update react-router-dom to 6.3.0 and restrict its version in Feast UI ([#4556](https://github.com/feast-dev/feast/issues/4556)) ([4293608](https://github.com/feast-dev/feast/commit/42936084a7d214d65faea5359ae70eefda8d23ad)), closes [#3794](https://github.com/feast-dev/feast/issues/3794) [/github.com/remix-run/react-router/blob/main/CHANGELOG.md#v630](https://github.com//github.com/remix-run/react-router/blob/main/CHANGELOG.md/issues/v630) +* Update the base image for feature-server. ([#4576](https://github.com/feast-dev/feast/issues/4576)) ([0390d8a](https://github.com/feast-dev/feast/commit/0390d8a86f50360a4df89165db62972328d22ca4)) +* Update the base image of materilization engine. ([#4580](https://github.com/feast-dev/feast/issues/4580)) ([f8592d8](https://github.com/feast-dev/feast/commit/f8592d86b2903ebfebc505bbf9392927aae5609c)) +* Updated README link ([#4669](https://github.com/feast-dev/feast/issues/4669)) ([35fbdc9](https://github.com/feast-dev/feast/commit/35fbdc91cc8a327be2a8cd162f7ccff983b16932)) +* Updating the documentation and adding tests for project length ([#4628](https://github.com/feast-dev/feast/issues/4628)) ([945b0fa](https://github.com/feast-dev/feast/commit/945b0faadd40c8dc76d104bce14ee902bd513127)) +* Using get_type_hints instead of inspect signature for udf return annotation ([#4391](https://github.com/feast-dev/feast/issues/4391)) ([3a32e8a](https://github.com/feast-dev/feast/commit/3a32e8ae28110db0934fc26ec6992eb606fed012)) +* Using repo_config parameter in teardown to allow for feature-store-yaml overrides ([#4413](https://github.com/feast-dev/feast/issues/4413)) ([0baeeb5](https://github.com/feast-dev/feast/commit/0baeeb5ec524c1e6209edab9605ca8a098a2ec88)) +* Validating permission to update an existing request on both the new and the old instance ([#4449](https://github.com/feast-dev/feast/issues/4449)) ([635a01b](https://github.com/feast-dev/feast/commit/635a01b4c77db781d67f9f5ebb1067806b1e2a13)) + + +### Features + +* Add boto3 session based auth for dynamodb online store for cross account access ([#4606](https://github.com/feast-dev/feast/issues/4606)) ([00eaf74](https://github.com/feast-dev/feast/commit/00eaf744f50f56f449c7ad8f6ceb92a09304ee0b)) +* Add cli list/describe for SavedDatasets, StreamFeatureViews, & … ([#4487](https://github.com/feast-dev/feast/issues/4487)) ([7b250e5](https://github.com/feast-dev/feast/commit/7b250e5eff5de56f5c5da103e91051276940298a)) +* Add connection_name field to Snowflake config ([#4600](https://github.com/feast-dev/feast/issues/4600)) ([10ce2aa](https://github.com/feast-dev/feast/commit/10ce2aa0050e419acfae27971a6fff87bade3ba4)) +* Add health check service to registry server ([#4421](https://github.com/feast-dev/feast/issues/4421)) ([46655f0](https://github.com/feast-dev/feast/commit/46655f06de339ee09245de3e1a648eb4e3bcd729)) +* Add more __repr__ methods ([#4676](https://github.com/feast-dev/feast/issues/4676)) ([e726c09](https://github.com/feast-dev/feast/commit/e726c096f2de93d6dc0a807c97c47476cc79dd61)) +* Add registry methods for dealing with all FV types ([#4435](https://github.com/feast-dev/feast/issues/4435)) ([ac381b2](https://github.com/feast-dev/feast/commit/ac381b292cfa29804ee5f0822f876d227a0989d9)) +* Added Project object to Feast Objects ([#4475](https://github.com/feast-dev/feast/issues/4475)) ([4a6b663](https://github.com/feast-dev/feast/commit/4a6b663f80bc91d6de35ed2ec428d34811d17a18)) +* Added support for reading from Reader Endpoints for AWS Aurora use cases ([#4494](https://github.com/feast-dev/feast/issues/4494)) ([d793c77](https://github.com/feast-dev/feast/commit/d793c77d923df95a186b9d4829b167f1a5a304e6)) +* Adding documentation for On Demand Feature Transformations with writes ([#4607](https://github.com/feast-dev/feast/issues/4607)) ([8e0c1b5](https://github.com/feast-dev/feast/commit/8e0c1b51665357d44229239401a04b76396b9047)) +* Adding mode='python' for get_historical_features on ODFVs ([#4653](https://github.com/feast-dev/feast/issues/4653)) ([c40d539](https://github.com/feast-dev/feast/commit/c40d539b85fe537077dd26904d78624da3d33951)) +* Adding registry cache support for get_on_demand_feature_view ([#4572](https://github.com/feast-dev/feast/issues/4572)) ([354c059](https://github.com/feast-dev/feast/commit/354c059e5475f9c3927d9180a421118507a22cf0)) +* Adding SSL support for online server ([#4677](https://github.com/feast-dev/feast/issues/4677)) ([80a5b3c](https://github.com/feast-dev/feast/commit/80a5b3c499faca8625d60267a34dcbddfe0c042a)) +* Adding write capability to online store to on demand feature views ([#4585](https://github.com/feast-dev/feast/issues/4585)) ([ef9e0bb](https://github.com/feast-dev/feast/commit/ef9e0bbdb2a80250786b87972a53c4cf5890bb76)), closes [#4603](https://github.com/feast-dev/feast/issues/4603) +* Allow feast snowflake to read in byte string for private-key authentication ([#4384](https://github.com/feast-dev/feast/issues/4384)) ([5215a21](https://github.com/feast-dev/feast/commit/5215a2139a9d824dc2d8f45181bd177a1e8e9561)) +* An action to test operator at PR time ([#4635](https://github.com/feast-dev/feast/issues/4635)) ([14c1000](https://github.com/feast-dev/feast/commit/14c1000554c590cb89ecb5ef44c57aa1b5dd1387)) +* Create ADOPTERS.md ([#4410](https://github.com/feast-dev/feast/issues/4410)) ([721ec74](https://github.com/feast-dev/feast/commit/721ec74f17ee95e375054f21135e54e0687104a7)) +* Create initial structure of Feast Go Operator ([#4596](https://github.com/feast-dev/feast/issues/4596)) ([b5ab6c7](https://github.com/feast-dev/feast/commit/b5ab6c799d529aaea19a196cedca7bb2c93cbbe9)) +* Faiss and In memory store ([#4464](https://github.com/feast-dev/feast/issues/4464)) ([a1ff129](https://github.com/feast-dev/feast/commit/a1ff1290002376b4c9ac8ad14e60df4622bb47a4)) +* Feast Security Model (aka RBAC) ([#4380](https://github.com/feast-dev/feast/issues/4380)) ([1771f66](https://github.com/feast-dev/feast/commit/1771f668247ef3b46ea7dac634e557e249bc1ba9)), closes [#36](https://github.com/feast-dev/feast/issues/36) +* Instrument Feast using Prometheus and OpenTelemetry ([#4366](https://github.com/feast-dev/feast/issues/4366)) ([a571e08](https://github.com/feast-dev/feast/commit/a571e08b97a95f8543d7dea27902c135ab3a4378)) +* Intra server to server communication ([#4433](https://github.com/feast-dev/feast/issues/4433)) ([729c874](https://github.com/feast-dev/feast/commit/729c874e8c30719f23ad287d3cb84f1d654274ec)) +* Publish TypeScript types in Feast UI package ([#4551](https://github.com/feast-dev/feast/issues/4551)) ([334e5d7](https://github.com/feast-dev/feast/commit/334e5d78855709d4ca56619f16eecb414f88ce2d)) +* Refactoring code to get oidc end points from discovery URL. ([#4429](https://github.com/feast-dev/feast/issues/4429)) ([896360a](https://github.com/feast-dev/feast/commit/896360af19a37c9a2a4634ec88021c4f69bdb141)) +* Return entity key in the retrieval document api ([#4511](https://github.com/feast-dev/feast/issues/4511)) ([5f5caf0](https://github.com/feast-dev/feast/commit/5f5caf0cac539ed779692e0ec819659cf5a33a0d)) +* Update roadmap.md ([#4445](https://github.com/feast-dev/feast/issues/4445)) ([34238d2](https://github.com/feast-dev/feast/commit/34238d2a0bfe9dbad753fec9613c83d848b1a520)) +* Update sqlite-vec package ([#4389](https://github.com/feast-dev/feast/issues/4389)) ([b734cb1](https://github.com/feast-dev/feast/commit/b734cb147a4afd28407ec57d95f70ff604f82954)) +* Updated Feast model Inference Architecture ([#4570](https://github.com/feast-dev/feast/issues/4570)) ([8cd0dcf](https://github.com/feast-dev/feast/commit/8cd0dcff0cc3b1387c7ca65018ebb09e03538242)) +* Updating docs to include model inference guidelines ([#4416](https://github.com/feast-dev/feast/issues/4416)) ([cebbe04](https://github.com/feast-dev/feast/commit/cebbe045597b85e1ae4394a8c14741e88347a6b8)) +* Updating FeatureViewProjection and OnDemandFeatureView to add batch_source and entities ([#4530](https://github.com/feast-dev/feast/issues/4530)) ([0795496](https://github.com/feast-dev/feast/commit/07954960c5501e2ecc1f1285ddf4aa68f9ac880b)) +* Upgrade React from 17.0.2 to 18.3.1 in Feast UI ([#4620](https://github.com/feast-dev/feast/issues/4620)) ([d6f3cb8](https://github.com/feast-dev/feast/commit/d6f3cb81a4c2a51bae2d29a185753cf6b1d2b16d)) + + +### Performance Improvements + +* Add init and cleanup of long lived resources ([#4642](https://github.com/feast-dev/feast/issues/4642)) ([47dc04d](https://github.com/feast-dev/feast/commit/47dc04d43f483944f97248abaccd15dba319281f)) +* Added indexes to sql tables to optimize query execution ([#4538](https://github.com/feast-dev/feast/issues/4538)) ([9688790](https://github.com/feast-dev/feast/commit/9688790a5e7a70f628a46021bde0201922c7e04d)) +* Default to async endpoints, use threadpool for sync ([#4647](https://github.com/feast-dev/feast/issues/4647)) ([c1f1912](https://github.com/feast-dev/feast/commit/c1f19127ab5c22dfe67869990480e7e9d8183ab1)) +* Implement dynamo write_batch_async ([#4675](https://github.com/feast-dev/feast/issues/4675)) ([ba4404c](https://github.com/feast-dev/feast/commit/ba4404cecfb196c4084f9bf892cd3528184d42c1)) +* Make /push async ([#4650](https://github.com/feast-dev/feast/issues/4650)) ([61abf89](https://github.com/feast-dev/feast/commit/61abf894aca7aa52042c40e77f64b49835f4324e)) +* Parallelize read calls by table and batch ([#4619](https://github.com/feast-dev/feast/issues/4619)) ([043eff1](https://github.com/feast-dev/feast/commit/043eff1a87bdf775a437503395acda87cbecf875)) + + +### BREAKING CHANGES + +* Consuming apps that use @elastic/eui should update it +to a compatible version. If you use @elastic/eui components that have +been renamed or replaced with others, you'll need to update your code +accordingly. + +Signed-off-by: Harri Lehtola + +* chore: Update Node version from 17 to 20 in UI unit tests + +Node 17 is not an LTS (long-term support) version and apparently +rejected by the latest versions of Elastic UI: + +> error @elastic/eui@95.12.0: The engine "node" is incompatible with +> this module. Expected version "16.x || 18.x || >=20.x". Got "17.9.1" + +Let's try with the latest LTS version. + +Signed-off-by: Harri Lehtola + # [0.40.0](https://github.com/feast-dev/feast/compare/v0.39.0...v0.40.0) (2024-07-31) diff --git a/infra/charts/feast-feature-server/Chart.yaml b/infra/charts/feast-feature-server/Chart.yaml index af47692f16..dd547843d1 100644 --- a/infra/charts/feast-feature-server/Chart.yaml +++ b/infra/charts/feast-feature-server/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 name: feast-feature-server description: Feast Feature Server in Go or Python type: application -version: 0.40.0 +version: 0.41.0 keywords: - machine learning - big data diff --git a/infra/charts/feast-feature-server/README.md b/infra/charts/feast-feature-server/README.md index bff7820d1f..a36f59d85e 100644 --- a/infra/charts/feast-feature-server/README.md +++ b/infra/charts/feast-feature-server/README.md @@ -1,6 +1,6 @@ # Feast Python / Go Feature Server Helm Charts -Current chart version is `0.40.0` +Current chart version is `0.41.0` ## Installation @@ -40,16 +40,16 @@ See [here](https://github.com/feast-dev/feast/tree/master/examples/python-helm-d | fullnameOverride | string | `""` | | | image.pullPolicy | string | `"IfNotPresent"` | | | image.repository | string | `"feastdev/feature-server"` | Docker image for Feature Server repository | -| image.tag | string | `"0.40.0"` | The Docker image tag (can be overwritten if custom feature server deps are needed for on demand transforms) | +| image.tag | string | `"0.41.0"` | The Docker image tag (can be overwritten if custom feature server deps are needed for on demand transforms) | | imagePullSecrets | list | `[]` | | | livenessProbe.initialDelaySeconds | int | `30` | | | livenessProbe.periodSeconds | int | `30` | | +| logLevel | string | `"WARNING"` | | | metrics.enabled | bool | `false` | | | metrics.otelCollector.endpoint | string | `""` | | | metrics.otelCollector.port | int | `4317` | | | nameOverride | string | `""` | | | nodeSelector | object | `{}` | | -| otel_service.name | string | `"otelcol"` | | | podAnnotations | object | `{}` | | | podSecurityContext | object | `{}` | | | readinessProbe.initialDelaySeconds | int | `20` | | @@ -59,4 +59,5 @@ See [here](https://github.com/feast-dev/feast/tree/master/examples/python-helm-d | securityContext | object | `{}` | | | service.port | int | `80` | | | service.type | string | `"ClusterIP"` | | +| serviceAccount.name | string | `""` | | | tolerations | list | `[]` | | \ No newline at end of file diff --git a/infra/charts/feast-feature-server/values.yaml b/infra/charts/feast-feature-server/values.yaml index f0bc55a646..d894177558 100644 --- a/infra/charts/feast-feature-server/values.yaml +++ b/infra/charts/feast-feature-server/values.yaml @@ -9,7 +9,7 @@ image: repository: feastdev/feature-server pullPolicy: IfNotPresent # image.tag -- The Docker image tag (can be overwritten if custom feature server deps are needed for on demand transforms) - tag: 0.40.0 + tag: 0.41.0 logLevel: "WARNING" # Set log level DEBUG, INFO, WARNING, ERROR, and CRITICAL (case-insensitive) diff --git a/infra/charts/feast/Chart.yaml b/infra/charts/feast/Chart.yaml index 1f030ca617..a192da8911 100644 --- a/infra/charts/feast/Chart.yaml +++ b/infra/charts/feast/Chart.yaml @@ -1,7 +1,7 @@ apiVersion: v1 description: Feature store for machine learning name: feast -version: 0.40.0 +version: 0.41.0 keywords: - machine learning - big data diff --git a/infra/charts/feast/README.md b/infra/charts/feast/README.md index bf8e4d7b8d..e49fbf6d96 100644 --- a/infra/charts/feast/README.md +++ b/infra/charts/feast/README.md @@ -8,7 +8,7 @@ This repo contains Helm charts for Feast Java components that are being installe ## Chart: Feast -Feature store for machine learning Current chart version is `0.40.0` +Feature store for machine learning Current chart version is `0.41.0` ## Installation @@ -65,8 +65,8 @@ See [here](https://github.com/feast-dev/feast/tree/master/examples/java-demo) fo | Repository | Name | Version | |------------|------|---------| | https://charts.helm.sh/stable | redis | 10.5.6 | -| https://feast-helm-charts.storage.googleapis.com | feature-server(feature-server) | 0.40.0 | -| https://feast-helm-charts.storage.googleapis.com | transformation-service(transformation-service) | 0.40.0 | +| https://feast-helm-charts.storage.googleapis.com | feature-server(feature-server) | 0.41.0 | +| https://feast-helm-charts.storage.googleapis.com | transformation-service(transformation-service) | 0.41.0 | ## Values diff --git a/infra/charts/feast/charts/feature-server/Chart.yaml b/infra/charts/feast/charts/feature-server/Chart.yaml index f91185be84..69748a362f 100644 --- a/infra/charts/feast/charts/feature-server/Chart.yaml +++ b/infra/charts/feast/charts/feature-server/Chart.yaml @@ -1,8 +1,8 @@ apiVersion: v1 description: "Feast Feature Server: Online feature serving service for Feast" name: feature-server -version: 0.40.0 -appVersion: v0.40.0 +version: 0.41.0 +appVersion: v0.41.0 keywords: - machine learning - big data diff --git a/infra/charts/feast/charts/feature-server/README.md b/infra/charts/feast/charts/feature-server/README.md index c75fc421c6..ab77911a8f 100644 --- a/infra/charts/feast/charts/feature-server/README.md +++ b/infra/charts/feast/charts/feature-server/README.md @@ -1,6 +1,6 @@ # feature-server -![Version: 0.40.0](https://img.shields.io/badge/Version-0.40.0-informational?style=flat-square) ![AppVersion: v0.40.0](https://img.shields.io/badge/AppVersion-v0.40.0-informational?style=flat-square) +![Version: 0.41.0](https://img.shields.io/badge/Version-0.41.0-informational?style=flat-square) ![AppVersion: v0.41.0](https://img.shields.io/badge/AppVersion-v0.41.0-informational?style=flat-square) Feast Feature Server: Online feature serving service for Feast @@ -17,7 +17,7 @@ Feast Feature Server: Online feature serving service for Feast | envOverrides | object | `{}` | Extra environment variables to set | | image.pullPolicy | string | `"IfNotPresent"` | Image pull policy | | image.repository | string | `"feastdev/feature-server-java"` | Docker image for Feature Server repository | -| image.tag | string | `"0.40.0"` | Image tag | +| image.tag | string | `"0.41.0"` | Image tag | | ingress.grpc.annotations | object | `{}` | Extra annotations for the ingress | | ingress.grpc.auth.enabled | bool | `false` | Flag to enable auth | | ingress.grpc.class | string | `"nginx"` | Which ingress controller to use | diff --git a/infra/charts/feast/charts/feature-server/values.yaml b/infra/charts/feast/charts/feature-server/values.yaml index d9c964bbca..646d735ef8 100644 --- a/infra/charts/feast/charts/feature-server/values.yaml +++ b/infra/charts/feast/charts/feature-server/values.yaml @@ -5,7 +5,7 @@ image: # image.repository -- Docker image for Feature Server repository repository: feastdev/feature-server-java # image.tag -- Image tag - tag: 0.40.0 + tag: 0.41.0 # image.pullPolicy -- Image pull policy pullPolicy: IfNotPresent diff --git a/infra/charts/feast/charts/transformation-service/Chart.yaml b/infra/charts/feast/charts/transformation-service/Chart.yaml index 7e336e7a3e..6c450852cb 100644 --- a/infra/charts/feast/charts/transformation-service/Chart.yaml +++ b/infra/charts/feast/charts/transformation-service/Chart.yaml @@ -1,8 +1,8 @@ apiVersion: v1 description: "Transformation service: to compute on-demand features" name: transformation-service -version: 0.40.0 -appVersion: v0.40.0 +version: 0.41.0 +appVersion: v0.41.0 keywords: - machine learning - big data diff --git a/infra/charts/feast/charts/transformation-service/README.md b/infra/charts/feast/charts/transformation-service/README.md index f90d5bda18..a00a21f034 100644 --- a/infra/charts/feast/charts/transformation-service/README.md +++ b/infra/charts/feast/charts/transformation-service/README.md @@ -1,6 +1,6 @@ # transformation-service -![Version: 0.40.0](https://img.shields.io/badge/Version-0.40.0-informational?style=flat-square) ![AppVersion: v0.40.0](https://img.shields.io/badge/AppVersion-v0.40.0-informational?style=flat-square) +![Version: 0.41.0](https://img.shields.io/badge/Version-0.41.0-informational?style=flat-square) ![AppVersion: v0.41.0](https://img.shields.io/badge/AppVersion-v0.41.0-informational?style=flat-square) Transformation service: to compute on-demand features @@ -13,7 +13,7 @@ Transformation service: to compute on-demand features | envOverrides | object | `{}` | Extra environment variables to set | | image.pullPolicy | string | `"IfNotPresent"` | Image pull policy | | image.repository | string | `"feastdev/feature-transformation-server"` | Docker image for Transformation Server repository | -| image.tag | string | `"0.40.0"` | Image tag | +| image.tag | string | `"0.41.0"` | Image tag | | nodeSelector | object | `{}` | Node labels for pod assignment | | podLabels | object | `{}` | Labels to be added to Feast Serving pods | | replicaCount | int | `1` | Number of pods that will be created | diff --git a/infra/charts/feast/charts/transformation-service/values.yaml b/infra/charts/feast/charts/transformation-service/values.yaml index aee47048e8..51cd72d659 100644 --- a/infra/charts/feast/charts/transformation-service/values.yaml +++ b/infra/charts/feast/charts/transformation-service/values.yaml @@ -5,7 +5,7 @@ image: # image.repository -- Docker image for Transformation Server repository repository: feastdev/feature-transformation-server # image.tag -- Image tag - tag: 0.40.0 + tag: 0.41.0 # image.pullPolicy -- Image pull policy pullPolicy: IfNotPresent diff --git a/infra/charts/feast/requirements.yaml b/infra/charts/feast/requirements.yaml index 7b1277fc69..bb69ee9ed3 100644 --- a/infra/charts/feast/requirements.yaml +++ b/infra/charts/feast/requirements.yaml @@ -1,12 +1,12 @@ dependencies: - name: feature-server alias: feature-server - version: 0.40.0 + version: 0.41.0 condition: feature-server.enabled repository: https://feast-helm-charts.storage.googleapis.com - name: transformation-service alias: transformation-service - version: 0.40.0 + version: 0.41.0 condition: transformation-service.enabled repository: https://feast-helm-charts.storage.googleapis.com - name: redis diff --git a/infra/feast-helm-operator/Makefile b/infra/feast-helm-operator/Makefile index 6712a37b4a..733bf7bc3d 100644 --- a/infra/feast-helm-operator/Makefile +++ b/infra/feast-helm-operator/Makefile @@ -3,7 +3,7 @@ # To re-generate a bundle for another specific version without changing the standard setup, you can: # - use the VERSION as arg of the bundle target (e.g make bundle VERSION=0.0.2) # - use environment variables to overwrite this value (e.g export VERSION=0.0.2) -VERSION ?= 0.40.0 +VERSION ?= 0.41.0 # CHANNELS define the bundle channels used in the bundle. # Add a new line here if you would like to change its default config. (E.g CHANNELS = "candidate,fast,stable") diff --git a/infra/feast-helm-operator/config/manager/kustomization.yaml b/infra/feast-helm-operator/config/manager/kustomization.yaml index 0d27c2149d..decb714a20 100644 --- a/infra/feast-helm-operator/config/manager/kustomization.yaml +++ b/infra/feast-helm-operator/config/manager/kustomization.yaml @@ -5,4 +5,4 @@ kind: Kustomization images: - name: controller newName: feastdev/feast-helm-operator - newTag: 0.40.0 + newTag: 0.41.0 diff --git a/infra/feast-operator/Makefile b/infra/feast-operator/Makefile index a6b922b062..54786eb5f1 100644 --- a/infra/feast-operator/Makefile +++ b/infra/feast-operator/Makefile @@ -3,7 +3,7 @@ # To re-generate a bundle for another specific version without changing the standard setup, you can: # - use the VERSION as arg of the bundle target (e.g make bundle VERSION=0.0.2) # - use environment variables to overwrite this value (e.g export VERSION=0.0.2) -VERSION ?= 0.40.0 +VERSION ?= 0.41.0 # CHANNELS define the bundle channels used in the bundle. # Add a new line here if you would like to change its default config. (E.g CHANNELS = "candidate,fast,stable") diff --git a/infra/feast-operator/config/manager/kustomization.yaml b/infra/feast-operator/config/manager/kustomization.yaml index aba3224be6..253475b945 100644 --- a/infra/feast-operator/config/manager/kustomization.yaml +++ b/infra/feast-operator/config/manager/kustomization.yaml @@ -5,4 +5,4 @@ kind: Kustomization images: - name: controller newName: feastdev/feast-operator - newTag: 0.40.0 + newTag: 0.41.0 diff --git a/java/pom.xml b/java/pom.xml index 2c1c32792c..416ebe5978 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -35,7 +35,7 @@ - 0.40.0 + 0.41.0 https://github.com/feast-dev/feast UTF-8 diff --git a/sdk/python/feast/ui/package.json b/sdk/python/feast/ui/package.json index 36777ca0be..2a6329a166 100644 --- a/sdk/python/feast/ui/package.json +++ b/sdk/python/feast/ui/package.json @@ -6,7 +6,7 @@ "@elastic/datemath": "^5.0.3", "@elastic/eui": "^55.0.1", "@emotion/react": "^11.9.0", - "@feast-dev/feast-ui": "0.40.0", + "@feast-dev/feast-ui": "0.41.0", "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^13.2.0", "@testing-library/user-event": "^13.5.0", diff --git a/sdk/python/feast/ui/yarn.lock b/sdk/python/feast/ui/yarn.lock index 7e83084445..24de47b123 100644 --- a/sdk/python/feast/ui/yarn.lock +++ b/sdk/python/feast/ui/yarn.lock @@ -95,13 +95,6 @@ dependencies: "@babel/types" "^7.16.7" -"@babel/helper-annotate-as-pure@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz#e7f06737b197d580a01edf75d97e2c8be99d3882" - integrity sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg== - dependencies: - "@babel/types" "^7.22.5" - "@babel/helper-builder-binary-assignment-operator-visitor@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.16.7.tgz#38d138561ea207f0f69eb1626a418e4f7e6a580b" @@ -211,13 +204,6 @@ dependencies: "@babel/types" "^7.17.0" -"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz#1a8f4c9f4027d23f520bd76b364d44434a72660c" - integrity sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg== - dependencies: - "@babel/types" "^7.22.5" - "@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz#25612a8091a999704461c8a222d0efec5d091437" @@ -251,11 +237,6 @@ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz#aa3a8ab4c3cceff8e65eb9e73d87dc4ff320b2f5" integrity sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA== -"@babel/helper-plugin-utils@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295" - integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg== - "@babel/helper-remap-async-to-generator@^7.16.8": version "7.16.8" resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.8.tgz#29ffaade68a367e2ed09c90901986918d25e57e3" @@ -609,13 +590,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.16.7" -"@babel/plugin-syntax-jsx@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz#a6b68e84fb76e759fc3b93e901876ffabbe1d918" - integrity sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/plugin-syntax-logical-assignment-operators@^7.10.4", "@babel/plugin-syntax-logical-assignment-operators@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" @@ -1132,6 +1106,13 @@ dependencies: regenerator-runtime "^0.13.11" +"@babel/runtime@^7.12.1", "@babel/runtime@^7.18.3", "@babel/runtime@^7.24.1": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1" + integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.16.7", "@babel/template@^7.3.3": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155" @@ -1150,7 +1131,7 @@ "@babel/parser" "^7.22.15" "@babel/types" "^7.22.15" -"@babel/traverse@^7.13.0", "@babel/traverse@^7.16.7", "@babel/traverse@^7.16.8", "@babel/traverse@^7.17.10", "@babel/traverse@^7.17.3", "@babel/traverse@^7.17.9", "@babel/traverse@^7.4.5", "@babel/traverse@^7.7.2": +"@babel/traverse@^7.13.0", "@babel/traverse@^7.16.7", "@babel/traverse@^7.16.8", "@babel/traverse@^7.17.10", "@babel/traverse@^7.17.3", "@babel/traverse@^7.17.9", "@babel/traverse@^7.7.2": version "7.23.2" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8" integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw== @@ -1336,6 +1317,62 @@ uuid "^8.3.0" vfile "^4.2.0" +"@elastic/eui@^95.12.0": + version "95.12.0" + resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-95.12.0.tgz#862f2be8b72248a62b40704b9e62f2f5d7d43853" + integrity sha512-SW4ru97FY2VitSqyCgURrM5OMk1W+Ww12b6S+VZN5ex50aNT296DfED/ByidlYaAoVihqjZuoB3HlQBBXydFpA== + dependencies: + "@hello-pangea/dnd" "^16.6.0" + "@types/lodash" "^4.14.202" + "@types/numeral" "^2.0.5" + "@types/react-window" "^1.8.8" + "@types/refractor" "^3.4.0" + chroma-js "^2.4.2" + classnames "^2.5.1" + lodash "^4.17.21" + mdast-util-to-hast "^10.2.0" + numeral "^2.0.6" + prop-types "^15.8.1" + react-dropzone "^11.7.1" + react-element-to-jsx-string "^15.0.0" + react-focus-on "^3.9.1" + react-is "^17.0.2" + react-remove-scroll-bar "^2.3.4" + react-virtualized-auto-sizer "^1.0.24" + react-window "^1.8.10" + refractor "^3.6.0" + rehype-raw "^5.1.0" + rehype-react "^6.2.1" + rehype-stringify "^8.0.0" + remark-breaks "^2.0.2" + remark-emoji "^2.1.0" + remark-parse-no-trim "^8.0.4" + remark-rehype "^8.1.0" + tabbable "^5.3.3" + text-diff "^1.0.1" + unified "^9.2.2" + unist-util-visit "^2.0.3" + url-parse "^1.5.10" + uuid "^8.3.0" + vfile "^4.2.1" + +"@emotion/babel-plugin@^11.12.0": + version "11.12.0" + resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.12.0.tgz#7b43debb250c313101b3f885eba634f1d723fcc2" + integrity sha512-y2WQb+oP8Jqvvclh8Q55gLUyb7UFvgv7eJfsj7td5TToBrIUtPay2kMrZi4xjq9qw2vD0ZR5fSho0yqoFgX7Rw== + dependencies: + "@babel/helper-module-imports" "^7.16.7" + "@babel/runtime" "^7.18.3" + "@emotion/hash" "^0.9.2" + "@emotion/memoize" "^0.9.0" + "@emotion/serialize" "^1.2.0" + babel-plugin-macros "^3.1.0" + convert-source-map "^1.5.0" + escape-string-regexp "^4.0.0" + find-root "^1.1.0" + source-map "^0.5.7" + stylis "4.2.0" + "@emotion/babel-plugin@^11.7.1": version "11.9.2" resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.9.2.tgz#723b6d394c89fb2ef782229d92ba95a740576e95" @@ -1354,6 +1391,17 @@ source-map "^0.5.7" stylis "4.0.13" +"@emotion/cache@^11.13.0": + version "11.13.1" + resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.13.1.tgz#fecfc54d51810beebf05bf2a161271a1a91895d7" + integrity sha512-iqouYkuEblRcXmylXIwwOodiEK5Ifl7JcX7o6V4jI3iW4mLXX3dmt5xwBtIkJiQEXFAI+pC8X0i67yiPkH9Ucw== + dependencies: + "@emotion/memoize" "^0.9.0" + "@emotion/sheet" "^1.4.0" + "@emotion/utils" "^1.4.0" + "@emotion/weak-memoize" "^0.4.0" + stylis "4.2.0" + "@emotion/cache@^11.7.1": version "11.7.1" resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.7.1.tgz#08d080e396a42e0037848214e8aa7bf879065539" @@ -1365,15 +1413,31 @@ "@emotion/weak-memoize" "^0.2.5" stylis "4.0.13" +"@emotion/css@^11.13.0": + version "11.13.4" + resolved "https://registry.yarnpkg.com/@emotion/css/-/css-11.13.4.tgz#a5128e34a23f5e2c891970b8ec98a60c5a2395e1" + integrity sha512-CthbOD5EBw+iN0rfM96Tuv5kaZN4nxPyYDvGUs0bc7wZBBiU/0mse+l+0O9RshW2d+v5HH1cme+BAbLJ/3Folw== + dependencies: + "@emotion/babel-plugin" "^11.12.0" + "@emotion/cache" "^11.13.0" + "@emotion/serialize" "^1.3.0" + "@emotion/sheet" "^1.4.0" + "@emotion/utils" "^1.4.0" + "@emotion/hash@^0.8.0": version "0.8.0" resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== -"@emotion/is-prop-valid@^1.1.0": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz#23116cf1ed18bfeac910ec6436561ecb1a3885cc" - integrity sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw== +"@emotion/hash@^0.9.2": + version "0.9.2" + resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.2.tgz#ff9221b9f58b4dfe61e619a7788734bd63f6898b" + integrity sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g== + +"@emotion/is-prop-valid@1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz#d4175076679c6a26faa92b03bb786f9e52612337" + integrity sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw== dependencies: "@emotion/memoize" "^0.8.1" @@ -1387,7 +1451,26 @@ resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.1.tgz#c1ddb040429c6d21d38cc945fe75c818cfb68e17" integrity sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA== -"@emotion/react@^11.7.1", "@emotion/react@^11.9.0": +"@emotion/memoize@^0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.9.0.tgz#745969d649977776b43fc7648c556aaa462b4102" + integrity sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ== + +"@emotion/react@^11.13.3": + version "11.13.3" + resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.13.3.tgz#a69d0de2a23f5b48e0acf210416638010e4bd2e4" + integrity sha512-lIsdU6JNrmYfJ5EbUCf4xW1ovy5wKQ2CkPRM4xogziOxH1nXxBSjpC9YqbFAP7circxMfYp+6x676BqWcEiixg== + dependencies: + "@babel/runtime" "^7.18.3" + "@emotion/babel-plugin" "^11.12.0" + "@emotion/cache" "^11.13.0" + "@emotion/serialize" "^1.3.1" + "@emotion/use-insertion-effect-with-fallbacks" "^1.1.0" + "@emotion/utils" "^1.4.0" + "@emotion/weak-memoize" "^0.4.0" + hoist-non-react-statics "^3.3.1" + +"@emotion/react@^11.9.0": version "11.9.0" resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.9.0.tgz#b6d42b1db3bd7511e7a7c4151dc8bc82e14593b8" integrity sha512-lBVSF5d0ceKtfKCDQJveNAtkC7ayxpVlgOohLgXqRwqWr9bOf4TZAFFyIcNngnV6xK6X4x2ZeXq7vliHkoVkxQ== @@ -1411,31 +1494,67 @@ "@emotion/utils" "^1.0.0" csstype "^3.0.2" +"@emotion/serialize@^1.2.0", "@emotion/serialize@^1.3.0", "@emotion/serialize@^1.3.1": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.3.2.tgz#e1c1a2e90708d5d85d81ccaee2dfeb3cc0cccf7a" + integrity sha512-grVnMvVPK9yUVE6rkKfAJlYZgo0cu3l9iMC77V7DW6E1DUIrU68pSEXRmFZFOFB1QFo57TncmOcvcbMDWsL4yA== + dependencies: + "@emotion/hash" "^0.9.2" + "@emotion/memoize" "^0.9.0" + "@emotion/unitless" "^0.10.0" + "@emotion/utils" "^1.4.1" + csstype "^3.0.2" + "@emotion/sheet@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.1.0.tgz#56d99c41f0a1cda2726a05aa6a20afd4c63e58d2" integrity sha512-u0AX4aSo25sMAygCuQTzS+HsImZFuS8llY8O7b9MDRzbJM0kVJlAz6KNDqcG7pOuQZJmj/8X/rAW+66kMnMW+g== -"@emotion/stylis@^0.8.4": - version "0.8.5" - resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.5.tgz#deacb389bd6ee77d1e7fcaccce9e16c5c7e78e04" - integrity sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ== +"@emotion/sheet@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.4.0.tgz#c9299c34d248bc26e82563735f78953d2efca83c" + integrity sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg== + +"@emotion/unitless@0.8.1": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.8.1.tgz#182b5a4704ef8ad91bde93f7a860a88fd92c79a3" + integrity sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ== + +"@emotion/unitless@^0.10.0": + version "0.10.0" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.10.0.tgz#2af2f7c7e5150f497bdabd848ce7b218a27cf745" + integrity sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg== -"@emotion/unitless@^0.7.4", "@emotion/unitless@^0.7.5": +"@emotion/unitless@^0.7.5": version "0.7.5" resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== +"@emotion/use-insertion-effect-with-fallbacks@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz#1a818a0b2c481efba0cf34e5ab1e0cb2dcb9dfaf" + integrity sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw== + "@emotion/utils@^1.0.0", "@emotion/utils@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.1.0.tgz#86b0b297f3f1a0f2bdb08eeac9a2f49afd40d0cf" integrity sha512-iRLa/Y4Rs5H/f2nimczYmS5kFJEbpiVvgN3XVfZ022IYhuNA1IRSHEizcof88LtCTXtl9S2Cxt32KgaXEu72JQ== +"@emotion/utils@^1.4.0", "@emotion/utils@^1.4.1": + version "1.4.1" + resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.4.1.tgz#b3adbb43de12ee2149541c4f1337d2eb7774f0ad" + integrity sha512-BymCXzCG3r72VKJxaYVwOXATqXIZ85cuvg0YOUDxMGNrKc1DJRZk8MgV5wyXRyEayIMd4FuXJIUgTBXvDNW5cA== + "@emotion/weak-memoize@^0.2.5": version "0.2.5" resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== +"@emotion/weak-memoize@^0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz#5e13fac887f08c44f76b0ccaf3370eb00fec9bb6" + integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg== + "@eslint/eslintrc@^1.2.3": version "1.2.3" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.2.3.tgz#fcaa2bcef39e13d6e9e7f6271f4cc7cae1174886" @@ -1451,32 +1570,40 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@feast-dev/feast-ui@0.40.0": - version "0.40.0" - resolved "https://registry.yarnpkg.com/@feast-dev/feast-ui/-/feast-ui-0.40.0.tgz#0dc60cbbd4f63d161927321c0bbf57bbfe6b7d09" - integrity sha512-jiCtMYCBvNSfHCjemFRa0NFIIAR5y6spWBnUZyc4GXY2YxGcznw+PZSzOoi7JrOwpNzNPB0PTBUqJgBAxus20w== +"@feast-dev/feast-ui@0.41.0": + version "0.41.0" + resolved "https://registry.yarnpkg.com/@feast-dev/feast-ui/-/feast-ui-0.41.0.tgz#67eca6328131ee524ee6a6f286cfc4386f698053" + integrity sha512-BkVb4zfR+j95IX9FBzeXFyCimG5Za1a3jyLqjmETRO3hpp5OJanpc2N35AaOn8ZPqka00Be/b8NZ8TjbsRWyVg== dependencies: "@elastic/datemath" "^5.0.3" - "@elastic/eui" "^55.0.1" - "@emotion/react" "^11.7.1" - "@types/d3" "^7.1.0" - "@types/jest" "^27.0.1" - "@types/node" "^16.7.13" - "@types/react" "^17.0.20" - "@types/react-dom" "^17.0.9" - d3 "^7.3.0" + "@elastic/eui" "^95.12.0" + "@emotion/css" "^11.13.0" + "@emotion/react" "^11.13.3" inter-ui "^3.19.3" moment "^2.29.1" - prop-types "^15.8.1" protobufjs "^7.1.1" query-string "^7.1.1" - react-code-blocks "^0.0.9-0" - react-query "^3.34.12" - react-router-dom "6" - react-scripts "^5.0.0" + react-code-blocks "^0.1.6" + react-query "^3.39.3" + react-router-dom "<6.4.0" + react-scripts "^5.0.1" + tslib "^2.3.1" use-query-params "^1.2.3" zod "^3.11.6" +"@hello-pangea/dnd@^16.6.0": + version "16.6.0" + resolved "https://registry.yarnpkg.com/@hello-pangea/dnd/-/dnd-16.6.0.tgz#7509639c7bd13f55e537b65a9dcfcd54e7c99ac7" + integrity sha512-vfZ4GydqbtUPXSLfAvKvXQ6xwRzIjUSjVU0Sx+70VOhc2xx6CdmJXJ8YhH70RpbTUGjxctslQTHul9sIOxCfFQ== + dependencies: + "@babel/runtime" "^7.24.1" + css-box-model "^1.2.1" + memoize-one "^6.0.0" + raf-schd "^4.0.3" + react-redux "^8.1.3" + redux "^4.2.1" + use-memo-one "^1.1.3" + "@humanwhocodes/config-array@^0.9.2": version "0.9.5" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.9.5.tgz#2cbaf9a89460da24b5ca6531b8bbfc23e1df50c7" @@ -2509,6 +2636,14 @@ "@types/react" "*" hoist-non-react-statics "^3.3.0" +"@types/hoist-non-react-statics@^3.3.1": + version "3.3.5" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz#dab7867ef789d87e2b4b0003c9d65c49cc44a494" + integrity sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg== + dependencies: + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + "@types/html-minifier-terser@^6.0.0": version "6.1.0" resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#4fc33a00c1d0c16987b1a20cf92d20614c55ac35" @@ -2540,7 +2675,7 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@*", "@types/jest@^27.0.1": +"@types/jest@*": version "27.5.0" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.5.0.tgz#e04ed1824ca6b1dd0438997ba60f99a7405d4c7b" integrity sha512-9RBFx7r4k+msyj/arpfaa0WOOEcaAZNmN+j80KFbFCoSqCJGHTz7YMAMGQW9Xmqm5w6l5c25vbSjMwlikJi5+g== @@ -2563,6 +2698,11 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.182.tgz#05301a4d5e62963227eaafe0ce04dd77c54ea5c2" integrity sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q== +"@types/lodash@^4.14.202": + version "4.17.12" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.12.tgz#25d71312bf66512105d71e55d42e22c36bcfc689" + integrity sha512-sviUmCE8AYdaF/KIHLDJBQgeYzPBI0vf/17NaYehBJfYD1j6/L95Slh07NlyK2iNyBNaEkb3En2jRt+a8y3xZQ== + "@types/mdast@^3.0.0": version "3.0.10" resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.10.tgz#4724244a82a4598884cbbe9bcfd73dff927ee8af" @@ -2585,16 +2725,16 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.8.3.tgz#ce750ab4017effa51aed6a7230651778d54e327c" integrity sha512-0os9vz6BpGwxGe9LOhgP/ncvYN5Tx1fNcd2TM3rD/aCGBkysb+ZWpXEocG24h6ZzOi13+VB8HndAQFezsSOw1w== -"@types/node@^16.7.13": - version "16.11.34" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.34.tgz#520224e4be4448c279ecad09639ab460cc441a50" - integrity sha512-UrWGDyLAlQ2Z8bNOGWTsqbP9ZcBeTYBVuTRNxXTztBy5KhWUFI3BaeDWoCP/CzV/EVGgO1NTYzv9ZytBI9GAEw== - "@types/numeral@^0.0.28": version "0.0.28" resolved "https://registry.yarnpkg.com/@types/numeral/-/numeral-0.0.28.tgz#e43928f0bda10b169b6f7ecf99e3ddf836b8ebe4" integrity sha512-Sjsy10w6XFHDktJJdXzBJmoondAKW+LcGpRFH+9+zXEDj0cOH8BxJuZA9vUDSMAzU1YRJlsPKmZEEiTYDlICLw== +"@types/numeral@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@types/numeral/-/numeral-2.0.5.tgz#388e5c4ff4b0e1787f130753cbbe83d3ba770858" + integrity sha512-kH8I7OSSwQu9DS9JYdFWbuvhVzvFRoCPCkGxNwoGgaPeDfEPJlcxNvEOypZhQ3XXHsGbfIuYcxcJxKUfJHnRfw== + "@types/parse-json@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" @@ -2642,13 +2782,6 @@ dependencies: "@types/react" "*" -"@types/react-dom@^17.0.9": - version "17.0.16" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.16.tgz#7caba93cf2806c51e64d620d8dff4bae57e06cc4" - integrity sha512-DWcXf8EbMrO/gWnQU7Z88Ws/p16qxGpPyjTKTpmBSFKeE+HveVubqGO1CVK7FrwlWD5MuOcvh8gtd0/XO38NdQ== - dependencies: - "@types/react" "^17" - "@types/react-dom@^18.0.0": version "18.0.3" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.3.tgz#a022ea08c75a476fe5e96b675c3e673363853831" @@ -2687,6 +2820,13 @@ dependencies: "@types/react" "*" +"@types/react-window@^1.8.8": + version "1.8.8" + resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.8.tgz#c20645414d142364fbe735818e1c1e0a145696e3" + integrity sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q== + dependencies: + "@types/react" "*" + "@types/react@*": version "18.0.9" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.9.tgz#d6712a38bd6cd83469603e7359511126f122e878" @@ -2696,15 +2836,6 @@ "@types/scheduler" "*" csstype "^3.0.2" -"@types/react@^17", "@types/react@^17.0.20": - version "17.0.45" - resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.45.tgz#9b3d5b661fd26365fefef0e766a1c6c30ccf7b3f" - integrity sha512-YfhQ22Lah2e3CHPsb93tRwIGNiSwkuz1/blk4e6QrWS0jQzCSNbGLtOEYhPg02W0yGTTmpajp7dCTbBAMN3qsg== - dependencies: - "@types/prop-types" "*" - "@types/scheduler" "*" - csstype "^3.0.2" - "@types/refractor@^3.0.0": version "3.0.2" resolved "https://registry.yarnpkg.com/@types/refractor/-/refractor-3.0.2.tgz#2d42128d59f78f84d2c799ffc5ab5cadbcba2d82" @@ -2712,6 +2843,13 @@ dependencies: "@types/prismjs" "*" +"@types/refractor@^3.4.0": + version "3.4.1" + resolved "https://registry.yarnpkg.com/@types/refractor/-/refractor-3.4.1.tgz#8b109804f77b3da8fad543d3f575fef1ece8835a" + integrity sha512-wYuorIiCTSuvRT9srwt+taF6mH/ww+SyN2psM0sjef2qW+sS8GmshgDGTEDgWB1sTVGgYVE6EK7dBA2MxQxibg== + dependencies: + "@types/prismjs" "*" + "@types/resize-observer-browser@^0.1.5": version "0.1.7" resolved "https://registry.yarnpkg.com/@types/resize-observer-browser/-/resize-observer-browser-0.1.7.tgz#294aaadf24ac6580b8fbd1fe3ab7b59fe85f9ef3" @@ -2761,6 +2899,11 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== +"@types/stylis@4.2.5": + version "4.2.5" + resolved "https://registry.yarnpkg.com/@types/stylis/-/stylis-4.2.5.tgz#1daa6456f40959d06157698a653a9ab0a70281df" + integrity sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw== + "@types/testing-library__jest-dom@^5.9.1": version "5.14.3" resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.3.tgz#ee6c7ffe9f8595882ee7bda8af33ae7b8789ef17" @@ -2778,6 +2921,11 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== +"@types/use-sync-external-store@^0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43" + integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA== + "@types/vfile-message@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@types/vfile-message/-/vfile-message-2.0.0.tgz#690e46af0fdfc1f9faae00cd049cc888957927d5" @@ -3221,6 +3369,13 @@ aria-hidden@^1.1.3: dependencies: tslib "^1.0.0" +aria-hidden@^1.2.2: + version "1.2.4" + resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.4.tgz#b78e383fdbc04d05762c78b4a25a501e736c4522" + integrity sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A== + dependencies: + tslib "^2.0.0" + aria-query@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b" @@ -3436,17 +3591,6 @@ babel-plugin-polyfill-regenerator@^0.3.0: dependencies: "@babel/helper-define-polyfill-provider" "^0.3.1" -"babel-plugin-styled-components@>= 1.12.0": - version "2.1.4" - resolved "https://registry.yarnpkg.com/babel-plugin-styled-components/-/babel-plugin-styled-components-2.1.4.tgz#9a1f37c7f32ef927b4b008b529feb4a2c82b1092" - integrity sha512-Xgp9g+A/cG47sUyRwwYxGM4bR/jDRg5N6it/8+HxCnbT5XNKSKDT9xm4oag/osgqjC2It/vH0yXsomOG6k558g== - dependencies: - "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-module-imports" "^7.22.5" - "@babel/plugin-syntax-jsx" "^7.22.5" - lodash "^4.17.21" - picomatch "^2.3.1" - babel-plugin-transform-react-remove-prop-types@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz#f2edaf9b4c6a5fbe5c1d678bfb531078c1555f3a" @@ -3834,6 +3978,11 @@ chroma-js@^2.1.0: resolved "https://registry.yarnpkg.com/chroma-js/-/chroma-js-2.4.2.tgz#dffc214ed0c11fa8eefca2c36651d8e57cbfb2b0" integrity sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A== +chroma-js@^2.4.2: + version "2.6.0" + resolved "https://registry.yarnpkg.com/chroma-js/-/chroma-js-2.6.0.tgz#578743dd359698a75067a19fa5571dec54d0b70b" + integrity sha512-BLHvCB9s8Z1EV4ethr6xnkl/P2YRFOGqfgvuMG/MyCbZPrTA+NeiByY6XvgF0zP4/2deU2CXnWyMa3zu1LqQ3A== + chrome-trace-event@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" @@ -3854,6 +4003,11 @@ classnames@^2.2.6: resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA== +classnames@^2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" + integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== + clean-css@^5.2.2: version "5.3.0" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.0.tgz#ad3d8238d5f3549e83d5f87205189494bc7cbb59" @@ -3861,15 +4015,6 @@ clean-css@^5.2.2: dependencies: source-map "~0.6.0" -clipboard@^2.0.0: - version "2.0.11" - resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.11.tgz#62180360b97dd668b6b3a84ec226975762a70be5" - integrity sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw== - dependencies: - good-listener "^1.2.2" - select "^1.1.2" - tiny-emitter "^2.0.0" - cliui@^7.0.2: version "7.0.4" resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" @@ -4114,7 +4259,7 @@ css-blank-pseudo@^3.0.3: dependencies: postcss-selector-parser "^6.0.9" -css-box-model@^1.2.0: +css-box-model@^1.2.0, css-box-model@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1" integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw== @@ -4195,7 +4340,7 @@ css-select@^4.1.3: domutils "^2.8.0" nth-check "^2.0.1" -css-to-react-native@^3.0.0: +css-to-react-native@3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/css-to-react-native/-/css-to-react-native-3.2.0.tgz#cdd8099f71024e149e4f6fe17a7d46ecd55f1e32" integrity sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ== @@ -4327,6 +4472,11 @@ cssstyle@^2.3.0: dependencies: cssom "~0.3.6" +csstype@3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== + csstype@^3.0.2: version "3.0.11" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.11.tgz#d66700c5eacfac1940deb4e3ee5642792d85cd33" @@ -4540,7 +4690,7 @@ d3-zoom@3: d3-selection "2 - 3" d3-transition "2 - 3" -d3@^7.3.0, d3@^7.4.4: +d3@^7.4.4: version "7.4.4" resolved "https://registry.yarnpkg.com/d3/-/d3-7.4.4.tgz#bfbf87487c37d3196efebd5a63e3a0ed8299d8ff" integrity sha512-97FE+MYdAlV3R9P74+R3Uar7wUKkIFu89UWMjEaDhiJ9VxKvqaMxauImy8PC2DdBkdM2BxJOIoLxPrcZUyrKoQ== @@ -4682,11 +4832,6 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= -delegate@^3.1.2: - version "3.2.0" - resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166" - integrity sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw== - depd@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" @@ -5441,7 +5586,7 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" -fault@^1.0.2: +fault@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/fault/-/fault-1.0.4.tgz#eafcfc0a6d214fc94601e170df29954a4f842f13" integrity sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA== @@ -5585,6 +5730,13 @@ focus-lock@^0.11.2: dependencies: tslib "^2.0.3" +focus-lock@^1.3.5: + version "1.3.5" + resolved "https://registry.yarnpkg.com/focus-lock/-/focus-lock-1.3.5.tgz#aa644576e5ec47d227b57eb14e1efb2abf33914c" + integrity sha512-QFaHbhv9WPUeLYBDe/PAuLKJ4Dd9OPvKs9xZBr3yLXnUrDNaVXKu2baDBXe3naPY30hgHYSsf2JW4jzas2mDEQ== + dependencies: + tslib "^2.0.3" + follow-redirects@^1.0.0: version "1.15.6" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" @@ -5836,13 +5988,6 @@ globby@^11.0.4: merge2 "^1.4.1" slash "^3.0.0" -good-listener@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50" - integrity sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw== - dependencies: - delegate "^3.1.2" - gopd@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" @@ -6021,16 +6166,6 @@ hast-util-whitespace@^1.0.0: resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-1.0.4.tgz#e4fe77c4a9ae1cb2e6c25e02df0043d0164f6e41" integrity sha512-I5GTdSfhYfAPNztx2xJRQpG8cuDSNt599/7YUn7Gx/WxNMsG+a835k97TDkFgk123cwjfwINaZknkKkphx/f2A== -hastscript@^5.0.0: - version "5.1.2" - resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-5.1.2.tgz#bde2c2e56d04c62dd24e8c5df288d050a355fb8a" - integrity sha512-WlztFuK+Lrvi3EggsqOkQ52rKbxkXL3RwB6t5lwoa8QLMemoWfBuL43eDrwOamJyR7uKQKdmKYaBH1NZBiIRrQ== - dependencies: - comma-separated-tokens "^1.0.0" - hast-util-parse-selector "^2.0.0" - property-information "^5.0.0" - space-separated-tokens "^1.0.0" - hastscript@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-6.0.0.tgz#e8768d7eac56c3fdeac8a92830d58e811e5bf640" @@ -6047,10 +6182,15 @@ he@^1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== -highlight.js@~9.15.0, highlight.js@~9.15.1: - version "9.15.10" - resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.15.10.tgz#7b18ed75c90348c045eef9ed08ca1319a2219ad2" - integrity sha512-RoV7OkQm0T3os3Dd2VHLNMoaoDVx77Wygln3n9l5YV172XonWG6rgQD3XnF/BuFFZw9A0TJgmMSO8FEWQgvcXw== +highlight.js@^10.4.1, highlight.js@~10.7.0: + version "10.7.3" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531" + integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A== + +highlightjs-vue@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz#fdfe97fbea6354e70ee44e3a955875e114db086d" + integrity sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA== history@^5.2.0: version "5.3.0" @@ -6059,7 +6199,7 @@ history@^5.2.0: dependencies: "@babel/runtime" "^7.7.6" -hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: +hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -7380,13 +7520,13 @@ lower-case@^2.0.2: dependencies: tslib "^2.0.3" -lowlight@1.12.1: - version "1.12.1" - resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.12.1.tgz#014acf8dd73a370e02ff1cc61debcde3bb1681eb" - integrity sha512-OqaVxMGIESnawn+TU/QMV5BJLbUghUfjDWPAtFqDYDmDtr4FnB+op8xM+pR7nKlauHNUHXGt0VgWatFB8voS5w== +lowlight@^1.17.0: + version "1.20.0" + resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.20.0.tgz#ddb197d33462ad0d93bf19d17b6c301aa3941888" + integrity sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw== dependencies: - fault "^1.0.2" - highlight.js "~9.15.0" + fault "^1.0.0" + highlight.js "~10.7.0" lru-cache@^6.0.0: version "6.0.0" @@ -7494,6 +7634,11 @@ memfs@^3.4.3: resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== +memoize-one@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045" + integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw== + merge-descriptors@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" @@ -7639,6 +7784,11 @@ nanoid@^3.3.3: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== +nanoid@^3.3.7: + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -7965,18 +8115,6 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse-entities@^1.1.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-1.2.2.tgz#c31bf0f653b6661354f8973559cb86dd1d5edf50" - integrity sha512-NzfpbxW/NPrzZ/yYSoQxyqUZMZXIdCfE0OIN4ESsnptHJECoUk3FZktxNuzQf4tjt5UEopnxpYJbvYuxIFDdsg== - dependencies: - character-entities "^1.0.0" - character-entities-legacy "^1.0.0" - character-reference-invalid "^1.0.0" - is-alphanumerical "^1.0.0" - is-decimal "^1.0.0" - is-hexadecimal "^1.0.0" - parse-entities@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-2.0.0.tgz#53c6eb5b9314a1f4ec99fa0fdf7ce01ecda0cbe8" @@ -8613,6 +8751,15 @@ postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0, postcss-value-parser@^ resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== +postcss@8.4.38: + version "8.4.38" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e" + integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== + dependencies: + nanoid "^3.3.7" + picocolors "^1.0.0" + source-map-js "^1.2.0" + postcss@^7.0.35: version "7.0.39" resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.39.tgz#9624375d965630e2e1f2c02a935c82a59cb48309" @@ -8672,18 +8819,11 @@ pretty-format@^28.1.0: ansi-styles "^5.0.0" react-is "^18.0.0" -prismjs@^1.8.4: +prismjs@^1.27.0: version "1.29.0" resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.29.0.tgz#f113555a8fa9b57c35e637bba27509dcf802dd12" integrity sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q== -prismjs@~1.17.0: - version "1.17.1" - resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.17.1.tgz#e669fcbd4cdd873c35102881c33b14d0d68519be" - integrity sha512-PrEDJAFdUGbOP6xK/UsfkC5ghJsPJviKgnQOoxaDbBjwc8op68Quupwt1DeAFoG8GImPhiKXAvvsH7wDSLsu1Q== - optionalDependencies: - clipboard "^2.0.0" - prismjs@~1.27.0: version "1.27.0" resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.27.0.tgz#bb6ee3138a0b438a3653dd4d6ce0cc6510a45057" @@ -8798,7 +8938,7 @@ quick-lru@^5.1.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== -raf-schd@^4.0.2: +raf-schd@^4.0.2, raf-schd@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a" integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ== @@ -8864,15 +9004,15 @@ react-clientside-effect@^1.2.6: dependencies: "@babel/runtime" "^7.12.13" -react-code-blocks@^0.0.9-0: - version "0.0.9-0" - resolved "https://registry.yarnpkg.com/react-code-blocks/-/react-code-blocks-0.0.9-0.tgz#0c6d04d8a40b74cffe95f24f1a8e62a0fda8c014" - integrity sha512-jdYJVZwGtsr6WIUaqILy5fkF1acf57YV5s0V3+w5o9v3omYnqBeO6EuZi1Vf2x1hahkYGEedsp46+ofdkYlqyw== +react-code-blocks@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/react-code-blocks/-/react-code-blocks-0.1.6.tgz#ec64e7899223d3e910eb916465a66d95ce1ae1b2" + integrity sha512-ENNuxG07yO+OuX1ChRje3ieefPRz6yrIpHmebQlaFQgzcAHbUfVeTINpOpoI9bSRSObeYo/OdHsporeToZ7fcg== dependencies: "@babel/runtime" "^7.10.4" - react-syntax-highlighter "^12.2.1" - styled-components "^5.1.1" - tslib "^2.0.0" + react-syntax-highlighter "^15.5.0" + styled-components "^6.1.0" + tslib "^2.6.0" react-dev-utils@^12.0.1: version "12.0.1" @@ -8913,7 +9053,7 @@ react-dom@^17.0.2: object-assign "^4.1.1" scheduler "^0.20.2" -react-dropzone@^11.5.3: +react-dropzone@^11.5.3, react-dropzone@^11.7.1: version "11.7.1" resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-11.7.1.tgz#3851bb75b26af0bf1b17ce1449fd980e643b9356" integrity sha512-zxCMwhfPy1olUEbw3FLNPLhAm/HnaYH5aELIEglRbqabizKAdHs0h+WuyOpmA+v1JXn0++fpQDdNfUagWt5hJQ== @@ -8931,11 +9071,32 @@ react-element-to-jsx-string@^14.3.4: is-plain-object "5.0.0" react-is "17.0.2" +react-element-to-jsx-string@^15.0.0: + version "15.0.0" + resolved "https://registry.yarnpkg.com/react-element-to-jsx-string/-/react-element-to-jsx-string-15.0.0.tgz#1cafd5b6ad41946ffc8755e254da3fc752a01ac6" + integrity sha512-UDg4lXB6BzlobN60P8fHWVPX3Kyw8ORrTeBtClmIlGdkOOE+GYQSFvmEU5iLLpwp/6v42DINwNcwOhOLfQ//FQ== + dependencies: + "@base2/pretty-print-object" "1.0.1" + is-plain-object "5.0.0" + react-is "18.1.0" + react-error-overlay@^6.0.11: version "6.0.11" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb" integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg== +react-focus-lock@^2.11.3: + version "2.13.2" + resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.13.2.tgz#e1addac2f8b9550bc0581f3c416755ba0f81f5ef" + integrity sha512-T/7bsofxYqnod2xadvuwjGKHOoL5GH7/EIPI5UyEvaU/c2CcphvGI371opFtuY/SYdbMsNiuF4HsHQ50nA/TKQ== + dependencies: + "@babel/runtime" "^7.0.0" + focus-lock "^1.3.5" + prop-types "^15.6.2" + react-clientside-effect "^1.2.6" + use-callback-ref "^1.3.2" + use-sidecar "^1.1.2" + react-focus-lock@^2.9.0: version "2.9.1" resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.9.1.tgz#094cfc19b4f334122c73bb0bff65d77a0c92dd16" @@ -8961,6 +9122,18 @@ react-focus-on@^3.5.4: use-callback-ref "^1.3.0" use-sidecar "^1.1.2" +react-focus-on@^3.9.1: + version "3.9.4" + resolved "https://registry.yarnpkg.com/react-focus-on/-/react-focus-on-3.9.4.tgz#0b6c13273d86243c330d1aa53af39290f543da7b" + integrity sha512-NFKmeH6++wu8e7LJcbwV8TTd4L5w/U5LMXTMOdUcXhCcZ7F5VOvgeTHd4XN1PD7TNmdvldDu/ENROOykUQ4yQg== + dependencies: + aria-hidden "^1.2.2" + react-focus-lock "^2.11.3" + react-remove-scroll "^2.6.0" + react-style-singleton "^2.2.1" + tslib "^2.3.1" + use-sidecar "^1.1.2" + react-input-autosize@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-3.0.0.tgz#6b5898c790d4478d69420b55441fcc31d5c50a85" @@ -8973,16 +9146,16 @@ react-is@17.0.2, react-is@^17.0.1, react-is@^17.0.2: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== +react-is@18.1.0, react-is@^18.0.0: + version "18.1.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.1.0.tgz#61aaed3096d30eacf2a2127118b5b41387d32a67" + integrity sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg== + react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -react-is@^18.0.0: - version "18.1.0" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.1.0.tgz#61aaed3096d30eacf2a2127118b5b41387d32a67" - integrity sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg== - react-query@^3.34.12: version "3.39.0" resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.39.0.tgz#0caca7b0da98e65008bbcd4df0d25618c2100050" @@ -8992,6 +9165,15 @@ react-query@^3.34.12: broadcast-channel "^3.4.1" match-sorter "^6.0.2" +react-query@^3.39.3: + version "3.39.3" + resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.39.3.tgz#4cea7127c6c26bdea2de5fb63e51044330b03f35" + integrity sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g== + dependencies: + "@babel/runtime" "^7.5.5" + broadcast-channel "^3.4.1" + match-sorter "^6.0.2" + react-redux@^7.2.0: version "7.2.8" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.8.tgz#a894068315e65de5b1b68899f9c6ee0923dd28de" @@ -9004,6 +9186,18 @@ react-redux@^7.2.0: prop-types "^15.7.2" react-is "^17.0.2" +react-redux@^8.1.3: + version "8.1.3" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.1.3.tgz#4fdc0462d0acb59af29a13c27ffef6f49ab4df46" + integrity sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw== + dependencies: + "@babel/runtime" "^7.12.1" + "@types/hoist-non-react-statics" "^3.3.1" + "@types/use-sync-external-store" "^0.0.3" + hoist-non-react-statics "^3.3.2" + react-is "^18.0.0" + use-sync-external-store "^1.0.0" + react-refresh@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046" @@ -9017,6 +9211,14 @@ react-remove-scroll-bar@^2.3.1: react-style-singleton "^2.2.0" tslib "^2.0.0" +react-remove-scroll-bar@^2.3.4, react-remove-scroll-bar@^2.3.6: + version "2.3.6" + resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz#3e585e9d163be84a010180b18721e851ac81a29c" + integrity sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g== + dependencies: + react-style-singleton "^2.2.1" + tslib "^2.0.0" + react-remove-scroll@^2.5.2: version "2.5.3" resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.5.3.tgz#a152196e710e8e5811be39dc352fd8a90b05c961" @@ -9028,7 +9230,18 @@ react-remove-scroll@^2.5.2: use-callback-ref "^1.3.0" use-sidecar "^1.1.2" -react-router-dom@6: +react-remove-scroll@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.6.0.tgz#fb03a0845d7768a4f1519a99fdb84983b793dc07" + integrity sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ== + dependencies: + react-remove-scroll-bar "^2.3.6" + react-style-singleton "^2.2.1" + tslib "^2.1.0" + use-callback-ref "^1.3.0" + use-sidecar "^1.1.2" + +react-router-dom@6, react-router-dom@<6.4.0: version "6.3.0" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.3.0.tgz#a0216da813454e521905b5fa55e0e5176123f43d" integrity sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw== @@ -9043,7 +9256,7 @@ react-router@6.3.0: dependencies: history "^5.2.0" -react-scripts@^5.0.0: +react-scripts@^5.0.0, react-scripts@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-5.0.1.tgz#6285dbd65a8ba6e49ca8d651ce30645a6d980003" integrity sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ== @@ -9107,22 +9320,45 @@ react-style-singleton@^2.2.0: invariant "^2.2.4" tslib "^2.0.0" -react-syntax-highlighter@^12.2.1: - version "12.2.1" - resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-12.2.1.tgz#14d78352da1c1c3f93c6698b70ec7c706b83493e" - integrity sha512-CTsp0ZWijwKRYFg9xhkWD4DSpQqE4vb2NKVMdPAkomnILSmsNBHE0n5GuI5zB+PU3ySVvXvdt9jo+ViD9XibCA== +react-style-singleton@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4" + integrity sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g== + dependencies: + get-nonce "^1.0.0" + invariant "^2.2.4" + tslib "^2.0.0" + +react-syntax-highlighter@^15.5.0: + version "15.6.1" + resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz#fa567cb0a9f96be7bbccf2c13a3c4b5657d9543e" + integrity sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg== dependencies: "@babel/runtime" "^7.3.1" - highlight.js "~9.15.1" - lowlight "1.12.1" - prismjs "^1.8.4" - refractor "^2.4.1" + highlight.js "^10.4.1" + highlightjs-vue "^1.0.0" + lowlight "^1.17.0" + prismjs "^1.27.0" + refractor "^3.6.0" + +react-virtualized-auto-sizer@^1.0.24: + version "1.0.24" + resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.24.tgz#3ebdc92f4b05ad65693b3cc8e7d8dd54924c0227" + integrity sha512-3kCn7N9NEb3FlvJrSHWGQ4iVl+ydQObq2fHMn12i5wbtm74zHOPhz/i64OL3c1S1vi9i2GXtZqNqUJTQ+BnNfg== react-virtualized-auto-sizer@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.6.tgz#66c5b1c9278064c5ef1699ed40a29c11518f97ca" integrity sha512-7tQ0BmZqfVF6YYEWcIGuoR3OdYe8I/ZFbNclFlGOC3pMqunkYF/oL30NCjSGl9sMEb17AnzixDz98Kqc3N76HQ== +react-window@^1.8.10: + version "1.8.10" + resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.10.tgz#9e6b08548316814b443f7002b1cf8fd3a1bdde03" + integrity sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg== + dependencies: + "@babel/runtime" "^7.0.0" + memoize-one ">=3.1.1 <6" + react-window@^1.8.6: version "1.8.7" resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.7.tgz#5e9fd0d23f48f432d7022cdb327219353a15f0d4" @@ -9190,16 +9426,14 @@ redux@^4.0.0, redux@^4.0.4: dependencies: "@babel/runtime" "^7.9.2" -refractor@^2.4.1: - version "2.10.1" - resolved "https://registry.yarnpkg.com/refractor/-/refractor-2.10.1.tgz#166c32f114ed16fd96190ad21d5193d3afc7d34e" - integrity sha512-Xh9o7hQiQlDbxo5/XkOX6H+x/q8rmlmZKr97Ie1Q8ZM32IRRd3B/UxuA/yXDW79DBSXGWxm2yRTbcTVmAciJRw== +redux@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197" + integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w== dependencies: - hastscript "^5.0.0" - parse-entities "^1.1.2" - prismjs "~1.17.0" + "@babel/runtime" "^7.9.2" -refractor@^3.5.0: +refractor@^3.5.0, refractor@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/refractor/-/refractor-3.6.0.tgz#ac318f5a0715ead790fcfb0c71f4dd83d977935a" integrity sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA== @@ -9230,6 +9464,11 @@ regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.9: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== +regenerator-runtime@^0.14.0: + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== + regenerator-transform@^0.15.0: version "0.15.0" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.0.tgz#cbd9ead5d77fae1a48d957cf889ad0586adb6537" @@ -9280,14 +9519,14 @@ regjsparser@^0.8.2: dependencies: jsesc "~0.5.0" -rehype-raw@^5.0.0: +rehype-raw@^5.0.0, rehype-raw@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/rehype-raw/-/rehype-raw-5.1.0.tgz#66d5e8d7188ada2d31bc137bc19a1000cf2c6b7e" integrity sha512-MDvHAb/5mUnif2R+0IPCYJU8WjHa9UzGtM/F4AVy5GixPlDZ1z3HacYy4xojDU+uBa+0X/3PIfyQI26/2ljJNA== dependencies: hast-util-raw "^6.1.0" -rehype-react@^6.0.0: +rehype-react@^6.0.0, rehype-react@^6.2.1: version "6.2.1" resolved "https://registry.yarnpkg.com/rehype-react/-/rehype-react-6.2.1.tgz#9b9bf188451ad6f63796b784fe1f51165c67b73a" integrity sha512-f9KIrjktvLvmbGc7si25HepocOg4z0MuNOtweigKzBcDjiGSTGhyz6VSgaV5K421Cq1O+z4/oxRJ5G9owo0KVg== @@ -9323,6 +9562,27 @@ remark-emoji@^2.1.0: node-emoji "^1.10.0" unist-util-visit "^2.0.3" +remark-parse-no-trim@^8.0.4: + version "8.0.4" + resolved "https://registry.yarnpkg.com/remark-parse-no-trim/-/remark-parse-no-trim-8.0.4.tgz#f5c9531644284071d4a57a49e19a42ad4e8040bd" + integrity sha512-WtqeHNTZ0LSdyemmY1/G6y9WoEFblTtgckfKF5/NUnri919/0/dEu8RCDfvXtJvu96soMvT+mLWWgYVUaiHoag== + dependencies: + ccount "^1.0.0" + collapse-white-space "^1.0.2" + is-alphabetical "^1.0.0" + is-decimal "^1.0.0" + is-whitespace-character "^1.0.0" + is-word-character "^1.0.0" + markdown-escapes "^1.0.0" + parse-entities "^2.0.0" + repeat-string "^1.5.4" + state-toggle "^1.0.0" + trim-trailing-lines "^1.0.0" + unherit "^1.0.4" + unist-util-remove-position "^2.0.0" + vfile-location "^3.0.0" + xtend "^4.0.1" + remark-parse@^8.0.3: version "8.0.3" resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-8.0.3.tgz#9c62aa3b35b79a486454c690472906075f40c7e1" @@ -9345,7 +9605,7 @@ remark-parse@^8.0.3: vfile-location "^3.0.0" xtend "^4.0.1" -remark-rehype@^8.0.0: +remark-rehype@^8.0.0, remark-rehype@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/remark-rehype/-/remark-rehype-8.1.0.tgz#610509a043484c1e697437fa5eb3fd992617c945" integrity sha512-EbCu9kHgAxKmW1yEYjx3QafMyGY3q8noUbNUI5xyKbaFP89wbhDrKxyIQNukNYthzjNHZu6J7hwFg7hRm1svYA== @@ -9588,11 +9848,6 @@ select-hose@^2.0.0: resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= -select@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" - integrity sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA== - selfsigned@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.0.1.tgz#8b2df7fa56bf014d19b6007655fff209c0ef0a56" @@ -9707,7 +9962,7 @@ setprototypeof@1.2.0: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== -shallowequal@^1.1.0: +shallowequal@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== @@ -9787,6 +10042,11 @@ source-map-js@^1.0.1, source-map-js@^1.0.2: resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== +source-map-js@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + source-map-loader@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-3.0.1.tgz#9ae5edc7c2d42570934be4c95d1ccc6352eba52d" @@ -10066,21 +10326,20 @@ style-to-object@^0.3.0: dependencies: inline-style-parser "0.1.1" -styled-components@^5.1.1: - version "5.3.11" - resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-5.3.11.tgz#9fda7bf1108e39bf3f3e612fcc18170dedcd57a8" - integrity sha512-uuzIIfnVkagcVHv9nE0VPlHPSCmXIUGKfJ42LNjxCCTDTL5sgnJ8Z7GZBq0EnLYGln77tPpEpExt2+qa+cZqSw== - dependencies: - "@babel/helper-module-imports" "^7.0.0" - "@babel/traverse" "^7.4.5" - "@emotion/is-prop-valid" "^1.1.0" - "@emotion/stylis" "^0.8.4" - "@emotion/unitless" "^0.7.4" - babel-plugin-styled-components ">= 1.12.0" - css-to-react-native "^3.0.0" - hoist-non-react-statics "^3.0.0" - shallowequal "^1.1.0" - supports-color "^5.5.0" +styled-components@^6.1.0: + version "6.1.13" + resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-6.1.13.tgz#2d777750b773b31469bd79df754a32479e9f475e" + integrity sha512-M0+N2xSnAtwcVAQeFEsGWFFxXDftHUD7XrKla06QbpUMmbmtFBMMTcKWvFXtWxuD5qQkB8iU5gk6QASlx2ZRMw== + dependencies: + "@emotion/is-prop-valid" "1.2.2" + "@emotion/unitless" "0.8.1" + "@types/stylis" "4.2.5" + css-to-react-native "3.2.0" + csstype "3.1.3" + postcss "8.4.38" + shallowequal "1.1.0" + stylis "4.3.2" + tslib "2.6.2" stylehacks@^5.1.0: version "5.1.0" @@ -10095,7 +10354,17 @@ stylis@4.0.13: resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.13.tgz#f5db332e376d13cc84ecfe5dace9a2a51d954c91" integrity sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag== -supports-color@^5.3.0, supports-color@^5.5.0: +stylis@4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51" + integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw== + +stylis@4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.3.2.tgz#8f76b70777dd53eb669c6f58c997bf0a9972e444" + integrity sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg== + +supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== @@ -10176,6 +10445,11 @@ tabbable@^5.2.1: resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-5.3.2.tgz#66d6119ee8a533634c3f17deb0caa1c379e36ac7" integrity sha512-6G/8EWRFx8CiSe2++/xHhXkmCRq2rHtDtZbQFHx34cvDfZzIBfvwG9zGUNTWMXWLCYvDj3aQqOzdl3oCxKuBkQ== +tabbable@^5.3.3: + version "5.3.3" + resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-5.3.3.tgz#aac0ff88c73b22d6c3c5a50b1586310006b47fbf" + integrity sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA== + tailwindcss@^3.0.2: version "3.0.24" resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.0.24.tgz#22e31e801a44a78a1d9a81ecc52e13b69d85704d" @@ -10307,11 +10581,6 @@ thunky@^1.0.2: resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== -tiny-emitter@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423" - integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q== - tiny-invariant@^1.0.6: version "1.2.0" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.2.0.tgz#a1141f86b672a9148c72e978a19a73b9b94a15a9" @@ -10393,6 +10662,11 @@ tsconfig-paths@^3.14.1: minimist "^1.2.6" strip-bom "^3.0.0" +tslib@2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + tslib@^1.0.0, tslib@^1.8.1, tslib@^1.9.3: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" @@ -10403,6 +10677,11 @@ tslib@^2.0.0, tslib@^2.0.3, tslib@^2.3.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== +tslib@^2.1.0, tslib@^2.6.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.0.tgz#d124c86c3c05a40a91e6fdea4021bd31d377971b" + integrity sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" @@ -10505,7 +10784,7 @@ unicode-property-aliases-ecmascript@^2.0.0: resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz#0a36cb9a585c4f6abd51ad1deddb285c165297c8" integrity sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ== -unified@^9.2.0: +unified@^9.2.0, unified@^9.2.2: version "9.2.2" resolved "https://registry.yarnpkg.com/unified/-/unified-9.2.2.tgz#67649a1abfc3ab85d2969502902775eb03146975" integrity sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ== @@ -10664,11 +10943,23 @@ use-callback-ref@^1.3.0: dependencies: tslib "^2.0.0" +use-callback-ref@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.2.tgz#6134c7f6ff76e2be0b56c809b17a650c942b1693" + integrity sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA== + dependencies: + tslib "^2.0.0" + use-memo-one@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.2.tgz#0c8203a329f76e040047a35a1197defe342fab20" integrity sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ== +use-memo-one@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.3.tgz#2fd2e43a2169eabc7496960ace8c79efef975e99" + integrity sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ== + use-query-params@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/use-query-params/-/use-query-params-1.2.3.tgz#306c31a0cbc714e8a3b4bd7e91a6a9aaccaa5e22" @@ -10684,6 +10975,11 @@ use-sidecar@^1.1.2: detect-node-es "^1.1.0" tslib "^2.0.0" +use-sync-external-store@^1.0.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9" + integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw== + util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -10754,7 +11050,7 @@ vfile-message@^2.0.0: "@types/unist" "^2.0.0" unist-util-stringify-position "^2.0.0" -vfile@^4.0.0, vfile@^4.2.0: +vfile@^4.0.0, vfile@^4.2.0, vfile@^4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/vfile/-/vfile-4.2.1.tgz#03f1dce28fc625c625bc6514350fbdb00fa9e624" integrity sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA== diff --git a/ui/package.json b/ui/package.json index 9a1876809b..b9371f42f1 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "@feast-dev/feast-ui", - "version": "0.40.0", + "version": "0.41.0", "private": false, "files": [ "dist" From 1f6378efb5f80193e5d9c6b7ae15fa742118bf87 Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Fri, 25 Oct 2024 20:46:32 -0400 Subject: [PATCH 182/185] chore: Update github action to remove make install-protoc-dependencies (#4700) Signed-off-by: Francisco Javier Arceo --- .github/workflows/build_wheels.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index 8fca17cf63..8e52ba12c9 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -97,7 +97,6 @@ jobs: # There's a `git restore` in here because `make install-go-ci-dependencies` is actually messing up go.mod & go.sum. run: | pip install -U pip setuptools wheel twine - make install-protoc-dependencies make build-ui git status git restore go.mod go.sum From 7bea8ee1bbfd3110292dc3f54147011389d41ac7 Mon Sep 17 00:00:00 2001 From: Francisco Javier Arceo Date: Fri, 25 Oct 2024 21:13:19 -0400 Subject: [PATCH 183/185] Revert "chore(release): release 0.41.0" This reverts commit e02e8dc09dddae7011cfbf5a020979a31039b057. --- CHANGELOG.md | 133 ---- infra/charts/feast-feature-server/Chart.yaml | 2 +- infra/charts/feast-feature-server/README.md | 7 +- infra/charts/feast-feature-server/values.yaml | 2 +- infra/charts/feast/Chart.yaml | 2 +- infra/charts/feast/README.md | 6 +- .../feast/charts/feature-server/Chart.yaml | 4 +- .../feast/charts/feature-server/README.md | 4 +- .../feast/charts/feature-server/values.yaml | 2 +- .../charts/transformation-service/Chart.yaml | 4 +- .../charts/transformation-service/README.md | 4 +- .../charts/transformation-service/values.yaml | 2 +- infra/charts/feast/requirements.yaml | 4 +- infra/feast-helm-operator/Makefile | 2 +- .../config/manager/kustomization.yaml | 2 +- infra/feast-operator/Makefile | 2 +- .../config/manager/kustomization.yaml | 2 +- java/pom.xml | 2 +- sdk/python/feast/ui/package.json | 2 +- sdk/python/feast/ui/yarn.lock | 730 ++++++------------ ui/package.json | 2 +- 21 files changed, 245 insertions(+), 675 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8368cf6718..6b7c8be4b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,138 +1,5 @@ # Changelog -# [0.41.0](https://github.com/feast-dev/feast/compare/v0.40.0...v0.41.0) (2024-10-26) - - -* chore!: Update @elastic/eui and @emotion/react in Feast UI ([#4597](https://github.com/feast-dev/feast/issues/4597)) ([b9ddbf9](https://github.com/feast-dev/feast/commit/b9ddbf9351a55b1a5c102102b06ad3b2f28b3d1f)) - - -### Bug Fixes - -* Add --chdir to test_workflow.py ([#4453](https://github.com/feast-dev/feast/issues/4453)) ([6b2f026](https://github.com/feast-dev/feast/commit/6b2f026747b8adebe659aed3d4d2f95d551d5d1e)) -* Add feast-operator files to semantic-release script ([#4382](https://github.com/feast-dev/feast/issues/4382)) ([8eceff2](https://github.com/feast-dev/feast/commit/8eceff26ba00fd446d27ad5ce2ee9d039c57fd9a)) -* Add feast-operator Makefile to semantic-release script ([#4424](https://github.com/feast-dev/feast/issues/4424)) ([d18d01d](https://github.com/feast-dev/feast/commit/d18d01de8356ffdc29b35c93978f36e7541d415e)) -* Added Offline Store Arrow client errors handler ([#4524](https://github.com/feast-dev/feast/issues/4524)) ([7535b40](https://github.com/feast-dev/feast/commit/7535b4036ce980c9a05bc33a9e61a7938ea1303e)) -* Added Online Store REST client errors handler ([#4488](https://github.com/feast-dev/feast/issues/4488)) ([2118719](https://github.com/feast-dev/feast/commit/21187199173f4c4f5417205d99535af6be492a9a)) -* Added Permission API docs ([#4485](https://github.com/feast-dev/feast/issues/4485)) ([2bd03fa](https://github.com/feast-dev/feast/commit/2bd03fa4da5e76f6b29b0b54b455d5552d256838)) -* Added support for multiple name patterns to Permissions ([#4633](https://github.com/feast-dev/feast/issues/4633)) ([f05e928](https://github.com/feast-dev/feast/commit/f05e92861da64d1c5e9cfe3c6307b3422d0d83b8)) -* Adding protobuf<5 as a required dependency due to snowflake limitations ([#4537](https://github.com/feast-dev/feast/issues/4537)) ([cecca83](https://github.com/feast-dev/feast/commit/cecca8360bed62ab2f4fddc5d3a888247ea0a87a)) -* Avoid the python 3.9+ threadpool cleanup bug ([#4627](https://github.com/feast-dev/feast/issues/4627)) ([ba05893](https://github.com/feast-dev/feast/commit/ba05893ba6db2d8d1e7bcc8cf8162f4fb72c9563)) -* Bigquery dataset create table disposition ([#4649](https://github.com/feast-dev/feast/issues/4649)) ([58e03d1](https://github.com/feast-dev/feast/commit/58e03d17448a883ef57bcd6d8926d1e54ddcdece)) -* Changes template file path to relative path ([#4624](https://github.com/feast-dev/feast/issues/4624)) ([3e313b1](https://github.com/feast-dev/feast/commit/3e313b15efc7fc72d35d70315fc8b7c172fc7993)) -* Check for snowflake functions when setting up materialization engine ([#4456](https://github.com/feast-dev/feast/issues/4456)) ([c365b4e](https://github.com/feast-dev/feast/commit/c365b4e71a16fb69883608c5f781c6d55502bb8e)) -* Correctly handle list values in _python_value_to_proto_value ([#4608](https://github.com/feast-dev/feast/issues/4608)) ([c0a1026](https://github.com/feast-dev/feast/commit/c0a10269914c2ca01fe1cf6b24b120bfa58d04e7)) -* Default to pandas mode if not specified in ODFV proto in database ([#4420](https://github.com/feast-dev/feast/issues/4420)) ([d235832](https://github.com/feast-dev/feast/commit/d235832b78027b98df8e8a9e434a51a0c78b3092)) -* Deleting data from feast_metadata when we delete project ([#4550](https://github.com/feast-dev/feast/issues/4550)) ([351a2d0](https://github.com/feast-dev/feast/commit/351a2d0a7f9808178ab9d201083eb2894ce7384f)) -* Disable active_timer When registry_ttl_sec is 0 ([#4499](https://github.com/feast-dev/feast/issues/4499)) ([c94f32f](https://github.com/feast-dev/feast/commit/c94f32f2b637c7b7d917d2456432180af7569cf5)) -* Escape special characters in the Postgres password ([#4394](https://github.com/feast-dev/feast/issues/4394)) ([419ca5e](https://github.com/feast-dev/feast/commit/419ca5e9523ff38f27141b79ae12ebb0646c6617)) -* FeastExtrasDependencyImportError when using SparkOfflineStore without S3 ([#4594](https://github.com/feast-dev/feast/issues/4594)) ([1ba94f7](https://github.com/feast-dev/feast/commit/1ba94f7e2018fea0114f1703dd3942d589071825)) -* Fix Feast project name test ([#4685](https://github.com/feast-dev/feast/issues/4685)) ([9f41fd6](https://github.com/feast-dev/feast/commit/9f41fd6673b9576c802c2378b56e04b9a090d99d)) -* Fix for SQL registry initialization fails [#4543](https://github.com/feast-dev/feast/issues/4543) ([#4544](https://github.com/feast-dev/feast/issues/4544)) ([4e2eacc](https://github.com/feast-dev/feast/commit/4e2eacc1beea8f8866b78968abadfd42eee63d6a)) -* Fix gitignore issue ([#4674](https://github.com/feast-dev/feast/issues/4674)) ([2807dfa](https://github.com/feast-dev/feast/commit/2807dfaf46d3d9e79f84b0ff22dbaeede377c89b)) -* Fix online pg import ([#4581](https://github.com/feast-dev/feast/issues/4581)) ([1f17caa](https://github.com/feast-dev/feast/commit/1f17caacdaa573d08dbf8dc68b20e73a187ed8a4)) -* Fix the mypy type check issue. ([#4498](https://github.com/feast-dev/feast/issues/4498)) ([7ecc615](https://github.com/feast-dev/feast/commit/7ecc615945b7bb48e103ca6eb278b39759d71c5a)) -* Fix vector store config ([#4583](https://github.com/feast-dev/feast/issues/4583)) ([11c00d4](https://github.com/feast-dev/feast/commit/11c00d43fd1b2c5caf4d49f705bd55c704edae8a)) -* Fixes validator field access for 'project_id' in BigQuery offline Store ([#4509](https://github.com/feast-dev/feast/issues/4509)) ([9a0398e](https://github.com/feast-dev/feast/commit/9a0398e2e18585172d857cf3202a81551d31609b)) -* Fixing failure of protos during ODFV transformations for missing entities ([#4667](https://github.com/feast-dev/feast/issues/4667)) ([41aaeeb](https://github.com/feast-dev/feast/commit/41aaeebaa5908f44cda28a4410e2fca412f53e92)) -* Fixing the master branch build failure. ([#4563](https://github.com/feast-dev/feast/issues/4563)) ([0192b2e](https://github.com/feast-dev/feast/commit/0192b2eb245c8e0ea9a913195ddf28382dc23982)) -* Hao xu request source timestamp_field ([#4495](https://github.com/feast-dev/feast/issues/4495)) ([96344b2](https://github.com/feast-dev/feast/commit/96344b2b6830dcc280567542d111d1b0f39879e0)) -* Ignore the type check as both functions calls are not belonging to Feast code. ([#4500](https://github.com/feast-dev/feast/issues/4500)) ([867f532](https://github.com/feast-dev/feast/commit/867f532154977790e3bb11f2a94baa4f2289de99)) -* Import grpc only for type checking in errors.py ([#4533](https://github.com/feast-dev/feast/issues/4533)) ([f308572](https://github.com/feast-dev/feast/commit/f308572715d0593951f71bb3da5c5be6de29a2f9)) -* Initial commit targetting grpc registry server ([#4458](https://github.com/feast-dev/feast/issues/4458)) ([484240c](https://github.com/feast-dev/feast/commit/484240c4e783d68bc521b62b723c2dcbd00fab5e)), closes [#4465](https://github.com/feast-dev/feast/issues/4465) -* Links to the RBAC documentation under Concepts and Components ([#4430](https://github.com/feast-dev/feast/issues/4430)) ([0a48f7b](https://github.com/feast-dev/feast/commit/0a48f7bb436febb0171c78a559a577eedeff421f)) -* Locate feature_store.yaml from __file__ ([#4443](https://github.com/feast-dev/feast/issues/4443)) ([20290ce](https://github.com/feast-dev/feast/commit/20290ce28c513f705db1dbb6b0f719ba1846217f)) -* Logger settings for feature servers and updated logger for permission flow ([#4531](https://github.com/feast-dev/feast/issues/4531)) ([50b8f23](https://github.com/feast-dev/feast/commit/50b8f238b6f9adbc9ff0b20e18b78b2948c2f440)) -* Move tslib from devDependencies to dependencies in Feast UI ([#4525](https://github.com/feast-dev/feast/issues/4525)) ([c5a4d90](https://github.com/feast-dev/feast/commit/c5a4d907bf34f4cf7477b212cd2820b0e7d24b7b)) -* Null value compatibility for unit timestamp list value type ([#4378](https://github.com/feast-dev/feast/issues/4378)) ([8f264b6](https://github.com/feast-dev/feast/commit/8f264b6807a07874dc01207c655baeef7dfaa7b2)) -* Patch FAISS online return signature ([#4671](https://github.com/feast-dev/feast/issues/4671)) ([0d45e95](https://github.com/feast-dev/feast/commit/0d45e95767773846861d53feb4af4c6bc1451b5e)) -* Quickstart documentation changes ([#4618](https://github.com/feast-dev/feast/issues/4618)) ([7ac0908](https://github.com/feast-dev/feast/commit/7ac0908f3846cc0ab05082f748506814c84b2e9c)) -* Refactor auth_client_manager_factory.py in function get_auth_client_m… ([#4505](https://github.com/feast-dev/feast/issues/4505)) ([def8633](https://github.com/feast-dev/feast/commit/def863360bf0e553d242900ee915e953c6c3f9b6)) -* Remote apply using offline store ([#4559](https://github.com/feast-dev/feast/issues/4559)) ([ac62a32](https://github.com/feast-dev/feast/commit/ac62a323c86fba8096eefde85775dd7a857e9e25)) -* Remove Feast UI TypeScript dependencies from `peerDependencies` and `dependencies` ([#4554](https://github.com/feast-dev/feast/issues/4554)) ([e781e16](https://github.com/feast-dev/feast/commit/e781e1652cadc6576dbab369248d6e4afdb5f158)) -* Remove unnecessary peer dependencies from Feast UI ([#4577](https://github.com/feast-dev/feast/issues/4577)) ([9ac7f4e](https://github.com/feast-dev/feast/commit/9ac7f4ea77357219fc8420f05e493767bc5357c2)) -* Removed protobuf as a required dependency ([#4535](https://github.com/feast-dev/feast/issues/4535)) ([0fb76e9](https://github.com/feast-dev/feast/commit/0fb76e9041885659c68e294b0c033c62050bd374)) -* Removed the k8s dependency from required dependencies ([#4519](https://github.com/feast-dev/feast/issues/4519)) ([3073ea5](https://github.com/feast-dev/feast/commit/3073ea5911339a5744be45512a9a2ee8b250292b)) -* Removed usage of pull_request_target as much as possible to prevent security concerns ([#4549](https://github.com/feast-dev/feast/issues/4549)) ([3198371](https://github.com/feast-dev/feast/commit/3198371fc0e07f6b51b62c7e3abbc48729078bb9)) -* Replaced ClusterRoles with local RoleBindings ([#4625](https://github.com/feast-dev/feast/issues/4625)) ([ca9fb9b](https://github.com/feast-dev/feast/commit/ca9fb9bc8c3f3b06b4ba5fce362e26633144715c)) -* Retire pytz library ([#4406](https://github.com/feast-dev/feast/issues/4406)) ([23c6c86](https://github.com/feast-dev/feast/commit/23c6c862e1da4e9523530eb48c7ce79319dc442d)) -* Typos related to k8s ([#4442](https://github.com/feast-dev/feast/issues/4442)) ([dda0088](https://github.com/feast-dev/feast/commit/dda0088f25eab5828613bd6d080aeddf681641f0)) -* Update java testcontainers to use Compose V2 ([#4381](https://github.com/feast-dev/feast/issues/4381)) ([9a33fce](https://github.com/feast-dev/feast/commit/9a33fce695c54226b3afd7b998e284f358bab141)) -* Update min versions for pyarrow and protobuf ([#4646](https://github.com/feast-dev/feast/issues/4646)) ([c7ddd4b](https://github.com/feast-dev/feast/commit/c7ddd4bb48290d6d9327fcb6349b84b4d14334af)) -* Update react-router-dom to 6.3.0 and restrict its version in Feast UI ([#4556](https://github.com/feast-dev/feast/issues/4556)) ([4293608](https://github.com/feast-dev/feast/commit/42936084a7d214d65faea5359ae70eefda8d23ad)), closes [#3794](https://github.com/feast-dev/feast/issues/3794) [/github.com/remix-run/react-router/blob/main/CHANGELOG.md#v630](https://github.com//github.com/remix-run/react-router/blob/main/CHANGELOG.md/issues/v630) -* Update the base image for feature-server. ([#4576](https://github.com/feast-dev/feast/issues/4576)) ([0390d8a](https://github.com/feast-dev/feast/commit/0390d8a86f50360a4df89165db62972328d22ca4)) -* Update the base image of materilization engine. ([#4580](https://github.com/feast-dev/feast/issues/4580)) ([f8592d8](https://github.com/feast-dev/feast/commit/f8592d86b2903ebfebc505bbf9392927aae5609c)) -* Updated README link ([#4669](https://github.com/feast-dev/feast/issues/4669)) ([35fbdc9](https://github.com/feast-dev/feast/commit/35fbdc91cc8a327be2a8cd162f7ccff983b16932)) -* Updating the documentation and adding tests for project length ([#4628](https://github.com/feast-dev/feast/issues/4628)) ([945b0fa](https://github.com/feast-dev/feast/commit/945b0faadd40c8dc76d104bce14ee902bd513127)) -* Using get_type_hints instead of inspect signature for udf return annotation ([#4391](https://github.com/feast-dev/feast/issues/4391)) ([3a32e8a](https://github.com/feast-dev/feast/commit/3a32e8ae28110db0934fc26ec6992eb606fed012)) -* Using repo_config parameter in teardown to allow for feature-store-yaml overrides ([#4413](https://github.com/feast-dev/feast/issues/4413)) ([0baeeb5](https://github.com/feast-dev/feast/commit/0baeeb5ec524c1e6209edab9605ca8a098a2ec88)) -* Validating permission to update an existing request on both the new and the old instance ([#4449](https://github.com/feast-dev/feast/issues/4449)) ([635a01b](https://github.com/feast-dev/feast/commit/635a01b4c77db781d67f9f5ebb1067806b1e2a13)) - - -### Features - -* Add boto3 session based auth for dynamodb online store for cross account access ([#4606](https://github.com/feast-dev/feast/issues/4606)) ([00eaf74](https://github.com/feast-dev/feast/commit/00eaf744f50f56f449c7ad8f6ceb92a09304ee0b)) -* Add cli list/describe for SavedDatasets, StreamFeatureViews, & … ([#4487](https://github.com/feast-dev/feast/issues/4487)) ([7b250e5](https://github.com/feast-dev/feast/commit/7b250e5eff5de56f5c5da103e91051276940298a)) -* Add connection_name field to Snowflake config ([#4600](https://github.com/feast-dev/feast/issues/4600)) ([10ce2aa](https://github.com/feast-dev/feast/commit/10ce2aa0050e419acfae27971a6fff87bade3ba4)) -* Add health check service to registry server ([#4421](https://github.com/feast-dev/feast/issues/4421)) ([46655f0](https://github.com/feast-dev/feast/commit/46655f06de339ee09245de3e1a648eb4e3bcd729)) -* Add more __repr__ methods ([#4676](https://github.com/feast-dev/feast/issues/4676)) ([e726c09](https://github.com/feast-dev/feast/commit/e726c096f2de93d6dc0a807c97c47476cc79dd61)) -* Add registry methods for dealing with all FV types ([#4435](https://github.com/feast-dev/feast/issues/4435)) ([ac381b2](https://github.com/feast-dev/feast/commit/ac381b292cfa29804ee5f0822f876d227a0989d9)) -* Added Project object to Feast Objects ([#4475](https://github.com/feast-dev/feast/issues/4475)) ([4a6b663](https://github.com/feast-dev/feast/commit/4a6b663f80bc91d6de35ed2ec428d34811d17a18)) -* Added support for reading from Reader Endpoints for AWS Aurora use cases ([#4494](https://github.com/feast-dev/feast/issues/4494)) ([d793c77](https://github.com/feast-dev/feast/commit/d793c77d923df95a186b9d4829b167f1a5a304e6)) -* Adding documentation for On Demand Feature Transformations with writes ([#4607](https://github.com/feast-dev/feast/issues/4607)) ([8e0c1b5](https://github.com/feast-dev/feast/commit/8e0c1b51665357d44229239401a04b76396b9047)) -* Adding mode='python' for get_historical_features on ODFVs ([#4653](https://github.com/feast-dev/feast/issues/4653)) ([c40d539](https://github.com/feast-dev/feast/commit/c40d539b85fe537077dd26904d78624da3d33951)) -* Adding registry cache support for get_on_demand_feature_view ([#4572](https://github.com/feast-dev/feast/issues/4572)) ([354c059](https://github.com/feast-dev/feast/commit/354c059e5475f9c3927d9180a421118507a22cf0)) -* Adding SSL support for online server ([#4677](https://github.com/feast-dev/feast/issues/4677)) ([80a5b3c](https://github.com/feast-dev/feast/commit/80a5b3c499faca8625d60267a34dcbddfe0c042a)) -* Adding write capability to online store to on demand feature views ([#4585](https://github.com/feast-dev/feast/issues/4585)) ([ef9e0bb](https://github.com/feast-dev/feast/commit/ef9e0bbdb2a80250786b87972a53c4cf5890bb76)), closes [#4603](https://github.com/feast-dev/feast/issues/4603) -* Allow feast snowflake to read in byte string for private-key authentication ([#4384](https://github.com/feast-dev/feast/issues/4384)) ([5215a21](https://github.com/feast-dev/feast/commit/5215a2139a9d824dc2d8f45181bd177a1e8e9561)) -* An action to test operator at PR time ([#4635](https://github.com/feast-dev/feast/issues/4635)) ([14c1000](https://github.com/feast-dev/feast/commit/14c1000554c590cb89ecb5ef44c57aa1b5dd1387)) -* Create ADOPTERS.md ([#4410](https://github.com/feast-dev/feast/issues/4410)) ([721ec74](https://github.com/feast-dev/feast/commit/721ec74f17ee95e375054f21135e54e0687104a7)) -* Create initial structure of Feast Go Operator ([#4596](https://github.com/feast-dev/feast/issues/4596)) ([b5ab6c7](https://github.com/feast-dev/feast/commit/b5ab6c799d529aaea19a196cedca7bb2c93cbbe9)) -* Faiss and In memory store ([#4464](https://github.com/feast-dev/feast/issues/4464)) ([a1ff129](https://github.com/feast-dev/feast/commit/a1ff1290002376b4c9ac8ad14e60df4622bb47a4)) -* Feast Security Model (aka RBAC) ([#4380](https://github.com/feast-dev/feast/issues/4380)) ([1771f66](https://github.com/feast-dev/feast/commit/1771f668247ef3b46ea7dac634e557e249bc1ba9)), closes [#36](https://github.com/feast-dev/feast/issues/36) -* Instrument Feast using Prometheus and OpenTelemetry ([#4366](https://github.com/feast-dev/feast/issues/4366)) ([a571e08](https://github.com/feast-dev/feast/commit/a571e08b97a95f8543d7dea27902c135ab3a4378)) -* Intra server to server communication ([#4433](https://github.com/feast-dev/feast/issues/4433)) ([729c874](https://github.com/feast-dev/feast/commit/729c874e8c30719f23ad287d3cb84f1d654274ec)) -* Publish TypeScript types in Feast UI package ([#4551](https://github.com/feast-dev/feast/issues/4551)) ([334e5d7](https://github.com/feast-dev/feast/commit/334e5d78855709d4ca56619f16eecb414f88ce2d)) -* Refactoring code to get oidc end points from discovery URL. ([#4429](https://github.com/feast-dev/feast/issues/4429)) ([896360a](https://github.com/feast-dev/feast/commit/896360af19a37c9a2a4634ec88021c4f69bdb141)) -* Return entity key in the retrieval document api ([#4511](https://github.com/feast-dev/feast/issues/4511)) ([5f5caf0](https://github.com/feast-dev/feast/commit/5f5caf0cac539ed779692e0ec819659cf5a33a0d)) -* Update roadmap.md ([#4445](https://github.com/feast-dev/feast/issues/4445)) ([34238d2](https://github.com/feast-dev/feast/commit/34238d2a0bfe9dbad753fec9613c83d848b1a520)) -* Update sqlite-vec package ([#4389](https://github.com/feast-dev/feast/issues/4389)) ([b734cb1](https://github.com/feast-dev/feast/commit/b734cb147a4afd28407ec57d95f70ff604f82954)) -* Updated Feast model Inference Architecture ([#4570](https://github.com/feast-dev/feast/issues/4570)) ([8cd0dcf](https://github.com/feast-dev/feast/commit/8cd0dcff0cc3b1387c7ca65018ebb09e03538242)) -* Updating docs to include model inference guidelines ([#4416](https://github.com/feast-dev/feast/issues/4416)) ([cebbe04](https://github.com/feast-dev/feast/commit/cebbe045597b85e1ae4394a8c14741e88347a6b8)) -* Updating FeatureViewProjection and OnDemandFeatureView to add batch_source and entities ([#4530](https://github.com/feast-dev/feast/issues/4530)) ([0795496](https://github.com/feast-dev/feast/commit/07954960c5501e2ecc1f1285ddf4aa68f9ac880b)) -* Upgrade React from 17.0.2 to 18.3.1 in Feast UI ([#4620](https://github.com/feast-dev/feast/issues/4620)) ([d6f3cb8](https://github.com/feast-dev/feast/commit/d6f3cb81a4c2a51bae2d29a185753cf6b1d2b16d)) - - -### Performance Improvements - -* Add init and cleanup of long lived resources ([#4642](https://github.com/feast-dev/feast/issues/4642)) ([47dc04d](https://github.com/feast-dev/feast/commit/47dc04d43f483944f97248abaccd15dba319281f)) -* Added indexes to sql tables to optimize query execution ([#4538](https://github.com/feast-dev/feast/issues/4538)) ([9688790](https://github.com/feast-dev/feast/commit/9688790a5e7a70f628a46021bde0201922c7e04d)) -* Default to async endpoints, use threadpool for sync ([#4647](https://github.com/feast-dev/feast/issues/4647)) ([c1f1912](https://github.com/feast-dev/feast/commit/c1f19127ab5c22dfe67869990480e7e9d8183ab1)) -* Implement dynamo write_batch_async ([#4675](https://github.com/feast-dev/feast/issues/4675)) ([ba4404c](https://github.com/feast-dev/feast/commit/ba4404cecfb196c4084f9bf892cd3528184d42c1)) -* Make /push async ([#4650](https://github.com/feast-dev/feast/issues/4650)) ([61abf89](https://github.com/feast-dev/feast/commit/61abf894aca7aa52042c40e77f64b49835f4324e)) -* Parallelize read calls by table and batch ([#4619](https://github.com/feast-dev/feast/issues/4619)) ([043eff1](https://github.com/feast-dev/feast/commit/043eff1a87bdf775a437503395acda87cbecf875)) - - -### BREAKING CHANGES - -* Consuming apps that use @elastic/eui should update it -to a compatible version. If you use @elastic/eui components that have -been renamed or replaced with others, you'll need to update your code -accordingly. - -Signed-off-by: Harri Lehtola - -* chore: Update Node version from 17 to 20 in UI unit tests - -Node 17 is not an LTS (long-term support) version and apparently -rejected by the latest versions of Elastic UI: - -> error @elastic/eui@95.12.0: The engine "node" is incompatible with -> this module. Expected version "16.x || 18.x || >=20.x". Got "17.9.1" - -Let's try with the latest LTS version. - -Signed-off-by: Harri Lehtola - # [0.40.0](https://github.com/feast-dev/feast/compare/v0.39.0...v0.40.0) (2024-07-31) diff --git a/infra/charts/feast-feature-server/Chart.yaml b/infra/charts/feast-feature-server/Chart.yaml index dd547843d1..af47692f16 100644 --- a/infra/charts/feast-feature-server/Chart.yaml +++ b/infra/charts/feast-feature-server/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 name: feast-feature-server description: Feast Feature Server in Go or Python type: application -version: 0.41.0 +version: 0.40.0 keywords: - machine learning - big data diff --git a/infra/charts/feast-feature-server/README.md b/infra/charts/feast-feature-server/README.md index a36f59d85e..bff7820d1f 100644 --- a/infra/charts/feast-feature-server/README.md +++ b/infra/charts/feast-feature-server/README.md @@ -1,6 +1,6 @@ # Feast Python / Go Feature Server Helm Charts -Current chart version is `0.41.0` +Current chart version is `0.40.0` ## Installation @@ -40,16 +40,16 @@ See [here](https://github.com/feast-dev/feast/tree/master/examples/python-helm-d | fullnameOverride | string | `""` | | | image.pullPolicy | string | `"IfNotPresent"` | | | image.repository | string | `"feastdev/feature-server"` | Docker image for Feature Server repository | -| image.tag | string | `"0.41.0"` | The Docker image tag (can be overwritten if custom feature server deps are needed for on demand transforms) | +| image.tag | string | `"0.40.0"` | The Docker image tag (can be overwritten if custom feature server deps are needed for on demand transforms) | | imagePullSecrets | list | `[]` | | | livenessProbe.initialDelaySeconds | int | `30` | | | livenessProbe.periodSeconds | int | `30` | | -| logLevel | string | `"WARNING"` | | | metrics.enabled | bool | `false` | | | metrics.otelCollector.endpoint | string | `""` | | | metrics.otelCollector.port | int | `4317` | | | nameOverride | string | `""` | | | nodeSelector | object | `{}` | | +| otel_service.name | string | `"otelcol"` | | | podAnnotations | object | `{}` | | | podSecurityContext | object | `{}` | | | readinessProbe.initialDelaySeconds | int | `20` | | @@ -59,5 +59,4 @@ See [here](https://github.com/feast-dev/feast/tree/master/examples/python-helm-d | securityContext | object | `{}` | | | service.port | int | `80` | | | service.type | string | `"ClusterIP"` | | -| serviceAccount.name | string | `""` | | | tolerations | list | `[]` | | \ No newline at end of file diff --git a/infra/charts/feast-feature-server/values.yaml b/infra/charts/feast-feature-server/values.yaml index d894177558..f0bc55a646 100644 --- a/infra/charts/feast-feature-server/values.yaml +++ b/infra/charts/feast-feature-server/values.yaml @@ -9,7 +9,7 @@ image: repository: feastdev/feature-server pullPolicy: IfNotPresent # image.tag -- The Docker image tag (can be overwritten if custom feature server deps are needed for on demand transforms) - tag: 0.41.0 + tag: 0.40.0 logLevel: "WARNING" # Set log level DEBUG, INFO, WARNING, ERROR, and CRITICAL (case-insensitive) diff --git a/infra/charts/feast/Chart.yaml b/infra/charts/feast/Chart.yaml index a192da8911..1f030ca617 100644 --- a/infra/charts/feast/Chart.yaml +++ b/infra/charts/feast/Chart.yaml @@ -1,7 +1,7 @@ apiVersion: v1 description: Feature store for machine learning name: feast -version: 0.41.0 +version: 0.40.0 keywords: - machine learning - big data diff --git a/infra/charts/feast/README.md b/infra/charts/feast/README.md index e49fbf6d96..bf8e4d7b8d 100644 --- a/infra/charts/feast/README.md +++ b/infra/charts/feast/README.md @@ -8,7 +8,7 @@ This repo contains Helm charts for Feast Java components that are being installe ## Chart: Feast -Feature store for machine learning Current chart version is `0.41.0` +Feature store for machine learning Current chart version is `0.40.0` ## Installation @@ -65,8 +65,8 @@ See [here](https://github.com/feast-dev/feast/tree/master/examples/java-demo) fo | Repository | Name | Version | |------------|------|---------| | https://charts.helm.sh/stable | redis | 10.5.6 | -| https://feast-helm-charts.storage.googleapis.com | feature-server(feature-server) | 0.41.0 | -| https://feast-helm-charts.storage.googleapis.com | transformation-service(transformation-service) | 0.41.0 | +| https://feast-helm-charts.storage.googleapis.com | feature-server(feature-server) | 0.40.0 | +| https://feast-helm-charts.storage.googleapis.com | transformation-service(transformation-service) | 0.40.0 | ## Values diff --git a/infra/charts/feast/charts/feature-server/Chart.yaml b/infra/charts/feast/charts/feature-server/Chart.yaml index 69748a362f..f91185be84 100644 --- a/infra/charts/feast/charts/feature-server/Chart.yaml +++ b/infra/charts/feast/charts/feature-server/Chart.yaml @@ -1,8 +1,8 @@ apiVersion: v1 description: "Feast Feature Server: Online feature serving service for Feast" name: feature-server -version: 0.41.0 -appVersion: v0.41.0 +version: 0.40.0 +appVersion: v0.40.0 keywords: - machine learning - big data diff --git a/infra/charts/feast/charts/feature-server/README.md b/infra/charts/feast/charts/feature-server/README.md index ab77911a8f..c75fc421c6 100644 --- a/infra/charts/feast/charts/feature-server/README.md +++ b/infra/charts/feast/charts/feature-server/README.md @@ -1,6 +1,6 @@ # feature-server -![Version: 0.41.0](https://img.shields.io/badge/Version-0.41.0-informational?style=flat-square) ![AppVersion: v0.41.0](https://img.shields.io/badge/AppVersion-v0.41.0-informational?style=flat-square) +![Version: 0.40.0](https://img.shields.io/badge/Version-0.40.0-informational?style=flat-square) ![AppVersion: v0.40.0](https://img.shields.io/badge/AppVersion-v0.40.0-informational?style=flat-square) Feast Feature Server: Online feature serving service for Feast @@ -17,7 +17,7 @@ Feast Feature Server: Online feature serving service for Feast | envOverrides | object | `{}` | Extra environment variables to set | | image.pullPolicy | string | `"IfNotPresent"` | Image pull policy | | image.repository | string | `"feastdev/feature-server-java"` | Docker image for Feature Server repository | -| image.tag | string | `"0.41.0"` | Image tag | +| image.tag | string | `"0.40.0"` | Image tag | | ingress.grpc.annotations | object | `{}` | Extra annotations for the ingress | | ingress.grpc.auth.enabled | bool | `false` | Flag to enable auth | | ingress.grpc.class | string | `"nginx"` | Which ingress controller to use | diff --git a/infra/charts/feast/charts/feature-server/values.yaml b/infra/charts/feast/charts/feature-server/values.yaml index 646d735ef8..d9c964bbca 100644 --- a/infra/charts/feast/charts/feature-server/values.yaml +++ b/infra/charts/feast/charts/feature-server/values.yaml @@ -5,7 +5,7 @@ image: # image.repository -- Docker image for Feature Server repository repository: feastdev/feature-server-java # image.tag -- Image tag - tag: 0.41.0 + tag: 0.40.0 # image.pullPolicy -- Image pull policy pullPolicy: IfNotPresent diff --git a/infra/charts/feast/charts/transformation-service/Chart.yaml b/infra/charts/feast/charts/transformation-service/Chart.yaml index 6c450852cb..7e336e7a3e 100644 --- a/infra/charts/feast/charts/transformation-service/Chart.yaml +++ b/infra/charts/feast/charts/transformation-service/Chart.yaml @@ -1,8 +1,8 @@ apiVersion: v1 description: "Transformation service: to compute on-demand features" name: transformation-service -version: 0.41.0 -appVersion: v0.41.0 +version: 0.40.0 +appVersion: v0.40.0 keywords: - machine learning - big data diff --git a/infra/charts/feast/charts/transformation-service/README.md b/infra/charts/feast/charts/transformation-service/README.md index a00a21f034..f90d5bda18 100644 --- a/infra/charts/feast/charts/transformation-service/README.md +++ b/infra/charts/feast/charts/transformation-service/README.md @@ -1,6 +1,6 @@ # transformation-service -![Version: 0.41.0](https://img.shields.io/badge/Version-0.41.0-informational?style=flat-square) ![AppVersion: v0.41.0](https://img.shields.io/badge/AppVersion-v0.41.0-informational?style=flat-square) +![Version: 0.40.0](https://img.shields.io/badge/Version-0.40.0-informational?style=flat-square) ![AppVersion: v0.40.0](https://img.shields.io/badge/AppVersion-v0.40.0-informational?style=flat-square) Transformation service: to compute on-demand features @@ -13,7 +13,7 @@ Transformation service: to compute on-demand features | envOverrides | object | `{}` | Extra environment variables to set | | image.pullPolicy | string | `"IfNotPresent"` | Image pull policy | | image.repository | string | `"feastdev/feature-transformation-server"` | Docker image for Transformation Server repository | -| image.tag | string | `"0.41.0"` | Image tag | +| image.tag | string | `"0.40.0"` | Image tag | | nodeSelector | object | `{}` | Node labels for pod assignment | | podLabels | object | `{}` | Labels to be added to Feast Serving pods | | replicaCount | int | `1` | Number of pods that will be created | diff --git a/infra/charts/feast/charts/transformation-service/values.yaml b/infra/charts/feast/charts/transformation-service/values.yaml index 51cd72d659..aee47048e8 100644 --- a/infra/charts/feast/charts/transformation-service/values.yaml +++ b/infra/charts/feast/charts/transformation-service/values.yaml @@ -5,7 +5,7 @@ image: # image.repository -- Docker image for Transformation Server repository repository: feastdev/feature-transformation-server # image.tag -- Image tag - tag: 0.41.0 + tag: 0.40.0 # image.pullPolicy -- Image pull policy pullPolicy: IfNotPresent diff --git a/infra/charts/feast/requirements.yaml b/infra/charts/feast/requirements.yaml index bb69ee9ed3..7b1277fc69 100644 --- a/infra/charts/feast/requirements.yaml +++ b/infra/charts/feast/requirements.yaml @@ -1,12 +1,12 @@ dependencies: - name: feature-server alias: feature-server - version: 0.41.0 + version: 0.40.0 condition: feature-server.enabled repository: https://feast-helm-charts.storage.googleapis.com - name: transformation-service alias: transformation-service - version: 0.41.0 + version: 0.40.0 condition: transformation-service.enabled repository: https://feast-helm-charts.storage.googleapis.com - name: redis diff --git a/infra/feast-helm-operator/Makefile b/infra/feast-helm-operator/Makefile index 733bf7bc3d..6712a37b4a 100644 --- a/infra/feast-helm-operator/Makefile +++ b/infra/feast-helm-operator/Makefile @@ -3,7 +3,7 @@ # To re-generate a bundle for another specific version without changing the standard setup, you can: # - use the VERSION as arg of the bundle target (e.g make bundle VERSION=0.0.2) # - use environment variables to overwrite this value (e.g export VERSION=0.0.2) -VERSION ?= 0.41.0 +VERSION ?= 0.40.0 # CHANNELS define the bundle channels used in the bundle. # Add a new line here if you would like to change its default config. (E.g CHANNELS = "candidate,fast,stable") diff --git a/infra/feast-helm-operator/config/manager/kustomization.yaml b/infra/feast-helm-operator/config/manager/kustomization.yaml index decb714a20..0d27c2149d 100644 --- a/infra/feast-helm-operator/config/manager/kustomization.yaml +++ b/infra/feast-helm-operator/config/manager/kustomization.yaml @@ -5,4 +5,4 @@ kind: Kustomization images: - name: controller newName: feastdev/feast-helm-operator - newTag: 0.41.0 + newTag: 0.40.0 diff --git a/infra/feast-operator/Makefile b/infra/feast-operator/Makefile index 54786eb5f1..a6b922b062 100644 --- a/infra/feast-operator/Makefile +++ b/infra/feast-operator/Makefile @@ -3,7 +3,7 @@ # To re-generate a bundle for another specific version without changing the standard setup, you can: # - use the VERSION as arg of the bundle target (e.g make bundle VERSION=0.0.2) # - use environment variables to overwrite this value (e.g export VERSION=0.0.2) -VERSION ?= 0.41.0 +VERSION ?= 0.40.0 # CHANNELS define the bundle channels used in the bundle. # Add a new line here if you would like to change its default config. (E.g CHANNELS = "candidate,fast,stable") diff --git a/infra/feast-operator/config/manager/kustomization.yaml b/infra/feast-operator/config/manager/kustomization.yaml index 253475b945..aba3224be6 100644 --- a/infra/feast-operator/config/manager/kustomization.yaml +++ b/infra/feast-operator/config/manager/kustomization.yaml @@ -5,4 +5,4 @@ kind: Kustomization images: - name: controller newName: feastdev/feast-operator - newTag: 0.41.0 + newTag: 0.40.0 diff --git a/java/pom.xml b/java/pom.xml index 416ebe5978..2c1c32792c 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -35,7 +35,7 @@ - 0.41.0 + 0.40.0 https://github.com/feast-dev/feast UTF-8 diff --git a/sdk/python/feast/ui/package.json b/sdk/python/feast/ui/package.json index 2a6329a166..36777ca0be 100644 --- a/sdk/python/feast/ui/package.json +++ b/sdk/python/feast/ui/package.json @@ -6,7 +6,7 @@ "@elastic/datemath": "^5.0.3", "@elastic/eui": "^55.0.1", "@emotion/react": "^11.9.0", - "@feast-dev/feast-ui": "0.41.0", + "@feast-dev/feast-ui": "0.40.0", "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^13.2.0", "@testing-library/user-event": "^13.5.0", diff --git a/sdk/python/feast/ui/yarn.lock b/sdk/python/feast/ui/yarn.lock index 24de47b123..7e83084445 100644 --- a/sdk/python/feast/ui/yarn.lock +++ b/sdk/python/feast/ui/yarn.lock @@ -95,6 +95,13 @@ dependencies: "@babel/types" "^7.16.7" +"@babel/helper-annotate-as-pure@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz#e7f06737b197d580a01edf75d97e2c8be99d3882" + integrity sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-builder-binary-assignment-operator-visitor@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.16.7.tgz#38d138561ea207f0f69eb1626a418e4f7e6a580b" @@ -204,6 +211,13 @@ dependencies: "@babel/types" "^7.17.0" +"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz#1a8f4c9f4027d23f520bd76b364d44434a72660c" + integrity sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz#25612a8091a999704461c8a222d0efec5d091437" @@ -237,6 +251,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz#aa3a8ab4c3cceff8e65eb9e73d87dc4ff320b2f5" integrity sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA== +"@babel/helper-plugin-utils@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295" + integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg== + "@babel/helper-remap-async-to-generator@^7.16.8": version "7.16.8" resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.8.tgz#29ffaade68a367e2ed09c90901986918d25e57e3" @@ -590,6 +609,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.16.7" +"@babel/plugin-syntax-jsx@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz#a6b68e84fb76e759fc3b93e901876ffabbe1d918" + integrity sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-logical-assignment-operators@^7.10.4", "@babel/plugin-syntax-logical-assignment-operators@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" @@ -1106,13 +1132,6 @@ dependencies: regenerator-runtime "^0.13.11" -"@babel/runtime@^7.12.1", "@babel/runtime@^7.18.3", "@babel/runtime@^7.24.1": - version "7.26.0" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1" - integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw== - dependencies: - regenerator-runtime "^0.14.0" - "@babel/template@^7.16.7", "@babel/template@^7.3.3": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155" @@ -1131,7 +1150,7 @@ "@babel/parser" "^7.22.15" "@babel/types" "^7.22.15" -"@babel/traverse@^7.13.0", "@babel/traverse@^7.16.7", "@babel/traverse@^7.16.8", "@babel/traverse@^7.17.10", "@babel/traverse@^7.17.3", "@babel/traverse@^7.17.9", "@babel/traverse@^7.7.2": +"@babel/traverse@^7.13.0", "@babel/traverse@^7.16.7", "@babel/traverse@^7.16.8", "@babel/traverse@^7.17.10", "@babel/traverse@^7.17.3", "@babel/traverse@^7.17.9", "@babel/traverse@^7.4.5", "@babel/traverse@^7.7.2": version "7.23.2" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8" integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw== @@ -1317,62 +1336,6 @@ uuid "^8.3.0" vfile "^4.2.0" -"@elastic/eui@^95.12.0": - version "95.12.0" - resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-95.12.0.tgz#862f2be8b72248a62b40704b9e62f2f5d7d43853" - integrity sha512-SW4ru97FY2VitSqyCgURrM5OMk1W+Ww12b6S+VZN5ex50aNT296DfED/ByidlYaAoVihqjZuoB3HlQBBXydFpA== - dependencies: - "@hello-pangea/dnd" "^16.6.0" - "@types/lodash" "^4.14.202" - "@types/numeral" "^2.0.5" - "@types/react-window" "^1.8.8" - "@types/refractor" "^3.4.0" - chroma-js "^2.4.2" - classnames "^2.5.1" - lodash "^4.17.21" - mdast-util-to-hast "^10.2.0" - numeral "^2.0.6" - prop-types "^15.8.1" - react-dropzone "^11.7.1" - react-element-to-jsx-string "^15.0.0" - react-focus-on "^3.9.1" - react-is "^17.0.2" - react-remove-scroll-bar "^2.3.4" - react-virtualized-auto-sizer "^1.0.24" - react-window "^1.8.10" - refractor "^3.6.0" - rehype-raw "^5.1.0" - rehype-react "^6.2.1" - rehype-stringify "^8.0.0" - remark-breaks "^2.0.2" - remark-emoji "^2.1.0" - remark-parse-no-trim "^8.0.4" - remark-rehype "^8.1.0" - tabbable "^5.3.3" - text-diff "^1.0.1" - unified "^9.2.2" - unist-util-visit "^2.0.3" - url-parse "^1.5.10" - uuid "^8.3.0" - vfile "^4.2.1" - -"@emotion/babel-plugin@^11.12.0": - version "11.12.0" - resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.12.0.tgz#7b43debb250c313101b3f885eba634f1d723fcc2" - integrity sha512-y2WQb+oP8Jqvvclh8Q55gLUyb7UFvgv7eJfsj7td5TToBrIUtPay2kMrZi4xjq9qw2vD0ZR5fSho0yqoFgX7Rw== - dependencies: - "@babel/helper-module-imports" "^7.16.7" - "@babel/runtime" "^7.18.3" - "@emotion/hash" "^0.9.2" - "@emotion/memoize" "^0.9.0" - "@emotion/serialize" "^1.2.0" - babel-plugin-macros "^3.1.0" - convert-source-map "^1.5.0" - escape-string-regexp "^4.0.0" - find-root "^1.1.0" - source-map "^0.5.7" - stylis "4.2.0" - "@emotion/babel-plugin@^11.7.1": version "11.9.2" resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.9.2.tgz#723b6d394c89fb2ef782229d92ba95a740576e95" @@ -1391,17 +1354,6 @@ source-map "^0.5.7" stylis "4.0.13" -"@emotion/cache@^11.13.0": - version "11.13.1" - resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.13.1.tgz#fecfc54d51810beebf05bf2a161271a1a91895d7" - integrity sha512-iqouYkuEblRcXmylXIwwOodiEK5Ifl7JcX7o6V4jI3iW4mLXX3dmt5xwBtIkJiQEXFAI+pC8X0i67yiPkH9Ucw== - dependencies: - "@emotion/memoize" "^0.9.0" - "@emotion/sheet" "^1.4.0" - "@emotion/utils" "^1.4.0" - "@emotion/weak-memoize" "^0.4.0" - stylis "4.2.0" - "@emotion/cache@^11.7.1": version "11.7.1" resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.7.1.tgz#08d080e396a42e0037848214e8aa7bf879065539" @@ -1413,31 +1365,15 @@ "@emotion/weak-memoize" "^0.2.5" stylis "4.0.13" -"@emotion/css@^11.13.0": - version "11.13.4" - resolved "https://registry.yarnpkg.com/@emotion/css/-/css-11.13.4.tgz#a5128e34a23f5e2c891970b8ec98a60c5a2395e1" - integrity sha512-CthbOD5EBw+iN0rfM96Tuv5kaZN4nxPyYDvGUs0bc7wZBBiU/0mse+l+0O9RshW2d+v5HH1cme+BAbLJ/3Folw== - dependencies: - "@emotion/babel-plugin" "^11.12.0" - "@emotion/cache" "^11.13.0" - "@emotion/serialize" "^1.3.0" - "@emotion/sheet" "^1.4.0" - "@emotion/utils" "^1.4.0" - "@emotion/hash@^0.8.0": version "0.8.0" resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== -"@emotion/hash@^0.9.2": - version "0.9.2" - resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.2.tgz#ff9221b9f58b4dfe61e619a7788734bd63f6898b" - integrity sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g== - -"@emotion/is-prop-valid@1.2.2": - version "1.2.2" - resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz#d4175076679c6a26faa92b03bb786f9e52612337" - integrity sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw== +"@emotion/is-prop-valid@^1.1.0": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz#23116cf1ed18bfeac910ec6436561ecb1a3885cc" + integrity sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw== dependencies: "@emotion/memoize" "^0.8.1" @@ -1451,26 +1387,7 @@ resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.1.tgz#c1ddb040429c6d21d38cc945fe75c818cfb68e17" integrity sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA== -"@emotion/memoize@^0.9.0": - version "0.9.0" - resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.9.0.tgz#745969d649977776b43fc7648c556aaa462b4102" - integrity sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ== - -"@emotion/react@^11.13.3": - version "11.13.3" - resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.13.3.tgz#a69d0de2a23f5b48e0acf210416638010e4bd2e4" - integrity sha512-lIsdU6JNrmYfJ5EbUCf4xW1ovy5wKQ2CkPRM4xogziOxH1nXxBSjpC9YqbFAP7circxMfYp+6x676BqWcEiixg== - dependencies: - "@babel/runtime" "^7.18.3" - "@emotion/babel-plugin" "^11.12.0" - "@emotion/cache" "^11.13.0" - "@emotion/serialize" "^1.3.1" - "@emotion/use-insertion-effect-with-fallbacks" "^1.1.0" - "@emotion/utils" "^1.4.0" - "@emotion/weak-memoize" "^0.4.0" - hoist-non-react-statics "^3.3.1" - -"@emotion/react@^11.9.0": +"@emotion/react@^11.7.1", "@emotion/react@^11.9.0": version "11.9.0" resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.9.0.tgz#b6d42b1db3bd7511e7a7c4151dc8bc82e14593b8" integrity sha512-lBVSF5d0ceKtfKCDQJveNAtkC7ayxpVlgOohLgXqRwqWr9bOf4TZAFFyIcNngnV6xK6X4x2ZeXq7vliHkoVkxQ== @@ -1494,67 +1411,31 @@ "@emotion/utils" "^1.0.0" csstype "^3.0.2" -"@emotion/serialize@^1.2.0", "@emotion/serialize@^1.3.0", "@emotion/serialize@^1.3.1": - version "1.3.2" - resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.3.2.tgz#e1c1a2e90708d5d85d81ccaee2dfeb3cc0cccf7a" - integrity sha512-grVnMvVPK9yUVE6rkKfAJlYZgo0cu3l9iMC77V7DW6E1DUIrU68pSEXRmFZFOFB1QFo57TncmOcvcbMDWsL4yA== - dependencies: - "@emotion/hash" "^0.9.2" - "@emotion/memoize" "^0.9.0" - "@emotion/unitless" "^0.10.0" - "@emotion/utils" "^1.4.1" - csstype "^3.0.2" - "@emotion/sheet@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.1.0.tgz#56d99c41f0a1cda2726a05aa6a20afd4c63e58d2" integrity sha512-u0AX4aSo25sMAygCuQTzS+HsImZFuS8llY8O7b9MDRzbJM0kVJlAz6KNDqcG7pOuQZJmj/8X/rAW+66kMnMW+g== -"@emotion/sheet@^1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.4.0.tgz#c9299c34d248bc26e82563735f78953d2efca83c" - integrity sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg== - -"@emotion/unitless@0.8.1": - version "0.8.1" - resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.8.1.tgz#182b5a4704ef8ad91bde93f7a860a88fd92c79a3" - integrity sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ== - -"@emotion/unitless@^0.10.0": - version "0.10.0" - resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.10.0.tgz#2af2f7c7e5150f497bdabd848ce7b218a27cf745" - integrity sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg== +"@emotion/stylis@^0.8.4": + version "0.8.5" + resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.5.tgz#deacb389bd6ee77d1e7fcaccce9e16c5c7e78e04" + integrity sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ== -"@emotion/unitless@^0.7.5": +"@emotion/unitless@^0.7.4", "@emotion/unitless@^0.7.5": version "0.7.5" resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== -"@emotion/use-insertion-effect-with-fallbacks@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz#1a818a0b2c481efba0cf34e5ab1e0cb2dcb9dfaf" - integrity sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw== - "@emotion/utils@^1.0.0", "@emotion/utils@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.1.0.tgz#86b0b297f3f1a0f2bdb08eeac9a2f49afd40d0cf" integrity sha512-iRLa/Y4Rs5H/f2nimczYmS5kFJEbpiVvgN3XVfZ022IYhuNA1IRSHEizcof88LtCTXtl9S2Cxt32KgaXEu72JQ== -"@emotion/utils@^1.4.0", "@emotion/utils@^1.4.1": - version "1.4.1" - resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.4.1.tgz#b3adbb43de12ee2149541c4f1337d2eb7774f0ad" - integrity sha512-BymCXzCG3r72VKJxaYVwOXATqXIZ85cuvg0YOUDxMGNrKc1DJRZk8MgV5wyXRyEayIMd4FuXJIUgTBXvDNW5cA== - "@emotion/weak-memoize@^0.2.5": version "0.2.5" resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== -"@emotion/weak-memoize@^0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz#5e13fac887f08c44f76b0ccaf3370eb00fec9bb6" - integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg== - "@eslint/eslintrc@^1.2.3": version "1.2.3" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.2.3.tgz#fcaa2bcef39e13d6e9e7f6271f4cc7cae1174886" @@ -1570,40 +1451,32 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@feast-dev/feast-ui@0.41.0": - version "0.41.0" - resolved "https://registry.yarnpkg.com/@feast-dev/feast-ui/-/feast-ui-0.41.0.tgz#67eca6328131ee524ee6a6f286cfc4386f698053" - integrity sha512-BkVb4zfR+j95IX9FBzeXFyCimG5Za1a3jyLqjmETRO3hpp5OJanpc2N35AaOn8ZPqka00Be/b8NZ8TjbsRWyVg== +"@feast-dev/feast-ui@0.40.0": + version "0.40.0" + resolved "https://registry.yarnpkg.com/@feast-dev/feast-ui/-/feast-ui-0.40.0.tgz#0dc60cbbd4f63d161927321c0bbf57bbfe6b7d09" + integrity sha512-jiCtMYCBvNSfHCjemFRa0NFIIAR5y6spWBnUZyc4GXY2YxGcznw+PZSzOoi7JrOwpNzNPB0PTBUqJgBAxus20w== dependencies: "@elastic/datemath" "^5.0.3" - "@elastic/eui" "^95.12.0" - "@emotion/css" "^11.13.0" - "@emotion/react" "^11.13.3" + "@elastic/eui" "^55.0.1" + "@emotion/react" "^11.7.1" + "@types/d3" "^7.1.0" + "@types/jest" "^27.0.1" + "@types/node" "^16.7.13" + "@types/react" "^17.0.20" + "@types/react-dom" "^17.0.9" + d3 "^7.3.0" inter-ui "^3.19.3" moment "^2.29.1" + prop-types "^15.8.1" protobufjs "^7.1.1" query-string "^7.1.1" - react-code-blocks "^0.1.6" - react-query "^3.39.3" - react-router-dom "<6.4.0" - react-scripts "^5.0.1" - tslib "^2.3.1" + react-code-blocks "^0.0.9-0" + react-query "^3.34.12" + react-router-dom "6" + react-scripts "^5.0.0" use-query-params "^1.2.3" zod "^3.11.6" -"@hello-pangea/dnd@^16.6.0": - version "16.6.0" - resolved "https://registry.yarnpkg.com/@hello-pangea/dnd/-/dnd-16.6.0.tgz#7509639c7bd13f55e537b65a9dcfcd54e7c99ac7" - integrity sha512-vfZ4GydqbtUPXSLfAvKvXQ6xwRzIjUSjVU0Sx+70VOhc2xx6CdmJXJ8YhH70RpbTUGjxctslQTHul9sIOxCfFQ== - dependencies: - "@babel/runtime" "^7.24.1" - css-box-model "^1.2.1" - memoize-one "^6.0.0" - raf-schd "^4.0.3" - react-redux "^8.1.3" - redux "^4.2.1" - use-memo-one "^1.1.3" - "@humanwhocodes/config-array@^0.9.2": version "0.9.5" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.9.5.tgz#2cbaf9a89460da24b5ca6531b8bbfc23e1df50c7" @@ -2636,14 +2509,6 @@ "@types/react" "*" hoist-non-react-statics "^3.3.0" -"@types/hoist-non-react-statics@^3.3.1": - version "3.3.5" - resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz#dab7867ef789d87e2b4b0003c9d65c49cc44a494" - integrity sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg== - dependencies: - "@types/react" "*" - hoist-non-react-statics "^3.3.0" - "@types/html-minifier-terser@^6.0.0": version "6.1.0" resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#4fc33a00c1d0c16987b1a20cf92d20614c55ac35" @@ -2675,7 +2540,7 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@*": +"@types/jest@*", "@types/jest@^27.0.1": version "27.5.0" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.5.0.tgz#e04ed1824ca6b1dd0438997ba60f99a7405d4c7b" integrity sha512-9RBFx7r4k+msyj/arpfaa0WOOEcaAZNmN+j80KFbFCoSqCJGHTz7YMAMGQW9Xmqm5w6l5c25vbSjMwlikJi5+g== @@ -2698,11 +2563,6 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.182.tgz#05301a4d5e62963227eaafe0ce04dd77c54ea5c2" integrity sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q== -"@types/lodash@^4.14.202": - version "4.17.12" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.12.tgz#25d71312bf66512105d71e55d42e22c36bcfc689" - integrity sha512-sviUmCE8AYdaF/KIHLDJBQgeYzPBI0vf/17NaYehBJfYD1j6/L95Slh07NlyK2iNyBNaEkb3En2jRt+a8y3xZQ== - "@types/mdast@^3.0.0": version "3.0.10" resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.10.tgz#4724244a82a4598884cbbe9bcfd73dff927ee8af" @@ -2725,16 +2585,16 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.8.3.tgz#ce750ab4017effa51aed6a7230651778d54e327c" integrity sha512-0os9vz6BpGwxGe9LOhgP/ncvYN5Tx1fNcd2TM3rD/aCGBkysb+ZWpXEocG24h6ZzOi13+VB8HndAQFezsSOw1w== +"@types/node@^16.7.13": + version "16.11.34" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.34.tgz#520224e4be4448c279ecad09639ab460cc441a50" + integrity sha512-UrWGDyLAlQ2Z8bNOGWTsqbP9ZcBeTYBVuTRNxXTztBy5KhWUFI3BaeDWoCP/CzV/EVGgO1NTYzv9ZytBI9GAEw== + "@types/numeral@^0.0.28": version "0.0.28" resolved "https://registry.yarnpkg.com/@types/numeral/-/numeral-0.0.28.tgz#e43928f0bda10b169b6f7ecf99e3ddf836b8ebe4" integrity sha512-Sjsy10w6XFHDktJJdXzBJmoondAKW+LcGpRFH+9+zXEDj0cOH8BxJuZA9vUDSMAzU1YRJlsPKmZEEiTYDlICLw== -"@types/numeral@^2.0.5": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@types/numeral/-/numeral-2.0.5.tgz#388e5c4ff4b0e1787f130753cbbe83d3ba770858" - integrity sha512-kH8I7OSSwQu9DS9JYdFWbuvhVzvFRoCPCkGxNwoGgaPeDfEPJlcxNvEOypZhQ3XXHsGbfIuYcxcJxKUfJHnRfw== - "@types/parse-json@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" @@ -2782,6 +2642,13 @@ dependencies: "@types/react" "*" +"@types/react-dom@^17.0.9": + version "17.0.16" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.16.tgz#7caba93cf2806c51e64d620d8dff4bae57e06cc4" + integrity sha512-DWcXf8EbMrO/gWnQU7Z88Ws/p16qxGpPyjTKTpmBSFKeE+HveVubqGO1CVK7FrwlWD5MuOcvh8gtd0/XO38NdQ== + dependencies: + "@types/react" "^17" + "@types/react-dom@^18.0.0": version "18.0.3" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.3.tgz#a022ea08c75a476fe5e96b675c3e673363853831" @@ -2820,13 +2687,6 @@ dependencies: "@types/react" "*" -"@types/react-window@^1.8.8": - version "1.8.8" - resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.8.tgz#c20645414d142364fbe735818e1c1e0a145696e3" - integrity sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q== - dependencies: - "@types/react" "*" - "@types/react@*": version "18.0.9" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.9.tgz#d6712a38bd6cd83469603e7359511126f122e878" @@ -2836,6 +2696,15 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/react@^17", "@types/react@^17.0.20": + version "17.0.45" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.45.tgz#9b3d5b661fd26365fefef0e766a1c6c30ccf7b3f" + integrity sha512-YfhQ22Lah2e3CHPsb93tRwIGNiSwkuz1/blk4e6QrWS0jQzCSNbGLtOEYhPg02W0yGTTmpajp7dCTbBAMN3qsg== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + "@types/refractor@^3.0.0": version "3.0.2" resolved "https://registry.yarnpkg.com/@types/refractor/-/refractor-3.0.2.tgz#2d42128d59f78f84d2c799ffc5ab5cadbcba2d82" @@ -2843,13 +2712,6 @@ dependencies: "@types/prismjs" "*" -"@types/refractor@^3.4.0": - version "3.4.1" - resolved "https://registry.yarnpkg.com/@types/refractor/-/refractor-3.4.1.tgz#8b109804f77b3da8fad543d3f575fef1ece8835a" - integrity sha512-wYuorIiCTSuvRT9srwt+taF6mH/ww+SyN2psM0sjef2qW+sS8GmshgDGTEDgWB1sTVGgYVE6EK7dBA2MxQxibg== - dependencies: - "@types/prismjs" "*" - "@types/resize-observer-browser@^0.1.5": version "0.1.7" resolved "https://registry.yarnpkg.com/@types/resize-observer-browser/-/resize-observer-browser-0.1.7.tgz#294aaadf24ac6580b8fbd1fe3ab7b59fe85f9ef3" @@ -2899,11 +2761,6 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== -"@types/stylis@4.2.5": - version "4.2.5" - resolved "https://registry.yarnpkg.com/@types/stylis/-/stylis-4.2.5.tgz#1daa6456f40959d06157698a653a9ab0a70281df" - integrity sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw== - "@types/testing-library__jest-dom@^5.9.1": version "5.14.3" resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.3.tgz#ee6c7ffe9f8595882ee7bda8af33ae7b8789ef17" @@ -2921,11 +2778,6 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== -"@types/use-sync-external-store@^0.0.3": - version "0.0.3" - resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43" - integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA== - "@types/vfile-message@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@types/vfile-message/-/vfile-message-2.0.0.tgz#690e46af0fdfc1f9faae00cd049cc888957927d5" @@ -3369,13 +3221,6 @@ aria-hidden@^1.1.3: dependencies: tslib "^1.0.0" -aria-hidden@^1.2.2: - version "1.2.4" - resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.4.tgz#b78e383fdbc04d05762c78b4a25a501e736c4522" - integrity sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A== - dependencies: - tslib "^2.0.0" - aria-query@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b" @@ -3591,6 +3436,17 @@ babel-plugin-polyfill-regenerator@^0.3.0: dependencies: "@babel/helper-define-polyfill-provider" "^0.3.1" +"babel-plugin-styled-components@>= 1.12.0": + version "2.1.4" + resolved "https://registry.yarnpkg.com/babel-plugin-styled-components/-/babel-plugin-styled-components-2.1.4.tgz#9a1f37c7f32ef927b4b008b529feb4a2c82b1092" + integrity sha512-Xgp9g+A/cG47sUyRwwYxGM4bR/jDRg5N6it/8+HxCnbT5XNKSKDT9xm4oag/osgqjC2It/vH0yXsomOG6k558g== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-module-imports" "^7.22.5" + "@babel/plugin-syntax-jsx" "^7.22.5" + lodash "^4.17.21" + picomatch "^2.3.1" + babel-plugin-transform-react-remove-prop-types@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz#f2edaf9b4c6a5fbe5c1d678bfb531078c1555f3a" @@ -3978,11 +3834,6 @@ chroma-js@^2.1.0: resolved "https://registry.yarnpkg.com/chroma-js/-/chroma-js-2.4.2.tgz#dffc214ed0c11fa8eefca2c36651d8e57cbfb2b0" integrity sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A== -chroma-js@^2.4.2: - version "2.6.0" - resolved "https://registry.yarnpkg.com/chroma-js/-/chroma-js-2.6.0.tgz#578743dd359698a75067a19fa5571dec54d0b70b" - integrity sha512-BLHvCB9s8Z1EV4ethr6xnkl/P2YRFOGqfgvuMG/MyCbZPrTA+NeiByY6XvgF0zP4/2deU2CXnWyMa3zu1LqQ3A== - chrome-trace-event@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" @@ -4003,11 +3854,6 @@ classnames@^2.2.6: resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA== -classnames@^2.5.1: - version "2.5.1" - resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" - integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== - clean-css@^5.2.2: version "5.3.0" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.0.tgz#ad3d8238d5f3549e83d5f87205189494bc7cbb59" @@ -4015,6 +3861,15 @@ clean-css@^5.2.2: dependencies: source-map "~0.6.0" +clipboard@^2.0.0: + version "2.0.11" + resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.11.tgz#62180360b97dd668b6b3a84ec226975762a70be5" + integrity sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw== + dependencies: + good-listener "^1.2.2" + select "^1.1.2" + tiny-emitter "^2.0.0" + cliui@^7.0.2: version "7.0.4" resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" @@ -4259,7 +4114,7 @@ css-blank-pseudo@^3.0.3: dependencies: postcss-selector-parser "^6.0.9" -css-box-model@^1.2.0, css-box-model@^1.2.1: +css-box-model@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1" integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw== @@ -4340,7 +4195,7 @@ css-select@^4.1.3: domutils "^2.8.0" nth-check "^2.0.1" -css-to-react-native@3.2.0: +css-to-react-native@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/css-to-react-native/-/css-to-react-native-3.2.0.tgz#cdd8099f71024e149e4f6fe17a7d46ecd55f1e32" integrity sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ== @@ -4472,11 +4327,6 @@ cssstyle@^2.3.0: dependencies: cssom "~0.3.6" -csstype@3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" - integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== - csstype@^3.0.2: version "3.0.11" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.11.tgz#d66700c5eacfac1940deb4e3ee5642792d85cd33" @@ -4690,7 +4540,7 @@ d3-zoom@3: d3-selection "2 - 3" d3-transition "2 - 3" -d3@^7.4.4: +d3@^7.3.0, d3@^7.4.4: version "7.4.4" resolved "https://registry.yarnpkg.com/d3/-/d3-7.4.4.tgz#bfbf87487c37d3196efebd5a63e3a0ed8299d8ff" integrity sha512-97FE+MYdAlV3R9P74+R3Uar7wUKkIFu89UWMjEaDhiJ9VxKvqaMxauImy8PC2DdBkdM2BxJOIoLxPrcZUyrKoQ== @@ -4832,6 +4682,11 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= +delegate@^3.1.2: + version "3.2.0" + resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166" + integrity sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw== + depd@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" @@ -5586,7 +5441,7 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" -fault@^1.0.0: +fault@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/fault/-/fault-1.0.4.tgz#eafcfc0a6d214fc94601e170df29954a4f842f13" integrity sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA== @@ -5730,13 +5585,6 @@ focus-lock@^0.11.2: dependencies: tslib "^2.0.3" -focus-lock@^1.3.5: - version "1.3.5" - resolved "https://registry.yarnpkg.com/focus-lock/-/focus-lock-1.3.5.tgz#aa644576e5ec47d227b57eb14e1efb2abf33914c" - integrity sha512-QFaHbhv9WPUeLYBDe/PAuLKJ4Dd9OPvKs9xZBr3yLXnUrDNaVXKu2baDBXe3naPY30hgHYSsf2JW4jzas2mDEQ== - dependencies: - tslib "^2.0.3" - follow-redirects@^1.0.0: version "1.15.6" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" @@ -5988,6 +5836,13 @@ globby@^11.0.4: merge2 "^1.4.1" slash "^3.0.0" +good-listener@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50" + integrity sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw== + dependencies: + delegate "^3.1.2" + gopd@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" @@ -6166,6 +6021,16 @@ hast-util-whitespace@^1.0.0: resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-1.0.4.tgz#e4fe77c4a9ae1cb2e6c25e02df0043d0164f6e41" integrity sha512-I5GTdSfhYfAPNztx2xJRQpG8cuDSNt599/7YUn7Gx/WxNMsG+a835k97TDkFgk123cwjfwINaZknkKkphx/f2A== +hastscript@^5.0.0: + version "5.1.2" + resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-5.1.2.tgz#bde2c2e56d04c62dd24e8c5df288d050a355fb8a" + integrity sha512-WlztFuK+Lrvi3EggsqOkQ52rKbxkXL3RwB6t5lwoa8QLMemoWfBuL43eDrwOamJyR7uKQKdmKYaBH1NZBiIRrQ== + dependencies: + comma-separated-tokens "^1.0.0" + hast-util-parse-selector "^2.0.0" + property-information "^5.0.0" + space-separated-tokens "^1.0.0" + hastscript@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-6.0.0.tgz#e8768d7eac56c3fdeac8a92830d58e811e5bf640" @@ -6182,15 +6047,10 @@ he@^1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== -highlight.js@^10.4.1, highlight.js@~10.7.0: - version "10.7.3" - resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531" - integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A== - -highlightjs-vue@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz#fdfe97fbea6354e70ee44e3a955875e114db086d" - integrity sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA== +highlight.js@~9.15.0, highlight.js@~9.15.1: + version "9.15.10" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.15.10.tgz#7b18ed75c90348c045eef9ed08ca1319a2219ad2" + integrity sha512-RoV7OkQm0T3os3Dd2VHLNMoaoDVx77Wygln3n9l5YV172XonWG6rgQD3XnF/BuFFZw9A0TJgmMSO8FEWQgvcXw== history@^5.2.0: version "5.3.0" @@ -6199,7 +6059,7 @@ history@^5.2.0: dependencies: "@babel/runtime" "^7.7.6" -hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: +hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -7520,13 +7380,13 @@ lower-case@^2.0.2: dependencies: tslib "^2.0.3" -lowlight@^1.17.0: - version "1.20.0" - resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.20.0.tgz#ddb197d33462ad0d93bf19d17b6c301aa3941888" - integrity sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw== +lowlight@1.12.1: + version "1.12.1" + resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.12.1.tgz#014acf8dd73a370e02ff1cc61debcde3bb1681eb" + integrity sha512-OqaVxMGIESnawn+TU/QMV5BJLbUghUfjDWPAtFqDYDmDtr4FnB+op8xM+pR7nKlauHNUHXGt0VgWatFB8voS5w== dependencies: - fault "^1.0.0" - highlight.js "~10.7.0" + fault "^1.0.2" + highlight.js "~9.15.0" lru-cache@^6.0.0: version "6.0.0" @@ -7634,11 +7494,6 @@ memfs@^3.4.3: resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== -memoize-one@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045" - integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw== - merge-descriptors@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" @@ -7784,11 +7639,6 @@ nanoid@^3.3.3: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== -nanoid@^3.3.7: - version "3.3.7" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" - integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== - natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -8115,6 +7965,18 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" +parse-entities@^1.1.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-1.2.2.tgz#c31bf0f653b6661354f8973559cb86dd1d5edf50" + integrity sha512-NzfpbxW/NPrzZ/yYSoQxyqUZMZXIdCfE0OIN4ESsnptHJECoUk3FZktxNuzQf4tjt5UEopnxpYJbvYuxIFDdsg== + dependencies: + character-entities "^1.0.0" + character-entities-legacy "^1.0.0" + character-reference-invalid "^1.0.0" + is-alphanumerical "^1.0.0" + is-decimal "^1.0.0" + is-hexadecimal "^1.0.0" + parse-entities@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-2.0.0.tgz#53c6eb5b9314a1f4ec99fa0fdf7ce01ecda0cbe8" @@ -8751,15 +8613,6 @@ postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0, postcss-value-parser@^ resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@8.4.38: - version "8.4.38" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e" - integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== - dependencies: - nanoid "^3.3.7" - picocolors "^1.0.0" - source-map-js "^1.2.0" - postcss@^7.0.35: version "7.0.39" resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.39.tgz#9624375d965630e2e1f2c02a935c82a59cb48309" @@ -8819,11 +8672,18 @@ pretty-format@^28.1.0: ansi-styles "^5.0.0" react-is "^18.0.0" -prismjs@^1.27.0: +prismjs@^1.8.4: version "1.29.0" resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.29.0.tgz#f113555a8fa9b57c35e637bba27509dcf802dd12" integrity sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q== +prismjs@~1.17.0: + version "1.17.1" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.17.1.tgz#e669fcbd4cdd873c35102881c33b14d0d68519be" + integrity sha512-PrEDJAFdUGbOP6xK/UsfkC5ghJsPJviKgnQOoxaDbBjwc8op68Quupwt1DeAFoG8GImPhiKXAvvsH7wDSLsu1Q== + optionalDependencies: + clipboard "^2.0.0" + prismjs@~1.27.0: version "1.27.0" resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.27.0.tgz#bb6ee3138a0b438a3653dd4d6ce0cc6510a45057" @@ -8938,7 +8798,7 @@ quick-lru@^5.1.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== -raf-schd@^4.0.2, raf-schd@^4.0.3: +raf-schd@^4.0.2: version "4.0.3" resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a" integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ== @@ -9004,15 +8864,15 @@ react-clientside-effect@^1.2.6: dependencies: "@babel/runtime" "^7.12.13" -react-code-blocks@^0.1.6: - version "0.1.6" - resolved "https://registry.yarnpkg.com/react-code-blocks/-/react-code-blocks-0.1.6.tgz#ec64e7899223d3e910eb916465a66d95ce1ae1b2" - integrity sha512-ENNuxG07yO+OuX1ChRje3ieefPRz6yrIpHmebQlaFQgzcAHbUfVeTINpOpoI9bSRSObeYo/OdHsporeToZ7fcg== +react-code-blocks@^0.0.9-0: + version "0.0.9-0" + resolved "https://registry.yarnpkg.com/react-code-blocks/-/react-code-blocks-0.0.9-0.tgz#0c6d04d8a40b74cffe95f24f1a8e62a0fda8c014" + integrity sha512-jdYJVZwGtsr6WIUaqILy5fkF1acf57YV5s0V3+w5o9v3omYnqBeO6EuZi1Vf2x1hahkYGEedsp46+ofdkYlqyw== dependencies: "@babel/runtime" "^7.10.4" - react-syntax-highlighter "^15.5.0" - styled-components "^6.1.0" - tslib "^2.6.0" + react-syntax-highlighter "^12.2.1" + styled-components "^5.1.1" + tslib "^2.0.0" react-dev-utils@^12.0.1: version "12.0.1" @@ -9053,7 +8913,7 @@ react-dom@^17.0.2: object-assign "^4.1.1" scheduler "^0.20.2" -react-dropzone@^11.5.3, react-dropzone@^11.7.1: +react-dropzone@^11.5.3: version "11.7.1" resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-11.7.1.tgz#3851bb75b26af0bf1b17ce1449fd980e643b9356" integrity sha512-zxCMwhfPy1olUEbw3FLNPLhAm/HnaYH5aELIEglRbqabizKAdHs0h+WuyOpmA+v1JXn0++fpQDdNfUagWt5hJQ== @@ -9071,32 +8931,11 @@ react-element-to-jsx-string@^14.3.4: is-plain-object "5.0.0" react-is "17.0.2" -react-element-to-jsx-string@^15.0.0: - version "15.0.0" - resolved "https://registry.yarnpkg.com/react-element-to-jsx-string/-/react-element-to-jsx-string-15.0.0.tgz#1cafd5b6ad41946ffc8755e254da3fc752a01ac6" - integrity sha512-UDg4lXB6BzlobN60P8fHWVPX3Kyw8ORrTeBtClmIlGdkOOE+GYQSFvmEU5iLLpwp/6v42DINwNcwOhOLfQ//FQ== - dependencies: - "@base2/pretty-print-object" "1.0.1" - is-plain-object "5.0.0" - react-is "18.1.0" - react-error-overlay@^6.0.11: version "6.0.11" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb" integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg== -react-focus-lock@^2.11.3: - version "2.13.2" - resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.13.2.tgz#e1addac2f8b9550bc0581f3c416755ba0f81f5ef" - integrity sha512-T/7bsofxYqnod2xadvuwjGKHOoL5GH7/EIPI5UyEvaU/c2CcphvGI371opFtuY/SYdbMsNiuF4HsHQ50nA/TKQ== - dependencies: - "@babel/runtime" "^7.0.0" - focus-lock "^1.3.5" - prop-types "^15.6.2" - react-clientside-effect "^1.2.6" - use-callback-ref "^1.3.2" - use-sidecar "^1.1.2" - react-focus-lock@^2.9.0: version "2.9.1" resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.9.1.tgz#094cfc19b4f334122c73bb0bff65d77a0c92dd16" @@ -9122,18 +8961,6 @@ react-focus-on@^3.5.4: use-callback-ref "^1.3.0" use-sidecar "^1.1.2" -react-focus-on@^3.9.1: - version "3.9.4" - resolved "https://registry.yarnpkg.com/react-focus-on/-/react-focus-on-3.9.4.tgz#0b6c13273d86243c330d1aa53af39290f543da7b" - integrity sha512-NFKmeH6++wu8e7LJcbwV8TTd4L5w/U5LMXTMOdUcXhCcZ7F5VOvgeTHd4XN1PD7TNmdvldDu/ENROOykUQ4yQg== - dependencies: - aria-hidden "^1.2.2" - react-focus-lock "^2.11.3" - react-remove-scroll "^2.6.0" - react-style-singleton "^2.2.1" - tslib "^2.3.1" - use-sidecar "^1.1.2" - react-input-autosize@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-3.0.0.tgz#6b5898c790d4478d69420b55441fcc31d5c50a85" @@ -9146,16 +8973,16 @@ react-is@17.0.2, react-is@^17.0.1, react-is@^17.0.2: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== -react-is@18.1.0, react-is@^18.0.0: - version "18.1.0" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.1.0.tgz#61aaed3096d30eacf2a2127118b5b41387d32a67" - integrity sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg== - react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-is@^18.0.0: + version "18.1.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.1.0.tgz#61aaed3096d30eacf2a2127118b5b41387d32a67" + integrity sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg== + react-query@^3.34.12: version "3.39.0" resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.39.0.tgz#0caca7b0da98e65008bbcd4df0d25618c2100050" @@ -9165,15 +8992,6 @@ react-query@^3.34.12: broadcast-channel "^3.4.1" match-sorter "^6.0.2" -react-query@^3.39.3: - version "3.39.3" - resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.39.3.tgz#4cea7127c6c26bdea2de5fb63e51044330b03f35" - integrity sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g== - dependencies: - "@babel/runtime" "^7.5.5" - broadcast-channel "^3.4.1" - match-sorter "^6.0.2" - react-redux@^7.2.0: version "7.2.8" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.8.tgz#a894068315e65de5b1b68899f9c6ee0923dd28de" @@ -9186,18 +9004,6 @@ react-redux@^7.2.0: prop-types "^15.7.2" react-is "^17.0.2" -react-redux@^8.1.3: - version "8.1.3" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.1.3.tgz#4fdc0462d0acb59af29a13c27ffef6f49ab4df46" - integrity sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw== - dependencies: - "@babel/runtime" "^7.12.1" - "@types/hoist-non-react-statics" "^3.3.1" - "@types/use-sync-external-store" "^0.0.3" - hoist-non-react-statics "^3.3.2" - react-is "^18.0.0" - use-sync-external-store "^1.0.0" - react-refresh@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046" @@ -9211,14 +9017,6 @@ react-remove-scroll-bar@^2.3.1: react-style-singleton "^2.2.0" tslib "^2.0.0" -react-remove-scroll-bar@^2.3.4, react-remove-scroll-bar@^2.3.6: - version "2.3.6" - resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz#3e585e9d163be84a010180b18721e851ac81a29c" - integrity sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g== - dependencies: - react-style-singleton "^2.2.1" - tslib "^2.0.0" - react-remove-scroll@^2.5.2: version "2.5.3" resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.5.3.tgz#a152196e710e8e5811be39dc352fd8a90b05c961" @@ -9230,18 +9028,7 @@ react-remove-scroll@^2.5.2: use-callback-ref "^1.3.0" use-sidecar "^1.1.2" -react-remove-scroll@^2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.6.0.tgz#fb03a0845d7768a4f1519a99fdb84983b793dc07" - integrity sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ== - dependencies: - react-remove-scroll-bar "^2.3.6" - react-style-singleton "^2.2.1" - tslib "^2.1.0" - use-callback-ref "^1.3.0" - use-sidecar "^1.1.2" - -react-router-dom@6, react-router-dom@<6.4.0: +react-router-dom@6: version "6.3.0" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.3.0.tgz#a0216da813454e521905b5fa55e0e5176123f43d" integrity sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw== @@ -9256,7 +9043,7 @@ react-router@6.3.0: dependencies: history "^5.2.0" -react-scripts@^5.0.0, react-scripts@^5.0.1: +react-scripts@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-5.0.1.tgz#6285dbd65a8ba6e49ca8d651ce30645a6d980003" integrity sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ== @@ -9320,45 +9107,22 @@ react-style-singleton@^2.2.0: invariant "^2.2.4" tslib "^2.0.0" -react-style-singleton@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4" - integrity sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g== - dependencies: - get-nonce "^1.0.0" - invariant "^2.2.4" - tslib "^2.0.0" - -react-syntax-highlighter@^15.5.0: - version "15.6.1" - resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz#fa567cb0a9f96be7bbccf2c13a3c4b5657d9543e" - integrity sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg== +react-syntax-highlighter@^12.2.1: + version "12.2.1" + resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-12.2.1.tgz#14d78352da1c1c3f93c6698b70ec7c706b83493e" + integrity sha512-CTsp0ZWijwKRYFg9xhkWD4DSpQqE4vb2NKVMdPAkomnILSmsNBHE0n5GuI5zB+PU3ySVvXvdt9jo+ViD9XibCA== dependencies: "@babel/runtime" "^7.3.1" - highlight.js "^10.4.1" - highlightjs-vue "^1.0.0" - lowlight "^1.17.0" - prismjs "^1.27.0" - refractor "^3.6.0" - -react-virtualized-auto-sizer@^1.0.24: - version "1.0.24" - resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.24.tgz#3ebdc92f4b05ad65693b3cc8e7d8dd54924c0227" - integrity sha512-3kCn7N9NEb3FlvJrSHWGQ4iVl+ydQObq2fHMn12i5wbtm74zHOPhz/i64OL3c1S1vi9i2GXtZqNqUJTQ+BnNfg== + highlight.js "~9.15.1" + lowlight "1.12.1" + prismjs "^1.8.4" + refractor "^2.4.1" react-virtualized-auto-sizer@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.6.tgz#66c5b1c9278064c5ef1699ed40a29c11518f97ca" integrity sha512-7tQ0BmZqfVF6YYEWcIGuoR3OdYe8I/ZFbNclFlGOC3pMqunkYF/oL30NCjSGl9sMEb17AnzixDz98Kqc3N76HQ== -react-window@^1.8.10: - version "1.8.10" - resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.10.tgz#9e6b08548316814b443f7002b1cf8fd3a1bdde03" - integrity sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg== - dependencies: - "@babel/runtime" "^7.0.0" - memoize-one ">=3.1.1 <6" - react-window@^1.8.6: version "1.8.7" resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.7.tgz#5e9fd0d23f48f432d7022cdb327219353a15f0d4" @@ -9426,14 +9190,16 @@ redux@^4.0.0, redux@^4.0.4: dependencies: "@babel/runtime" "^7.9.2" -redux@^4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197" - integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w== +refractor@^2.4.1: + version "2.10.1" + resolved "https://registry.yarnpkg.com/refractor/-/refractor-2.10.1.tgz#166c32f114ed16fd96190ad21d5193d3afc7d34e" + integrity sha512-Xh9o7hQiQlDbxo5/XkOX6H+x/q8rmlmZKr97Ie1Q8ZM32IRRd3B/UxuA/yXDW79DBSXGWxm2yRTbcTVmAciJRw== dependencies: - "@babel/runtime" "^7.9.2" + hastscript "^5.0.0" + parse-entities "^1.1.2" + prismjs "~1.17.0" -refractor@^3.5.0, refractor@^3.6.0: +refractor@^3.5.0: version "3.6.0" resolved "https://registry.yarnpkg.com/refractor/-/refractor-3.6.0.tgz#ac318f5a0715ead790fcfb0c71f4dd83d977935a" integrity sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA== @@ -9464,11 +9230,6 @@ regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.9: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== -regenerator-runtime@^0.14.0: - version "0.14.1" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" - integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== - regenerator-transform@^0.15.0: version "0.15.0" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.0.tgz#cbd9ead5d77fae1a48d957cf889ad0586adb6537" @@ -9519,14 +9280,14 @@ regjsparser@^0.8.2: dependencies: jsesc "~0.5.0" -rehype-raw@^5.0.0, rehype-raw@^5.1.0: +rehype-raw@^5.0.0: version "5.1.0" resolved "https://registry.yarnpkg.com/rehype-raw/-/rehype-raw-5.1.0.tgz#66d5e8d7188ada2d31bc137bc19a1000cf2c6b7e" integrity sha512-MDvHAb/5mUnif2R+0IPCYJU8WjHa9UzGtM/F4AVy5GixPlDZ1z3HacYy4xojDU+uBa+0X/3PIfyQI26/2ljJNA== dependencies: hast-util-raw "^6.1.0" -rehype-react@^6.0.0, rehype-react@^6.2.1: +rehype-react@^6.0.0: version "6.2.1" resolved "https://registry.yarnpkg.com/rehype-react/-/rehype-react-6.2.1.tgz#9b9bf188451ad6f63796b784fe1f51165c67b73a" integrity sha512-f9KIrjktvLvmbGc7si25HepocOg4z0MuNOtweigKzBcDjiGSTGhyz6VSgaV5K421Cq1O+z4/oxRJ5G9owo0KVg== @@ -9562,27 +9323,6 @@ remark-emoji@^2.1.0: node-emoji "^1.10.0" unist-util-visit "^2.0.3" -remark-parse-no-trim@^8.0.4: - version "8.0.4" - resolved "https://registry.yarnpkg.com/remark-parse-no-trim/-/remark-parse-no-trim-8.0.4.tgz#f5c9531644284071d4a57a49e19a42ad4e8040bd" - integrity sha512-WtqeHNTZ0LSdyemmY1/G6y9WoEFblTtgckfKF5/NUnri919/0/dEu8RCDfvXtJvu96soMvT+mLWWgYVUaiHoag== - dependencies: - ccount "^1.0.0" - collapse-white-space "^1.0.2" - is-alphabetical "^1.0.0" - is-decimal "^1.0.0" - is-whitespace-character "^1.0.0" - is-word-character "^1.0.0" - markdown-escapes "^1.0.0" - parse-entities "^2.0.0" - repeat-string "^1.5.4" - state-toggle "^1.0.0" - trim-trailing-lines "^1.0.0" - unherit "^1.0.4" - unist-util-remove-position "^2.0.0" - vfile-location "^3.0.0" - xtend "^4.0.1" - remark-parse@^8.0.3: version "8.0.3" resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-8.0.3.tgz#9c62aa3b35b79a486454c690472906075f40c7e1" @@ -9605,7 +9345,7 @@ remark-parse@^8.0.3: vfile-location "^3.0.0" xtend "^4.0.1" -remark-rehype@^8.0.0, remark-rehype@^8.1.0: +remark-rehype@^8.0.0: version "8.1.0" resolved "https://registry.yarnpkg.com/remark-rehype/-/remark-rehype-8.1.0.tgz#610509a043484c1e697437fa5eb3fd992617c945" integrity sha512-EbCu9kHgAxKmW1yEYjx3QafMyGY3q8noUbNUI5xyKbaFP89wbhDrKxyIQNukNYthzjNHZu6J7hwFg7hRm1svYA== @@ -9848,6 +9588,11 @@ select-hose@^2.0.0: resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= +select@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" + integrity sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA== + selfsigned@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.0.1.tgz#8b2df7fa56bf014d19b6007655fff209c0ef0a56" @@ -9962,7 +9707,7 @@ setprototypeof@1.2.0: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== -shallowequal@1.1.0: +shallowequal@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== @@ -10042,11 +9787,6 @@ source-map-js@^1.0.1, source-map-js@^1.0.2: resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== -source-map-js@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" - integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== - source-map-loader@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-3.0.1.tgz#9ae5edc7c2d42570934be4c95d1ccc6352eba52d" @@ -10326,20 +10066,21 @@ style-to-object@^0.3.0: dependencies: inline-style-parser "0.1.1" -styled-components@^6.1.0: - version "6.1.13" - resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-6.1.13.tgz#2d777750b773b31469bd79df754a32479e9f475e" - integrity sha512-M0+N2xSnAtwcVAQeFEsGWFFxXDftHUD7XrKla06QbpUMmbmtFBMMTcKWvFXtWxuD5qQkB8iU5gk6QASlx2ZRMw== - dependencies: - "@emotion/is-prop-valid" "1.2.2" - "@emotion/unitless" "0.8.1" - "@types/stylis" "4.2.5" - css-to-react-native "3.2.0" - csstype "3.1.3" - postcss "8.4.38" - shallowequal "1.1.0" - stylis "4.3.2" - tslib "2.6.2" +styled-components@^5.1.1: + version "5.3.11" + resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-5.3.11.tgz#9fda7bf1108e39bf3f3e612fcc18170dedcd57a8" + integrity sha512-uuzIIfnVkagcVHv9nE0VPlHPSCmXIUGKfJ42LNjxCCTDTL5sgnJ8Z7GZBq0EnLYGln77tPpEpExt2+qa+cZqSw== + dependencies: + "@babel/helper-module-imports" "^7.0.0" + "@babel/traverse" "^7.4.5" + "@emotion/is-prop-valid" "^1.1.0" + "@emotion/stylis" "^0.8.4" + "@emotion/unitless" "^0.7.4" + babel-plugin-styled-components ">= 1.12.0" + css-to-react-native "^3.0.0" + hoist-non-react-statics "^3.0.0" + shallowequal "^1.1.0" + supports-color "^5.5.0" stylehacks@^5.1.0: version "5.1.0" @@ -10354,17 +10095,7 @@ stylis@4.0.13: resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.13.tgz#f5db332e376d13cc84ecfe5dace9a2a51d954c91" integrity sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag== -stylis@4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51" - integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw== - -stylis@4.3.2: - version "4.3.2" - resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.3.2.tgz#8f76b70777dd53eb669c6f58c997bf0a9972e444" - integrity sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg== - -supports-color@^5.3.0: +supports-color@^5.3.0, supports-color@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== @@ -10445,11 +10176,6 @@ tabbable@^5.2.1: resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-5.3.2.tgz#66d6119ee8a533634c3f17deb0caa1c379e36ac7" integrity sha512-6G/8EWRFx8CiSe2++/xHhXkmCRq2rHtDtZbQFHx34cvDfZzIBfvwG9zGUNTWMXWLCYvDj3aQqOzdl3oCxKuBkQ== -tabbable@^5.3.3: - version "5.3.3" - resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-5.3.3.tgz#aac0ff88c73b22d6c3c5a50b1586310006b47fbf" - integrity sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA== - tailwindcss@^3.0.2: version "3.0.24" resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.0.24.tgz#22e31e801a44a78a1d9a81ecc52e13b69d85704d" @@ -10581,6 +10307,11 @@ thunky@^1.0.2: resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== +tiny-emitter@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423" + integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q== + tiny-invariant@^1.0.6: version "1.2.0" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.2.0.tgz#a1141f86b672a9148c72e978a19a73b9b94a15a9" @@ -10662,11 +10393,6 @@ tsconfig-paths@^3.14.1: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@2.6.2: - version "2.6.2" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" - integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== - tslib@^1.0.0, tslib@^1.8.1, tslib@^1.9.3: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" @@ -10677,11 +10403,6 @@ tslib@^2.0.0, tslib@^2.0.3, tslib@^2.3.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== -tslib@^2.1.0, tslib@^2.6.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.0.tgz#d124c86c3c05a40a91e6fdea4021bd31d377971b" - integrity sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA== - tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" @@ -10784,7 +10505,7 @@ unicode-property-aliases-ecmascript@^2.0.0: resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz#0a36cb9a585c4f6abd51ad1deddb285c165297c8" integrity sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ== -unified@^9.2.0, unified@^9.2.2: +unified@^9.2.0: version "9.2.2" resolved "https://registry.yarnpkg.com/unified/-/unified-9.2.2.tgz#67649a1abfc3ab85d2969502902775eb03146975" integrity sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ== @@ -10943,23 +10664,11 @@ use-callback-ref@^1.3.0: dependencies: tslib "^2.0.0" -use-callback-ref@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.2.tgz#6134c7f6ff76e2be0b56c809b17a650c942b1693" - integrity sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA== - dependencies: - tslib "^2.0.0" - use-memo-one@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.2.tgz#0c8203a329f76e040047a35a1197defe342fab20" integrity sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ== -use-memo-one@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.3.tgz#2fd2e43a2169eabc7496960ace8c79efef975e99" - integrity sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ== - use-query-params@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/use-query-params/-/use-query-params-1.2.3.tgz#306c31a0cbc714e8a3b4bd7e91a6a9aaccaa5e22" @@ -10975,11 +10684,6 @@ use-sidecar@^1.1.2: detect-node-es "^1.1.0" tslib "^2.0.0" -use-sync-external-store@^1.0.0: - version "1.2.2" - resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9" - integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw== - util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -11050,7 +10754,7 @@ vfile-message@^2.0.0: "@types/unist" "^2.0.0" unist-util-stringify-position "^2.0.0" -vfile@^4.0.0, vfile@^4.2.0, vfile@^4.2.1: +vfile@^4.0.0, vfile@^4.2.0: version "4.2.1" resolved "https://registry.yarnpkg.com/vfile/-/vfile-4.2.1.tgz#03f1dce28fc625c625bc6514350fbdb00fa9e624" integrity sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA== diff --git a/ui/package.json b/ui/package.json index b9371f42f1..9a1876809b 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "@feast-dev/feast-ui", - "version": "0.41.0", + "version": "0.40.0", "private": false, "files": [ "dist" From 0915aef1d5f1803444b9dd9678242bfc00983317 Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Fri, 25 Oct 2024 22:22:39 -0400 Subject: [PATCH 184/185] chore: Update denormalized.md docs --- docs/reference/denormalized.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/denormalized.md b/docs/reference/denormalized.md index 13bec910c8..281e97de55 100644 --- a/docs/reference/denormalized.md +++ b/docs/reference/denormalized.md @@ -1,6 +1,6 @@ # Streaming feature computation with Denormalized -Denormalized makes it easy to compute real-time features and write them directly to your Feast feature store. This guide will walk you through setting up a streaming pipeline that computes feature aggregations and pushes them to Feast in real-time. +Denormalized makes it easy to compute real-time features and write them directly to your Feast online store. This guide will walk you through setting up a streaming pipeline that computes feature aggregations and pushes them to Feast in real-time. ![Denormalized/Feast integration diagram](../assets/feast-denormalized.png) From a439b936ca07d9571603aa5faec47d99e0b5523f Mon Sep 17 00:00:00 2001 From: feast-ci-bot Date: Sat, 26 Oct 2024 02:41:02 +0000 Subject: [PATCH 185/185] chore(release): release 0.41.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # [0.41.0](https://github.com/feast-dev/feast/compare/v0.40.0...v0.41.0) (2024-10-26) * chore!: Update @elastic/eui and @emotion/react in Feast UI ([#4597](https://github.com/feast-dev/feast/issues/4597)) ([b9ddbf9](https://github.com/feast-dev/feast/commit/b9ddbf9351a55b1a5c102102b06ad3b2f28b3d1f)) ### Bug Fixes * Add --chdir to test_workflow.py ([#4453](https://github.com/feast-dev/feast/issues/4453)) ([6b2f026](https://github.com/feast-dev/feast/commit/6b2f026747b8adebe659aed3d4d2f95d551d5d1e)) * Add feast-operator files to semantic-release script ([#4382](https://github.com/feast-dev/feast/issues/4382)) ([8eceff2](https://github.com/feast-dev/feast/commit/8eceff26ba00fd446d27ad5ce2ee9d039c57fd9a)) * Add feast-operator Makefile to semantic-release script ([#4424](https://github.com/feast-dev/feast/issues/4424)) ([d18d01d](https://github.com/feast-dev/feast/commit/d18d01de8356ffdc29b35c93978f36e7541d415e)) * Added Offline Store Arrow client errors handler ([#4524](https://github.com/feast-dev/feast/issues/4524)) ([7535b40](https://github.com/feast-dev/feast/commit/7535b4036ce980c9a05bc33a9e61a7938ea1303e)) * Added Online Store REST client errors handler ([#4488](https://github.com/feast-dev/feast/issues/4488)) ([2118719](https://github.com/feast-dev/feast/commit/21187199173f4c4f5417205d99535af6be492a9a)) * Added Permission API docs ([#4485](https://github.com/feast-dev/feast/issues/4485)) ([2bd03fa](https://github.com/feast-dev/feast/commit/2bd03fa4da5e76f6b29b0b54b455d5552d256838)) * Added support for multiple name patterns to Permissions ([#4633](https://github.com/feast-dev/feast/issues/4633)) ([f05e928](https://github.com/feast-dev/feast/commit/f05e92861da64d1c5e9cfe3c6307b3422d0d83b8)) * Adding protobuf<5 as a required dependency due to snowflake limitations ([#4537](https://github.com/feast-dev/feast/issues/4537)) ([cecca83](https://github.com/feast-dev/feast/commit/cecca8360bed62ab2f4fddc5d3a888247ea0a87a)) * Avoid the python 3.9+ threadpool cleanup bug ([#4627](https://github.com/feast-dev/feast/issues/4627)) ([ba05893](https://github.com/feast-dev/feast/commit/ba05893ba6db2d8d1e7bcc8cf8162f4fb72c9563)) * Bigquery dataset create table disposition ([#4649](https://github.com/feast-dev/feast/issues/4649)) ([58e03d1](https://github.com/feast-dev/feast/commit/58e03d17448a883ef57bcd6d8926d1e54ddcdece)) * Changes template file path to relative path ([#4624](https://github.com/feast-dev/feast/issues/4624)) ([3e313b1](https://github.com/feast-dev/feast/commit/3e313b15efc7fc72d35d70315fc8b7c172fc7993)) * Check for snowflake functions when setting up materialization engine ([#4456](https://github.com/feast-dev/feast/issues/4456)) ([c365b4e](https://github.com/feast-dev/feast/commit/c365b4e71a16fb69883608c5f781c6d55502bb8e)) * Correctly handle list values in _python_value_to_proto_value ([#4608](https://github.com/feast-dev/feast/issues/4608)) ([c0a1026](https://github.com/feast-dev/feast/commit/c0a10269914c2ca01fe1cf6b24b120bfa58d04e7)) * Default to pandas mode if not specified in ODFV proto in database ([#4420](https://github.com/feast-dev/feast/issues/4420)) ([d235832](https://github.com/feast-dev/feast/commit/d235832b78027b98df8e8a9e434a51a0c78b3092)) * Deleting data from feast_metadata when we delete project ([#4550](https://github.com/feast-dev/feast/issues/4550)) ([351a2d0](https://github.com/feast-dev/feast/commit/351a2d0a7f9808178ab9d201083eb2894ce7384f)) * Disable active_timer When registry_ttl_sec is 0 ([#4499](https://github.com/feast-dev/feast/issues/4499)) ([c94f32f](https://github.com/feast-dev/feast/commit/c94f32f2b637c7b7d917d2456432180af7569cf5)) * Escape special characters in the Postgres password ([#4394](https://github.com/feast-dev/feast/issues/4394)) ([419ca5e](https://github.com/feast-dev/feast/commit/419ca5e9523ff38f27141b79ae12ebb0646c6617)) * FeastExtrasDependencyImportError when using SparkOfflineStore without S3 ([#4594](https://github.com/feast-dev/feast/issues/4594)) ([1ba94f7](https://github.com/feast-dev/feast/commit/1ba94f7e2018fea0114f1703dd3942d589071825)) * Fix Feast project name test ([#4685](https://github.com/feast-dev/feast/issues/4685)) ([9f41fd6](https://github.com/feast-dev/feast/commit/9f41fd6673b9576c802c2378b56e04b9a090d99d)) * Fix for SQL registry initialization fails [#4543](https://github.com/feast-dev/feast/issues/4543) ([#4544](https://github.com/feast-dev/feast/issues/4544)) ([4e2eacc](https://github.com/feast-dev/feast/commit/4e2eacc1beea8f8866b78968abadfd42eee63d6a)) * Fix gitignore issue ([#4674](https://github.com/feast-dev/feast/issues/4674)) ([2807dfa](https://github.com/feast-dev/feast/commit/2807dfaf46d3d9e79f84b0ff22dbaeede377c89b)) * Fix online pg import ([#4581](https://github.com/feast-dev/feast/issues/4581)) ([1f17caa](https://github.com/feast-dev/feast/commit/1f17caacdaa573d08dbf8dc68b20e73a187ed8a4)) * Fix the mypy type check issue. ([#4498](https://github.com/feast-dev/feast/issues/4498)) ([7ecc615](https://github.com/feast-dev/feast/commit/7ecc615945b7bb48e103ca6eb278b39759d71c5a)) * Fix vector store config ([#4583](https://github.com/feast-dev/feast/issues/4583)) ([11c00d4](https://github.com/feast-dev/feast/commit/11c00d43fd1b2c5caf4d49f705bd55c704edae8a)) * Fixes validator field access for 'project_id' in BigQuery offline Store ([#4509](https://github.com/feast-dev/feast/issues/4509)) ([9a0398e](https://github.com/feast-dev/feast/commit/9a0398e2e18585172d857cf3202a81551d31609b)) * Fixing failure of protos during ODFV transformations for missing entities ([#4667](https://github.com/feast-dev/feast/issues/4667)) ([41aaeeb](https://github.com/feast-dev/feast/commit/41aaeebaa5908f44cda28a4410e2fca412f53e92)) * Fixing the master branch build failure. ([#4563](https://github.com/feast-dev/feast/issues/4563)) ([0192b2e](https://github.com/feast-dev/feast/commit/0192b2eb245c8e0ea9a913195ddf28382dc23982)) * Hao xu request source timestamp_field ([#4495](https://github.com/feast-dev/feast/issues/4495)) ([96344b2](https://github.com/feast-dev/feast/commit/96344b2b6830dcc280567542d111d1b0f39879e0)) * Ignore the type check as both functions calls are not belonging to Feast code. ([#4500](https://github.com/feast-dev/feast/issues/4500)) ([867f532](https://github.com/feast-dev/feast/commit/867f532154977790e3bb11f2a94baa4f2289de99)) * Import grpc only for type checking in errors.py ([#4533](https://github.com/feast-dev/feast/issues/4533)) ([f308572](https://github.com/feast-dev/feast/commit/f308572715d0593951f71bb3da5c5be6de29a2f9)) * Initial commit targetting grpc registry server ([#4458](https://github.com/feast-dev/feast/issues/4458)) ([484240c](https://github.com/feast-dev/feast/commit/484240c4e783d68bc521b62b723c2dcbd00fab5e)), closes [#4465](https://github.com/feast-dev/feast/issues/4465) * Links to the RBAC documentation under Concepts and Components ([#4430](https://github.com/feast-dev/feast/issues/4430)) ([0a48f7b](https://github.com/feast-dev/feast/commit/0a48f7bb436febb0171c78a559a577eedeff421f)) * Locate feature_store.yaml from __file__ ([#4443](https://github.com/feast-dev/feast/issues/4443)) ([20290ce](https://github.com/feast-dev/feast/commit/20290ce28c513f705db1dbb6b0f719ba1846217f)) * Logger settings for feature servers and updated logger for permission flow ([#4531](https://github.com/feast-dev/feast/issues/4531)) ([50b8f23](https://github.com/feast-dev/feast/commit/50b8f238b6f9adbc9ff0b20e18b78b2948c2f440)) * Move tslib from devDependencies to dependencies in Feast UI ([#4525](https://github.com/feast-dev/feast/issues/4525)) ([c5a4d90](https://github.com/feast-dev/feast/commit/c5a4d907bf34f4cf7477b212cd2820b0e7d24b7b)) * Null value compatibility for unit timestamp list value type ([#4378](https://github.com/feast-dev/feast/issues/4378)) ([8f264b6](https://github.com/feast-dev/feast/commit/8f264b6807a07874dc01207c655baeef7dfaa7b2)) * Patch FAISS online return signature ([#4671](https://github.com/feast-dev/feast/issues/4671)) ([0d45e95](https://github.com/feast-dev/feast/commit/0d45e95767773846861d53feb4af4c6bc1451b5e)) * Quickstart documentation changes ([#4618](https://github.com/feast-dev/feast/issues/4618)) ([7ac0908](https://github.com/feast-dev/feast/commit/7ac0908f3846cc0ab05082f748506814c84b2e9c)) * Refactor auth_client_manager_factory.py in function get_auth_client_m… ([#4505](https://github.com/feast-dev/feast/issues/4505)) ([def8633](https://github.com/feast-dev/feast/commit/def863360bf0e553d242900ee915e953c6c3f9b6)) * Remote apply using offline store ([#4559](https://github.com/feast-dev/feast/issues/4559)) ([ac62a32](https://github.com/feast-dev/feast/commit/ac62a323c86fba8096eefde85775dd7a857e9e25)) * Remove Feast UI TypeScript dependencies from `peerDependencies` and `dependencies` ([#4554](https://github.com/feast-dev/feast/issues/4554)) ([e781e16](https://github.com/feast-dev/feast/commit/e781e1652cadc6576dbab369248d6e4afdb5f158)) * Remove unnecessary peer dependencies from Feast UI ([#4577](https://github.com/feast-dev/feast/issues/4577)) ([9ac7f4e](https://github.com/feast-dev/feast/commit/9ac7f4ea77357219fc8420f05e493767bc5357c2)) * Removed protobuf as a required dependency ([#4535](https://github.com/feast-dev/feast/issues/4535)) ([0fb76e9](https://github.com/feast-dev/feast/commit/0fb76e9041885659c68e294b0c033c62050bd374)) * Removed the k8s dependency from required dependencies ([#4519](https://github.com/feast-dev/feast/issues/4519)) ([3073ea5](https://github.com/feast-dev/feast/commit/3073ea5911339a5744be45512a9a2ee8b250292b)) * Removed usage of pull_request_target as much as possible to prevent security concerns ([#4549](https://github.com/feast-dev/feast/issues/4549)) ([3198371](https://github.com/feast-dev/feast/commit/3198371fc0e07f6b51b62c7e3abbc48729078bb9)) * Replaced ClusterRoles with local RoleBindings ([#4625](https://github.com/feast-dev/feast/issues/4625)) ([ca9fb9b](https://github.com/feast-dev/feast/commit/ca9fb9bc8c3f3b06b4ba5fce362e26633144715c)) * Retire pytz library ([#4406](https://github.com/feast-dev/feast/issues/4406)) ([23c6c86](https://github.com/feast-dev/feast/commit/23c6c862e1da4e9523530eb48c7ce79319dc442d)) * Typos related to k8s ([#4442](https://github.com/feast-dev/feast/issues/4442)) ([dda0088](https://github.com/feast-dev/feast/commit/dda0088f25eab5828613bd6d080aeddf681641f0)) * Update java testcontainers to use Compose V2 ([#4381](https://github.com/feast-dev/feast/issues/4381)) ([9a33fce](https://github.com/feast-dev/feast/commit/9a33fce695c54226b3afd7b998e284f358bab141)) * Update min versions for pyarrow and protobuf ([#4646](https://github.com/feast-dev/feast/issues/4646)) ([c7ddd4b](https://github.com/feast-dev/feast/commit/c7ddd4bb48290d6d9327fcb6349b84b4d14334af)) * Update react-router-dom to 6.3.0 and restrict its version in Feast UI ([#4556](https://github.com/feast-dev/feast/issues/4556)) ([4293608](https://github.com/feast-dev/feast/commit/42936084a7d214d65faea5359ae70eefda8d23ad)), closes [#3794](https://github.com/feast-dev/feast/issues/3794) [/github.com/remix-run/react-router/blob/main/CHANGELOG.md#v630](https://github.com//github.com/remix-run/react-router/blob/main/CHANGELOG.md/issues/v630) * Update the base image for feature-server. ([#4576](https://github.com/feast-dev/feast/issues/4576)) ([0390d8a](https://github.com/feast-dev/feast/commit/0390d8a86f50360a4df89165db62972328d22ca4)) * Update the base image of materilization engine. ([#4580](https://github.com/feast-dev/feast/issues/4580)) ([f8592d8](https://github.com/feast-dev/feast/commit/f8592d86b2903ebfebc505bbf9392927aae5609c)) * Updated README link ([#4669](https://github.com/feast-dev/feast/issues/4669)) ([35fbdc9](https://github.com/feast-dev/feast/commit/35fbdc91cc8a327be2a8cd162f7ccff983b16932)) * Updating the documentation and adding tests for project length ([#4628](https://github.com/feast-dev/feast/issues/4628)) ([945b0fa](https://github.com/feast-dev/feast/commit/945b0faadd40c8dc76d104bce14ee902bd513127)) * Using get_type_hints instead of inspect signature for udf return annotation ([#4391](https://github.com/feast-dev/feast/issues/4391)) ([3a32e8a](https://github.com/feast-dev/feast/commit/3a32e8ae28110db0934fc26ec6992eb606fed012)) * Using repo_config parameter in teardown to allow for feature-store-yaml overrides ([#4413](https://github.com/feast-dev/feast/issues/4413)) ([0baeeb5](https://github.com/feast-dev/feast/commit/0baeeb5ec524c1e6209edab9605ca8a098a2ec88)) * Validating permission to update an existing request on both the new and the old instance ([#4449](https://github.com/feast-dev/feast/issues/4449)) ([635a01b](https://github.com/feast-dev/feast/commit/635a01b4c77db781d67f9f5ebb1067806b1e2a13)) ### Features * Add boto3 session based auth for dynamodb online store for cross account access ([#4606](https://github.com/feast-dev/feast/issues/4606)) ([00eaf74](https://github.com/feast-dev/feast/commit/00eaf744f50f56f449c7ad8f6ceb92a09304ee0b)) * Add cli list/describe for SavedDatasets, StreamFeatureViews, & … ([#4487](https://github.com/feast-dev/feast/issues/4487)) ([7b250e5](https://github.com/feast-dev/feast/commit/7b250e5eff5de56f5c5da103e91051276940298a)) * Add connection_name field to Snowflake config ([#4600](https://github.com/feast-dev/feast/issues/4600)) ([10ce2aa](https://github.com/feast-dev/feast/commit/10ce2aa0050e419acfae27971a6fff87bade3ba4)) * Add health check service to registry server ([#4421](https://github.com/feast-dev/feast/issues/4421)) ([46655f0](https://github.com/feast-dev/feast/commit/46655f06de339ee09245de3e1a648eb4e3bcd729)) * Add more __repr__ methods ([#4676](https://github.com/feast-dev/feast/issues/4676)) ([e726c09](https://github.com/feast-dev/feast/commit/e726c096f2de93d6dc0a807c97c47476cc79dd61)) * Add registry methods for dealing with all FV types ([#4435](https://github.com/feast-dev/feast/issues/4435)) ([ac381b2](https://github.com/feast-dev/feast/commit/ac381b292cfa29804ee5f0822f876d227a0989d9)) * Added Project object to Feast Objects ([#4475](https://github.com/feast-dev/feast/issues/4475)) ([4a6b663](https://github.com/feast-dev/feast/commit/4a6b663f80bc91d6de35ed2ec428d34811d17a18)) * Added support for reading from Reader Endpoints for AWS Aurora use cases ([#4494](https://github.com/feast-dev/feast/issues/4494)) ([d793c77](https://github.com/feast-dev/feast/commit/d793c77d923df95a186b9d4829b167f1a5a304e6)) * Adding documentation for On Demand Feature Transformations with writes ([#4607](https://github.com/feast-dev/feast/issues/4607)) ([8e0c1b5](https://github.com/feast-dev/feast/commit/8e0c1b51665357d44229239401a04b76396b9047)) * Adding mode='python' for get_historical_features on ODFVs ([#4653](https://github.com/feast-dev/feast/issues/4653)) ([c40d539](https://github.com/feast-dev/feast/commit/c40d539b85fe537077dd26904d78624da3d33951)) * Adding registry cache support for get_on_demand_feature_view ([#4572](https://github.com/feast-dev/feast/issues/4572)) ([354c059](https://github.com/feast-dev/feast/commit/354c059e5475f9c3927d9180a421118507a22cf0)) * Adding SSL support for online server ([#4677](https://github.com/feast-dev/feast/issues/4677)) ([80a5b3c](https://github.com/feast-dev/feast/commit/80a5b3c499faca8625d60267a34dcbddfe0c042a)) * Adding write capability to online store to on demand feature views ([#4585](https://github.com/feast-dev/feast/issues/4585)) ([ef9e0bb](https://github.com/feast-dev/feast/commit/ef9e0bbdb2a80250786b87972a53c4cf5890bb76)), closes [#4603](https://github.com/feast-dev/feast/issues/4603) * Allow feast snowflake to read in byte string for private-key authentication ([#4384](https://github.com/feast-dev/feast/issues/4384)) ([5215a21](https://github.com/feast-dev/feast/commit/5215a2139a9d824dc2d8f45181bd177a1e8e9561)) * An action to test operator at PR time ([#4635](https://github.com/feast-dev/feast/issues/4635)) ([14c1000](https://github.com/feast-dev/feast/commit/14c1000554c590cb89ecb5ef44c57aa1b5dd1387)) * Create ADOPTERS.md ([#4410](https://github.com/feast-dev/feast/issues/4410)) ([721ec74](https://github.com/feast-dev/feast/commit/721ec74f17ee95e375054f21135e54e0687104a7)) * Create initial structure of Feast Go Operator ([#4596](https://github.com/feast-dev/feast/issues/4596)) ([b5ab6c7](https://github.com/feast-dev/feast/commit/b5ab6c799d529aaea19a196cedca7bb2c93cbbe9)) * Faiss and In memory store ([#4464](https://github.com/feast-dev/feast/issues/4464)) ([a1ff129](https://github.com/feast-dev/feast/commit/a1ff1290002376b4c9ac8ad14e60df4622bb47a4)) * Feast Security Model (aka RBAC) ([#4380](https://github.com/feast-dev/feast/issues/4380)) ([1771f66](https://github.com/feast-dev/feast/commit/1771f668247ef3b46ea7dac634e557e249bc1ba9)), closes [#36](https://github.com/feast-dev/feast/issues/36) * Instrument Feast using Prometheus and OpenTelemetry ([#4366](https://github.com/feast-dev/feast/issues/4366)) ([a571e08](https://github.com/feast-dev/feast/commit/a571e08b97a95f8543d7dea27902c135ab3a4378)) * Intra server to server communication ([#4433](https://github.com/feast-dev/feast/issues/4433)) ([729c874](https://github.com/feast-dev/feast/commit/729c874e8c30719f23ad287d3cb84f1d654274ec)) * Publish TypeScript types in Feast UI package ([#4551](https://github.com/feast-dev/feast/issues/4551)) ([334e5d7](https://github.com/feast-dev/feast/commit/334e5d78855709d4ca56619f16eecb414f88ce2d)) * Refactoring code to get oidc end points from discovery URL. ([#4429](https://github.com/feast-dev/feast/issues/4429)) ([896360a](https://github.com/feast-dev/feast/commit/896360af19a37c9a2a4634ec88021c4f69bdb141)) * Return entity key in the retrieval document api ([#4511](https://github.com/feast-dev/feast/issues/4511)) ([5f5caf0](https://github.com/feast-dev/feast/commit/5f5caf0cac539ed779692e0ec819659cf5a33a0d)) * Update roadmap.md ([#4445](https://github.com/feast-dev/feast/issues/4445)) ([34238d2](https://github.com/feast-dev/feast/commit/34238d2a0bfe9dbad753fec9613c83d848b1a520)) * Update sqlite-vec package ([#4389](https://github.com/feast-dev/feast/issues/4389)) ([b734cb1](https://github.com/feast-dev/feast/commit/b734cb147a4afd28407ec57d95f70ff604f82954)) * Updated Feast model Inference Architecture ([#4570](https://github.com/feast-dev/feast/issues/4570)) ([8cd0dcf](https://github.com/feast-dev/feast/commit/8cd0dcff0cc3b1387c7ca65018ebb09e03538242)) * Updating docs to include model inference guidelines ([#4416](https://github.com/feast-dev/feast/issues/4416)) ([cebbe04](https://github.com/feast-dev/feast/commit/cebbe045597b85e1ae4394a8c14741e88347a6b8)) * Updating FeatureViewProjection and OnDemandFeatureView to add batch_source and entities ([#4530](https://github.com/feast-dev/feast/issues/4530)) ([0795496](https://github.com/feast-dev/feast/commit/07954960c5501e2ecc1f1285ddf4aa68f9ac880b)) * Upgrade React from 17.0.2 to 18.3.1 in Feast UI ([#4620](https://github.com/feast-dev/feast/issues/4620)) ([d6f3cb8](https://github.com/feast-dev/feast/commit/d6f3cb81a4c2a51bae2d29a185753cf6b1d2b16d)) ### Performance Improvements * Add init and cleanup of long lived resources ([#4642](https://github.com/feast-dev/feast/issues/4642)) ([47dc04d](https://github.com/feast-dev/feast/commit/47dc04d43f483944f97248abaccd15dba319281f)) * Added indexes to sql tables to optimize query execution ([#4538](https://github.com/feast-dev/feast/issues/4538)) ([9688790](https://github.com/feast-dev/feast/commit/9688790a5e7a70f628a46021bde0201922c7e04d)) * Default to async endpoints, use threadpool for sync ([#4647](https://github.com/feast-dev/feast/issues/4647)) ([c1f1912](https://github.com/feast-dev/feast/commit/c1f19127ab5c22dfe67869990480e7e9d8183ab1)) * Implement dynamo write_batch_async ([#4675](https://github.com/feast-dev/feast/issues/4675)) ([ba4404c](https://github.com/feast-dev/feast/commit/ba4404cecfb196c4084f9bf892cd3528184d42c1)) * Make /push async ([#4650](https://github.com/feast-dev/feast/issues/4650)) ([61abf89](https://github.com/feast-dev/feast/commit/61abf894aca7aa52042c40e77f64b49835f4324e)) * Parallelize read calls by table and batch ([#4619](https://github.com/feast-dev/feast/issues/4619)) ([043eff1](https://github.com/feast-dev/feast/commit/043eff1a87bdf775a437503395acda87cbecf875)) ### BREAKING CHANGES * Consuming apps that use @elastic/eui should update it to a compatible version. If you use @elastic/eui components that have been renamed or replaced with others, you'll need to update your code accordingly. Signed-off-by: Harri Lehtola * chore: Update Node version from 17 to 20 in UI unit tests Node 17 is not an LTS (long-term support) version and apparently rejected by the latest versions of Elastic UI: > error @elastic/eui@95.12.0: The engine "node" is incompatible with > this module. Expected version "16.x || 18.x || >=20.x". Got "17.9.1" Let's try with the latest LTS version. Signed-off-by: Harri Lehtola --- CHANGELOG.md | 133 ++++ infra/charts/feast-feature-server/Chart.yaml | 2 +- infra/charts/feast-feature-server/README.md | 7 +- infra/charts/feast-feature-server/values.yaml | 2 +- infra/charts/feast/Chart.yaml | 2 +- infra/charts/feast/README.md | 6 +- .../feast/charts/feature-server/Chart.yaml | 4 +- .../feast/charts/feature-server/README.md | 4 +- .../feast/charts/feature-server/values.yaml | 2 +- .../charts/transformation-service/Chart.yaml | 4 +- .../charts/transformation-service/README.md | 4 +- .../charts/transformation-service/values.yaml | 2 +- infra/charts/feast/requirements.yaml | 4 +- infra/feast-helm-operator/Makefile | 2 +- .../config/manager/kustomization.yaml | 2 +- infra/feast-operator/Makefile | 2 +- .../config/manager/kustomization.yaml | 2 +- java/pom.xml | 2 +- sdk/python/feast/ui/package.json | 2 +- sdk/python/feast/ui/yarn.lock | 730 ++++++++++++------ ui/package.json | 2 +- 21 files changed, 675 insertions(+), 245 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b7c8be4b7..8368cf6718 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,138 @@ # Changelog +# [0.41.0](https://github.com/feast-dev/feast/compare/v0.40.0...v0.41.0) (2024-10-26) + + +* chore!: Update @elastic/eui and @emotion/react in Feast UI ([#4597](https://github.com/feast-dev/feast/issues/4597)) ([b9ddbf9](https://github.com/feast-dev/feast/commit/b9ddbf9351a55b1a5c102102b06ad3b2f28b3d1f)) + + +### Bug Fixes + +* Add --chdir to test_workflow.py ([#4453](https://github.com/feast-dev/feast/issues/4453)) ([6b2f026](https://github.com/feast-dev/feast/commit/6b2f026747b8adebe659aed3d4d2f95d551d5d1e)) +* Add feast-operator files to semantic-release script ([#4382](https://github.com/feast-dev/feast/issues/4382)) ([8eceff2](https://github.com/feast-dev/feast/commit/8eceff26ba00fd446d27ad5ce2ee9d039c57fd9a)) +* Add feast-operator Makefile to semantic-release script ([#4424](https://github.com/feast-dev/feast/issues/4424)) ([d18d01d](https://github.com/feast-dev/feast/commit/d18d01de8356ffdc29b35c93978f36e7541d415e)) +* Added Offline Store Arrow client errors handler ([#4524](https://github.com/feast-dev/feast/issues/4524)) ([7535b40](https://github.com/feast-dev/feast/commit/7535b4036ce980c9a05bc33a9e61a7938ea1303e)) +* Added Online Store REST client errors handler ([#4488](https://github.com/feast-dev/feast/issues/4488)) ([2118719](https://github.com/feast-dev/feast/commit/21187199173f4c4f5417205d99535af6be492a9a)) +* Added Permission API docs ([#4485](https://github.com/feast-dev/feast/issues/4485)) ([2bd03fa](https://github.com/feast-dev/feast/commit/2bd03fa4da5e76f6b29b0b54b455d5552d256838)) +* Added support for multiple name patterns to Permissions ([#4633](https://github.com/feast-dev/feast/issues/4633)) ([f05e928](https://github.com/feast-dev/feast/commit/f05e92861da64d1c5e9cfe3c6307b3422d0d83b8)) +* Adding protobuf<5 as a required dependency due to snowflake limitations ([#4537](https://github.com/feast-dev/feast/issues/4537)) ([cecca83](https://github.com/feast-dev/feast/commit/cecca8360bed62ab2f4fddc5d3a888247ea0a87a)) +* Avoid the python 3.9+ threadpool cleanup bug ([#4627](https://github.com/feast-dev/feast/issues/4627)) ([ba05893](https://github.com/feast-dev/feast/commit/ba05893ba6db2d8d1e7bcc8cf8162f4fb72c9563)) +* Bigquery dataset create table disposition ([#4649](https://github.com/feast-dev/feast/issues/4649)) ([58e03d1](https://github.com/feast-dev/feast/commit/58e03d17448a883ef57bcd6d8926d1e54ddcdece)) +* Changes template file path to relative path ([#4624](https://github.com/feast-dev/feast/issues/4624)) ([3e313b1](https://github.com/feast-dev/feast/commit/3e313b15efc7fc72d35d70315fc8b7c172fc7993)) +* Check for snowflake functions when setting up materialization engine ([#4456](https://github.com/feast-dev/feast/issues/4456)) ([c365b4e](https://github.com/feast-dev/feast/commit/c365b4e71a16fb69883608c5f781c6d55502bb8e)) +* Correctly handle list values in _python_value_to_proto_value ([#4608](https://github.com/feast-dev/feast/issues/4608)) ([c0a1026](https://github.com/feast-dev/feast/commit/c0a10269914c2ca01fe1cf6b24b120bfa58d04e7)) +* Default to pandas mode if not specified in ODFV proto in database ([#4420](https://github.com/feast-dev/feast/issues/4420)) ([d235832](https://github.com/feast-dev/feast/commit/d235832b78027b98df8e8a9e434a51a0c78b3092)) +* Deleting data from feast_metadata when we delete project ([#4550](https://github.com/feast-dev/feast/issues/4550)) ([351a2d0](https://github.com/feast-dev/feast/commit/351a2d0a7f9808178ab9d201083eb2894ce7384f)) +* Disable active_timer When registry_ttl_sec is 0 ([#4499](https://github.com/feast-dev/feast/issues/4499)) ([c94f32f](https://github.com/feast-dev/feast/commit/c94f32f2b637c7b7d917d2456432180af7569cf5)) +* Escape special characters in the Postgres password ([#4394](https://github.com/feast-dev/feast/issues/4394)) ([419ca5e](https://github.com/feast-dev/feast/commit/419ca5e9523ff38f27141b79ae12ebb0646c6617)) +* FeastExtrasDependencyImportError when using SparkOfflineStore without S3 ([#4594](https://github.com/feast-dev/feast/issues/4594)) ([1ba94f7](https://github.com/feast-dev/feast/commit/1ba94f7e2018fea0114f1703dd3942d589071825)) +* Fix Feast project name test ([#4685](https://github.com/feast-dev/feast/issues/4685)) ([9f41fd6](https://github.com/feast-dev/feast/commit/9f41fd6673b9576c802c2378b56e04b9a090d99d)) +* Fix for SQL registry initialization fails [#4543](https://github.com/feast-dev/feast/issues/4543) ([#4544](https://github.com/feast-dev/feast/issues/4544)) ([4e2eacc](https://github.com/feast-dev/feast/commit/4e2eacc1beea8f8866b78968abadfd42eee63d6a)) +* Fix gitignore issue ([#4674](https://github.com/feast-dev/feast/issues/4674)) ([2807dfa](https://github.com/feast-dev/feast/commit/2807dfaf46d3d9e79f84b0ff22dbaeede377c89b)) +* Fix online pg import ([#4581](https://github.com/feast-dev/feast/issues/4581)) ([1f17caa](https://github.com/feast-dev/feast/commit/1f17caacdaa573d08dbf8dc68b20e73a187ed8a4)) +* Fix the mypy type check issue. ([#4498](https://github.com/feast-dev/feast/issues/4498)) ([7ecc615](https://github.com/feast-dev/feast/commit/7ecc615945b7bb48e103ca6eb278b39759d71c5a)) +* Fix vector store config ([#4583](https://github.com/feast-dev/feast/issues/4583)) ([11c00d4](https://github.com/feast-dev/feast/commit/11c00d43fd1b2c5caf4d49f705bd55c704edae8a)) +* Fixes validator field access for 'project_id' in BigQuery offline Store ([#4509](https://github.com/feast-dev/feast/issues/4509)) ([9a0398e](https://github.com/feast-dev/feast/commit/9a0398e2e18585172d857cf3202a81551d31609b)) +* Fixing failure of protos during ODFV transformations for missing entities ([#4667](https://github.com/feast-dev/feast/issues/4667)) ([41aaeeb](https://github.com/feast-dev/feast/commit/41aaeebaa5908f44cda28a4410e2fca412f53e92)) +* Fixing the master branch build failure. ([#4563](https://github.com/feast-dev/feast/issues/4563)) ([0192b2e](https://github.com/feast-dev/feast/commit/0192b2eb245c8e0ea9a913195ddf28382dc23982)) +* Hao xu request source timestamp_field ([#4495](https://github.com/feast-dev/feast/issues/4495)) ([96344b2](https://github.com/feast-dev/feast/commit/96344b2b6830dcc280567542d111d1b0f39879e0)) +* Ignore the type check as both functions calls are not belonging to Feast code. ([#4500](https://github.com/feast-dev/feast/issues/4500)) ([867f532](https://github.com/feast-dev/feast/commit/867f532154977790e3bb11f2a94baa4f2289de99)) +* Import grpc only for type checking in errors.py ([#4533](https://github.com/feast-dev/feast/issues/4533)) ([f308572](https://github.com/feast-dev/feast/commit/f308572715d0593951f71bb3da5c5be6de29a2f9)) +* Initial commit targetting grpc registry server ([#4458](https://github.com/feast-dev/feast/issues/4458)) ([484240c](https://github.com/feast-dev/feast/commit/484240c4e783d68bc521b62b723c2dcbd00fab5e)), closes [#4465](https://github.com/feast-dev/feast/issues/4465) +* Links to the RBAC documentation under Concepts and Components ([#4430](https://github.com/feast-dev/feast/issues/4430)) ([0a48f7b](https://github.com/feast-dev/feast/commit/0a48f7bb436febb0171c78a559a577eedeff421f)) +* Locate feature_store.yaml from __file__ ([#4443](https://github.com/feast-dev/feast/issues/4443)) ([20290ce](https://github.com/feast-dev/feast/commit/20290ce28c513f705db1dbb6b0f719ba1846217f)) +* Logger settings for feature servers and updated logger for permission flow ([#4531](https://github.com/feast-dev/feast/issues/4531)) ([50b8f23](https://github.com/feast-dev/feast/commit/50b8f238b6f9adbc9ff0b20e18b78b2948c2f440)) +* Move tslib from devDependencies to dependencies in Feast UI ([#4525](https://github.com/feast-dev/feast/issues/4525)) ([c5a4d90](https://github.com/feast-dev/feast/commit/c5a4d907bf34f4cf7477b212cd2820b0e7d24b7b)) +* Null value compatibility for unit timestamp list value type ([#4378](https://github.com/feast-dev/feast/issues/4378)) ([8f264b6](https://github.com/feast-dev/feast/commit/8f264b6807a07874dc01207c655baeef7dfaa7b2)) +* Patch FAISS online return signature ([#4671](https://github.com/feast-dev/feast/issues/4671)) ([0d45e95](https://github.com/feast-dev/feast/commit/0d45e95767773846861d53feb4af4c6bc1451b5e)) +* Quickstart documentation changes ([#4618](https://github.com/feast-dev/feast/issues/4618)) ([7ac0908](https://github.com/feast-dev/feast/commit/7ac0908f3846cc0ab05082f748506814c84b2e9c)) +* Refactor auth_client_manager_factory.py in function get_auth_client_m… ([#4505](https://github.com/feast-dev/feast/issues/4505)) ([def8633](https://github.com/feast-dev/feast/commit/def863360bf0e553d242900ee915e953c6c3f9b6)) +* Remote apply using offline store ([#4559](https://github.com/feast-dev/feast/issues/4559)) ([ac62a32](https://github.com/feast-dev/feast/commit/ac62a323c86fba8096eefde85775dd7a857e9e25)) +* Remove Feast UI TypeScript dependencies from `peerDependencies` and `dependencies` ([#4554](https://github.com/feast-dev/feast/issues/4554)) ([e781e16](https://github.com/feast-dev/feast/commit/e781e1652cadc6576dbab369248d6e4afdb5f158)) +* Remove unnecessary peer dependencies from Feast UI ([#4577](https://github.com/feast-dev/feast/issues/4577)) ([9ac7f4e](https://github.com/feast-dev/feast/commit/9ac7f4ea77357219fc8420f05e493767bc5357c2)) +* Removed protobuf as a required dependency ([#4535](https://github.com/feast-dev/feast/issues/4535)) ([0fb76e9](https://github.com/feast-dev/feast/commit/0fb76e9041885659c68e294b0c033c62050bd374)) +* Removed the k8s dependency from required dependencies ([#4519](https://github.com/feast-dev/feast/issues/4519)) ([3073ea5](https://github.com/feast-dev/feast/commit/3073ea5911339a5744be45512a9a2ee8b250292b)) +* Removed usage of pull_request_target as much as possible to prevent security concerns ([#4549](https://github.com/feast-dev/feast/issues/4549)) ([3198371](https://github.com/feast-dev/feast/commit/3198371fc0e07f6b51b62c7e3abbc48729078bb9)) +* Replaced ClusterRoles with local RoleBindings ([#4625](https://github.com/feast-dev/feast/issues/4625)) ([ca9fb9b](https://github.com/feast-dev/feast/commit/ca9fb9bc8c3f3b06b4ba5fce362e26633144715c)) +* Retire pytz library ([#4406](https://github.com/feast-dev/feast/issues/4406)) ([23c6c86](https://github.com/feast-dev/feast/commit/23c6c862e1da4e9523530eb48c7ce79319dc442d)) +* Typos related to k8s ([#4442](https://github.com/feast-dev/feast/issues/4442)) ([dda0088](https://github.com/feast-dev/feast/commit/dda0088f25eab5828613bd6d080aeddf681641f0)) +* Update java testcontainers to use Compose V2 ([#4381](https://github.com/feast-dev/feast/issues/4381)) ([9a33fce](https://github.com/feast-dev/feast/commit/9a33fce695c54226b3afd7b998e284f358bab141)) +* Update min versions for pyarrow and protobuf ([#4646](https://github.com/feast-dev/feast/issues/4646)) ([c7ddd4b](https://github.com/feast-dev/feast/commit/c7ddd4bb48290d6d9327fcb6349b84b4d14334af)) +* Update react-router-dom to 6.3.0 and restrict its version in Feast UI ([#4556](https://github.com/feast-dev/feast/issues/4556)) ([4293608](https://github.com/feast-dev/feast/commit/42936084a7d214d65faea5359ae70eefda8d23ad)), closes [#3794](https://github.com/feast-dev/feast/issues/3794) [/github.com/remix-run/react-router/blob/main/CHANGELOG.md#v630](https://github.com//github.com/remix-run/react-router/blob/main/CHANGELOG.md/issues/v630) +* Update the base image for feature-server. ([#4576](https://github.com/feast-dev/feast/issues/4576)) ([0390d8a](https://github.com/feast-dev/feast/commit/0390d8a86f50360a4df89165db62972328d22ca4)) +* Update the base image of materilization engine. ([#4580](https://github.com/feast-dev/feast/issues/4580)) ([f8592d8](https://github.com/feast-dev/feast/commit/f8592d86b2903ebfebc505bbf9392927aae5609c)) +* Updated README link ([#4669](https://github.com/feast-dev/feast/issues/4669)) ([35fbdc9](https://github.com/feast-dev/feast/commit/35fbdc91cc8a327be2a8cd162f7ccff983b16932)) +* Updating the documentation and adding tests for project length ([#4628](https://github.com/feast-dev/feast/issues/4628)) ([945b0fa](https://github.com/feast-dev/feast/commit/945b0faadd40c8dc76d104bce14ee902bd513127)) +* Using get_type_hints instead of inspect signature for udf return annotation ([#4391](https://github.com/feast-dev/feast/issues/4391)) ([3a32e8a](https://github.com/feast-dev/feast/commit/3a32e8ae28110db0934fc26ec6992eb606fed012)) +* Using repo_config parameter in teardown to allow for feature-store-yaml overrides ([#4413](https://github.com/feast-dev/feast/issues/4413)) ([0baeeb5](https://github.com/feast-dev/feast/commit/0baeeb5ec524c1e6209edab9605ca8a098a2ec88)) +* Validating permission to update an existing request on both the new and the old instance ([#4449](https://github.com/feast-dev/feast/issues/4449)) ([635a01b](https://github.com/feast-dev/feast/commit/635a01b4c77db781d67f9f5ebb1067806b1e2a13)) + + +### Features + +* Add boto3 session based auth for dynamodb online store for cross account access ([#4606](https://github.com/feast-dev/feast/issues/4606)) ([00eaf74](https://github.com/feast-dev/feast/commit/00eaf744f50f56f449c7ad8f6ceb92a09304ee0b)) +* Add cli list/describe for SavedDatasets, StreamFeatureViews, & … ([#4487](https://github.com/feast-dev/feast/issues/4487)) ([7b250e5](https://github.com/feast-dev/feast/commit/7b250e5eff5de56f5c5da103e91051276940298a)) +* Add connection_name field to Snowflake config ([#4600](https://github.com/feast-dev/feast/issues/4600)) ([10ce2aa](https://github.com/feast-dev/feast/commit/10ce2aa0050e419acfae27971a6fff87bade3ba4)) +* Add health check service to registry server ([#4421](https://github.com/feast-dev/feast/issues/4421)) ([46655f0](https://github.com/feast-dev/feast/commit/46655f06de339ee09245de3e1a648eb4e3bcd729)) +* Add more __repr__ methods ([#4676](https://github.com/feast-dev/feast/issues/4676)) ([e726c09](https://github.com/feast-dev/feast/commit/e726c096f2de93d6dc0a807c97c47476cc79dd61)) +* Add registry methods for dealing with all FV types ([#4435](https://github.com/feast-dev/feast/issues/4435)) ([ac381b2](https://github.com/feast-dev/feast/commit/ac381b292cfa29804ee5f0822f876d227a0989d9)) +* Added Project object to Feast Objects ([#4475](https://github.com/feast-dev/feast/issues/4475)) ([4a6b663](https://github.com/feast-dev/feast/commit/4a6b663f80bc91d6de35ed2ec428d34811d17a18)) +* Added support for reading from Reader Endpoints for AWS Aurora use cases ([#4494](https://github.com/feast-dev/feast/issues/4494)) ([d793c77](https://github.com/feast-dev/feast/commit/d793c77d923df95a186b9d4829b167f1a5a304e6)) +* Adding documentation for On Demand Feature Transformations with writes ([#4607](https://github.com/feast-dev/feast/issues/4607)) ([8e0c1b5](https://github.com/feast-dev/feast/commit/8e0c1b51665357d44229239401a04b76396b9047)) +* Adding mode='python' for get_historical_features on ODFVs ([#4653](https://github.com/feast-dev/feast/issues/4653)) ([c40d539](https://github.com/feast-dev/feast/commit/c40d539b85fe537077dd26904d78624da3d33951)) +* Adding registry cache support for get_on_demand_feature_view ([#4572](https://github.com/feast-dev/feast/issues/4572)) ([354c059](https://github.com/feast-dev/feast/commit/354c059e5475f9c3927d9180a421118507a22cf0)) +* Adding SSL support for online server ([#4677](https://github.com/feast-dev/feast/issues/4677)) ([80a5b3c](https://github.com/feast-dev/feast/commit/80a5b3c499faca8625d60267a34dcbddfe0c042a)) +* Adding write capability to online store to on demand feature views ([#4585](https://github.com/feast-dev/feast/issues/4585)) ([ef9e0bb](https://github.com/feast-dev/feast/commit/ef9e0bbdb2a80250786b87972a53c4cf5890bb76)), closes [#4603](https://github.com/feast-dev/feast/issues/4603) +* Allow feast snowflake to read in byte string for private-key authentication ([#4384](https://github.com/feast-dev/feast/issues/4384)) ([5215a21](https://github.com/feast-dev/feast/commit/5215a2139a9d824dc2d8f45181bd177a1e8e9561)) +* An action to test operator at PR time ([#4635](https://github.com/feast-dev/feast/issues/4635)) ([14c1000](https://github.com/feast-dev/feast/commit/14c1000554c590cb89ecb5ef44c57aa1b5dd1387)) +* Create ADOPTERS.md ([#4410](https://github.com/feast-dev/feast/issues/4410)) ([721ec74](https://github.com/feast-dev/feast/commit/721ec74f17ee95e375054f21135e54e0687104a7)) +* Create initial structure of Feast Go Operator ([#4596](https://github.com/feast-dev/feast/issues/4596)) ([b5ab6c7](https://github.com/feast-dev/feast/commit/b5ab6c799d529aaea19a196cedca7bb2c93cbbe9)) +* Faiss and In memory store ([#4464](https://github.com/feast-dev/feast/issues/4464)) ([a1ff129](https://github.com/feast-dev/feast/commit/a1ff1290002376b4c9ac8ad14e60df4622bb47a4)) +* Feast Security Model (aka RBAC) ([#4380](https://github.com/feast-dev/feast/issues/4380)) ([1771f66](https://github.com/feast-dev/feast/commit/1771f668247ef3b46ea7dac634e557e249bc1ba9)), closes [#36](https://github.com/feast-dev/feast/issues/36) +* Instrument Feast using Prometheus and OpenTelemetry ([#4366](https://github.com/feast-dev/feast/issues/4366)) ([a571e08](https://github.com/feast-dev/feast/commit/a571e08b97a95f8543d7dea27902c135ab3a4378)) +* Intra server to server communication ([#4433](https://github.com/feast-dev/feast/issues/4433)) ([729c874](https://github.com/feast-dev/feast/commit/729c874e8c30719f23ad287d3cb84f1d654274ec)) +* Publish TypeScript types in Feast UI package ([#4551](https://github.com/feast-dev/feast/issues/4551)) ([334e5d7](https://github.com/feast-dev/feast/commit/334e5d78855709d4ca56619f16eecb414f88ce2d)) +* Refactoring code to get oidc end points from discovery URL. ([#4429](https://github.com/feast-dev/feast/issues/4429)) ([896360a](https://github.com/feast-dev/feast/commit/896360af19a37c9a2a4634ec88021c4f69bdb141)) +* Return entity key in the retrieval document api ([#4511](https://github.com/feast-dev/feast/issues/4511)) ([5f5caf0](https://github.com/feast-dev/feast/commit/5f5caf0cac539ed779692e0ec819659cf5a33a0d)) +* Update roadmap.md ([#4445](https://github.com/feast-dev/feast/issues/4445)) ([34238d2](https://github.com/feast-dev/feast/commit/34238d2a0bfe9dbad753fec9613c83d848b1a520)) +* Update sqlite-vec package ([#4389](https://github.com/feast-dev/feast/issues/4389)) ([b734cb1](https://github.com/feast-dev/feast/commit/b734cb147a4afd28407ec57d95f70ff604f82954)) +* Updated Feast model Inference Architecture ([#4570](https://github.com/feast-dev/feast/issues/4570)) ([8cd0dcf](https://github.com/feast-dev/feast/commit/8cd0dcff0cc3b1387c7ca65018ebb09e03538242)) +* Updating docs to include model inference guidelines ([#4416](https://github.com/feast-dev/feast/issues/4416)) ([cebbe04](https://github.com/feast-dev/feast/commit/cebbe045597b85e1ae4394a8c14741e88347a6b8)) +* Updating FeatureViewProjection and OnDemandFeatureView to add batch_source and entities ([#4530](https://github.com/feast-dev/feast/issues/4530)) ([0795496](https://github.com/feast-dev/feast/commit/07954960c5501e2ecc1f1285ddf4aa68f9ac880b)) +* Upgrade React from 17.0.2 to 18.3.1 in Feast UI ([#4620](https://github.com/feast-dev/feast/issues/4620)) ([d6f3cb8](https://github.com/feast-dev/feast/commit/d6f3cb81a4c2a51bae2d29a185753cf6b1d2b16d)) + + +### Performance Improvements + +* Add init and cleanup of long lived resources ([#4642](https://github.com/feast-dev/feast/issues/4642)) ([47dc04d](https://github.com/feast-dev/feast/commit/47dc04d43f483944f97248abaccd15dba319281f)) +* Added indexes to sql tables to optimize query execution ([#4538](https://github.com/feast-dev/feast/issues/4538)) ([9688790](https://github.com/feast-dev/feast/commit/9688790a5e7a70f628a46021bde0201922c7e04d)) +* Default to async endpoints, use threadpool for sync ([#4647](https://github.com/feast-dev/feast/issues/4647)) ([c1f1912](https://github.com/feast-dev/feast/commit/c1f19127ab5c22dfe67869990480e7e9d8183ab1)) +* Implement dynamo write_batch_async ([#4675](https://github.com/feast-dev/feast/issues/4675)) ([ba4404c](https://github.com/feast-dev/feast/commit/ba4404cecfb196c4084f9bf892cd3528184d42c1)) +* Make /push async ([#4650](https://github.com/feast-dev/feast/issues/4650)) ([61abf89](https://github.com/feast-dev/feast/commit/61abf894aca7aa52042c40e77f64b49835f4324e)) +* Parallelize read calls by table and batch ([#4619](https://github.com/feast-dev/feast/issues/4619)) ([043eff1](https://github.com/feast-dev/feast/commit/043eff1a87bdf775a437503395acda87cbecf875)) + + +### BREAKING CHANGES + +* Consuming apps that use @elastic/eui should update it +to a compatible version. If you use @elastic/eui components that have +been renamed or replaced with others, you'll need to update your code +accordingly. + +Signed-off-by: Harri Lehtola + +* chore: Update Node version from 17 to 20 in UI unit tests + +Node 17 is not an LTS (long-term support) version and apparently +rejected by the latest versions of Elastic UI: + +> error @elastic/eui@95.12.0: The engine "node" is incompatible with +> this module. Expected version "16.x || 18.x || >=20.x". Got "17.9.1" + +Let's try with the latest LTS version. + +Signed-off-by: Harri Lehtola + # [0.40.0](https://github.com/feast-dev/feast/compare/v0.39.0...v0.40.0) (2024-07-31) diff --git a/infra/charts/feast-feature-server/Chart.yaml b/infra/charts/feast-feature-server/Chart.yaml index af47692f16..dd547843d1 100644 --- a/infra/charts/feast-feature-server/Chart.yaml +++ b/infra/charts/feast-feature-server/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 name: feast-feature-server description: Feast Feature Server in Go or Python type: application -version: 0.40.0 +version: 0.41.0 keywords: - machine learning - big data diff --git a/infra/charts/feast-feature-server/README.md b/infra/charts/feast-feature-server/README.md index bff7820d1f..a36f59d85e 100644 --- a/infra/charts/feast-feature-server/README.md +++ b/infra/charts/feast-feature-server/README.md @@ -1,6 +1,6 @@ # Feast Python / Go Feature Server Helm Charts -Current chart version is `0.40.0` +Current chart version is `0.41.0` ## Installation @@ -40,16 +40,16 @@ See [here](https://github.com/feast-dev/feast/tree/master/examples/python-helm-d | fullnameOverride | string | `""` | | | image.pullPolicy | string | `"IfNotPresent"` | | | image.repository | string | `"feastdev/feature-server"` | Docker image for Feature Server repository | -| image.tag | string | `"0.40.0"` | The Docker image tag (can be overwritten if custom feature server deps are needed for on demand transforms) | +| image.tag | string | `"0.41.0"` | The Docker image tag (can be overwritten if custom feature server deps are needed for on demand transforms) | | imagePullSecrets | list | `[]` | | | livenessProbe.initialDelaySeconds | int | `30` | | | livenessProbe.periodSeconds | int | `30` | | +| logLevel | string | `"WARNING"` | | | metrics.enabled | bool | `false` | | | metrics.otelCollector.endpoint | string | `""` | | | metrics.otelCollector.port | int | `4317` | | | nameOverride | string | `""` | | | nodeSelector | object | `{}` | | -| otel_service.name | string | `"otelcol"` | | | podAnnotations | object | `{}` | | | podSecurityContext | object | `{}` | | | readinessProbe.initialDelaySeconds | int | `20` | | @@ -59,4 +59,5 @@ See [here](https://github.com/feast-dev/feast/tree/master/examples/python-helm-d | securityContext | object | `{}` | | | service.port | int | `80` | | | service.type | string | `"ClusterIP"` | | +| serviceAccount.name | string | `""` | | | tolerations | list | `[]` | | \ No newline at end of file diff --git a/infra/charts/feast-feature-server/values.yaml b/infra/charts/feast-feature-server/values.yaml index f0bc55a646..d894177558 100644 --- a/infra/charts/feast-feature-server/values.yaml +++ b/infra/charts/feast-feature-server/values.yaml @@ -9,7 +9,7 @@ image: repository: feastdev/feature-server pullPolicy: IfNotPresent # image.tag -- The Docker image tag (can be overwritten if custom feature server deps are needed for on demand transforms) - tag: 0.40.0 + tag: 0.41.0 logLevel: "WARNING" # Set log level DEBUG, INFO, WARNING, ERROR, and CRITICAL (case-insensitive) diff --git a/infra/charts/feast/Chart.yaml b/infra/charts/feast/Chart.yaml index 1f030ca617..a192da8911 100644 --- a/infra/charts/feast/Chart.yaml +++ b/infra/charts/feast/Chart.yaml @@ -1,7 +1,7 @@ apiVersion: v1 description: Feature store for machine learning name: feast -version: 0.40.0 +version: 0.41.0 keywords: - machine learning - big data diff --git a/infra/charts/feast/README.md b/infra/charts/feast/README.md index bf8e4d7b8d..e49fbf6d96 100644 --- a/infra/charts/feast/README.md +++ b/infra/charts/feast/README.md @@ -8,7 +8,7 @@ This repo contains Helm charts for Feast Java components that are being installe ## Chart: Feast -Feature store for machine learning Current chart version is `0.40.0` +Feature store for machine learning Current chart version is `0.41.0` ## Installation @@ -65,8 +65,8 @@ See [here](https://github.com/feast-dev/feast/tree/master/examples/java-demo) fo | Repository | Name | Version | |------------|------|---------| | https://charts.helm.sh/stable | redis | 10.5.6 | -| https://feast-helm-charts.storage.googleapis.com | feature-server(feature-server) | 0.40.0 | -| https://feast-helm-charts.storage.googleapis.com | transformation-service(transformation-service) | 0.40.0 | +| https://feast-helm-charts.storage.googleapis.com | feature-server(feature-server) | 0.41.0 | +| https://feast-helm-charts.storage.googleapis.com | transformation-service(transformation-service) | 0.41.0 | ## Values diff --git a/infra/charts/feast/charts/feature-server/Chart.yaml b/infra/charts/feast/charts/feature-server/Chart.yaml index f91185be84..69748a362f 100644 --- a/infra/charts/feast/charts/feature-server/Chart.yaml +++ b/infra/charts/feast/charts/feature-server/Chart.yaml @@ -1,8 +1,8 @@ apiVersion: v1 description: "Feast Feature Server: Online feature serving service for Feast" name: feature-server -version: 0.40.0 -appVersion: v0.40.0 +version: 0.41.0 +appVersion: v0.41.0 keywords: - machine learning - big data diff --git a/infra/charts/feast/charts/feature-server/README.md b/infra/charts/feast/charts/feature-server/README.md index c75fc421c6..ab77911a8f 100644 --- a/infra/charts/feast/charts/feature-server/README.md +++ b/infra/charts/feast/charts/feature-server/README.md @@ -1,6 +1,6 @@ # feature-server -![Version: 0.40.0](https://img.shields.io/badge/Version-0.40.0-informational?style=flat-square) ![AppVersion: v0.40.0](https://img.shields.io/badge/AppVersion-v0.40.0-informational?style=flat-square) +![Version: 0.41.0](https://img.shields.io/badge/Version-0.41.0-informational?style=flat-square) ![AppVersion: v0.41.0](https://img.shields.io/badge/AppVersion-v0.41.0-informational?style=flat-square) Feast Feature Server: Online feature serving service for Feast @@ -17,7 +17,7 @@ Feast Feature Server: Online feature serving service for Feast | envOverrides | object | `{}` | Extra environment variables to set | | image.pullPolicy | string | `"IfNotPresent"` | Image pull policy | | image.repository | string | `"feastdev/feature-server-java"` | Docker image for Feature Server repository | -| image.tag | string | `"0.40.0"` | Image tag | +| image.tag | string | `"0.41.0"` | Image tag | | ingress.grpc.annotations | object | `{}` | Extra annotations for the ingress | | ingress.grpc.auth.enabled | bool | `false` | Flag to enable auth | | ingress.grpc.class | string | `"nginx"` | Which ingress controller to use | diff --git a/infra/charts/feast/charts/feature-server/values.yaml b/infra/charts/feast/charts/feature-server/values.yaml index d9c964bbca..646d735ef8 100644 --- a/infra/charts/feast/charts/feature-server/values.yaml +++ b/infra/charts/feast/charts/feature-server/values.yaml @@ -5,7 +5,7 @@ image: # image.repository -- Docker image for Feature Server repository repository: feastdev/feature-server-java # image.tag -- Image tag - tag: 0.40.0 + tag: 0.41.0 # image.pullPolicy -- Image pull policy pullPolicy: IfNotPresent diff --git a/infra/charts/feast/charts/transformation-service/Chart.yaml b/infra/charts/feast/charts/transformation-service/Chart.yaml index 7e336e7a3e..6c450852cb 100644 --- a/infra/charts/feast/charts/transformation-service/Chart.yaml +++ b/infra/charts/feast/charts/transformation-service/Chart.yaml @@ -1,8 +1,8 @@ apiVersion: v1 description: "Transformation service: to compute on-demand features" name: transformation-service -version: 0.40.0 -appVersion: v0.40.0 +version: 0.41.0 +appVersion: v0.41.0 keywords: - machine learning - big data diff --git a/infra/charts/feast/charts/transformation-service/README.md b/infra/charts/feast/charts/transformation-service/README.md index f90d5bda18..a00a21f034 100644 --- a/infra/charts/feast/charts/transformation-service/README.md +++ b/infra/charts/feast/charts/transformation-service/README.md @@ -1,6 +1,6 @@ # transformation-service -![Version: 0.40.0](https://img.shields.io/badge/Version-0.40.0-informational?style=flat-square) ![AppVersion: v0.40.0](https://img.shields.io/badge/AppVersion-v0.40.0-informational?style=flat-square) +![Version: 0.41.0](https://img.shields.io/badge/Version-0.41.0-informational?style=flat-square) ![AppVersion: v0.41.0](https://img.shields.io/badge/AppVersion-v0.41.0-informational?style=flat-square) Transformation service: to compute on-demand features @@ -13,7 +13,7 @@ Transformation service: to compute on-demand features | envOverrides | object | `{}` | Extra environment variables to set | | image.pullPolicy | string | `"IfNotPresent"` | Image pull policy | | image.repository | string | `"feastdev/feature-transformation-server"` | Docker image for Transformation Server repository | -| image.tag | string | `"0.40.0"` | Image tag | +| image.tag | string | `"0.41.0"` | Image tag | | nodeSelector | object | `{}` | Node labels for pod assignment | | podLabels | object | `{}` | Labels to be added to Feast Serving pods | | replicaCount | int | `1` | Number of pods that will be created | diff --git a/infra/charts/feast/charts/transformation-service/values.yaml b/infra/charts/feast/charts/transformation-service/values.yaml index aee47048e8..51cd72d659 100644 --- a/infra/charts/feast/charts/transformation-service/values.yaml +++ b/infra/charts/feast/charts/transformation-service/values.yaml @@ -5,7 +5,7 @@ image: # image.repository -- Docker image for Transformation Server repository repository: feastdev/feature-transformation-server # image.tag -- Image tag - tag: 0.40.0 + tag: 0.41.0 # image.pullPolicy -- Image pull policy pullPolicy: IfNotPresent diff --git a/infra/charts/feast/requirements.yaml b/infra/charts/feast/requirements.yaml index 7b1277fc69..bb69ee9ed3 100644 --- a/infra/charts/feast/requirements.yaml +++ b/infra/charts/feast/requirements.yaml @@ -1,12 +1,12 @@ dependencies: - name: feature-server alias: feature-server - version: 0.40.0 + version: 0.41.0 condition: feature-server.enabled repository: https://feast-helm-charts.storage.googleapis.com - name: transformation-service alias: transformation-service - version: 0.40.0 + version: 0.41.0 condition: transformation-service.enabled repository: https://feast-helm-charts.storage.googleapis.com - name: redis diff --git a/infra/feast-helm-operator/Makefile b/infra/feast-helm-operator/Makefile index 6712a37b4a..733bf7bc3d 100644 --- a/infra/feast-helm-operator/Makefile +++ b/infra/feast-helm-operator/Makefile @@ -3,7 +3,7 @@ # To re-generate a bundle for another specific version without changing the standard setup, you can: # - use the VERSION as arg of the bundle target (e.g make bundle VERSION=0.0.2) # - use environment variables to overwrite this value (e.g export VERSION=0.0.2) -VERSION ?= 0.40.0 +VERSION ?= 0.41.0 # CHANNELS define the bundle channels used in the bundle. # Add a new line here if you would like to change its default config. (E.g CHANNELS = "candidate,fast,stable") diff --git a/infra/feast-helm-operator/config/manager/kustomization.yaml b/infra/feast-helm-operator/config/manager/kustomization.yaml index 0d27c2149d..decb714a20 100644 --- a/infra/feast-helm-operator/config/manager/kustomization.yaml +++ b/infra/feast-helm-operator/config/manager/kustomization.yaml @@ -5,4 +5,4 @@ kind: Kustomization images: - name: controller newName: feastdev/feast-helm-operator - newTag: 0.40.0 + newTag: 0.41.0 diff --git a/infra/feast-operator/Makefile b/infra/feast-operator/Makefile index a6b922b062..54786eb5f1 100644 --- a/infra/feast-operator/Makefile +++ b/infra/feast-operator/Makefile @@ -3,7 +3,7 @@ # To re-generate a bundle for another specific version without changing the standard setup, you can: # - use the VERSION as arg of the bundle target (e.g make bundle VERSION=0.0.2) # - use environment variables to overwrite this value (e.g export VERSION=0.0.2) -VERSION ?= 0.40.0 +VERSION ?= 0.41.0 # CHANNELS define the bundle channels used in the bundle. # Add a new line here if you would like to change its default config. (E.g CHANNELS = "candidate,fast,stable") diff --git a/infra/feast-operator/config/manager/kustomization.yaml b/infra/feast-operator/config/manager/kustomization.yaml index aba3224be6..253475b945 100644 --- a/infra/feast-operator/config/manager/kustomization.yaml +++ b/infra/feast-operator/config/manager/kustomization.yaml @@ -5,4 +5,4 @@ kind: Kustomization images: - name: controller newName: feastdev/feast-operator - newTag: 0.40.0 + newTag: 0.41.0 diff --git a/java/pom.xml b/java/pom.xml index 2c1c32792c..416ebe5978 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -35,7 +35,7 @@ - 0.40.0 + 0.41.0 https://github.com/feast-dev/feast UTF-8 diff --git a/sdk/python/feast/ui/package.json b/sdk/python/feast/ui/package.json index 36777ca0be..2a6329a166 100644 --- a/sdk/python/feast/ui/package.json +++ b/sdk/python/feast/ui/package.json @@ -6,7 +6,7 @@ "@elastic/datemath": "^5.0.3", "@elastic/eui": "^55.0.1", "@emotion/react": "^11.9.0", - "@feast-dev/feast-ui": "0.40.0", + "@feast-dev/feast-ui": "0.41.0", "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^13.2.0", "@testing-library/user-event": "^13.5.0", diff --git a/sdk/python/feast/ui/yarn.lock b/sdk/python/feast/ui/yarn.lock index 7e83084445..24de47b123 100644 --- a/sdk/python/feast/ui/yarn.lock +++ b/sdk/python/feast/ui/yarn.lock @@ -95,13 +95,6 @@ dependencies: "@babel/types" "^7.16.7" -"@babel/helper-annotate-as-pure@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz#e7f06737b197d580a01edf75d97e2c8be99d3882" - integrity sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg== - dependencies: - "@babel/types" "^7.22.5" - "@babel/helper-builder-binary-assignment-operator-visitor@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.16.7.tgz#38d138561ea207f0f69eb1626a418e4f7e6a580b" @@ -211,13 +204,6 @@ dependencies: "@babel/types" "^7.17.0" -"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz#1a8f4c9f4027d23f520bd76b364d44434a72660c" - integrity sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg== - dependencies: - "@babel/types" "^7.22.5" - "@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz#25612a8091a999704461c8a222d0efec5d091437" @@ -251,11 +237,6 @@ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz#aa3a8ab4c3cceff8e65eb9e73d87dc4ff320b2f5" integrity sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA== -"@babel/helper-plugin-utils@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295" - integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg== - "@babel/helper-remap-async-to-generator@^7.16.8": version "7.16.8" resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.8.tgz#29ffaade68a367e2ed09c90901986918d25e57e3" @@ -609,13 +590,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.16.7" -"@babel/plugin-syntax-jsx@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz#a6b68e84fb76e759fc3b93e901876ffabbe1d918" - integrity sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/plugin-syntax-logical-assignment-operators@^7.10.4", "@babel/plugin-syntax-logical-assignment-operators@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" @@ -1132,6 +1106,13 @@ dependencies: regenerator-runtime "^0.13.11" +"@babel/runtime@^7.12.1", "@babel/runtime@^7.18.3", "@babel/runtime@^7.24.1": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1" + integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.16.7", "@babel/template@^7.3.3": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155" @@ -1150,7 +1131,7 @@ "@babel/parser" "^7.22.15" "@babel/types" "^7.22.15" -"@babel/traverse@^7.13.0", "@babel/traverse@^7.16.7", "@babel/traverse@^7.16.8", "@babel/traverse@^7.17.10", "@babel/traverse@^7.17.3", "@babel/traverse@^7.17.9", "@babel/traverse@^7.4.5", "@babel/traverse@^7.7.2": +"@babel/traverse@^7.13.0", "@babel/traverse@^7.16.7", "@babel/traverse@^7.16.8", "@babel/traverse@^7.17.10", "@babel/traverse@^7.17.3", "@babel/traverse@^7.17.9", "@babel/traverse@^7.7.2": version "7.23.2" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8" integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw== @@ -1336,6 +1317,62 @@ uuid "^8.3.0" vfile "^4.2.0" +"@elastic/eui@^95.12.0": + version "95.12.0" + resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-95.12.0.tgz#862f2be8b72248a62b40704b9e62f2f5d7d43853" + integrity sha512-SW4ru97FY2VitSqyCgURrM5OMk1W+Ww12b6S+VZN5ex50aNT296DfED/ByidlYaAoVihqjZuoB3HlQBBXydFpA== + dependencies: + "@hello-pangea/dnd" "^16.6.0" + "@types/lodash" "^4.14.202" + "@types/numeral" "^2.0.5" + "@types/react-window" "^1.8.8" + "@types/refractor" "^3.4.0" + chroma-js "^2.4.2" + classnames "^2.5.1" + lodash "^4.17.21" + mdast-util-to-hast "^10.2.0" + numeral "^2.0.6" + prop-types "^15.8.1" + react-dropzone "^11.7.1" + react-element-to-jsx-string "^15.0.0" + react-focus-on "^3.9.1" + react-is "^17.0.2" + react-remove-scroll-bar "^2.3.4" + react-virtualized-auto-sizer "^1.0.24" + react-window "^1.8.10" + refractor "^3.6.0" + rehype-raw "^5.1.0" + rehype-react "^6.2.1" + rehype-stringify "^8.0.0" + remark-breaks "^2.0.2" + remark-emoji "^2.1.0" + remark-parse-no-trim "^8.0.4" + remark-rehype "^8.1.0" + tabbable "^5.3.3" + text-diff "^1.0.1" + unified "^9.2.2" + unist-util-visit "^2.0.3" + url-parse "^1.5.10" + uuid "^8.3.0" + vfile "^4.2.1" + +"@emotion/babel-plugin@^11.12.0": + version "11.12.0" + resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.12.0.tgz#7b43debb250c313101b3f885eba634f1d723fcc2" + integrity sha512-y2WQb+oP8Jqvvclh8Q55gLUyb7UFvgv7eJfsj7td5TToBrIUtPay2kMrZi4xjq9qw2vD0ZR5fSho0yqoFgX7Rw== + dependencies: + "@babel/helper-module-imports" "^7.16.7" + "@babel/runtime" "^7.18.3" + "@emotion/hash" "^0.9.2" + "@emotion/memoize" "^0.9.0" + "@emotion/serialize" "^1.2.0" + babel-plugin-macros "^3.1.0" + convert-source-map "^1.5.0" + escape-string-regexp "^4.0.0" + find-root "^1.1.0" + source-map "^0.5.7" + stylis "4.2.0" + "@emotion/babel-plugin@^11.7.1": version "11.9.2" resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.9.2.tgz#723b6d394c89fb2ef782229d92ba95a740576e95" @@ -1354,6 +1391,17 @@ source-map "^0.5.7" stylis "4.0.13" +"@emotion/cache@^11.13.0": + version "11.13.1" + resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.13.1.tgz#fecfc54d51810beebf05bf2a161271a1a91895d7" + integrity sha512-iqouYkuEblRcXmylXIwwOodiEK5Ifl7JcX7o6V4jI3iW4mLXX3dmt5xwBtIkJiQEXFAI+pC8X0i67yiPkH9Ucw== + dependencies: + "@emotion/memoize" "^0.9.0" + "@emotion/sheet" "^1.4.0" + "@emotion/utils" "^1.4.0" + "@emotion/weak-memoize" "^0.4.0" + stylis "4.2.0" + "@emotion/cache@^11.7.1": version "11.7.1" resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.7.1.tgz#08d080e396a42e0037848214e8aa7bf879065539" @@ -1365,15 +1413,31 @@ "@emotion/weak-memoize" "^0.2.5" stylis "4.0.13" +"@emotion/css@^11.13.0": + version "11.13.4" + resolved "https://registry.yarnpkg.com/@emotion/css/-/css-11.13.4.tgz#a5128e34a23f5e2c891970b8ec98a60c5a2395e1" + integrity sha512-CthbOD5EBw+iN0rfM96Tuv5kaZN4nxPyYDvGUs0bc7wZBBiU/0mse+l+0O9RshW2d+v5HH1cme+BAbLJ/3Folw== + dependencies: + "@emotion/babel-plugin" "^11.12.0" + "@emotion/cache" "^11.13.0" + "@emotion/serialize" "^1.3.0" + "@emotion/sheet" "^1.4.0" + "@emotion/utils" "^1.4.0" + "@emotion/hash@^0.8.0": version "0.8.0" resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== -"@emotion/is-prop-valid@^1.1.0": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz#23116cf1ed18bfeac910ec6436561ecb1a3885cc" - integrity sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw== +"@emotion/hash@^0.9.2": + version "0.9.2" + resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.2.tgz#ff9221b9f58b4dfe61e619a7788734bd63f6898b" + integrity sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g== + +"@emotion/is-prop-valid@1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz#d4175076679c6a26faa92b03bb786f9e52612337" + integrity sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw== dependencies: "@emotion/memoize" "^0.8.1" @@ -1387,7 +1451,26 @@ resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.1.tgz#c1ddb040429c6d21d38cc945fe75c818cfb68e17" integrity sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA== -"@emotion/react@^11.7.1", "@emotion/react@^11.9.0": +"@emotion/memoize@^0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.9.0.tgz#745969d649977776b43fc7648c556aaa462b4102" + integrity sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ== + +"@emotion/react@^11.13.3": + version "11.13.3" + resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.13.3.tgz#a69d0de2a23f5b48e0acf210416638010e4bd2e4" + integrity sha512-lIsdU6JNrmYfJ5EbUCf4xW1ovy5wKQ2CkPRM4xogziOxH1nXxBSjpC9YqbFAP7circxMfYp+6x676BqWcEiixg== + dependencies: + "@babel/runtime" "^7.18.3" + "@emotion/babel-plugin" "^11.12.0" + "@emotion/cache" "^11.13.0" + "@emotion/serialize" "^1.3.1" + "@emotion/use-insertion-effect-with-fallbacks" "^1.1.0" + "@emotion/utils" "^1.4.0" + "@emotion/weak-memoize" "^0.4.0" + hoist-non-react-statics "^3.3.1" + +"@emotion/react@^11.9.0": version "11.9.0" resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.9.0.tgz#b6d42b1db3bd7511e7a7c4151dc8bc82e14593b8" integrity sha512-lBVSF5d0ceKtfKCDQJveNAtkC7ayxpVlgOohLgXqRwqWr9bOf4TZAFFyIcNngnV6xK6X4x2ZeXq7vliHkoVkxQ== @@ -1411,31 +1494,67 @@ "@emotion/utils" "^1.0.0" csstype "^3.0.2" +"@emotion/serialize@^1.2.0", "@emotion/serialize@^1.3.0", "@emotion/serialize@^1.3.1": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.3.2.tgz#e1c1a2e90708d5d85d81ccaee2dfeb3cc0cccf7a" + integrity sha512-grVnMvVPK9yUVE6rkKfAJlYZgo0cu3l9iMC77V7DW6E1DUIrU68pSEXRmFZFOFB1QFo57TncmOcvcbMDWsL4yA== + dependencies: + "@emotion/hash" "^0.9.2" + "@emotion/memoize" "^0.9.0" + "@emotion/unitless" "^0.10.0" + "@emotion/utils" "^1.4.1" + csstype "^3.0.2" + "@emotion/sheet@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.1.0.tgz#56d99c41f0a1cda2726a05aa6a20afd4c63e58d2" integrity sha512-u0AX4aSo25sMAygCuQTzS+HsImZFuS8llY8O7b9MDRzbJM0kVJlAz6KNDqcG7pOuQZJmj/8X/rAW+66kMnMW+g== -"@emotion/stylis@^0.8.4": - version "0.8.5" - resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.5.tgz#deacb389bd6ee77d1e7fcaccce9e16c5c7e78e04" - integrity sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ== +"@emotion/sheet@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.4.0.tgz#c9299c34d248bc26e82563735f78953d2efca83c" + integrity sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg== + +"@emotion/unitless@0.8.1": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.8.1.tgz#182b5a4704ef8ad91bde93f7a860a88fd92c79a3" + integrity sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ== + +"@emotion/unitless@^0.10.0": + version "0.10.0" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.10.0.tgz#2af2f7c7e5150f497bdabd848ce7b218a27cf745" + integrity sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg== -"@emotion/unitless@^0.7.4", "@emotion/unitless@^0.7.5": +"@emotion/unitless@^0.7.5": version "0.7.5" resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== +"@emotion/use-insertion-effect-with-fallbacks@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz#1a818a0b2c481efba0cf34e5ab1e0cb2dcb9dfaf" + integrity sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw== + "@emotion/utils@^1.0.0", "@emotion/utils@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.1.0.tgz#86b0b297f3f1a0f2bdb08eeac9a2f49afd40d0cf" integrity sha512-iRLa/Y4Rs5H/f2nimczYmS5kFJEbpiVvgN3XVfZ022IYhuNA1IRSHEizcof88LtCTXtl9S2Cxt32KgaXEu72JQ== +"@emotion/utils@^1.4.0", "@emotion/utils@^1.4.1": + version "1.4.1" + resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.4.1.tgz#b3adbb43de12ee2149541c4f1337d2eb7774f0ad" + integrity sha512-BymCXzCG3r72VKJxaYVwOXATqXIZ85cuvg0YOUDxMGNrKc1DJRZk8MgV5wyXRyEayIMd4FuXJIUgTBXvDNW5cA== + "@emotion/weak-memoize@^0.2.5": version "0.2.5" resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== +"@emotion/weak-memoize@^0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz#5e13fac887f08c44f76b0ccaf3370eb00fec9bb6" + integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg== + "@eslint/eslintrc@^1.2.3": version "1.2.3" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.2.3.tgz#fcaa2bcef39e13d6e9e7f6271f4cc7cae1174886" @@ -1451,32 +1570,40 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@feast-dev/feast-ui@0.40.0": - version "0.40.0" - resolved "https://registry.yarnpkg.com/@feast-dev/feast-ui/-/feast-ui-0.40.0.tgz#0dc60cbbd4f63d161927321c0bbf57bbfe6b7d09" - integrity sha512-jiCtMYCBvNSfHCjemFRa0NFIIAR5y6spWBnUZyc4GXY2YxGcznw+PZSzOoi7JrOwpNzNPB0PTBUqJgBAxus20w== +"@feast-dev/feast-ui@0.41.0": + version "0.41.0" + resolved "https://registry.yarnpkg.com/@feast-dev/feast-ui/-/feast-ui-0.41.0.tgz#67eca6328131ee524ee6a6f286cfc4386f698053" + integrity sha512-BkVb4zfR+j95IX9FBzeXFyCimG5Za1a3jyLqjmETRO3hpp5OJanpc2N35AaOn8ZPqka00Be/b8NZ8TjbsRWyVg== dependencies: "@elastic/datemath" "^5.0.3" - "@elastic/eui" "^55.0.1" - "@emotion/react" "^11.7.1" - "@types/d3" "^7.1.0" - "@types/jest" "^27.0.1" - "@types/node" "^16.7.13" - "@types/react" "^17.0.20" - "@types/react-dom" "^17.0.9" - d3 "^7.3.0" + "@elastic/eui" "^95.12.0" + "@emotion/css" "^11.13.0" + "@emotion/react" "^11.13.3" inter-ui "^3.19.3" moment "^2.29.1" - prop-types "^15.8.1" protobufjs "^7.1.1" query-string "^7.1.1" - react-code-blocks "^0.0.9-0" - react-query "^3.34.12" - react-router-dom "6" - react-scripts "^5.0.0" + react-code-blocks "^0.1.6" + react-query "^3.39.3" + react-router-dom "<6.4.0" + react-scripts "^5.0.1" + tslib "^2.3.1" use-query-params "^1.2.3" zod "^3.11.6" +"@hello-pangea/dnd@^16.6.0": + version "16.6.0" + resolved "https://registry.yarnpkg.com/@hello-pangea/dnd/-/dnd-16.6.0.tgz#7509639c7bd13f55e537b65a9dcfcd54e7c99ac7" + integrity sha512-vfZ4GydqbtUPXSLfAvKvXQ6xwRzIjUSjVU0Sx+70VOhc2xx6CdmJXJ8YhH70RpbTUGjxctslQTHul9sIOxCfFQ== + dependencies: + "@babel/runtime" "^7.24.1" + css-box-model "^1.2.1" + memoize-one "^6.0.0" + raf-schd "^4.0.3" + react-redux "^8.1.3" + redux "^4.2.1" + use-memo-one "^1.1.3" + "@humanwhocodes/config-array@^0.9.2": version "0.9.5" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.9.5.tgz#2cbaf9a89460da24b5ca6531b8bbfc23e1df50c7" @@ -2509,6 +2636,14 @@ "@types/react" "*" hoist-non-react-statics "^3.3.0" +"@types/hoist-non-react-statics@^3.3.1": + version "3.3.5" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz#dab7867ef789d87e2b4b0003c9d65c49cc44a494" + integrity sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg== + dependencies: + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + "@types/html-minifier-terser@^6.0.0": version "6.1.0" resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#4fc33a00c1d0c16987b1a20cf92d20614c55ac35" @@ -2540,7 +2675,7 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@*", "@types/jest@^27.0.1": +"@types/jest@*": version "27.5.0" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.5.0.tgz#e04ed1824ca6b1dd0438997ba60f99a7405d4c7b" integrity sha512-9RBFx7r4k+msyj/arpfaa0WOOEcaAZNmN+j80KFbFCoSqCJGHTz7YMAMGQW9Xmqm5w6l5c25vbSjMwlikJi5+g== @@ -2563,6 +2698,11 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.182.tgz#05301a4d5e62963227eaafe0ce04dd77c54ea5c2" integrity sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q== +"@types/lodash@^4.14.202": + version "4.17.12" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.12.tgz#25d71312bf66512105d71e55d42e22c36bcfc689" + integrity sha512-sviUmCE8AYdaF/KIHLDJBQgeYzPBI0vf/17NaYehBJfYD1j6/L95Slh07NlyK2iNyBNaEkb3En2jRt+a8y3xZQ== + "@types/mdast@^3.0.0": version "3.0.10" resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.10.tgz#4724244a82a4598884cbbe9bcfd73dff927ee8af" @@ -2585,16 +2725,16 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.8.3.tgz#ce750ab4017effa51aed6a7230651778d54e327c" integrity sha512-0os9vz6BpGwxGe9LOhgP/ncvYN5Tx1fNcd2TM3rD/aCGBkysb+ZWpXEocG24h6ZzOi13+VB8HndAQFezsSOw1w== -"@types/node@^16.7.13": - version "16.11.34" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.34.tgz#520224e4be4448c279ecad09639ab460cc441a50" - integrity sha512-UrWGDyLAlQ2Z8bNOGWTsqbP9ZcBeTYBVuTRNxXTztBy5KhWUFI3BaeDWoCP/CzV/EVGgO1NTYzv9ZytBI9GAEw== - "@types/numeral@^0.0.28": version "0.0.28" resolved "https://registry.yarnpkg.com/@types/numeral/-/numeral-0.0.28.tgz#e43928f0bda10b169b6f7ecf99e3ddf836b8ebe4" integrity sha512-Sjsy10w6XFHDktJJdXzBJmoondAKW+LcGpRFH+9+zXEDj0cOH8BxJuZA9vUDSMAzU1YRJlsPKmZEEiTYDlICLw== +"@types/numeral@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@types/numeral/-/numeral-2.0.5.tgz#388e5c4ff4b0e1787f130753cbbe83d3ba770858" + integrity sha512-kH8I7OSSwQu9DS9JYdFWbuvhVzvFRoCPCkGxNwoGgaPeDfEPJlcxNvEOypZhQ3XXHsGbfIuYcxcJxKUfJHnRfw== + "@types/parse-json@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" @@ -2642,13 +2782,6 @@ dependencies: "@types/react" "*" -"@types/react-dom@^17.0.9": - version "17.0.16" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.16.tgz#7caba93cf2806c51e64d620d8dff4bae57e06cc4" - integrity sha512-DWcXf8EbMrO/gWnQU7Z88Ws/p16qxGpPyjTKTpmBSFKeE+HveVubqGO1CVK7FrwlWD5MuOcvh8gtd0/XO38NdQ== - dependencies: - "@types/react" "^17" - "@types/react-dom@^18.0.0": version "18.0.3" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.3.tgz#a022ea08c75a476fe5e96b675c3e673363853831" @@ -2687,6 +2820,13 @@ dependencies: "@types/react" "*" +"@types/react-window@^1.8.8": + version "1.8.8" + resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.8.tgz#c20645414d142364fbe735818e1c1e0a145696e3" + integrity sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q== + dependencies: + "@types/react" "*" + "@types/react@*": version "18.0.9" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.9.tgz#d6712a38bd6cd83469603e7359511126f122e878" @@ -2696,15 +2836,6 @@ "@types/scheduler" "*" csstype "^3.0.2" -"@types/react@^17", "@types/react@^17.0.20": - version "17.0.45" - resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.45.tgz#9b3d5b661fd26365fefef0e766a1c6c30ccf7b3f" - integrity sha512-YfhQ22Lah2e3CHPsb93tRwIGNiSwkuz1/blk4e6QrWS0jQzCSNbGLtOEYhPg02W0yGTTmpajp7dCTbBAMN3qsg== - dependencies: - "@types/prop-types" "*" - "@types/scheduler" "*" - csstype "^3.0.2" - "@types/refractor@^3.0.0": version "3.0.2" resolved "https://registry.yarnpkg.com/@types/refractor/-/refractor-3.0.2.tgz#2d42128d59f78f84d2c799ffc5ab5cadbcba2d82" @@ -2712,6 +2843,13 @@ dependencies: "@types/prismjs" "*" +"@types/refractor@^3.4.0": + version "3.4.1" + resolved "https://registry.yarnpkg.com/@types/refractor/-/refractor-3.4.1.tgz#8b109804f77b3da8fad543d3f575fef1ece8835a" + integrity sha512-wYuorIiCTSuvRT9srwt+taF6mH/ww+SyN2psM0sjef2qW+sS8GmshgDGTEDgWB1sTVGgYVE6EK7dBA2MxQxibg== + dependencies: + "@types/prismjs" "*" + "@types/resize-observer-browser@^0.1.5": version "0.1.7" resolved "https://registry.yarnpkg.com/@types/resize-observer-browser/-/resize-observer-browser-0.1.7.tgz#294aaadf24ac6580b8fbd1fe3ab7b59fe85f9ef3" @@ -2761,6 +2899,11 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== +"@types/stylis@4.2.5": + version "4.2.5" + resolved "https://registry.yarnpkg.com/@types/stylis/-/stylis-4.2.5.tgz#1daa6456f40959d06157698a653a9ab0a70281df" + integrity sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw== + "@types/testing-library__jest-dom@^5.9.1": version "5.14.3" resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.3.tgz#ee6c7ffe9f8595882ee7bda8af33ae7b8789ef17" @@ -2778,6 +2921,11 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== +"@types/use-sync-external-store@^0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43" + integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA== + "@types/vfile-message@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@types/vfile-message/-/vfile-message-2.0.0.tgz#690e46af0fdfc1f9faae00cd049cc888957927d5" @@ -3221,6 +3369,13 @@ aria-hidden@^1.1.3: dependencies: tslib "^1.0.0" +aria-hidden@^1.2.2: + version "1.2.4" + resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.4.tgz#b78e383fdbc04d05762c78b4a25a501e736c4522" + integrity sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A== + dependencies: + tslib "^2.0.0" + aria-query@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b" @@ -3436,17 +3591,6 @@ babel-plugin-polyfill-regenerator@^0.3.0: dependencies: "@babel/helper-define-polyfill-provider" "^0.3.1" -"babel-plugin-styled-components@>= 1.12.0": - version "2.1.4" - resolved "https://registry.yarnpkg.com/babel-plugin-styled-components/-/babel-plugin-styled-components-2.1.4.tgz#9a1f37c7f32ef927b4b008b529feb4a2c82b1092" - integrity sha512-Xgp9g+A/cG47sUyRwwYxGM4bR/jDRg5N6it/8+HxCnbT5XNKSKDT9xm4oag/osgqjC2It/vH0yXsomOG6k558g== - dependencies: - "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-module-imports" "^7.22.5" - "@babel/plugin-syntax-jsx" "^7.22.5" - lodash "^4.17.21" - picomatch "^2.3.1" - babel-plugin-transform-react-remove-prop-types@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz#f2edaf9b4c6a5fbe5c1d678bfb531078c1555f3a" @@ -3834,6 +3978,11 @@ chroma-js@^2.1.0: resolved "https://registry.yarnpkg.com/chroma-js/-/chroma-js-2.4.2.tgz#dffc214ed0c11fa8eefca2c36651d8e57cbfb2b0" integrity sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A== +chroma-js@^2.4.2: + version "2.6.0" + resolved "https://registry.yarnpkg.com/chroma-js/-/chroma-js-2.6.0.tgz#578743dd359698a75067a19fa5571dec54d0b70b" + integrity sha512-BLHvCB9s8Z1EV4ethr6xnkl/P2YRFOGqfgvuMG/MyCbZPrTA+NeiByY6XvgF0zP4/2deU2CXnWyMa3zu1LqQ3A== + chrome-trace-event@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" @@ -3854,6 +4003,11 @@ classnames@^2.2.6: resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA== +classnames@^2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" + integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== + clean-css@^5.2.2: version "5.3.0" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.0.tgz#ad3d8238d5f3549e83d5f87205189494bc7cbb59" @@ -3861,15 +4015,6 @@ clean-css@^5.2.2: dependencies: source-map "~0.6.0" -clipboard@^2.0.0: - version "2.0.11" - resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.11.tgz#62180360b97dd668b6b3a84ec226975762a70be5" - integrity sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw== - dependencies: - good-listener "^1.2.2" - select "^1.1.2" - tiny-emitter "^2.0.0" - cliui@^7.0.2: version "7.0.4" resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" @@ -4114,7 +4259,7 @@ css-blank-pseudo@^3.0.3: dependencies: postcss-selector-parser "^6.0.9" -css-box-model@^1.2.0: +css-box-model@^1.2.0, css-box-model@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1" integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw== @@ -4195,7 +4340,7 @@ css-select@^4.1.3: domutils "^2.8.0" nth-check "^2.0.1" -css-to-react-native@^3.0.0: +css-to-react-native@3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/css-to-react-native/-/css-to-react-native-3.2.0.tgz#cdd8099f71024e149e4f6fe17a7d46ecd55f1e32" integrity sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ== @@ -4327,6 +4472,11 @@ cssstyle@^2.3.0: dependencies: cssom "~0.3.6" +csstype@3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== + csstype@^3.0.2: version "3.0.11" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.11.tgz#d66700c5eacfac1940deb4e3ee5642792d85cd33" @@ -4540,7 +4690,7 @@ d3-zoom@3: d3-selection "2 - 3" d3-transition "2 - 3" -d3@^7.3.0, d3@^7.4.4: +d3@^7.4.4: version "7.4.4" resolved "https://registry.yarnpkg.com/d3/-/d3-7.4.4.tgz#bfbf87487c37d3196efebd5a63e3a0ed8299d8ff" integrity sha512-97FE+MYdAlV3R9P74+R3Uar7wUKkIFu89UWMjEaDhiJ9VxKvqaMxauImy8PC2DdBkdM2BxJOIoLxPrcZUyrKoQ== @@ -4682,11 +4832,6 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= -delegate@^3.1.2: - version "3.2.0" - resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166" - integrity sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw== - depd@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" @@ -5441,7 +5586,7 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" -fault@^1.0.2: +fault@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/fault/-/fault-1.0.4.tgz#eafcfc0a6d214fc94601e170df29954a4f842f13" integrity sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA== @@ -5585,6 +5730,13 @@ focus-lock@^0.11.2: dependencies: tslib "^2.0.3" +focus-lock@^1.3.5: + version "1.3.5" + resolved "https://registry.yarnpkg.com/focus-lock/-/focus-lock-1.3.5.tgz#aa644576e5ec47d227b57eb14e1efb2abf33914c" + integrity sha512-QFaHbhv9WPUeLYBDe/PAuLKJ4Dd9OPvKs9xZBr3yLXnUrDNaVXKu2baDBXe3naPY30hgHYSsf2JW4jzas2mDEQ== + dependencies: + tslib "^2.0.3" + follow-redirects@^1.0.0: version "1.15.6" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" @@ -5836,13 +5988,6 @@ globby@^11.0.4: merge2 "^1.4.1" slash "^3.0.0" -good-listener@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50" - integrity sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw== - dependencies: - delegate "^3.1.2" - gopd@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" @@ -6021,16 +6166,6 @@ hast-util-whitespace@^1.0.0: resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-1.0.4.tgz#e4fe77c4a9ae1cb2e6c25e02df0043d0164f6e41" integrity sha512-I5GTdSfhYfAPNztx2xJRQpG8cuDSNt599/7YUn7Gx/WxNMsG+a835k97TDkFgk123cwjfwINaZknkKkphx/f2A== -hastscript@^5.0.0: - version "5.1.2" - resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-5.1.2.tgz#bde2c2e56d04c62dd24e8c5df288d050a355fb8a" - integrity sha512-WlztFuK+Lrvi3EggsqOkQ52rKbxkXL3RwB6t5lwoa8QLMemoWfBuL43eDrwOamJyR7uKQKdmKYaBH1NZBiIRrQ== - dependencies: - comma-separated-tokens "^1.0.0" - hast-util-parse-selector "^2.0.0" - property-information "^5.0.0" - space-separated-tokens "^1.0.0" - hastscript@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-6.0.0.tgz#e8768d7eac56c3fdeac8a92830d58e811e5bf640" @@ -6047,10 +6182,15 @@ he@^1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== -highlight.js@~9.15.0, highlight.js@~9.15.1: - version "9.15.10" - resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.15.10.tgz#7b18ed75c90348c045eef9ed08ca1319a2219ad2" - integrity sha512-RoV7OkQm0T3os3Dd2VHLNMoaoDVx77Wygln3n9l5YV172XonWG6rgQD3XnF/BuFFZw9A0TJgmMSO8FEWQgvcXw== +highlight.js@^10.4.1, highlight.js@~10.7.0: + version "10.7.3" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531" + integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A== + +highlightjs-vue@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz#fdfe97fbea6354e70ee44e3a955875e114db086d" + integrity sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA== history@^5.2.0: version "5.3.0" @@ -6059,7 +6199,7 @@ history@^5.2.0: dependencies: "@babel/runtime" "^7.7.6" -hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: +hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -7380,13 +7520,13 @@ lower-case@^2.0.2: dependencies: tslib "^2.0.3" -lowlight@1.12.1: - version "1.12.1" - resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.12.1.tgz#014acf8dd73a370e02ff1cc61debcde3bb1681eb" - integrity sha512-OqaVxMGIESnawn+TU/QMV5BJLbUghUfjDWPAtFqDYDmDtr4FnB+op8xM+pR7nKlauHNUHXGt0VgWatFB8voS5w== +lowlight@^1.17.0: + version "1.20.0" + resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.20.0.tgz#ddb197d33462ad0d93bf19d17b6c301aa3941888" + integrity sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw== dependencies: - fault "^1.0.2" - highlight.js "~9.15.0" + fault "^1.0.0" + highlight.js "~10.7.0" lru-cache@^6.0.0: version "6.0.0" @@ -7494,6 +7634,11 @@ memfs@^3.4.3: resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== +memoize-one@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045" + integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw== + merge-descriptors@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" @@ -7639,6 +7784,11 @@ nanoid@^3.3.3: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== +nanoid@^3.3.7: + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -7965,18 +8115,6 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse-entities@^1.1.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-1.2.2.tgz#c31bf0f653b6661354f8973559cb86dd1d5edf50" - integrity sha512-NzfpbxW/NPrzZ/yYSoQxyqUZMZXIdCfE0OIN4ESsnptHJECoUk3FZktxNuzQf4tjt5UEopnxpYJbvYuxIFDdsg== - dependencies: - character-entities "^1.0.0" - character-entities-legacy "^1.0.0" - character-reference-invalid "^1.0.0" - is-alphanumerical "^1.0.0" - is-decimal "^1.0.0" - is-hexadecimal "^1.0.0" - parse-entities@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-2.0.0.tgz#53c6eb5b9314a1f4ec99fa0fdf7ce01ecda0cbe8" @@ -8613,6 +8751,15 @@ postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0, postcss-value-parser@^ resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== +postcss@8.4.38: + version "8.4.38" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e" + integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== + dependencies: + nanoid "^3.3.7" + picocolors "^1.0.0" + source-map-js "^1.2.0" + postcss@^7.0.35: version "7.0.39" resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.39.tgz#9624375d965630e2e1f2c02a935c82a59cb48309" @@ -8672,18 +8819,11 @@ pretty-format@^28.1.0: ansi-styles "^5.0.0" react-is "^18.0.0" -prismjs@^1.8.4: +prismjs@^1.27.0: version "1.29.0" resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.29.0.tgz#f113555a8fa9b57c35e637bba27509dcf802dd12" integrity sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q== -prismjs@~1.17.0: - version "1.17.1" - resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.17.1.tgz#e669fcbd4cdd873c35102881c33b14d0d68519be" - integrity sha512-PrEDJAFdUGbOP6xK/UsfkC5ghJsPJviKgnQOoxaDbBjwc8op68Quupwt1DeAFoG8GImPhiKXAvvsH7wDSLsu1Q== - optionalDependencies: - clipboard "^2.0.0" - prismjs@~1.27.0: version "1.27.0" resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.27.0.tgz#bb6ee3138a0b438a3653dd4d6ce0cc6510a45057" @@ -8798,7 +8938,7 @@ quick-lru@^5.1.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== -raf-schd@^4.0.2: +raf-schd@^4.0.2, raf-schd@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a" integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ== @@ -8864,15 +9004,15 @@ react-clientside-effect@^1.2.6: dependencies: "@babel/runtime" "^7.12.13" -react-code-blocks@^0.0.9-0: - version "0.0.9-0" - resolved "https://registry.yarnpkg.com/react-code-blocks/-/react-code-blocks-0.0.9-0.tgz#0c6d04d8a40b74cffe95f24f1a8e62a0fda8c014" - integrity sha512-jdYJVZwGtsr6WIUaqILy5fkF1acf57YV5s0V3+w5o9v3omYnqBeO6EuZi1Vf2x1hahkYGEedsp46+ofdkYlqyw== +react-code-blocks@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/react-code-blocks/-/react-code-blocks-0.1.6.tgz#ec64e7899223d3e910eb916465a66d95ce1ae1b2" + integrity sha512-ENNuxG07yO+OuX1ChRje3ieefPRz6yrIpHmebQlaFQgzcAHbUfVeTINpOpoI9bSRSObeYo/OdHsporeToZ7fcg== dependencies: "@babel/runtime" "^7.10.4" - react-syntax-highlighter "^12.2.1" - styled-components "^5.1.1" - tslib "^2.0.0" + react-syntax-highlighter "^15.5.0" + styled-components "^6.1.0" + tslib "^2.6.0" react-dev-utils@^12.0.1: version "12.0.1" @@ -8913,7 +9053,7 @@ react-dom@^17.0.2: object-assign "^4.1.1" scheduler "^0.20.2" -react-dropzone@^11.5.3: +react-dropzone@^11.5.3, react-dropzone@^11.7.1: version "11.7.1" resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-11.7.1.tgz#3851bb75b26af0bf1b17ce1449fd980e643b9356" integrity sha512-zxCMwhfPy1olUEbw3FLNPLhAm/HnaYH5aELIEglRbqabizKAdHs0h+WuyOpmA+v1JXn0++fpQDdNfUagWt5hJQ== @@ -8931,11 +9071,32 @@ react-element-to-jsx-string@^14.3.4: is-plain-object "5.0.0" react-is "17.0.2" +react-element-to-jsx-string@^15.0.0: + version "15.0.0" + resolved "https://registry.yarnpkg.com/react-element-to-jsx-string/-/react-element-to-jsx-string-15.0.0.tgz#1cafd5b6ad41946ffc8755e254da3fc752a01ac6" + integrity sha512-UDg4lXB6BzlobN60P8fHWVPX3Kyw8ORrTeBtClmIlGdkOOE+GYQSFvmEU5iLLpwp/6v42DINwNcwOhOLfQ//FQ== + dependencies: + "@base2/pretty-print-object" "1.0.1" + is-plain-object "5.0.0" + react-is "18.1.0" + react-error-overlay@^6.0.11: version "6.0.11" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb" integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg== +react-focus-lock@^2.11.3: + version "2.13.2" + resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.13.2.tgz#e1addac2f8b9550bc0581f3c416755ba0f81f5ef" + integrity sha512-T/7bsofxYqnod2xadvuwjGKHOoL5GH7/EIPI5UyEvaU/c2CcphvGI371opFtuY/SYdbMsNiuF4HsHQ50nA/TKQ== + dependencies: + "@babel/runtime" "^7.0.0" + focus-lock "^1.3.5" + prop-types "^15.6.2" + react-clientside-effect "^1.2.6" + use-callback-ref "^1.3.2" + use-sidecar "^1.1.2" + react-focus-lock@^2.9.0: version "2.9.1" resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.9.1.tgz#094cfc19b4f334122c73bb0bff65d77a0c92dd16" @@ -8961,6 +9122,18 @@ react-focus-on@^3.5.4: use-callback-ref "^1.3.0" use-sidecar "^1.1.2" +react-focus-on@^3.9.1: + version "3.9.4" + resolved "https://registry.yarnpkg.com/react-focus-on/-/react-focus-on-3.9.4.tgz#0b6c13273d86243c330d1aa53af39290f543da7b" + integrity sha512-NFKmeH6++wu8e7LJcbwV8TTd4L5w/U5LMXTMOdUcXhCcZ7F5VOvgeTHd4XN1PD7TNmdvldDu/ENROOykUQ4yQg== + dependencies: + aria-hidden "^1.2.2" + react-focus-lock "^2.11.3" + react-remove-scroll "^2.6.0" + react-style-singleton "^2.2.1" + tslib "^2.3.1" + use-sidecar "^1.1.2" + react-input-autosize@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-3.0.0.tgz#6b5898c790d4478d69420b55441fcc31d5c50a85" @@ -8973,16 +9146,16 @@ react-is@17.0.2, react-is@^17.0.1, react-is@^17.0.2: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== +react-is@18.1.0, react-is@^18.0.0: + version "18.1.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.1.0.tgz#61aaed3096d30eacf2a2127118b5b41387d32a67" + integrity sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg== + react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -react-is@^18.0.0: - version "18.1.0" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.1.0.tgz#61aaed3096d30eacf2a2127118b5b41387d32a67" - integrity sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg== - react-query@^3.34.12: version "3.39.0" resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.39.0.tgz#0caca7b0da98e65008bbcd4df0d25618c2100050" @@ -8992,6 +9165,15 @@ react-query@^3.34.12: broadcast-channel "^3.4.1" match-sorter "^6.0.2" +react-query@^3.39.3: + version "3.39.3" + resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.39.3.tgz#4cea7127c6c26bdea2de5fb63e51044330b03f35" + integrity sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g== + dependencies: + "@babel/runtime" "^7.5.5" + broadcast-channel "^3.4.1" + match-sorter "^6.0.2" + react-redux@^7.2.0: version "7.2.8" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.8.tgz#a894068315e65de5b1b68899f9c6ee0923dd28de" @@ -9004,6 +9186,18 @@ react-redux@^7.2.0: prop-types "^15.7.2" react-is "^17.0.2" +react-redux@^8.1.3: + version "8.1.3" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.1.3.tgz#4fdc0462d0acb59af29a13c27ffef6f49ab4df46" + integrity sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw== + dependencies: + "@babel/runtime" "^7.12.1" + "@types/hoist-non-react-statics" "^3.3.1" + "@types/use-sync-external-store" "^0.0.3" + hoist-non-react-statics "^3.3.2" + react-is "^18.0.0" + use-sync-external-store "^1.0.0" + react-refresh@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046" @@ -9017,6 +9211,14 @@ react-remove-scroll-bar@^2.3.1: react-style-singleton "^2.2.0" tslib "^2.0.0" +react-remove-scroll-bar@^2.3.4, react-remove-scroll-bar@^2.3.6: + version "2.3.6" + resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz#3e585e9d163be84a010180b18721e851ac81a29c" + integrity sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g== + dependencies: + react-style-singleton "^2.2.1" + tslib "^2.0.0" + react-remove-scroll@^2.5.2: version "2.5.3" resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.5.3.tgz#a152196e710e8e5811be39dc352fd8a90b05c961" @@ -9028,7 +9230,18 @@ react-remove-scroll@^2.5.2: use-callback-ref "^1.3.0" use-sidecar "^1.1.2" -react-router-dom@6: +react-remove-scroll@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.6.0.tgz#fb03a0845d7768a4f1519a99fdb84983b793dc07" + integrity sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ== + dependencies: + react-remove-scroll-bar "^2.3.6" + react-style-singleton "^2.2.1" + tslib "^2.1.0" + use-callback-ref "^1.3.0" + use-sidecar "^1.1.2" + +react-router-dom@6, react-router-dom@<6.4.0: version "6.3.0" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.3.0.tgz#a0216da813454e521905b5fa55e0e5176123f43d" integrity sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw== @@ -9043,7 +9256,7 @@ react-router@6.3.0: dependencies: history "^5.2.0" -react-scripts@^5.0.0: +react-scripts@^5.0.0, react-scripts@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-5.0.1.tgz#6285dbd65a8ba6e49ca8d651ce30645a6d980003" integrity sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ== @@ -9107,22 +9320,45 @@ react-style-singleton@^2.2.0: invariant "^2.2.4" tslib "^2.0.0" -react-syntax-highlighter@^12.2.1: - version "12.2.1" - resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-12.2.1.tgz#14d78352da1c1c3f93c6698b70ec7c706b83493e" - integrity sha512-CTsp0ZWijwKRYFg9xhkWD4DSpQqE4vb2NKVMdPAkomnILSmsNBHE0n5GuI5zB+PU3ySVvXvdt9jo+ViD9XibCA== +react-style-singleton@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4" + integrity sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g== + dependencies: + get-nonce "^1.0.0" + invariant "^2.2.4" + tslib "^2.0.0" + +react-syntax-highlighter@^15.5.0: + version "15.6.1" + resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz#fa567cb0a9f96be7bbccf2c13a3c4b5657d9543e" + integrity sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg== dependencies: "@babel/runtime" "^7.3.1" - highlight.js "~9.15.1" - lowlight "1.12.1" - prismjs "^1.8.4" - refractor "^2.4.1" + highlight.js "^10.4.1" + highlightjs-vue "^1.0.0" + lowlight "^1.17.0" + prismjs "^1.27.0" + refractor "^3.6.0" + +react-virtualized-auto-sizer@^1.0.24: + version "1.0.24" + resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.24.tgz#3ebdc92f4b05ad65693b3cc8e7d8dd54924c0227" + integrity sha512-3kCn7N9NEb3FlvJrSHWGQ4iVl+ydQObq2fHMn12i5wbtm74zHOPhz/i64OL3c1S1vi9i2GXtZqNqUJTQ+BnNfg== react-virtualized-auto-sizer@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.6.tgz#66c5b1c9278064c5ef1699ed40a29c11518f97ca" integrity sha512-7tQ0BmZqfVF6YYEWcIGuoR3OdYe8I/ZFbNclFlGOC3pMqunkYF/oL30NCjSGl9sMEb17AnzixDz98Kqc3N76HQ== +react-window@^1.8.10: + version "1.8.10" + resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.10.tgz#9e6b08548316814b443f7002b1cf8fd3a1bdde03" + integrity sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg== + dependencies: + "@babel/runtime" "^7.0.0" + memoize-one ">=3.1.1 <6" + react-window@^1.8.6: version "1.8.7" resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.7.tgz#5e9fd0d23f48f432d7022cdb327219353a15f0d4" @@ -9190,16 +9426,14 @@ redux@^4.0.0, redux@^4.0.4: dependencies: "@babel/runtime" "^7.9.2" -refractor@^2.4.1: - version "2.10.1" - resolved "https://registry.yarnpkg.com/refractor/-/refractor-2.10.1.tgz#166c32f114ed16fd96190ad21d5193d3afc7d34e" - integrity sha512-Xh9o7hQiQlDbxo5/XkOX6H+x/q8rmlmZKr97Ie1Q8ZM32IRRd3B/UxuA/yXDW79DBSXGWxm2yRTbcTVmAciJRw== +redux@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197" + integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w== dependencies: - hastscript "^5.0.0" - parse-entities "^1.1.2" - prismjs "~1.17.0" + "@babel/runtime" "^7.9.2" -refractor@^3.5.0: +refractor@^3.5.0, refractor@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/refractor/-/refractor-3.6.0.tgz#ac318f5a0715ead790fcfb0c71f4dd83d977935a" integrity sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA== @@ -9230,6 +9464,11 @@ regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.9: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== +regenerator-runtime@^0.14.0: + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== + regenerator-transform@^0.15.0: version "0.15.0" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.0.tgz#cbd9ead5d77fae1a48d957cf889ad0586adb6537" @@ -9280,14 +9519,14 @@ regjsparser@^0.8.2: dependencies: jsesc "~0.5.0" -rehype-raw@^5.0.0: +rehype-raw@^5.0.0, rehype-raw@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/rehype-raw/-/rehype-raw-5.1.0.tgz#66d5e8d7188ada2d31bc137bc19a1000cf2c6b7e" integrity sha512-MDvHAb/5mUnif2R+0IPCYJU8WjHa9UzGtM/F4AVy5GixPlDZ1z3HacYy4xojDU+uBa+0X/3PIfyQI26/2ljJNA== dependencies: hast-util-raw "^6.1.0" -rehype-react@^6.0.0: +rehype-react@^6.0.0, rehype-react@^6.2.1: version "6.2.1" resolved "https://registry.yarnpkg.com/rehype-react/-/rehype-react-6.2.1.tgz#9b9bf188451ad6f63796b784fe1f51165c67b73a" integrity sha512-f9KIrjktvLvmbGc7si25HepocOg4z0MuNOtweigKzBcDjiGSTGhyz6VSgaV5K421Cq1O+z4/oxRJ5G9owo0KVg== @@ -9323,6 +9562,27 @@ remark-emoji@^2.1.0: node-emoji "^1.10.0" unist-util-visit "^2.0.3" +remark-parse-no-trim@^8.0.4: + version "8.0.4" + resolved "https://registry.yarnpkg.com/remark-parse-no-trim/-/remark-parse-no-trim-8.0.4.tgz#f5c9531644284071d4a57a49e19a42ad4e8040bd" + integrity sha512-WtqeHNTZ0LSdyemmY1/G6y9WoEFblTtgckfKF5/NUnri919/0/dEu8RCDfvXtJvu96soMvT+mLWWgYVUaiHoag== + dependencies: + ccount "^1.0.0" + collapse-white-space "^1.0.2" + is-alphabetical "^1.0.0" + is-decimal "^1.0.0" + is-whitespace-character "^1.0.0" + is-word-character "^1.0.0" + markdown-escapes "^1.0.0" + parse-entities "^2.0.0" + repeat-string "^1.5.4" + state-toggle "^1.0.0" + trim-trailing-lines "^1.0.0" + unherit "^1.0.4" + unist-util-remove-position "^2.0.0" + vfile-location "^3.0.0" + xtend "^4.0.1" + remark-parse@^8.0.3: version "8.0.3" resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-8.0.3.tgz#9c62aa3b35b79a486454c690472906075f40c7e1" @@ -9345,7 +9605,7 @@ remark-parse@^8.0.3: vfile-location "^3.0.0" xtend "^4.0.1" -remark-rehype@^8.0.0: +remark-rehype@^8.0.0, remark-rehype@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/remark-rehype/-/remark-rehype-8.1.0.tgz#610509a043484c1e697437fa5eb3fd992617c945" integrity sha512-EbCu9kHgAxKmW1yEYjx3QafMyGY3q8noUbNUI5xyKbaFP89wbhDrKxyIQNukNYthzjNHZu6J7hwFg7hRm1svYA== @@ -9588,11 +9848,6 @@ select-hose@^2.0.0: resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= -select@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" - integrity sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA== - selfsigned@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.0.1.tgz#8b2df7fa56bf014d19b6007655fff209c0ef0a56" @@ -9707,7 +9962,7 @@ setprototypeof@1.2.0: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== -shallowequal@^1.1.0: +shallowequal@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== @@ -9787,6 +10042,11 @@ source-map-js@^1.0.1, source-map-js@^1.0.2: resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== +source-map-js@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + source-map-loader@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-3.0.1.tgz#9ae5edc7c2d42570934be4c95d1ccc6352eba52d" @@ -10066,21 +10326,20 @@ style-to-object@^0.3.0: dependencies: inline-style-parser "0.1.1" -styled-components@^5.1.1: - version "5.3.11" - resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-5.3.11.tgz#9fda7bf1108e39bf3f3e612fcc18170dedcd57a8" - integrity sha512-uuzIIfnVkagcVHv9nE0VPlHPSCmXIUGKfJ42LNjxCCTDTL5sgnJ8Z7GZBq0EnLYGln77tPpEpExt2+qa+cZqSw== - dependencies: - "@babel/helper-module-imports" "^7.0.0" - "@babel/traverse" "^7.4.5" - "@emotion/is-prop-valid" "^1.1.0" - "@emotion/stylis" "^0.8.4" - "@emotion/unitless" "^0.7.4" - babel-plugin-styled-components ">= 1.12.0" - css-to-react-native "^3.0.0" - hoist-non-react-statics "^3.0.0" - shallowequal "^1.1.0" - supports-color "^5.5.0" +styled-components@^6.1.0: + version "6.1.13" + resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-6.1.13.tgz#2d777750b773b31469bd79df754a32479e9f475e" + integrity sha512-M0+N2xSnAtwcVAQeFEsGWFFxXDftHUD7XrKla06QbpUMmbmtFBMMTcKWvFXtWxuD5qQkB8iU5gk6QASlx2ZRMw== + dependencies: + "@emotion/is-prop-valid" "1.2.2" + "@emotion/unitless" "0.8.1" + "@types/stylis" "4.2.5" + css-to-react-native "3.2.0" + csstype "3.1.3" + postcss "8.4.38" + shallowequal "1.1.0" + stylis "4.3.2" + tslib "2.6.2" stylehacks@^5.1.0: version "5.1.0" @@ -10095,7 +10354,17 @@ stylis@4.0.13: resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.13.tgz#f5db332e376d13cc84ecfe5dace9a2a51d954c91" integrity sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag== -supports-color@^5.3.0, supports-color@^5.5.0: +stylis@4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51" + integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw== + +stylis@4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.3.2.tgz#8f76b70777dd53eb669c6f58c997bf0a9972e444" + integrity sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg== + +supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== @@ -10176,6 +10445,11 @@ tabbable@^5.2.1: resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-5.3.2.tgz#66d6119ee8a533634c3f17deb0caa1c379e36ac7" integrity sha512-6G/8EWRFx8CiSe2++/xHhXkmCRq2rHtDtZbQFHx34cvDfZzIBfvwG9zGUNTWMXWLCYvDj3aQqOzdl3oCxKuBkQ== +tabbable@^5.3.3: + version "5.3.3" + resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-5.3.3.tgz#aac0ff88c73b22d6c3c5a50b1586310006b47fbf" + integrity sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA== + tailwindcss@^3.0.2: version "3.0.24" resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.0.24.tgz#22e31e801a44a78a1d9a81ecc52e13b69d85704d" @@ -10307,11 +10581,6 @@ thunky@^1.0.2: resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== -tiny-emitter@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423" - integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q== - tiny-invariant@^1.0.6: version "1.2.0" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.2.0.tgz#a1141f86b672a9148c72e978a19a73b9b94a15a9" @@ -10393,6 +10662,11 @@ tsconfig-paths@^3.14.1: minimist "^1.2.6" strip-bom "^3.0.0" +tslib@2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + tslib@^1.0.0, tslib@^1.8.1, tslib@^1.9.3: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" @@ -10403,6 +10677,11 @@ tslib@^2.0.0, tslib@^2.0.3, tslib@^2.3.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== +tslib@^2.1.0, tslib@^2.6.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.0.tgz#d124c86c3c05a40a91e6fdea4021bd31d377971b" + integrity sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" @@ -10505,7 +10784,7 @@ unicode-property-aliases-ecmascript@^2.0.0: resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz#0a36cb9a585c4f6abd51ad1deddb285c165297c8" integrity sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ== -unified@^9.2.0: +unified@^9.2.0, unified@^9.2.2: version "9.2.2" resolved "https://registry.yarnpkg.com/unified/-/unified-9.2.2.tgz#67649a1abfc3ab85d2969502902775eb03146975" integrity sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ== @@ -10664,11 +10943,23 @@ use-callback-ref@^1.3.0: dependencies: tslib "^2.0.0" +use-callback-ref@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.2.tgz#6134c7f6ff76e2be0b56c809b17a650c942b1693" + integrity sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA== + dependencies: + tslib "^2.0.0" + use-memo-one@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.2.tgz#0c8203a329f76e040047a35a1197defe342fab20" integrity sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ== +use-memo-one@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.3.tgz#2fd2e43a2169eabc7496960ace8c79efef975e99" + integrity sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ== + use-query-params@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/use-query-params/-/use-query-params-1.2.3.tgz#306c31a0cbc714e8a3b4bd7e91a6a9aaccaa5e22" @@ -10684,6 +10975,11 @@ use-sidecar@^1.1.2: detect-node-es "^1.1.0" tslib "^2.0.0" +use-sync-external-store@^1.0.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9" + integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw== + util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -10754,7 +11050,7 @@ vfile-message@^2.0.0: "@types/unist" "^2.0.0" unist-util-stringify-position "^2.0.0" -vfile@^4.0.0, vfile@^4.2.0: +vfile@^4.0.0, vfile@^4.2.0, vfile@^4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/vfile/-/vfile-4.2.1.tgz#03f1dce28fc625c625bc6514350fbdb00fa9e624" integrity sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA== diff --git a/ui/package.json b/ui/package.json index 9a1876809b..b9371f42f1 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "@feast-dev/feast-ui", - "version": "0.40.0", + "version": "0.41.0", "private": false, "files": [ "dist"

+ + + + + Star History Chart + + +

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 bzP0I>->T#r^_swjQTT}weHo<_rfm5KN?SH( literal 0 HcmV?d00001 From 5a09a7e4e11d533cfeaf21f553d086573e05f597 Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Sat, 21 Sep 2024 07:49:37 -0400 Subject: [PATCH 086/185] chore: Adding unit test to test feature view dummy entity serialization after apply() (#4553) * chore: Adding unit test to test feature view dummy entity serialization after apply() Signed-off-by: Francisco Javier Arceo * updated Signed-off-by: Francisco Javier Arceo --------- Signed-off-by: Francisco Javier Arceo --- .../test_local_feature_store.py | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/sdk/python/tests/unit/local_feast_tests/test_local_feature_store.py b/sdk/python/tests/unit/local_feast_tests/test_local_feature_store.py index c86441d56c..5ed16a8430 100644 --- a/sdk/python/tests/unit/local_feast_tests/test_local_feature_store.py +++ b/sdk/python/tests/unit/local_feast_tests/test_local_feature_store.py @@ -11,7 +11,7 @@ from feast.entity import Entity from feast.feast_object import ALL_RESOURCE_TYPES from feast.feature_store import FeatureStore -from feast.feature_view import FeatureView +from feast.feature_view import DUMMY_ENTITY_ID, FeatureView from feast.field import Field from feast.infra.offline_stores.file_source import FileSource from feast.infra.online_stores.sqlite import SqliteOnlineStoreConfig @@ -342,6 +342,51 @@ def test_apply_entities_and_feature_views(test_feature_store): test_feature_store.teardown() +@pytest.mark.parametrize( + "test_feature_store", + [lazy_fixture("feature_store_with_local_registry")], +) +def test_apply_dummuy_entity_and_feature_view_columns(test_feature_store): + assert isinstance(test_feature_store, FeatureStore) + # Create Feature Views + batch_source = FileSource( + file_format=ParquetFormat(), + path="file://feast/*", + timestamp_field="ts_col", + created_timestamp_column="timestamp", + ) + + e1 = Entity(name="fs1_my_entity_1", description="something") + + fv = FeatureView( + name="my_feature_view_no_entity", + schema=[ + Field(name="fs1_my_feature_1", dtype=Int64), + Field(name="fs1_my_feature_2", dtype=String), + Field(name="fs1_my_feature_3", dtype=Array(String)), + Field(name="fs1_my_feature_4", dtype=Array(Bytes)), + Field(name="fs1_my_entity_2", dtype=Int64), + ], + entities=[], + tags={"team": "matchmaking"}, + source=batch_source, + ttl=timedelta(minutes=5), + ) + + # Check that the entity_columns are empty before applying + assert fv.entity_columns == [] + + # Register Feature View + test_feature_store.apply([fv, e1]) + fv_actual = test_feature_store.get_feature_view("my_feature_view_no_entity") + + # Note that after the apply() the feature_view serializes the Dummy Entity ID + assert fv.entity_columns[0].name == DUMMY_ENTITY_ID + assert fv_actual.entity_columns[0].name == DUMMY_ENTITY_ID + + test_feature_store.teardown() + + @pytest.mark.parametrize( "test_feature_store", [lazy_fixture("feature_store_with_local_registry")], From 334e5d78855709d4ca56619f16eecb414f88ce2d Mon Sep 17 00:00:00 2001 From: Harri Lehtola <1781172+peruukki@users.noreply.github.com> Date: Sat, 21 Sep 2024 16:55:37 +0300 Subject: [PATCH 087/185] feat: Publish TypeScript types in Feast UI package (#4551) --- ui/package.json | 5 +++-- ui/tsconfig.build-lib.json | 11 +++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 ui/tsconfig.build-lib.json diff --git a/ui/package.json b/ui/package.json index 3a609f3c83..c104c00ae5 100644 --- a/ui/package.json +++ b/ui/package.json @@ -6,6 +6,7 @@ "dist" ], "main": "./dist/feast-ui.cjs", + "types": "./dist/FeastUI.d.ts", "module": "./dist/feast-ui.module.js", "peerDependencies": { "@elastic/datemath": "^5.0.3", @@ -54,8 +55,8 @@ "scripts": { "start": "npm run generate-protos && react-scripts start", "build": "npm run generate-protos && react-scripts build", - "build:lib": "npm run generate-protos && rimraf ./dist && tsc && rollup -c", - "build:lib-dev": "npm run generate-protos && rimraf ./dist && tsc && rollup -c && yalc publish -f", + "build:lib": "npm run generate-protos && rimraf ./dist && tsc --project ./tsconfig.build-lib.json && rollup -c", + "build:lib-dev": "npm run build:lib && yalc publish -f", "test": "npm run generate-protos && react-scripts test", "eject": "react-scripts eject", "generate-protos": "pbjs --no-encode -o src/protos.js -w commonjs -t static-module `find ../protos/feast/ -iname *.proto` && pbts -n protos -o src/protos.d.ts src/protos.js" diff --git a/ui/tsconfig.build-lib.json b/ui/tsconfig.build-lib.json new file mode 100644 index 0000000000..c29bd063f0 --- /dev/null +++ b/ui/tsconfig.build-lib.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "noEmit": false, + "outDir": "./dist", + "rootDir": "./src" + } +} From 351a2d0a7f9808178ab9d201083eb2894ce7384f Mon Sep 17 00:00:00 2001 From: Bhargav Dodla <13788369+EXPEbdodla@users.noreply.github.com> Date: Sat, 21 Sep 2024 15:15:20 -0700 Subject: [PATCH 088/185] fix: Deleting data from feast_metadata when we delete project (#4550) * fix: Deleting data from feast_metadata when we delete project Signed-off-by: Bhargav Dodla * fix: Deleting for snowflake Signed-off-by: Bhargav Dodla --------- Signed-off-by: Bhargav Dodla Co-authored-by: Bhargav Dodla --- sdk/python/feast/infra/registry/snowflake.py | 1 + sdk/python/feast/infra/registry/sql.py | 1 + 2 files changed, 2 insertions(+) diff --git a/sdk/python/feast/infra/registry/snowflake.py b/sdk/python/feast/infra/registry/snowflake.py index f9dd37e516..e68d9d64b5 100644 --- a/sdk/python/feast/infra/registry/snowflake.py +++ b/sdk/python/feast/infra/registry/snowflake.py @@ -1303,6 +1303,7 @@ def delete_project( "DATA_SOURCES", "ENTITIES", "PERMISSIONS", + "FEAST_METADATA", "PROJECTS", }: query = f""" diff --git a/sdk/python/feast/infra/registry/sql.py b/sdk/python/feast/infra/registry/sql.py index a6a2417c6e..6ae27acf4e 100644 --- a/sdk/python/feast/infra/registry/sql.py +++ b/sdk/python/feast/infra/registry/sql.py @@ -1237,6 +1237,7 @@ def delete_project( data_sources, entities, permissions, + feast_metadata, projects, }: stmt = delete(t).where(t.c.project_id == name) From e781e1652cadc6576dbab369248d6e4afdb5f158 Mon Sep 17 00:00:00 2001 From: Harri Lehtola <1781172+peruukki@users.noreply.github.com> Date: Sun, 22 Sep 2024 06:44:45 +0300 Subject: [PATCH 089/185] fix: Remove Feast UI TypeScript dependencies from `peerDependencies` and `dependencies` (#4554) --- ui/package.json | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/ui/package.json b/ui/package.json index c104c00ae5..978be97b88 100644 --- a/ui/package.json +++ b/ui/package.json @@ -12,9 +12,6 @@ "@elastic/datemath": "^5.0.3", "@elastic/eui": "^55.0.1", "@emotion/react": "^11.7.1", - "@types/d3": "^7.1.0", - "@types/react": "^17.0.20", - "@types/react-dom": "^17.0.9", "d3": "^7.3.0", "inter-ui": "^3.19.3", "moment": "^2.29.1", @@ -25,7 +22,6 @@ "react-query": "^3.34.12", "react-router-dom": "6", "react-scripts": "^5.0.0", - "typescript": "^4.4.2", "use-query-params": "^1.2.3", "zod": "^3.11.6" }, @@ -33,11 +29,6 @@ "@elastic/datemath": "^5.0.3", "@elastic/eui": "^55.0.1", "@emotion/react": "^11.7.1", - "@types/d3": "^7.1.0", - "@types/jest": "^27.0.1", - "@types/node": "^16.7.13", - "@types/react": "^17.0.20", - "@types/react-dom": "^17.0.9", "d3": "^7.3.0", "inter-ui": "^3.19.3", "moment": "^2.29.1", @@ -96,6 +87,11 @@ "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^12.0.0", "@testing-library/user-event": "^13.2.1", + "@types/d3": "^7.1.0", + "@types/jest": "^27.0.1", + "@types/node": "^16.7.13", + "@types/react": "^17.0.20", + "@types/react-dom": "^17.0.9", "msw": "^0.36.8", "protobufjs-cli": "^1.0.2", "react": "^17.0.2", From fcd1bd4c383ffa2794912fa93ec783baced85dea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 21 Sep 2024 23:54:36 -0400 Subject: [PATCH 090/185] chore: Bump gopkg.in/yaml.v3 from 3.0.0-20200313102051-9f266ea9e77c to 3.0.0 (#3752) chore: Bump gopkg.in/yaml.v3 Bumps gopkg.in/yaml.v3 from 3.0.0-20200313102051-9f266ea9e77c to 3.0.0. --- updated-dependencies: - dependency-name: gopkg.in/yaml.v3 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 0f73328c72..61063a0cda 100644 --- a/go.mod +++ b/go.mod @@ -46,5 +46,5 @@ require ( google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect + gopkg.in/yaml.v3 v3.0.0 // indirect ) diff --git a/go.sum b/go.sum index a793b09aec..83bbc041c5 100644 --- a/go.sum +++ b/go.sum @@ -1854,8 +1854,9 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= From 00910bcdc8f642cfe83778b5ee22db97470e29ee Mon Sep 17 00:00:00 2001 From: Tornike Gurgenidze Date: Sun, 22 Sep 2024 20:46:10 +0400 Subject: [PATCH 091/185] chore: Commit generated python proto files (#4546) * chore: commit generated python files to repo Signed-off-by: tokoko * merge from master Signed-off-by: tokoko * chore: remove protos from gitignore Signed-off-by: tokoko --------- Signed-off-by: tokoko --- .gitignore | 1 - Makefile | 13 +- infra/scripts/generate_protos.py | 80 + pyproject.toml | 3 - sdk/python/feast/protos/__init__.py | 0 sdk/python/feast/protos/feast/__init__.py | 0 .../protos/feast/core/Aggregation_pb2.py | 28 + .../protos/feast/core/Aggregation_pb2.pyi | 42 + .../protos/feast/core/Aggregation_pb2_grpc.py | 4 + .../feast/protos/feast/core/DataFormat_pb2.py | 39 + .../protos/feast/core/DataFormat_pb2.pyi | 143 ++ .../protos/feast/core/DataFormat_pb2_grpc.py | 4 + .../feast/protos/feast/core/DataSource_pb2.py | 72 + .../protos/feast/core/DataSource_pb2.pyi | 559 ++++++ .../protos/feast/core/DataSource_pb2_grpc.py | 4 + .../protos/feast/core/DatastoreTable_pb2.py | 28 + .../protos/feast/core/DatastoreTable_pb2.pyi | 67 + .../feast/core/DatastoreTable_pb2_grpc.py | 4 + .../protos/feast/core/DynamoDBTable_pb2.py | 27 + .../protos/feast/core/DynamoDBTable_pb2.pyi | 50 + .../feast/core/DynamoDBTable_pb2_grpc.py | 4 + .../feast/protos/feast/core/Entity_pb2.py | 37 + .../feast/protos/feast/core/Entity_pb2.pyi | 130 ++ .../protos/feast/core/Entity_pb2_grpc.py | 4 + .../protos/feast/core/FeatureService_pb2.py | 55 + .../protos/feast/core/FeatureService_pb2.pyi | 266 +++ .../feast/core/FeatureService_pb2_grpc.py | 4 + .../protos/feast/core/FeatureTable_pb2.py | 39 + .../protos/feast/core/FeatureTable_pb2.pyi | 166 ++ .../feast/core/FeatureTable_pb2_grpc.py | 4 + .../feast/core/FeatureViewProjection_pb2.py | 32 + .../feast/core/FeatureViewProjection_pb2.pyi | 66 + .../core/FeatureViewProjection_pb2_grpc.py | 4 + .../protos/feast/core/FeatureView_pb2.py | 41 + .../protos/feast/core/FeatureView_pb2.pyi | 194 +++ .../protos/feast/core/FeatureView_pb2_grpc.py | 4 + .../feast/protos/feast/core/Feature_pb2.py | 32 + .../feast/protos/feast/core/Feature_pb2.pyi | 75 + .../protos/feast/core/Feature_pb2_grpc.py | 4 + .../protos/feast/core/InfraObject_pb2.py | 34 + .../protos/feast/core/InfraObject_pb2.pyi | 101 ++ .../protos/feast/core/InfraObject_pb2_grpc.py | 4 + .../feast/core/OnDemandFeatureView_pb2.py | 53 + .../feast/core/OnDemandFeatureView_pb2.pyi | 219 +++ .../core/OnDemandFeatureView_pb2_grpc.py | 4 + .../feast/protos/feast/core/Permission_pb2.py | 45 + .../protos/feast/core/Permission_pb2.pyi | 195 +++ .../protos/feast/core/Permission_pb2_grpc.py | 4 + .../feast/protos/feast/core/Policy_pb2.py | 29 + .../feast/protos/feast/core/Policy_pb2.pyi | 58 + .../protos/feast/core/Policy_pb2_grpc.py | 4 + .../feast/protos/feast/core/Project_pb2.py | 36 + .../feast/protos/feast/core/Project_pb2.pyi | 119 ++ .../protos/feast/core/Project_pb2_grpc.py | 4 + .../feast/protos/feast/core/Registry_pb2.py | 44 + .../feast/protos/feast/core/Registry_pb2.pyi | 140 ++ .../protos/feast/core/Registry_pb2_grpc.py | 4 + .../protos/feast/core/SavedDataset_pb2.py | 39 + .../protos/feast/core/SavedDataset_pb2.pyi | 192 ++ .../feast/core/SavedDataset_pb2_grpc.py | 4 + .../protos/feast/core/SqliteTable_pb2.py | 27 + .../protos/feast/core/SqliteTable_pb2.pyi | 50 + .../protos/feast/core/SqliteTable_pb2_grpc.py | 4 + .../feast/protos/feast/core/Store_pb2.py | 37 + .../feast/protos/feast/core/Store_pb2.pyi | 234 +++ .../feast/protos/feast/core/Store_pb2_grpc.py | 4 + .../feast/core/StreamFeatureView_pb2.py | 42 + .../feast/core/StreamFeatureView_pb2.pyi | 170 ++ .../feast/core/StreamFeatureView_pb2_grpc.py | 4 + .../protos/feast/core/Transformation_pb2.py | 31 + .../protos/feast/core/Transformation_pb2.pyi | 80 + .../feast/core/Transformation_pb2_grpc.py | 4 + .../feast/core/ValidationProfile_pb2.py | 37 + .../feast/core/ValidationProfile_pb2.pyi | 136 ++ .../feast/core/ValidationProfile_pb2_grpc.py | 4 + .../feast/protos/feast/core/__init__.py | 0 .../feast/registry/RegistryServer_pb2.py | 198 +++ .../feast/registry/RegistryServer_pb2.pyi | 1318 ++++++++++++++ .../feast/registry/RegistryServer_pb2_grpc.py | 1542 +++++++++++++++++ .../feast/protos/feast/registry/__init__.py | 0 .../protos/feast/serving/Connector_pb2.py | 39 + .../protos/feast/serving/Connector_pb2.pyi | 97 ++ .../feast/serving/Connector_pb2_grpc.py | 66 + .../protos/feast/serving/GrpcServer_pb2.py | 43 + .../protos/feast/serving/GrpcServer_pb2.pyi | 120 ++ .../feast/serving/GrpcServer_pb2_grpc.py | 133 ++ .../feast/serving/ServingService_pb2.py | 63 + .../feast/serving/ServingService_pb2.pyi | 347 ++++ .../feast/serving/ServingService_pb2_grpc.py | 101 ++ .../serving/TransformationService_pb2.py | 39 + .../serving/TransformationService_pb2.pyi | 136 ++ .../serving/TransformationService_pb2_grpc.py | 99 ++ .../feast/protos/feast/serving/__init__.py | 0 .../feast/protos/feast/storage/Redis_pb2.py | 28 + .../feast/protos/feast/storage/Redis_pb2.pyi | 54 + .../protos/feast/storage/Redis_pb2_grpc.py | 4 + .../feast/protos/feast/storage/__init__.py | 0 .../feast/protos/feast/types/EntityKey_pb2.py | 28 + .../protos/feast/types/EntityKey_pb2.pyi | 51 + .../protos/feast/types/EntityKey_pb2_grpc.py | 4 + .../feast/protos/feast/types/Field_pb2.py | 32 + .../feast/protos/feast/types/Field_pb2.pyi | 73 + .../protos/feast/types/Field_pb2_grpc.py | 4 + .../feast/protos/feast/types/Value_pb2.py | 49 + .../feast/protos/feast/types/Value_pb2.pyi | 296 ++++ .../protos/feast/types/Value_pb2_grpc.py | 4 + .../feast/protos/feast/types/__init__.py | 0 .../requirements/py3.10-ci-requirements.txt | 163 +- .../requirements/py3.10-requirements.txt | 39 +- .../requirements/py3.11-ci-requirements.txt | 165 +- .../requirements/py3.11-requirements.txt | 39 +- .../requirements/py3.9-ci-requirements.txt | 167 +- .../requirements/py3.9-requirements.txt | 39 +- setup.py | 128 +- 114 files changed, 9568 insertions(+), 665 deletions(-) create mode 100644 infra/scripts/generate_protos.py create mode 100644 sdk/python/feast/protos/__init__.py create mode 100644 sdk/python/feast/protos/feast/__init__.py create mode 100644 sdk/python/feast/protos/feast/core/Aggregation_pb2.py create mode 100644 sdk/python/feast/protos/feast/core/Aggregation_pb2.pyi create mode 100644 sdk/python/feast/protos/feast/core/Aggregation_pb2_grpc.py create mode 100644 sdk/python/feast/protos/feast/core/DataFormat_pb2.py create mode 100644 sdk/python/feast/protos/feast/core/DataFormat_pb2.pyi create mode 100644 sdk/python/feast/protos/feast/core/DataFormat_pb2_grpc.py create mode 100644 sdk/python/feast/protos/feast/core/DataSource_pb2.py create mode 100644 sdk/python/feast/protos/feast/core/DataSource_pb2.pyi create mode 100644 sdk/python/feast/protos/feast/core/DataSource_pb2_grpc.py create mode 100644 sdk/python/feast/protos/feast/core/DatastoreTable_pb2.py create mode 100644 sdk/python/feast/protos/feast/core/DatastoreTable_pb2.pyi create mode 100644 sdk/python/feast/protos/feast/core/DatastoreTable_pb2_grpc.py create mode 100644 sdk/python/feast/protos/feast/core/DynamoDBTable_pb2.py create mode 100644 sdk/python/feast/protos/feast/core/DynamoDBTable_pb2.pyi create mode 100644 sdk/python/feast/protos/feast/core/DynamoDBTable_pb2_grpc.py create mode 100644 sdk/python/feast/protos/feast/core/Entity_pb2.py create mode 100644 sdk/python/feast/protos/feast/core/Entity_pb2.pyi create mode 100644 sdk/python/feast/protos/feast/core/Entity_pb2_grpc.py create mode 100644 sdk/python/feast/protos/feast/core/FeatureService_pb2.py create mode 100644 sdk/python/feast/protos/feast/core/FeatureService_pb2.pyi create mode 100644 sdk/python/feast/protos/feast/core/FeatureService_pb2_grpc.py create mode 100644 sdk/python/feast/protos/feast/core/FeatureTable_pb2.py create mode 100644 sdk/python/feast/protos/feast/core/FeatureTable_pb2.pyi create mode 100644 sdk/python/feast/protos/feast/core/FeatureTable_pb2_grpc.py create mode 100644 sdk/python/feast/protos/feast/core/FeatureViewProjection_pb2.py create mode 100644 sdk/python/feast/protos/feast/core/FeatureViewProjection_pb2.pyi create mode 100644 sdk/python/feast/protos/feast/core/FeatureViewProjection_pb2_grpc.py create mode 100644 sdk/python/feast/protos/feast/core/FeatureView_pb2.py create mode 100644 sdk/python/feast/protos/feast/core/FeatureView_pb2.pyi create mode 100644 sdk/python/feast/protos/feast/core/FeatureView_pb2_grpc.py create mode 100644 sdk/python/feast/protos/feast/core/Feature_pb2.py create mode 100644 sdk/python/feast/protos/feast/core/Feature_pb2.pyi create mode 100644 sdk/python/feast/protos/feast/core/Feature_pb2_grpc.py create mode 100644 sdk/python/feast/protos/feast/core/InfraObject_pb2.py create mode 100644 sdk/python/feast/protos/feast/core/InfraObject_pb2.pyi create mode 100644 sdk/python/feast/protos/feast/core/InfraObject_pb2_grpc.py create mode 100644 sdk/python/feast/protos/feast/core/OnDemandFeatureView_pb2.py create mode 100644 sdk/python/feast/protos/feast/core/OnDemandFeatureView_pb2.pyi create mode 100644 sdk/python/feast/protos/feast/core/OnDemandFeatureView_pb2_grpc.py create mode 100644 sdk/python/feast/protos/feast/core/Permission_pb2.py create mode 100644 sdk/python/feast/protos/feast/core/Permission_pb2.pyi create mode 100644 sdk/python/feast/protos/feast/core/Permission_pb2_grpc.py create mode 100644 sdk/python/feast/protos/feast/core/Policy_pb2.py create mode 100644 sdk/python/feast/protos/feast/core/Policy_pb2.pyi create mode 100644 sdk/python/feast/protos/feast/core/Policy_pb2_grpc.py create mode 100644 sdk/python/feast/protos/feast/core/Project_pb2.py create mode 100644 sdk/python/feast/protos/feast/core/Project_pb2.pyi create mode 100644 sdk/python/feast/protos/feast/core/Project_pb2_grpc.py create mode 100644 sdk/python/feast/protos/feast/core/Registry_pb2.py create mode 100644 sdk/python/feast/protos/feast/core/Registry_pb2.pyi create mode 100644 sdk/python/feast/protos/feast/core/Registry_pb2_grpc.py create mode 100644 sdk/python/feast/protos/feast/core/SavedDataset_pb2.py create mode 100644 sdk/python/feast/protos/feast/core/SavedDataset_pb2.pyi create mode 100644 sdk/python/feast/protos/feast/core/SavedDataset_pb2_grpc.py create mode 100644 sdk/python/feast/protos/feast/core/SqliteTable_pb2.py create mode 100644 sdk/python/feast/protos/feast/core/SqliteTable_pb2.pyi create mode 100644 sdk/python/feast/protos/feast/core/SqliteTable_pb2_grpc.py create mode 100644 sdk/python/feast/protos/feast/core/Store_pb2.py create mode 100644 sdk/python/feast/protos/feast/core/Store_pb2.pyi create mode 100644 sdk/python/feast/protos/feast/core/Store_pb2_grpc.py create mode 100644 sdk/python/feast/protos/feast/core/StreamFeatureView_pb2.py create mode 100644 sdk/python/feast/protos/feast/core/StreamFeatureView_pb2.pyi create mode 100644 sdk/python/feast/protos/feast/core/StreamFeatureView_pb2_grpc.py create mode 100644 sdk/python/feast/protos/feast/core/Transformation_pb2.py create mode 100644 sdk/python/feast/protos/feast/core/Transformation_pb2.pyi create mode 100644 sdk/python/feast/protos/feast/core/Transformation_pb2_grpc.py create mode 100644 sdk/python/feast/protos/feast/core/ValidationProfile_pb2.py create mode 100644 sdk/python/feast/protos/feast/core/ValidationProfile_pb2.pyi create mode 100644 sdk/python/feast/protos/feast/core/ValidationProfile_pb2_grpc.py create mode 100644 sdk/python/feast/protos/feast/core/__init__.py create mode 100644 sdk/python/feast/protos/feast/registry/RegistryServer_pb2.py create mode 100644 sdk/python/feast/protos/feast/registry/RegistryServer_pb2.pyi create mode 100644 sdk/python/feast/protos/feast/registry/RegistryServer_pb2_grpc.py create mode 100644 sdk/python/feast/protos/feast/registry/__init__.py create mode 100644 sdk/python/feast/protos/feast/serving/Connector_pb2.py create mode 100644 sdk/python/feast/protos/feast/serving/Connector_pb2.pyi create mode 100644 sdk/python/feast/protos/feast/serving/Connector_pb2_grpc.py create mode 100644 sdk/python/feast/protos/feast/serving/GrpcServer_pb2.py create mode 100644 sdk/python/feast/protos/feast/serving/GrpcServer_pb2.pyi create mode 100644 sdk/python/feast/protos/feast/serving/GrpcServer_pb2_grpc.py create mode 100644 sdk/python/feast/protos/feast/serving/ServingService_pb2.py create mode 100644 sdk/python/feast/protos/feast/serving/ServingService_pb2.pyi create mode 100644 sdk/python/feast/protos/feast/serving/ServingService_pb2_grpc.py create mode 100644 sdk/python/feast/protos/feast/serving/TransformationService_pb2.py create mode 100644 sdk/python/feast/protos/feast/serving/TransformationService_pb2.pyi create mode 100644 sdk/python/feast/protos/feast/serving/TransformationService_pb2_grpc.py create mode 100644 sdk/python/feast/protos/feast/serving/__init__.py create mode 100644 sdk/python/feast/protos/feast/storage/Redis_pb2.py create mode 100644 sdk/python/feast/protos/feast/storage/Redis_pb2.pyi create mode 100644 sdk/python/feast/protos/feast/storage/Redis_pb2_grpc.py create mode 100644 sdk/python/feast/protos/feast/storage/__init__.py create mode 100644 sdk/python/feast/protos/feast/types/EntityKey_pb2.py create mode 100644 sdk/python/feast/protos/feast/types/EntityKey_pb2.pyi create mode 100644 sdk/python/feast/protos/feast/types/EntityKey_pb2_grpc.py create mode 100644 sdk/python/feast/protos/feast/types/Field_pb2.py create mode 100644 sdk/python/feast/protos/feast/types/Field_pb2.pyi create mode 100644 sdk/python/feast/protos/feast/types/Field_pb2_grpc.py create mode 100644 sdk/python/feast/protos/feast/types/Value_pb2.py create mode 100644 sdk/python/feast/protos/feast/types/Value_pb2.pyi create mode 100644 sdk/python/feast/protos/feast/types/Value_pb2_grpc.py create mode 100644 sdk/python/feast/protos/feast/types/__init__.py diff --git a/.gitignore b/.gitignore index e4e82bfce4..d558463c65 100644 --- a/.gitignore +++ b/.gitignore @@ -185,7 +185,6 @@ dmypy.json # Protos sdk/python/docs/html -sdk/python/feast/protos/ sdk/go/protos/ go/protos/ diff --git a/Makefile b/Makefile index 8a9f643967..f4b34124f7 100644 --- a/Makefile +++ b/Makefile @@ -40,29 +40,20 @@ build: protos build-java build-docker install-python-ci-dependencies: python -m piptools sync sdk/python/requirements/py$(PYTHON_VERSION)-ci-requirements.txt pip install --no-deps -e . - python setup.py build_python_protos --inplace install-python-ci-dependencies-uv: uv pip sync --system sdk/python/requirements/py$(PYTHON_VERSION)-ci-requirements.txt uv pip install --system --no-deps -e . - python setup.py build_python_protos --inplace install-python-ci-dependencies-uv-venv: uv pip sync sdk/python/requirements/py$(PYTHON_VERSION)-ci-requirements.txt uv pip install --no-deps -e . - python setup.py build_python_protos --inplace - -install-protoc-dependencies: - pip install "protobuf<5" "grpcio-tools>=1.56.2,<2" "mypy-protobuf>=3.1" lock-python-ci-dependencies: uv pip compile --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py$(PYTHON_VERSION)-ci-requirements.txt -package-protos: - cp -r ${ROOT_DIR}/protos ${ROOT_DIR}/sdk/python/feast/protos - -compile-protos-python: install-protoc-dependencies - python setup.py build_python_protos --inplace +compile-protos-python: + python infra/scripts/generate_protos.py install-python: python -m piptools sync sdk/python/requirements/py$(PYTHON_VERSION)-requirements.txt diff --git a/infra/scripts/generate_protos.py b/infra/scripts/generate_protos.py new file mode 100644 index 0000000000..2ce7e29e12 --- /dev/null +++ b/infra/scripts/generate_protos.py @@ -0,0 +1,80 @@ +import os +import sys +import glob +import subprocess +from pathlib import Path + +repo_root = str(Path(__file__).resolve().parent) + +PROTO_SUBDIRS = ["core", "registry", "serving", "types", "storage"] +PYTHON_CODE_PREFIX = "sdk/python" + +class BuildPythonProtosCommand: + description = "Builds the proto files into Python files." + user_options = [ + ("inplace", "i", "Write generated proto files to source directory."), + ] + + def __init__(self): + self.python_protoc = [ + sys.executable, + "-m", + "grpc_tools.protoc", + ] + self.proto_folder = "protos" + self.sub_folders = PROTO_SUBDIRS + self.inplace = 0 + + @property + def python_folder(self): + return "sdk/python/feast/protos" + + def _generate_python_protos(self, path: str): + proto_files = glob.glob(os.path.join(self.proto_folder, path)) + Path(self.python_folder).mkdir(parents=True, exist_ok=True) + subprocess.check_call( + self.python_protoc + + [ + "-I", + self.proto_folder, + "--python_out", + self.python_folder, + "--grpc_python_out", + self.python_folder, + "--mypy_out", + self.python_folder, + ] + + proto_files + ) + + def run(self): + for sub_folder in self.sub_folders: + self._generate_python_protos(f"feast/{sub_folder}/*.proto") + # We need the __init__ files for each of the generated subdirs + # so that they are regular packages, and don't need the `--namespace-packages` flags + # when being typechecked using mypy. + with open(f"{self.python_folder}/feast/{sub_folder}/__init__.py", "w"): + pass + + with open(f"{self.python_folder}/__init__.py", "w"): + pass + with open(f"{self.python_folder}/feast/__init__.py", "w"): + pass + + for path in Path(self.python_folder).rglob("*.py"): + for folder in self.sub_folders: + # Read in the file + with open(path, "r") as file: + filedata = file.read() + + # Replace the target string + filedata = filedata.replace( + f"from feast.{folder}", f"from feast.protos.feast.{folder}" + ) + + # Write the file out again + with open(path, "w") as file: + file.write(filedata) + +if __name__ == "__main__": + BuildPythonProtosCommand().run() \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index c91608b6ce..d772bab9ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,5 @@ [build-system] requires = [ - "protobuf<5", - "grpcio-tools>=1.56.2,<2", - "mypy-protobuf>=3.1", "pybindgen==0.22.0", "setuptools>=60", "setuptools_scm>=6.2", diff --git a/sdk/python/feast/protos/__init__.py b/sdk/python/feast/protos/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sdk/python/feast/protos/feast/__init__.py b/sdk/python/feast/protos/feast/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sdk/python/feast/protos/feast/core/Aggregation_pb2.py b/sdk/python/feast/protos/feast/core/Aggregation_pb2.py new file mode 100644 index 0000000000..922f8f40aa --- /dev/null +++ b/sdk/python/feast/protos/feast/core/Aggregation_pb2.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: feast/core/Aggregation.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import duration_pb2 as google_dot_protobuf_dot_duration__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1c\x66\x65\x61st/core/Aggregation.proto\x12\nfeast.core\x1a\x1egoogle/protobuf/duration.proto\"\x92\x01\n\x0b\x41ggregation\x12\x0e\n\x06\x63olumn\x18\x01 \x01(\t\x12\x10\n\x08\x66unction\x18\x02 \x01(\t\x12.\n\x0btime_window\x18\x03 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x31\n\x0eslide_interval\x18\x04 \x01(\x0b\x32\x19.google.protobuf.DurationBU\n\x10\x66\x65\x61st.proto.coreB\x10\x41ggregationProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'feast.core.Aggregation_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\020feast.proto.coreB\020AggregationProtoZ/github.com/feast-dev/feast/go/protos/feast/core' + _globals['_AGGREGATION']._serialized_start=77 + _globals['_AGGREGATION']._serialized_end=223 +# @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/Aggregation_pb2.pyi b/sdk/python/feast/protos/feast/core/Aggregation_pb2.pyi new file mode 100644 index 0000000000..ceb8b1f813 --- /dev/null +++ b/sdk/python/feast/protos/feast/core/Aggregation_pb2.pyi @@ -0,0 +1,42 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" +import builtins +import google.protobuf.descriptor +import google.protobuf.duration_pb2 +import google.protobuf.message +import sys + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class Aggregation(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + COLUMN_FIELD_NUMBER: builtins.int + FUNCTION_FIELD_NUMBER: builtins.int + TIME_WINDOW_FIELD_NUMBER: builtins.int + SLIDE_INTERVAL_FIELD_NUMBER: builtins.int + column: builtins.str + function: builtins.str + @property + def time_window(self) -> google.protobuf.duration_pb2.Duration: ... + @property + def slide_interval(self) -> google.protobuf.duration_pb2.Duration: ... + def __init__( + self, + *, + column: builtins.str = ..., + function: builtins.str = ..., + time_window: google.protobuf.duration_pb2.Duration | None = ..., + slide_interval: google.protobuf.duration_pb2.Duration | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["slide_interval", b"slide_interval", "time_window", b"time_window"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["column", b"column", "function", b"function", "slide_interval", b"slide_interval", "time_window", b"time_window"]) -> None: ... + +global___Aggregation = Aggregation diff --git a/sdk/python/feast/protos/feast/core/Aggregation_pb2_grpc.py b/sdk/python/feast/protos/feast/core/Aggregation_pb2_grpc.py new file mode 100644 index 0000000000..2daafffebf --- /dev/null +++ b/sdk/python/feast/protos/feast/core/Aggregation_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + diff --git a/sdk/python/feast/protos/feast/core/DataFormat_pb2.py b/sdk/python/feast/protos/feast/core/DataFormat_pb2.py new file mode 100644 index 0000000000..a3883dcec3 --- /dev/null +++ b/sdk/python/feast/protos/feast/core/DataFormat_pb2.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: feast/core/DataFormat.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1b\x66\x65\x61st/core/DataFormat.proto\x12\nfeast.core\"\xb2\x01\n\nFileFormat\x12>\n\x0eparquet_format\x18\x01 \x01(\x0b\x32$.feast.core.FileFormat.ParquetFormatH\x00\x12:\n\x0c\x64\x65lta_format\x18\x02 \x01(\x0b\x32\".feast.core.FileFormat.DeltaFormatH\x00\x1a\x0f\n\rParquetFormat\x1a\r\n\x0b\x44\x65ltaFormatB\x08\n\x06\x66ormat\"\xb7\x02\n\x0cStreamFormat\x12:\n\x0b\x61vro_format\x18\x01 \x01(\x0b\x32#.feast.core.StreamFormat.AvroFormatH\x00\x12<\n\x0cproto_format\x18\x02 \x01(\x0b\x32$.feast.core.StreamFormat.ProtoFormatH\x00\x12:\n\x0bjson_format\x18\x03 \x01(\x0b\x32#.feast.core.StreamFormat.JsonFormatH\x00\x1a!\n\x0bProtoFormat\x12\x12\n\nclass_path\x18\x01 \x01(\t\x1a!\n\nAvroFormat\x12\x13\n\x0bschema_json\x18\x01 \x01(\t\x1a!\n\nJsonFormat\x12\x13\n\x0bschema_json\x18\x01 \x01(\tB\x08\n\x06\x66ormatBT\n\x10\x66\x65\x61st.proto.coreB\x0f\x44\x61taFormatProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'feast.core.DataFormat_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\020feast.proto.coreB\017DataFormatProtoZ/github.com/feast-dev/feast/go/protos/feast/core' + _globals['_FILEFORMAT']._serialized_start=44 + _globals['_FILEFORMAT']._serialized_end=222 + _globals['_FILEFORMAT_PARQUETFORMAT']._serialized_start=182 + _globals['_FILEFORMAT_PARQUETFORMAT']._serialized_end=197 + _globals['_FILEFORMAT_DELTAFORMAT']._serialized_start=199 + _globals['_FILEFORMAT_DELTAFORMAT']._serialized_end=212 + _globals['_STREAMFORMAT']._serialized_start=225 + _globals['_STREAMFORMAT']._serialized_end=536 + _globals['_STREAMFORMAT_PROTOFORMAT']._serialized_start=423 + _globals['_STREAMFORMAT_PROTOFORMAT']._serialized_end=456 + _globals['_STREAMFORMAT_AVROFORMAT']._serialized_start=458 + _globals['_STREAMFORMAT_AVROFORMAT']._serialized_end=491 + _globals['_STREAMFORMAT_JSONFORMAT']._serialized_start=493 + _globals['_STREAMFORMAT_JSONFORMAT']._serialized_end=526 +# @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/DataFormat_pb2.pyi b/sdk/python/feast/protos/feast/core/DataFormat_pb2.pyi new file mode 100644 index 0000000000..1f904e9886 --- /dev/null +++ b/sdk/python/feast/protos/feast/core/DataFormat_pb2.pyi @@ -0,0 +1,143 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file + +Copyright 2020 The Feast 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 + + https://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 builtins +import google.protobuf.descriptor +import google.protobuf.message +import sys + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class FileFormat(google.protobuf.message.Message): + """Defines the file format encoding the features/entity data in files""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class ParquetFormat(google.protobuf.message.Message): + """Defines options for the Parquet data format""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + + class DeltaFormat(google.protobuf.message.Message): + """Defines options for delta data format""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + + PARQUET_FORMAT_FIELD_NUMBER: builtins.int + DELTA_FORMAT_FIELD_NUMBER: builtins.int + @property + def parquet_format(self) -> global___FileFormat.ParquetFormat: ... + @property + def delta_format(self) -> global___FileFormat.DeltaFormat: ... + def __init__( + self, + *, + parquet_format: global___FileFormat.ParquetFormat | None = ..., + delta_format: global___FileFormat.DeltaFormat | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["delta_format", b"delta_format", "format", b"format", "parquet_format", b"parquet_format"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["delta_format", b"delta_format", "format", b"format", "parquet_format", b"parquet_format"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal["format", b"format"]) -> typing_extensions.Literal["parquet_format", "delta_format"] | None: ... + +global___FileFormat = FileFormat + +class StreamFormat(google.protobuf.message.Message): + """Defines the data format encoding features/entity data in data streams""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class ProtoFormat(google.protobuf.message.Message): + """Defines options for the protobuf data format""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + CLASS_PATH_FIELD_NUMBER: builtins.int + class_path: builtins.str + """Classpath to the generated Java Protobuf class that can be used to decode + Feature data from the obtained stream message + """ + def __init__( + self, + *, + class_path: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["class_path", b"class_path"]) -> None: ... + + class AvroFormat(google.protobuf.message.Message): + """Defines options for the avro data format""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + SCHEMA_JSON_FIELD_NUMBER: builtins.int + schema_json: builtins.str + """Optional if used in a File DataSource as schema is embedded in avro file. + Specifies the schema of the Avro message as JSON string. + """ + def __init__( + self, + *, + schema_json: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["schema_json", b"schema_json"]) -> None: ... + + class JsonFormat(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + SCHEMA_JSON_FIELD_NUMBER: builtins.int + schema_json: builtins.str + def __init__( + self, + *, + schema_json: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["schema_json", b"schema_json"]) -> None: ... + + AVRO_FORMAT_FIELD_NUMBER: builtins.int + PROTO_FORMAT_FIELD_NUMBER: builtins.int + JSON_FORMAT_FIELD_NUMBER: builtins.int + @property + def avro_format(self) -> global___StreamFormat.AvroFormat: ... + @property + def proto_format(self) -> global___StreamFormat.ProtoFormat: ... + @property + def json_format(self) -> global___StreamFormat.JsonFormat: ... + def __init__( + self, + *, + avro_format: global___StreamFormat.AvroFormat | None = ..., + proto_format: global___StreamFormat.ProtoFormat | None = ..., + json_format: global___StreamFormat.JsonFormat | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["avro_format", b"avro_format", "format", b"format", "json_format", b"json_format", "proto_format", b"proto_format"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["avro_format", b"avro_format", "format", b"format", "json_format", b"json_format", "proto_format", b"proto_format"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal["format", b"format"]) -> typing_extensions.Literal["avro_format", "proto_format", "json_format"] | None: ... + +global___StreamFormat = StreamFormat diff --git a/sdk/python/feast/protos/feast/core/DataFormat_pb2_grpc.py b/sdk/python/feast/protos/feast/core/DataFormat_pb2_grpc.py new file mode 100644 index 0000000000..2daafffebf --- /dev/null +++ b/sdk/python/feast/protos/feast/core/DataFormat_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + diff --git a/sdk/python/feast/protos/feast/core/DataSource_pb2.py b/sdk/python/feast/protos/feast/core/DataSource_pb2.py new file mode 100644 index 0000000000..b58c33a383 --- /dev/null +++ b/sdk/python/feast/protos/feast/core/DataSource_pb2.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: feast/core/DataSource.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import duration_pb2 as google_dot_protobuf_dot_duration__pb2 +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 +from feast.protos.feast.core import DataFormat_pb2 as feast_dot_core_dot_DataFormat__pb2 +from feast.protos.feast.types import Value_pb2 as feast_dot_types_dot_Value__pb2 +from feast.protos.feast.core import Feature_pb2 as feast_dot_core_dot_Feature__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1b\x66\x65\x61st/core/DataSource.proto\x12\nfeast.core\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1b\x66\x65\x61st/core/DataFormat.proto\x1a\x17\x66\x65\x61st/types/Value.proto\x1a\x18\x66\x65\x61st/core/Feature.proto\"\xc0\x16\n\nDataSource\x12\x0c\n\x04name\x18\x14 \x01(\t\x12\x0f\n\x07project\x18\x15 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x17 \x01(\t\x12.\n\x04tags\x18\x18 \x03(\x0b\x32 .feast.core.DataSource.TagsEntry\x12\r\n\x05owner\x18\x19 \x01(\t\x12/\n\x04type\x18\x01 \x01(\x0e\x32!.feast.core.DataSource.SourceType\x12?\n\rfield_mapping\x18\x02 \x03(\x0b\x32(.feast.core.DataSource.FieldMappingEntry\x12\x17\n\x0ftimestamp_field\x18\x03 \x01(\t\x12\x1d\n\x15\x64\x61te_partition_column\x18\x04 \x01(\t\x12 \n\x18\x63reated_timestamp_column\x18\x05 \x01(\t\x12\x1e\n\x16\x64\x61ta_source_class_type\x18\x11 \x01(\t\x12,\n\x0c\x62\x61tch_source\x18\x1a \x01(\x0b\x32\x16.feast.core.DataSource\x12/\n\x04meta\x18\x32 \x01(\x0b\x32!.feast.core.DataSource.SourceMeta\x12:\n\x0c\x66ile_options\x18\x0b \x01(\x0b\x32\".feast.core.DataSource.FileOptionsH\x00\x12\x42\n\x10\x62igquery_options\x18\x0c \x01(\x0b\x32&.feast.core.DataSource.BigQueryOptionsH\x00\x12<\n\rkafka_options\x18\r \x01(\x0b\x32#.feast.core.DataSource.KafkaOptionsH\x00\x12@\n\x0fkinesis_options\x18\x0e \x01(\x0b\x32%.feast.core.DataSource.KinesisOptionsH\x00\x12\x42\n\x10redshift_options\x18\x0f \x01(\x0b\x32&.feast.core.DataSource.RedshiftOptionsH\x00\x12I\n\x14request_data_options\x18\x12 \x01(\x0b\x32).feast.core.DataSource.RequestDataOptionsH\x00\x12\x44\n\x0e\x63ustom_options\x18\x10 \x01(\x0b\x32*.feast.core.DataSource.CustomSourceOptionsH\x00\x12\x44\n\x11snowflake_options\x18\x13 \x01(\x0b\x32\'.feast.core.DataSource.SnowflakeOptionsH\x00\x12:\n\x0cpush_options\x18\x16 \x01(\x0b\x32\".feast.core.DataSource.PushOptionsH\x00\x12<\n\rspark_options\x18\x1b \x01(\x0b\x32#.feast.core.DataSource.SparkOptionsH\x00\x12<\n\rtrino_options\x18\x1e \x01(\x0b\x32#.feast.core.DataSource.TrinoOptionsH\x00\x12>\n\x0e\x61thena_options\x18# \x01(\x0b\x32$.feast.core.DataSource.AthenaOptionsH\x00\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a\x33\n\x11\x46ieldMappingEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a\x82\x01\n\nSourceMeta\x12:\n\x16\x65\x61rliestEventTimestamp\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x38\n\x14latestEventTimestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x1a\x65\n\x0b\x46ileOptions\x12+\n\x0b\x66ile_format\x18\x01 \x01(\x0b\x32\x16.feast.core.FileFormat\x12\x0b\n\x03uri\x18\x02 \x01(\t\x12\x1c\n\x14s3_endpoint_override\x18\x03 \x01(\t\x1a/\n\x0f\x42igQueryOptions\x12\r\n\x05table\x18\x01 \x01(\t\x12\r\n\x05query\x18\x02 \x01(\t\x1a,\n\x0cTrinoOptions\x12\r\n\x05table\x18\x01 \x01(\t\x12\r\n\x05query\x18\x02 \x01(\t\x1a\xae\x01\n\x0cKafkaOptions\x12\x1f\n\x17kafka_bootstrap_servers\x18\x01 \x01(\t\x12\r\n\x05topic\x18\x02 \x01(\t\x12\x30\n\x0emessage_format\x18\x03 \x01(\x0b\x32\x18.feast.core.StreamFormat\x12<\n\x19watermark_delay_threshold\x18\x04 \x01(\x0b\x32\x19.google.protobuf.Duration\x1a\x66\n\x0eKinesisOptions\x12\x0e\n\x06region\x18\x01 \x01(\t\x12\x13\n\x0bstream_name\x18\x02 \x01(\t\x12/\n\rrecord_format\x18\x03 \x01(\x0b\x32\x18.feast.core.StreamFormat\x1aQ\n\x0fRedshiftOptions\x12\r\n\x05table\x18\x01 \x01(\t\x12\r\n\x05query\x18\x02 \x01(\t\x12\x0e\n\x06schema\x18\x03 \x01(\t\x12\x10\n\x08\x64\x61tabase\x18\x04 \x01(\t\x1aT\n\rAthenaOptions\x12\r\n\x05table\x18\x01 \x01(\t\x12\r\n\x05query\x18\x02 \x01(\t\x12\x10\n\x08\x64\x61tabase\x18\x03 \x01(\t\x12\x13\n\x0b\x64\x61ta_source\x18\x04 \x01(\t\x1aX\n\x10SnowflakeOptions\x12\r\n\x05table\x18\x01 \x01(\t\x12\r\n\x05query\x18\x02 \x01(\t\x12\x0e\n\x06schema\x18\x03 \x01(\t\x12\x10\n\x08\x64\x61tabase\x18\x04 \x01(\tJ\x04\x08\x05\x10\x06\x1aO\n\x0cSparkOptions\x12\r\n\x05table\x18\x01 \x01(\t\x12\r\n\x05query\x18\x02 \x01(\t\x12\x0c\n\x04path\x18\x03 \x01(\t\x12\x13\n\x0b\x66ile_format\x18\x04 \x01(\t\x1a,\n\x13\x43ustomSourceOptions\x12\x15\n\rconfiguration\x18\x01 \x01(\x0c\x1a\xf7\x01\n\x12RequestDataOptions\x12Z\n\x11\x64\x65precated_schema\x18\x02 \x03(\x0b\x32?.feast.core.DataSource.RequestDataOptions.DeprecatedSchemaEntry\x12)\n\x06schema\x18\x03 \x03(\x0b\x32\x19.feast.core.FeatureSpecV2\x1aT\n\x15\x44\x65precatedSchemaEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12*\n\x05value\x18\x02 \x01(\x0e\x32\x1b.feast.types.ValueType.Enum:\x02\x38\x01J\x04\x08\x01\x10\x02\x1a\x13\n\x0bPushOptionsJ\x04\x08\x01\x10\x02\"\xf8\x01\n\nSourceType\x12\x0b\n\x07INVALID\x10\x00\x12\x0e\n\nBATCH_FILE\x10\x01\x12\x13\n\x0f\x42\x41TCH_SNOWFLAKE\x10\x08\x12\x12\n\x0e\x42\x41TCH_BIGQUERY\x10\x02\x12\x12\n\x0e\x42\x41TCH_REDSHIFT\x10\x05\x12\x10\n\x0cSTREAM_KAFKA\x10\x03\x12\x12\n\x0eSTREAM_KINESIS\x10\x04\x12\x11\n\rCUSTOM_SOURCE\x10\x06\x12\x12\n\x0eREQUEST_SOURCE\x10\x07\x12\x0f\n\x0bPUSH_SOURCE\x10\t\x12\x0f\n\x0b\x42\x41TCH_TRINO\x10\n\x12\x0f\n\x0b\x42\x41TCH_SPARK\x10\x0b\x12\x10\n\x0c\x42\x41TCH_ATHENA\x10\x0c\x42\t\n\x07optionsJ\x04\x08\x06\x10\x0b\x42T\n\x10\x66\x65\x61st.proto.coreB\x0f\x44\x61taSourceProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'feast.core.DataSource_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\020feast.proto.coreB\017DataSourceProtoZ/github.com/feast-dev/feast/go/protos/feast/core' + _globals['_DATASOURCE_TAGSENTRY']._options = None + _globals['_DATASOURCE_TAGSENTRY']._serialized_options = b'8\001' + _globals['_DATASOURCE_FIELDMAPPINGENTRY']._options = None + _globals['_DATASOURCE_FIELDMAPPINGENTRY']._serialized_options = b'8\001' + _globals['_DATASOURCE_REQUESTDATAOPTIONS_DEPRECATEDSCHEMAENTRY']._options = None + _globals['_DATASOURCE_REQUESTDATAOPTIONS_DEPRECATEDSCHEMAENTRY']._serialized_options = b'8\001' + _globals['_DATASOURCE']._serialized_start=189 + _globals['_DATASOURCE']._serialized_end=3069 + _globals['_DATASOURCE_TAGSENTRY']._serialized_start=1436 + _globals['_DATASOURCE_TAGSENTRY']._serialized_end=1479 + _globals['_DATASOURCE_FIELDMAPPINGENTRY']._serialized_start=1481 + _globals['_DATASOURCE_FIELDMAPPINGENTRY']._serialized_end=1532 + _globals['_DATASOURCE_SOURCEMETA']._serialized_start=1535 + _globals['_DATASOURCE_SOURCEMETA']._serialized_end=1665 + _globals['_DATASOURCE_FILEOPTIONS']._serialized_start=1667 + _globals['_DATASOURCE_FILEOPTIONS']._serialized_end=1768 + _globals['_DATASOURCE_BIGQUERYOPTIONS']._serialized_start=1770 + _globals['_DATASOURCE_BIGQUERYOPTIONS']._serialized_end=1817 + _globals['_DATASOURCE_TRINOOPTIONS']._serialized_start=1819 + _globals['_DATASOURCE_TRINOOPTIONS']._serialized_end=1863 + _globals['_DATASOURCE_KAFKAOPTIONS']._serialized_start=1866 + _globals['_DATASOURCE_KAFKAOPTIONS']._serialized_end=2040 + _globals['_DATASOURCE_KINESISOPTIONS']._serialized_start=2042 + _globals['_DATASOURCE_KINESISOPTIONS']._serialized_end=2144 + _globals['_DATASOURCE_REDSHIFTOPTIONS']._serialized_start=2146 + _globals['_DATASOURCE_REDSHIFTOPTIONS']._serialized_end=2227 + _globals['_DATASOURCE_ATHENAOPTIONS']._serialized_start=2229 + _globals['_DATASOURCE_ATHENAOPTIONS']._serialized_end=2313 + _globals['_DATASOURCE_SNOWFLAKEOPTIONS']._serialized_start=2315 + _globals['_DATASOURCE_SNOWFLAKEOPTIONS']._serialized_end=2403 + _globals['_DATASOURCE_SPARKOPTIONS']._serialized_start=2405 + _globals['_DATASOURCE_SPARKOPTIONS']._serialized_end=2484 + _globals['_DATASOURCE_CUSTOMSOURCEOPTIONS']._serialized_start=2486 + _globals['_DATASOURCE_CUSTOMSOURCEOPTIONS']._serialized_end=2530 + _globals['_DATASOURCE_REQUESTDATAOPTIONS']._serialized_start=2533 + _globals['_DATASOURCE_REQUESTDATAOPTIONS']._serialized_end=2780 + _globals['_DATASOURCE_REQUESTDATAOPTIONS_DEPRECATEDSCHEMAENTRY']._serialized_start=2690 + _globals['_DATASOURCE_REQUESTDATAOPTIONS_DEPRECATEDSCHEMAENTRY']._serialized_end=2774 + _globals['_DATASOURCE_PUSHOPTIONS']._serialized_start=2782 + _globals['_DATASOURCE_PUSHOPTIONS']._serialized_end=2801 + _globals['_DATASOURCE_SOURCETYPE']._serialized_start=2804 + _globals['_DATASOURCE_SOURCETYPE']._serialized_end=3052 +# @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/DataSource_pb2.pyi b/sdk/python/feast/protos/feast/core/DataSource_pb2.pyi new file mode 100644 index 0000000000..94336638e1 --- /dev/null +++ b/sdk/python/feast/protos/feast/core/DataSource_pb2.pyi @@ -0,0 +1,559 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file + +Copyright 2020 The Feast 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 + + https://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 builtins +import collections.abc +import feast.core.DataFormat_pb2 +import feast.core.Feature_pb2 +import feast.types.Value_pb2 +import google.protobuf.descriptor +import google.protobuf.duration_pb2 +import google.protobuf.internal.containers +import google.protobuf.internal.enum_type_wrapper +import google.protobuf.message +import google.protobuf.timestamp_pb2 +import sys +import typing + +if sys.version_info >= (3, 10): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class DataSource(google.protobuf.message.Message): + """Defines a Data Source that can be used source Feature data + Next available id: 28 + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class _SourceType: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _SourceTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[DataSource._SourceType.ValueType], builtins.type): # noqa: F821 + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + INVALID: DataSource._SourceType.ValueType # 0 + BATCH_FILE: DataSource._SourceType.ValueType # 1 + BATCH_SNOWFLAKE: DataSource._SourceType.ValueType # 8 + BATCH_BIGQUERY: DataSource._SourceType.ValueType # 2 + BATCH_REDSHIFT: DataSource._SourceType.ValueType # 5 + STREAM_KAFKA: DataSource._SourceType.ValueType # 3 + STREAM_KINESIS: DataSource._SourceType.ValueType # 4 + CUSTOM_SOURCE: DataSource._SourceType.ValueType # 6 + REQUEST_SOURCE: DataSource._SourceType.ValueType # 7 + PUSH_SOURCE: DataSource._SourceType.ValueType # 9 + BATCH_TRINO: DataSource._SourceType.ValueType # 10 + BATCH_SPARK: DataSource._SourceType.ValueType # 11 + BATCH_ATHENA: DataSource._SourceType.ValueType # 12 + + class SourceType(_SourceType, metaclass=_SourceTypeEnumTypeWrapper): + """Type of Data Source. + Next available id: 12 + """ + + INVALID: DataSource.SourceType.ValueType # 0 + BATCH_FILE: DataSource.SourceType.ValueType # 1 + BATCH_SNOWFLAKE: DataSource.SourceType.ValueType # 8 + BATCH_BIGQUERY: DataSource.SourceType.ValueType # 2 + BATCH_REDSHIFT: DataSource.SourceType.ValueType # 5 + STREAM_KAFKA: DataSource.SourceType.ValueType # 3 + STREAM_KINESIS: DataSource.SourceType.ValueType # 4 + CUSTOM_SOURCE: DataSource.SourceType.ValueType # 6 + REQUEST_SOURCE: DataSource.SourceType.ValueType # 7 + PUSH_SOURCE: DataSource.SourceType.ValueType # 9 + BATCH_TRINO: DataSource.SourceType.ValueType # 10 + BATCH_SPARK: DataSource.SourceType.ValueType # 11 + BATCH_ATHENA: DataSource.SourceType.ValueType # 12 + + class TagsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + class FieldMappingEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + class SourceMeta(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + EARLIESTEVENTTIMESTAMP_FIELD_NUMBER: builtins.int + LATESTEVENTTIMESTAMP_FIELD_NUMBER: builtins.int + @property + def earliestEventTimestamp(self) -> google.protobuf.timestamp_pb2.Timestamp: ... + @property + def latestEventTimestamp(self) -> google.protobuf.timestamp_pb2.Timestamp: ... + def __init__( + self, + *, + earliestEventTimestamp: google.protobuf.timestamp_pb2.Timestamp | None = ..., + latestEventTimestamp: google.protobuf.timestamp_pb2.Timestamp | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["earliestEventTimestamp", b"earliestEventTimestamp", "latestEventTimestamp", b"latestEventTimestamp"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["earliestEventTimestamp", b"earliestEventTimestamp", "latestEventTimestamp", b"latestEventTimestamp"]) -> None: ... + + class FileOptions(google.protobuf.message.Message): + """Defines options for DataSource that sources features from a file""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + FILE_FORMAT_FIELD_NUMBER: builtins.int + URI_FIELD_NUMBER: builtins.int + S3_ENDPOINT_OVERRIDE_FIELD_NUMBER: builtins.int + @property + def file_format(self) -> feast.core.DataFormat_pb2.FileFormat: ... + uri: builtins.str + """Target URL of file to retrieve and source features from. + s3://path/to/file for AWS S3 storage + gs://path/to/file for GCP GCS storage + file:///path/to/file for local storage + """ + s3_endpoint_override: builtins.str + """override AWS S3 storage endpoint with custom S3 endpoint""" + def __init__( + self, + *, + file_format: feast.core.DataFormat_pb2.FileFormat | None = ..., + uri: builtins.str = ..., + s3_endpoint_override: builtins.str = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["file_format", b"file_format"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["file_format", b"file_format", "s3_endpoint_override", b"s3_endpoint_override", "uri", b"uri"]) -> None: ... + + class BigQueryOptions(google.protobuf.message.Message): + """Defines options for DataSource that sources features from a BigQuery Query""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + TABLE_FIELD_NUMBER: builtins.int + QUERY_FIELD_NUMBER: builtins.int + table: builtins.str + """Full table reference in the form of [project:dataset.table]""" + query: builtins.str + """SQL query that returns a table containing feature data. Must contain an event_timestamp column, and respective + entity columns + """ + def __init__( + self, + *, + table: builtins.str = ..., + query: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["query", b"query", "table", b"table"]) -> None: ... + + class TrinoOptions(google.protobuf.message.Message): + """Defines options for DataSource that sources features from a Trino Query""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + TABLE_FIELD_NUMBER: builtins.int + QUERY_FIELD_NUMBER: builtins.int + table: builtins.str + """Full table reference in the form of [project:dataset.table]""" + query: builtins.str + """SQL query that returns a table containing feature data. Must contain an event_timestamp column, and respective + entity columns + """ + def __init__( + self, + *, + table: builtins.str = ..., + query: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["query", b"query", "table", b"table"]) -> None: ... + + class KafkaOptions(google.protobuf.message.Message): + """Defines options for DataSource that sources features from Kafka messages. + Each message should be a Protobuf that can be decoded with the generated + Java Protobuf class at the given class path + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KAFKA_BOOTSTRAP_SERVERS_FIELD_NUMBER: builtins.int + TOPIC_FIELD_NUMBER: builtins.int + MESSAGE_FORMAT_FIELD_NUMBER: builtins.int + WATERMARK_DELAY_THRESHOLD_FIELD_NUMBER: builtins.int + kafka_bootstrap_servers: builtins.str + """Comma separated list of Kafka bootstrap servers. Used for feature tables without a defined source host[:port]]""" + topic: builtins.str + """Kafka topic to collect feature data from.""" + @property + def message_format(self) -> feast.core.DataFormat_pb2.StreamFormat: + """Defines the stream data format encoding feature/entity data in Kafka messages.""" + @property + def watermark_delay_threshold(self) -> google.protobuf.duration_pb2.Duration: + """Watermark delay threshold for stream data""" + def __init__( + self, + *, + kafka_bootstrap_servers: builtins.str = ..., + topic: builtins.str = ..., + message_format: feast.core.DataFormat_pb2.StreamFormat | None = ..., + watermark_delay_threshold: google.protobuf.duration_pb2.Duration | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["message_format", b"message_format", "watermark_delay_threshold", b"watermark_delay_threshold"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["kafka_bootstrap_servers", b"kafka_bootstrap_servers", "message_format", b"message_format", "topic", b"topic", "watermark_delay_threshold", b"watermark_delay_threshold"]) -> None: ... + + class KinesisOptions(google.protobuf.message.Message): + """Defines options for DataSource that sources features from Kinesis records. + Each record should be a Protobuf that can be decoded with the generated + Java Protobuf class at the given class path + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + REGION_FIELD_NUMBER: builtins.int + STREAM_NAME_FIELD_NUMBER: builtins.int + RECORD_FORMAT_FIELD_NUMBER: builtins.int + region: builtins.str + """AWS region of the Kinesis stream""" + stream_name: builtins.str + """Name of the Kinesis stream to obtain feature data from.""" + @property + def record_format(self) -> feast.core.DataFormat_pb2.StreamFormat: + """Defines the data format encoding the feature/entity data in Kinesis records. + Kinesis Data Sources support Avro and Proto as data formats. + """ + def __init__( + self, + *, + region: builtins.str = ..., + stream_name: builtins.str = ..., + record_format: feast.core.DataFormat_pb2.StreamFormat | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["record_format", b"record_format"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["record_format", b"record_format", "region", b"region", "stream_name", b"stream_name"]) -> None: ... + + class RedshiftOptions(google.protobuf.message.Message): + """Defines options for DataSource that sources features from a Redshift Query""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + TABLE_FIELD_NUMBER: builtins.int + QUERY_FIELD_NUMBER: builtins.int + SCHEMA_FIELD_NUMBER: builtins.int + DATABASE_FIELD_NUMBER: builtins.int + table: builtins.str + """Redshift table name""" + query: builtins.str + """SQL query that returns a table containing feature data. Must contain an event_timestamp column, and respective + entity columns + """ + schema: builtins.str + """Redshift schema name""" + database: builtins.str + """Redshift database name""" + def __init__( + self, + *, + table: builtins.str = ..., + query: builtins.str = ..., + schema: builtins.str = ..., + database: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["database", b"database", "query", b"query", "schema", b"schema", "table", b"table"]) -> None: ... + + class AthenaOptions(google.protobuf.message.Message): + """Defines options for DataSource that sources features from a Athena Query""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + TABLE_FIELD_NUMBER: builtins.int + QUERY_FIELD_NUMBER: builtins.int + DATABASE_FIELD_NUMBER: builtins.int + DATA_SOURCE_FIELD_NUMBER: builtins.int + table: builtins.str + """Athena table name""" + query: builtins.str + """SQL query that returns a table containing feature data. Must contain an event_timestamp column, and respective + entity columns + """ + database: builtins.str + """Athena database name""" + data_source: builtins.str + """Athena schema name""" + def __init__( + self, + *, + table: builtins.str = ..., + query: builtins.str = ..., + database: builtins.str = ..., + data_source: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["data_source", b"data_source", "database", b"database", "query", b"query", "table", b"table"]) -> None: ... + + class SnowflakeOptions(google.protobuf.message.Message): + """Defines options for DataSource that sources features from a Snowflake Query""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + TABLE_FIELD_NUMBER: builtins.int + QUERY_FIELD_NUMBER: builtins.int + SCHEMA_FIELD_NUMBER: builtins.int + DATABASE_FIELD_NUMBER: builtins.int + table: builtins.str + """Snowflake table name""" + query: builtins.str + """SQL query that returns a table containing feature data. Must contain an event_timestamp column, and respective + entity columns + """ + schema: builtins.str + """Snowflake schema name""" + database: builtins.str + """Snowflake schema name""" + def __init__( + self, + *, + table: builtins.str = ..., + query: builtins.str = ..., + schema: builtins.str = ..., + database: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["database", b"database", "query", b"query", "schema", b"schema", "table", b"table"]) -> None: ... + + class SparkOptions(google.protobuf.message.Message): + """Defines options for DataSource that sources features from a spark table/query""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + TABLE_FIELD_NUMBER: builtins.int + QUERY_FIELD_NUMBER: builtins.int + PATH_FIELD_NUMBER: builtins.int + FILE_FORMAT_FIELD_NUMBER: builtins.int + table: builtins.str + """Table name""" + query: builtins.str + """Spark SQl query that returns the table, this is an alternative to `table`""" + path: builtins.str + """Path from which spark can read the table, this is an alternative to `table`""" + file_format: builtins.str + """Format of files at `path` (e.g. parquet, avro, etc)""" + def __init__( + self, + *, + table: builtins.str = ..., + query: builtins.str = ..., + path: builtins.str = ..., + file_format: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["file_format", b"file_format", "path", b"path", "query", b"query", "table", b"table"]) -> None: ... + + class CustomSourceOptions(google.protobuf.message.Message): + """Defines configuration for custom third-party data sources.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + CONFIGURATION_FIELD_NUMBER: builtins.int + configuration: builtins.bytes + """Serialized configuration information for the data source. The implementer of the custom data source is + responsible for serializing and deserializing data from bytes + """ + def __init__( + self, + *, + configuration: builtins.bytes = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["configuration", b"configuration"]) -> None: ... + + class RequestDataOptions(google.protobuf.message.Message): + """Defines options for DataSource that sources features from request data""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class DeprecatedSchemaEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: feast.types.Value_pb2.ValueType.Enum.ValueType + def __init__( + self, + *, + key: builtins.str = ..., + value: feast.types.Value_pb2.ValueType.Enum.ValueType = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + DEPRECATED_SCHEMA_FIELD_NUMBER: builtins.int + SCHEMA_FIELD_NUMBER: builtins.int + @property + def deprecated_schema(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, feast.types.Value_pb2.ValueType.Enum.ValueType]: + """Mapping of feature name to type""" + @property + def schema(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.Feature_pb2.FeatureSpecV2]: ... + def __init__( + self, + *, + deprecated_schema: collections.abc.Mapping[builtins.str, feast.types.Value_pb2.ValueType.Enum.ValueType] | None = ..., + schema: collections.abc.Iterable[feast.core.Feature_pb2.FeatureSpecV2] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["deprecated_schema", b"deprecated_schema", "schema", b"schema"]) -> None: ... + + class PushOptions(google.protobuf.message.Message): + """Defines options for DataSource that supports pushing data to it. This allows data to be pushed to + the online store on-demand, such as by stream consumers. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + + NAME_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + DESCRIPTION_FIELD_NUMBER: builtins.int + TAGS_FIELD_NUMBER: builtins.int + OWNER_FIELD_NUMBER: builtins.int + TYPE_FIELD_NUMBER: builtins.int + FIELD_MAPPING_FIELD_NUMBER: builtins.int + TIMESTAMP_FIELD_FIELD_NUMBER: builtins.int + DATE_PARTITION_COLUMN_FIELD_NUMBER: builtins.int + CREATED_TIMESTAMP_COLUMN_FIELD_NUMBER: builtins.int + DATA_SOURCE_CLASS_TYPE_FIELD_NUMBER: builtins.int + BATCH_SOURCE_FIELD_NUMBER: builtins.int + META_FIELD_NUMBER: builtins.int + FILE_OPTIONS_FIELD_NUMBER: builtins.int + BIGQUERY_OPTIONS_FIELD_NUMBER: builtins.int + KAFKA_OPTIONS_FIELD_NUMBER: builtins.int + KINESIS_OPTIONS_FIELD_NUMBER: builtins.int + REDSHIFT_OPTIONS_FIELD_NUMBER: builtins.int + REQUEST_DATA_OPTIONS_FIELD_NUMBER: builtins.int + CUSTOM_OPTIONS_FIELD_NUMBER: builtins.int + SNOWFLAKE_OPTIONS_FIELD_NUMBER: builtins.int + PUSH_OPTIONS_FIELD_NUMBER: builtins.int + SPARK_OPTIONS_FIELD_NUMBER: builtins.int + TRINO_OPTIONS_FIELD_NUMBER: builtins.int + ATHENA_OPTIONS_FIELD_NUMBER: builtins.int + name: builtins.str + """Unique name of data source within the project""" + project: builtins.str + """Name of Feast project that this data source belongs to.""" + description: builtins.str + @property + def tags(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: ... + owner: builtins.str + type: global___DataSource.SourceType.ValueType + @property + def field_mapping(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: + """Defines mapping between fields in the sourced data + and fields in parent FeatureTable. + """ + timestamp_field: builtins.str + """Must specify event timestamp column name""" + date_partition_column: builtins.str + """(Optional) Specify partition column + useful for file sources + """ + created_timestamp_column: builtins.str + """Must specify creation timestamp column name""" + data_source_class_type: builtins.str + """This is an internal field that is represents the python class for the data source object a proto object represents. + This should be set by feast, and not by users. + The field is used primarily by custom data sources and is mandatory for them to set. Feast may set it for + first party sources as well. + """ + @property + def batch_source(self) -> global___DataSource: + """Optional batch source for streaming sources for historical features and materialization.""" + @property + def meta(self) -> global___DataSource.SourceMeta: ... + @property + def file_options(self) -> global___DataSource.FileOptions: ... + @property + def bigquery_options(self) -> global___DataSource.BigQueryOptions: ... + @property + def kafka_options(self) -> global___DataSource.KafkaOptions: ... + @property + def kinesis_options(self) -> global___DataSource.KinesisOptions: ... + @property + def redshift_options(self) -> global___DataSource.RedshiftOptions: ... + @property + def request_data_options(self) -> global___DataSource.RequestDataOptions: ... + @property + def custom_options(self) -> global___DataSource.CustomSourceOptions: ... + @property + def snowflake_options(self) -> global___DataSource.SnowflakeOptions: ... + @property + def push_options(self) -> global___DataSource.PushOptions: ... + @property + def spark_options(self) -> global___DataSource.SparkOptions: ... + @property + def trino_options(self) -> global___DataSource.TrinoOptions: ... + @property + def athena_options(self) -> global___DataSource.AthenaOptions: ... + def __init__( + self, + *, + name: builtins.str = ..., + project: builtins.str = ..., + description: builtins.str = ..., + tags: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + owner: builtins.str = ..., + type: global___DataSource.SourceType.ValueType = ..., + field_mapping: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + timestamp_field: builtins.str = ..., + date_partition_column: builtins.str = ..., + created_timestamp_column: builtins.str = ..., + data_source_class_type: builtins.str = ..., + batch_source: global___DataSource | None = ..., + meta: global___DataSource.SourceMeta | None = ..., + file_options: global___DataSource.FileOptions | None = ..., + bigquery_options: global___DataSource.BigQueryOptions | None = ..., + kafka_options: global___DataSource.KafkaOptions | None = ..., + kinesis_options: global___DataSource.KinesisOptions | None = ..., + redshift_options: global___DataSource.RedshiftOptions | None = ..., + request_data_options: global___DataSource.RequestDataOptions | None = ..., + custom_options: global___DataSource.CustomSourceOptions | None = ..., + snowflake_options: global___DataSource.SnowflakeOptions | None = ..., + push_options: global___DataSource.PushOptions | None = ..., + spark_options: global___DataSource.SparkOptions | None = ..., + trino_options: global___DataSource.TrinoOptions | None = ..., + athena_options: global___DataSource.AthenaOptions | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["athena_options", b"athena_options", "batch_source", b"batch_source", "bigquery_options", b"bigquery_options", "custom_options", b"custom_options", "file_options", b"file_options", "kafka_options", b"kafka_options", "kinesis_options", b"kinesis_options", "meta", b"meta", "options", b"options", "push_options", b"push_options", "redshift_options", b"redshift_options", "request_data_options", b"request_data_options", "snowflake_options", b"snowflake_options", "spark_options", b"spark_options", "trino_options", b"trino_options"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["athena_options", b"athena_options", "batch_source", b"batch_source", "bigquery_options", b"bigquery_options", "created_timestamp_column", b"created_timestamp_column", "custom_options", b"custom_options", "data_source_class_type", b"data_source_class_type", "date_partition_column", b"date_partition_column", "description", b"description", "field_mapping", b"field_mapping", "file_options", b"file_options", "kafka_options", b"kafka_options", "kinesis_options", b"kinesis_options", "meta", b"meta", "name", b"name", "options", b"options", "owner", b"owner", "project", b"project", "push_options", b"push_options", "redshift_options", b"redshift_options", "request_data_options", b"request_data_options", "snowflake_options", b"snowflake_options", "spark_options", b"spark_options", "tags", b"tags", "timestamp_field", b"timestamp_field", "trino_options", b"trino_options", "type", b"type"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal["options", b"options"]) -> typing_extensions.Literal["file_options", "bigquery_options", "kafka_options", "kinesis_options", "redshift_options", "request_data_options", "custom_options", "snowflake_options", "push_options", "spark_options", "trino_options", "athena_options"] | None: ... + +global___DataSource = DataSource diff --git a/sdk/python/feast/protos/feast/core/DataSource_pb2_grpc.py b/sdk/python/feast/protos/feast/core/DataSource_pb2_grpc.py new file mode 100644 index 0000000000..2daafffebf --- /dev/null +++ b/sdk/python/feast/protos/feast/core/DataSource_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + diff --git a/sdk/python/feast/protos/feast/core/DatastoreTable_pb2.py b/sdk/python/feast/protos/feast/core/DatastoreTable_pb2.py new file mode 100644 index 0000000000..c5dbc3ec64 --- /dev/null +++ b/sdk/python/feast/protos/feast/core/DatastoreTable_pb2.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: feast/core/DatastoreTable.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import wrappers_pb2 as google_dot_protobuf_dot_wrappers__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1f\x66\x65\x61st/core/DatastoreTable.proto\x12\nfeast.core\x1a\x1egoogle/protobuf/wrappers.proto\"\xc2\x01\n\x0e\x44\x61tastoreTable\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x30\n\nproject_id\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12/\n\tnamespace\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12.\n\x08\x64\x61tabase\x18\x05 \x01(\x0b\x32\x1c.google.protobuf.StringValueBX\n\x10\x66\x65\x61st.proto.coreB\x13\x44\x61tastoreTableProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'feast.core.DatastoreTable_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\020feast.proto.coreB\023DatastoreTableProtoZ/github.com/feast-dev/feast/go/protos/feast/core' + _globals['_DATASTORETABLE']._serialized_start=80 + _globals['_DATASTORETABLE']._serialized_end=274 +# @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/DatastoreTable_pb2.pyi b/sdk/python/feast/protos/feast/core/DatastoreTable_pb2.pyi new file mode 100644 index 0000000000..6339a97536 --- /dev/null +++ b/sdk/python/feast/protos/feast/core/DatastoreTable_pb2.pyi @@ -0,0 +1,67 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file + +* Copyright 2021 The Feast 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 +* +* https://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 builtins +import google.protobuf.descriptor +import google.protobuf.message +import google.protobuf.wrappers_pb2 +import sys + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class DatastoreTable(google.protobuf.message.Message): + """Represents a Datastore table""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + PROJECT_FIELD_NUMBER: builtins.int + NAME_FIELD_NUMBER: builtins.int + PROJECT_ID_FIELD_NUMBER: builtins.int + NAMESPACE_FIELD_NUMBER: builtins.int + DATABASE_FIELD_NUMBER: builtins.int + project: builtins.str + """Feast project of the table""" + name: builtins.str + """Name of the table""" + @property + def project_id(self) -> google.protobuf.wrappers_pb2.StringValue: + """GCP project id""" + @property + def namespace(self) -> google.protobuf.wrappers_pb2.StringValue: + """Datastore namespace""" + @property + def database(self) -> google.protobuf.wrappers_pb2.StringValue: + """Firestore database""" + def __init__( + self, + *, + project: builtins.str = ..., + name: builtins.str = ..., + project_id: google.protobuf.wrappers_pb2.StringValue | None = ..., + namespace: google.protobuf.wrappers_pb2.StringValue | None = ..., + database: google.protobuf.wrappers_pb2.StringValue | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["database", b"database", "namespace", b"namespace", "project_id", b"project_id"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["database", b"database", "name", b"name", "namespace", b"namespace", "project", b"project", "project_id", b"project_id"]) -> None: ... + +global___DatastoreTable = DatastoreTable diff --git a/sdk/python/feast/protos/feast/core/DatastoreTable_pb2_grpc.py b/sdk/python/feast/protos/feast/core/DatastoreTable_pb2_grpc.py new file mode 100644 index 0000000000..2daafffebf --- /dev/null +++ b/sdk/python/feast/protos/feast/core/DatastoreTable_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + diff --git a/sdk/python/feast/protos/feast/core/DynamoDBTable_pb2.py b/sdk/python/feast/protos/feast/core/DynamoDBTable_pb2.py new file mode 100644 index 0000000000..34b813f39a --- /dev/null +++ b/sdk/python/feast/protos/feast/core/DynamoDBTable_pb2.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: feast/core/DynamoDBTable.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1e\x66\x65\x61st/core/DynamoDBTable.proto\x12\nfeast.core\"-\n\rDynamoDBTable\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06region\x18\x02 \x01(\tBW\n\x10\x66\x65\x61st.proto.coreB\x12\x44ynamoDBTableProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'feast.core.DynamoDBTable_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\020feast.proto.coreB\022DynamoDBTableProtoZ/github.com/feast-dev/feast/go/protos/feast/core' + _globals['_DYNAMODBTABLE']._serialized_start=46 + _globals['_DYNAMODBTABLE']._serialized_end=91 +# @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/DynamoDBTable_pb2.pyi b/sdk/python/feast/protos/feast/core/DynamoDBTable_pb2.pyi new file mode 100644 index 0000000000..cd9edd9a03 --- /dev/null +++ b/sdk/python/feast/protos/feast/core/DynamoDBTable_pb2.pyi @@ -0,0 +1,50 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file + +* Copyright 2021 The Feast 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 +* +* https://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 builtins +import google.protobuf.descriptor +import google.protobuf.message +import sys + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class DynamoDBTable(google.protobuf.message.Message): + """Represents a DynamoDB table""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + REGION_FIELD_NUMBER: builtins.int + name: builtins.str + """Name of the table""" + region: builtins.str + """Region of the table""" + def __init__( + self, + *, + name: builtins.str = ..., + region: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["name", b"name", "region", b"region"]) -> None: ... + +global___DynamoDBTable = DynamoDBTable diff --git a/sdk/python/feast/protos/feast/core/DynamoDBTable_pb2_grpc.py b/sdk/python/feast/protos/feast/core/DynamoDBTable_pb2_grpc.py new file mode 100644 index 0000000000..2daafffebf --- /dev/null +++ b/sdk/python/feast/protos/feast/core/DynamoDBTable_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + diff --git a/sdk/python/feast/protos/feast/core/Entity_pb2.py b/sdk/python/feast/protos/feast/core/Entity_pb2.py new file mode 100644 index 0000000000..5a192854ca --- /dev/null +++ b/sdk/python/feast/protos/feast/core/Entity_pb2.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: feast/core/Entity.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from feast.protos.feast.types import Value_pb2 as feast_dot_types_dot_Value__pb2 +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17\x66\x65\x61st/core/Entity.proto\x12\nfeast.core\x1a\x17\x66\x65\x61st/types/Value.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"V\n\x06\x45ntity\x12&\n\x04spec\x18\x01 \x01(\x0b\x32\x18.feast.core.EntitySpecV2\x12$\n\x04meta\x18\x02 \x01(\x0b\x32\x16.feast.core.EntityMeta\"\xf3\x01\n\x0c\x45ntitySpecV2\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\t \x01(\t\x12/\n\nvalue_type\x18\x02 \x01(\x0e\x32\x1b.feast.types.ValueType.Enum\x12\x13\n\x0b\x64\x65scription\x18\x03 \x01(\t\x12\x10\n\x08join_key\x18\x04 \x01(\t\x12\x30\n\x04tags\x18\x08 \x03(\x0b\x32\".feast.core.EntitySpecV2.TagsEntry\x12\r\n\x05owner\x18\n \x01(\t\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x7f\n\nEntityMeta\x12\x35\n\x11\x63reated_timestamp\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12:\n\x16last_updated_timestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.TimestampBP\n\x10\x66\x65\x61st.proto.coreB\x0b\x45ntityProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'feast.core.Entity_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\020feast.proto.coreB\013EntityProtoZ/github.com/feast-dev/feast/go/protos/feast/core' + _globals['_ENTITYSPECV2_TAGSENTRY']._options = None + _globals['_ENTITYSPECV2_TAGSENTRY']._serialized_options = b'8\001' + _globals['_ENTITY']._serialized_start=97 + _globals['_ENTITY']._serialized_end=183 + _globals['_ENTITYSPECV2']._serialized_start=186 + _globals['_ENTITYSPECV2']._serialized_end=429 + _globals['_ENTITYSPECV2_TAGSENTRY']._serialized_start=386 + _globals['_ENTITYSPECV2_TAGSENTRY']._serialized_end=429 + _globals['_ENTITYMETA']._serialized_start=431 + _globals['_ENTITYMETA']._serialized_end=558 +# @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/Entity_pb2.pyi b/sdk/python/feast/protos/feast/core/Entity_pb2.pyi new file mode 100644 index 0000000000..732b3e1032 --- /dev/null +++ b/sdk/python/feast/protos/feast/core/Entity_pb2.pyi @@ -0,0 +1,130 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file + +* Copyright 2020 The Feast 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 +* +* https://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 builtins +import collections.abc +import feast.types.Value_pb2 +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.message +import google.protobuf.timestamp_pb2 +import sys + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class Entity(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + SPEC_FIELD_NUMBER: builtins.int + META_FIELD_NUMBER: builtins.int + @property + def spec(self) -> global___EntitySpecV2: + """User-specified specifications of this entity.""" + @property + def meta(self) -> global___EntityMeta: + """System-populated metadata for this entity.""" + def __init__( + self, + *, + spec: global___EntitySpecV2 | None = ..., + meta: global___EntityMeta | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["meta", b"meta", "spec", b"spec"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["meta", b"meta", "spec", b"spec"]) -> None: ... + +global___Entity = Entity + +class EntitySpecV2(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class TagsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + NAME_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + VALUE_TYPE_FIELD_NUMBER: builtins.int + DESCRIPTION_FIELD_NUMBER: builtins.int + JOIN_KEY_FIELD_NUMBER: builtins.int + TAGS_FIELD_NUMBER: builtins.int + OWNER_FIELD_NUMBER: builtins.int + name: builtins.str + """Name of the entity.""" + project: builtins.str + """Name of Feast project that this feature table belongs to.""" + value_type: feast.types.Value_pb2.ValueType.Enum.ValueType + """Type of the entity.""" + description: builtins.str + """Description of the entity.""" + join_key: builtins.str + """Join key for the entity (i.e. name of the column the entity maps to).""" + @property + def tags(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: + """User defined metadata""" + owner: builtins.str + """Owner of the entity.""" + def __init__( + self, + *, + name: builtins.str = ..., + project: builtins.str = ..., + value_type: feast.types.Value_pb2.ValueType.Enum.ValueType = ..., + description: builtins.str = ..., + join_key: builtins.str = ..., + tags: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + owner: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["description", b"description", "join_key", b"join_key", "name", b"name", "owner", b"owner", "project", b"project", "tags", b"tags", "value_type", b"value_type"]) -> None: ... + +global___EntitySpecV2 = EntitySpecV2 + +class EntityMeta(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + CREATED_TIMESTAMP_FIELD_NUMBER: builtins.int + LAST_UPDATED_TIMESTAMP_FIELD_NUMBER: builtins.int + @property + def created_timestamp(self) -> google.protobuf.timestamp_pb2.Timestamp: ... + @property + def last_updated_timestamp(self) -> google.protobuf.timestamp_pb2.Timestamp: ... + def __init__( + self, + *, + created_timestamp: google.protobuf.timestamp_pb2.Timestamp | None = ..., + last_updated_timestamp: google.protobuf.timestamp_pb2.Timestamp | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["created_timestamp", b"created_timestamp", "last_updated_timestamp", b"last_updated_timestamp"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["created_timestamp", b"created_timestamp", "last_updated_timestamp", b"last_updated_timestamp"]) -> None: ... + +global___EntityMeta = EntityMeta diff --git a/sdk/python/feast/protos/feast/core/Entity_pb2_grpc.py b/sdk/python/feast/protos/feast/core/Entity_pb2_grpc.py new file mode 100644 index 0000000000..2daafffebf --- /dev/null +++ b/sdk/python/feast/protos/feast/core/Entity_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + diff --git a/sdk/python/feast/protos/feast/core/FeatureService_pb2.py b/sdk/python/feast/protos/feast/core/FeatureService_pb2.py new file mode 100644 index 0000000000..cf6ac46ac5 --- /dev/null +++ b/sdk/python/feast/protos/feast/core/FeatureService_pb2.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: feast/core/FeatureService.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 +from feast.protos.feast.core import FeatureViewProjection_pb2 as feast_dot_core_dot_FeatureViewProjection__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1f\x66\x65\x61st/core/FeatureService.proto\x12\nfeast.core\x1a\x1fgoogle/protobuf/timestamp.proto\x1a&feast/core/FeatureViewProjection.proto\"l\n\x0e\x46\x65\x61tureService\x12,\n\x04spec\x18\x01 \x01(\x0b\x32\x1e.feast.core.FeatureServiceSpec\x12,\n\x04meta\x18\x02 \x01(\x0b\x32\x1e.feast.core.FeatureServiceMeta\"\xa4\x02\n\x12\x46\x65\x61tureServiceSpec\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x33\n\x08\x66\x65\x61tures\x18\x03 \x03(\x0b\x32!.feast.core.FeatureViewProjection\x12\x36\n\x04tags\x18\x04 \x03(\x0b\x32(.feast.core.FeatureServiceSpec.TagsEntry\x12\x13\n\x0b\x64\x65scription\x18\x05 \x01(\t\x12\r\n\x05owner\x18\x06 \x01(\t\x12\x31\n\x0elogging_config\x18\x07 \x01(\x0b\x32\x19.feast.core.LoggingConfig\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x87\x01\n\x12\x46\x65\x61tureServiceMeta\x12\x35\n\x11\x63reated_timestamp\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12:\n\x16last_updated_timestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\x9a\x07\n\rLoggingConfig\x12\x13\n\x0bsample_rate\x18\x01 \x01(\x02\x12\x45\n\x10\x66ile_destination\x18\x03 \x01(\x0b\x32).feast.core.LoggingConfig.FileDestinationH\x00\x12M\n\x14\x62igquery_destination\x18\x04 \x01(\x0b\x32-.feast.core.LoggingConfig.BigQueryDestinationH\x00\x12M\n\x14redshift_destination\x18\x05 \x01(\x0b\x32-.feast.core.LoggingConfig.RedshiftDestinationH\x00\x12O\n\x15snowflake_destination\x18\x06 \x01(\x0b\x32..feast.core.LoggingConfig.SnowflakeDestinationH\x00\x12I\n\x12\x63ustom_destination\x18\x07 \x01(\x0b\x32+.feast.core.LoggingConfig.CustomDestinationH\x00\x12I\n\x12\x61thena_destination\x18\x08 \x01(\x0b\x32+.feast.core.LoggingConfig.AthenaDestinationH\x00\x1aS\n\x0f\x46ileDestination\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x1c\n\x14s3_endpoint_override\x18\x02 \x01(\t\x12\x14\n\x0cpartition_by\x18\x03 \x03(\t\x1a(\n\x13\x42igQueryDestination\x12\x11\n\ttable_ref\x18\x01 \x01(\t\x1a)\n\x13RedshiftDestination\x12\x12\n\ntable_name\x18\x01 \x01(\t\x1a\'\n\x11\x41thenaDestination\x12\x12\n\ntable_name\x18\x01 \x01(\t\x1a*\n\x14SnowflakeDestination\x12\x12\n\ntable_name\x18\x01 \x01(\t\x1a\x99\x01\n\x11\x43ustomDestination\x12\x0c\n\x04kind\x18\x01 \x01(\t\x12G\n\x06\x63onfig\x18\x02 \x03(\x0b\x32\x37.feast.core.LoggingConfig.CustomDestination.ConfigEntry\x1a-\n\x0b\x43onfigEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\r\n\x0b\x64\x65stinationBX\n\x10\x66\x65\x61st.proto.coreB\x13\x46\x65\x61tureServiceProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'feast.core.FeatureService_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\020feast.proto.coreB\023FeatureServiceProtoZ/github.com/feast-dev/feast/go/protos/feast/core' + _globals['_FEATURESERVICESPEC_TAGSENTRY']._options = None + _globals['_FEATURESERVICESPEC_TAGSENTRY']._serialized_options = b'8\001' + _globals['_LOGGINGCONFIG_CUSTOMDESTINATION_CONFIGENTRY']._options = None + _globals['_LOGGINGCONFIG_CUSTOMDESTINATION_CONFIGENTRY']._serialized_options = b'8\001' + _globals['_FEATURESERVICE']._serialized_start=120 + _globals['_FEATURESERVICE']._serialized_end=228 + _globals['_FEATURESERVICESPEC']._serialized_start=231 + _globals['_FEATURESERVICESPEC']._serialized_end=523 + _globals['_FEATURESERVICESPEC_TAGSENTRY']._serialized_start=480 + _globals['_FEATURESERVICESPEC_TAGSENTRY']._serialized_end=523 + _globals['_FEATURESERVICEMETA']._serialized_start=526 + _globals['_FEATURESERVICEMETA']._serialized_end=661 + _globals['_LOGGINGCONFIG']._serialized_start=664 + _globals['_LOGGINGCONFIG']._serialized_end=1586 + _globals['_LOGGINGCONFIG_FILEDESTINATION']._serialized_start=1162 + _globals['_LOGGINGCONFIG_FILEDESTINATION']._serialized_end=1245 + _globals['_LOGGINGCONFIG_BIGQUERYDESTINATION']._serialized_start=1247 + _globals['_LOGGINGCONFIG_BIGQUERYDESTINATION']._serialized_end=1287 + _globals['_LOGGINGCONFIG_REDSHIFTDESTINATION']._serialized_start=1289 + _globals['_LOGGINGCONFIG_REDSHIFTDESTINATION']._serialized_end=1330 + _globals['_LOGGINGCONFIG_ATHENADESTINATION']._serialized_start=1332 + _globals['_LOGGINGCONFIG_ATHENADESTINATION']._serialized_end=1371 + _globals['_LOGGINGCONFIG_SNOWFLAKEDESTINATION']._serialized_start=1373 + _globals['_LOGGINGCONFIG_SNOWFLAKEDESTINATION']._serialized_end=1415 + _globals['_LOGGINGCONFIG_CUSTOMDESTINATION']._serialized_start=1418 + _globals['_LOGGINGCONFIG_CUSTOMDESTINATION']._serialized_end=1571 + _globals['_LOGGINGCONFIG_CUSTOMDESTINATION_CONFIGENTRY']._serialized_start=1526 + _globals['_LOGGINGCONFIG_CUSTOMDESTINATION_CONFIGENTRY']._serialized_end=1571 +# @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/FeatureService_pb2.pyi b/sdk/python/feast/protos/feast/core/FeatureService_pb2.pyi new file mode 100644 index 0000000000..b3305b72df --- /dev/null +++ b/sdk/python/feast/protos/feast/core/FeatureService_pb2.pyi @@ -0,0 +1,266 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" +import builtins +import collections.abc +import feast.core.FeatureViewProjection_pb2 +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.message +import google.protobuf.timestamp_pb2 +import sys + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class FeatureService(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + SPEC_FIELD_NUMBER: builtins.int + META_FIELD_NUMBER: builtins.int + @property + def spec(self) -> global___FeatureServiceSpec: + """User-specified specifications of this feature service.""" + @property + def meta(self) -> global___FeatureServiceMeta: + """System-populated metadata for this feature service.""" + def __init__( + self, + *, + spec: global___FeatureServiceSpec | None = ..., + meta: global___FeatureServiceMeta | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["meta", b"meta", "spec", b"spec"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["meta", b"meta", "spec", b"spec"]) -> None: ... + +global___FeatureService = FeatureService + +class FeatureServiceSpec(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class TagsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + NAME_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + FEATURES_FIELD_NUMBER: builtins.int + TAGS_FIELD_NUMBER: builtins.int + DESCRIPTION_FIELD_NUMBER: builtins.int + OWNER_FIELD_NUMBER: builtins.int + LOGGING_CONFIG_FIELD_NUMBER: builtins.int + name: builtins.str + """Name of the Feature Service. Must be unique. Not updated.""" + project: builtins.str + """Name of Feast project that this Feature Service belongs to.""" + @property + def features(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.FeatureViewProjection_pb2.FeatureViewProjection]: + """Represents a projection that's to be applied on top of the FeatureView. + Contains data such as the features to use from a FeatureView. + """ + @property + def tags(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: + """User defined metadata""" + description: builtins.str + """Description of the feature service.""" + owner: builtins.str + """Owner of the feature service.""" + @property + def logging_config(self) -> global___LoggingConfig: + """(optional) if provided logging will be enabled for this feature service.""" + def __init__( + self, + *, + name: builtins.str = ..., + project: builtins.str = ..., + features: collections.abc.Iterable[feast.core.FeatureViewProjection_pb2.FeatureViewProjection] | None = ..., + tags: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + description: builtins.str = ..., + owner: builtins.str = ..., + logging_config: global___LoggingConfig | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["logging_config", b"logging_config"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["description", b"description", "features", b"features", "logging_config", b"logging_config", "name", b"name", "owner", b"owner", "project", b"project", "tags", b"tags"]) -> None: ... + +global___FeatureServiceSpec = FeatureServiceSpec + +class FeatureServiceMeta(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + CREATED_TIMESTAMP_FIELD_NUMBER: builtins.int + LAST_UPDATED_TIMESTAMP_FIELD_NUMBER: builtins.int + @property + def created_timestamp(self) -> google.protobuf.timestamp_pb2.Timestamp: + """Time where this Feature Service is created""" + @property + def last_updated_timestamp(self) -> google.protobuf.timestamp_pb2.Timestamp: + """Time where this Feature Service is last updated""" + def __init__( + self, + *, + created_timestamp: google.protobuf.timestamp_pb2.Timestamp | None = ..., + last_updated_timestamp: google.protobuf.timestamp_pb2.Timestamp | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["created_timestamp", b"created_timestamp", "last_updated_timestamp", b"last_updated_timestamp"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["created_timestamp", b"created_timestamp", "last_updated_timestamp", b"last_updated_timestamp"]) -> None: ... + +global___FeatureServiceMeta = FeatureServiceMeta + +class LoggingConfig(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class FileDestination(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + PATH_FIELD_NUMBER: builtins.int + S3_ENDPOINT_OVERRIDE_FIELD_NUMBER: builtins.int + PARTITION_BY_FIELD_NUMBER: builtins.int + path: builtins.str + s3_endpoint_override: builtins.str + @property + def partition_by(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: + """column names to use for partitioning""" + def __init__( + self, + *, + path: builtins.str = ..., + s3_endpoint_override: builtins.str = ..., + partition_by: collections.abc.Iterable[builtins.str] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["partition_by", b"partition_by", "path", b"path", "s3_endpoint_override", b"s3_endpoint_override"]) -> None: ... + + class BigQueryDestination(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + TABLE_REF_FIELD_NUMBER: builtins.int + table_ref: builtins.str + """Full table reference in the form of [project:dataset.table]""" + def __init__( + self, + *, + table_ref: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["table_ref", b"table_ref"]) -> None: ... + + class RedshiftDestination(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + TABLE_NAME_FIELD_NUMBER: builtins.int + table_name: builtins.str + """Destination table name. ClusterId and database will be taken from an offline store config""" + def __init__( + self, + *, + table_name: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["table_name", b"table_name"]) -> None: ... + + class AthenaDestination(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + TABLE_NAME_FIELD_NUMBER: builtins.int + table_name: builtins.str + """Destination table name. data_source and database will be taken from an offline store config""" + def __init__( + self, + *, + table_name: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["table_name", b"table_name"]) -> None: ... + + class SnowflakeDestination(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + TABLE_NAME_FIELD_NUMBER: builtins.int + table_name: builtins.str + """Destination table name. Schema and database will be taken from an offline store config""" + def __init__( + self, + *, + table_name: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["table_name", b"table_name"]) -> None: ... + + class CustomDestination(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class ConfigEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + KIND_FIELD_NUMBER: builtins.int + CONFIG_FIELD_NUMBER: builtins.int + kind: builtins.str + @property + def config(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: ... + def __init__( + self, + *, + kind: builtins.str = ..., + config: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["config", b"config", "kind", b"kind"]) -> None: ... + + SAMPLE_RATE_FIELD_NUMBER: builtins.int + FILE_DESTINATION_FIELD_NUMBER: builtins.int + BIGQUERY_DESTINATION_FIELD_NUMBER: builtins.int + REDSHIFT_DESTINATION_FIELD_NUMBER: builtins.int + SNOWFLAKE_DESTINATION_FIELD_NUMBER: builtins.int + CUSTOM_DESTINATION_FIELD_NUMBER: builtins.int + ATHENA_DESTINATION_FIELD_NUMBER: builtins.int + sample_rate: builtins.float + @property + def file_destination(self) -> global___LoggingConfig.FileDestination: ... + @property + def bigquery_destination(self) -> global___LoggingConfig.BigQueryDestination: ... + @property + def redshift_destination(self) -> global___LoggingConfig.RedshiftDestination: ... + @property + def snowflake_destination(self) -> global___LoggingConfig.SnowflakeDestination: ... + @property + def custom_destination(self) -> global___LoggingConfig.CustomDestination: ... + @property + def athena_destination(self) -> global___LoggingConfig.AthenaDestination: ... + def __init__( + self, + *, + sample_rate: builtins.float = ..., + file_destination: global___LoggingConfig.FileDestination | None = ..., + bigquery_destination: global___LoggingConfig.BigQueryDestination | None = ..., + redshift_destination: global___LoggingConfig.RedshiftDestination | None = ..., + snowflake_destination: global___LoggingConfig.SnowflakeDestination | None = ..., + custom_destination: global___LoggingConfig.CustomDestination | None = ..., + athena_destination: global___LoggingConfig.AthenaDestination | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["athena_destination", b"athena_destination", "bigquery_destination", b"bigquery_destination", "custom_destination", b"custom_destination", "destination", b"destination", "file_destination", b"file_destination", "redshift_destination", b"redshift_destination", "snowflake_destination", b"snowflake_destination"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["athena_destination", b"athena_destination", "bigquery_destination", b"bigquery_destination", "custom_destination", b"custom_destination", "destination", b"destination", "file_destination", b"file_destination", "redshift_destination", b"redshift_destination", "sample_rate", b"sample_rate", "snowflake_destination", b"snowflake_destination"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal["destination", b"destination"]) -> typing_extensions.Literal["file_destination", "bigquery_destination", "redshift_destination", "snowflake_destination", "custom_destination", "athena_destination"] | None: ... + +global___LoggingConfig = LoggingConfig diff --git a/sdk/python/feast/protos/feast/core/FeatureService_pb2_grpc.py b/sdk/python/feast/protos/feast/core/FeatureService_pb2_grpc.py new file mode 100644 index 0000000000..2daafffebf --- /dev/null +++ b/sdk/python/feast/protos/feast/core/FeatureService_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + diff --git a/sdk/python/feast/protos/feast/core/FeatureTable_pb2.py b/sdk/python/feast/protos/feast/core/FeatureTable_pb2.py new file mode 100644 index 0000000000..713e72b5d3 --- /dev/null +++ b/sdk/python/feast/protos/feast/core/FeatureTable_pb2.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: feast/core/FeatureTable.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import duration_pb2 as google_dot_protobuf_dot_duration__pb2 +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 +from feast.protos.feast.core import DataSource_pb2 as feast_dot_core_dot_DataSource__pb2 +from feast.protos.feast.core import Feature_pb2 as feast_dot_core_dot_Feature__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1d\x66\x65\x61st/core/FeatureTable.proto\x12\nfeast.core\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1b\x66\x65\x61st/core/DataSource.proto\x1a\x18\x66\x65\x61st/core/Feature.proto\"f\n\x0c\x46\x65\x61tureTable\x12*\n\x04spec\x18\x01 \x01(\x0b\x32\x1c.feast.core.FeatureTableSpec\x12*\n\x04meta\x18\x02 \x01(\x0b\x32\x1c.feast.core.FeatureTableMeta\"\xe2\x02\n\x10\x46\x65\x61tureTableSpec\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\t \x01(\t\x12\x10\n\x08\x65ntities\x18\x03 \x03(\t\x12+\n\x08\x66\x65\x61tures\x18\x04 \x03(\x0b\x32\x19.feast.core.FeatureSpecV2\x12\x38\n\x06labels\x18\x05 \x03(\x0b\x32(.feast.core.FeatureTableSpec.LabelsEntry\x12*\n\x07max_age\x18\x06 \x01(\x0b\x32\x19.google.protobuf.Duration\x12,\n\x0c\x62\x61tch_source\x18\x07 \x01(\x0b\x32\x16.feast.core.DataSource\x12-\n\rstream_source\x18\x08 \x01(\x0b\x32\x16.feast.core.DataSource\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xa5\x01\n\x10\x46\x65\x61tureTableMeta\x12\x35\n\x11\x63reated_timestamp\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12:\n\x16last_updated_timestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x10\n\x08revision\x18\x03 \x01(\x03\x12\x0c\n\x04hash\x18\x04 \x01(\tBV\n\x10\x66\x65\x61st.proto.coreB\x11\x46\x65\x61tureTableProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'feast.core.FeatureTable_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\020feast.proto.coreB\021FeatureTableProtoZ/github.com/feast-dev/feast/go/protos/feast/core' + _globals['_FEATURETABLESPEC_LABELSENTRY']._options = None + _globals['_FEATURETABLESPEC_LABELSENTRY']._serialized_options = b'8\001' + _globals['_FEATURETABLE']._serialized_start=165 + _globals['_FEATURETABLE']._serialized_end=267 + _globals['_FEATURETABLESPEC']._serialized_start=270 + _globals['_FEATURETABLESPEC']._serialized_end=624 + _globals['_FEATURETABLESPEC_LABELSENTRY']._serialized_start=579 + _globals['_FEATURETABLESPEC_LABELSENTRY']._serialized_end=624 + _globals['_FEATURETABLEMETA']._serialized_start=627 + _globals['_FEATURETABLEMETA']._serialized_end=792 +# @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/FeatureTable_pb2.pyi b/sdk/python/feast/protos/feast/core/FeatureTable_pb2.pyi new file mode 100644 index 0000000000..dd41c2d214 --- /dev/null +++ b/sdk/python/feast/protos/feast/core/FeatureTable_pb2.pyi @@ -0,0 +1,166 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file + +Copyright 2020 The Feast 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 + + https://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 builtins +import collections.abc +import feast.core.DataSource_pb2 +import feast.core.Feature_pb2 +import google.protobuf.descriptor +import google.protobuf.duration_pb2 +import google.protobuf.internal.containers +import google.protobuf.message +import google.protobuf.timestamp_pb2 +import sys + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class FeatureTable(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + SPEC_FIELD_NUMBER: builtins.int + META_FIELD_NUMBER: builtins.int + @property + def spec(self) -> global___FeatureTableSpec: + """User-specified specifications of this feature table.""" + @property + def meta(self) -> global___FeatureTableMeta: + """System-populated metadata for this feature table.""" + def __init__( + self, + *, + spec: global___FeatureTableSpec | None = ..., + meta: global___FeatureTableMeta | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["meta", b"meta", "spec", b"spec"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["meta", b"meta", "spec", b"spec"]) -> None: ... + +global___FeatureTable = FeatureTable + +class FeatureTableSpec(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class LabelsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + NAME_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + ENTITIES_FIELD_NUMBER: builtins.int + FEATURES_FIELD_NUMBER: builtins.int + LABELS_FIELD_NUMBER: builtins.int + MAX_AGE_FIELD_NUMBER: builtins.int + BATCH_SOURCE_FIELD_NUMBER: builtins.int + STREAM_SOURCE_FIELD_NUMBER: builtins.int + name: builtins.str + """Name of the feature table. Must be unique. Not updated.""" + project: builtins.str + """Name of Feast project that this feature table belongs to.""" + @property + def entities(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: + """List names of entities to associate with the Features defined in this + Feature Table. Not updatable. + """ + @property + def features(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.Feature_pb2.FeatureSpecV2]: + """List of features specifications for each feature defined with this feature table.""" + @property + def labels(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: + """User defined metadata""" + @property + def max_age(self) -> google.protobuf.duration_pb2.Duration: + """Features in this feature table can only be retrieved from online serving + younger than max age. Age is measured as the duration of time between + the feature's event timestamp and when the feature is retrieved + Feature values outside max age will be returned as unset values and indicated to end user + """ + @property + def batch_source(self) -> feast.core.DataSource_pb2.DataSource: + """Batch/Offline DataSource to source batch/offline feature data. + Only batch DataSource can be specified + (ie source type should start with 'BATCH_') + """ + @property + def stream_source(self) -> feast.core.DataSource_pb2.DataSource: + """Stream/Online DataSource to source stream/online feature data. + Only stream DataSource can be specified + (ie source type should start with 'STREAM_') + """ + def __init__( + self, + *, + name: builtins.str = ..., + project: builtins.str = ..., + entities: collections.abc.Iterable[builtins.str] | None = ..., + features: collections.abc.Iterable[feast.core.Feature_pb2.FeatureSpecV2] | None = ..., + labels: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + max_age: google.protobuf.duration_pb2.Duration | None = ..., + batch_source: feast.core.DataSource_pb2.DataSource | None = ..., + stream_source: feast.core.DataSource_pb2.DataSource | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["batch_source", b"batch_source", "max_age", b"max_age", "stream_source", b"stream_source"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["batch_source", b"batch_source", "entities", b"entities", "features", b"features", "labels", b"labels", "max_age", b"max_age", "name", b"name", "project", b"project", "stream_source", b"stream_source"]) -> None: ... + +global___FeatureTableSpec = FeatureTableSpec + +class FeatureTableMeta(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + CREATED_TIMESTAMP_FIELD_NUMBER: builtins.int + LAST_UPDATED_TIMESTAMP_FIELD_NUMBER: builtins.int + REVISION_FIELD_NUMBER: builtins.int + HASH_FIELD_NUMBER: builtins.int + @property + def created_timestamp(self) -> google.protobuf.timestamp_pb2.Timestamp: + """Time where this Feature Table is created""" + @property + def last_updated_timestamp(self) -> google.protobuf.timestamp_pb2.Timestamp: + """Time where this Feature Table is last updated""" + revision: builtins.int + """Auto incrementing revision no. of this Feature Table""" + hash: builtins.str + """Hash entities, features, batch_source and stream_source to inform JobService if + jobs should be restarted should hash change + """ + def __init__( + self, + *, + created_timestamp: google.protobuf.timestamp_pb2.Timestamp | None = ..., + last_updated_timestamp: google.protobuf.timestamp_pb2.Timestamp | None = ..., + revision: builtins.int = ..., + hash: builtins.str = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["created_timestamp", b"created_timestamp", "last_updated_timestamp", b"last_updated_timestamp"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["created_timestamp", b"created_timestamp", "hash", b"hash", "last_updated_timestamp", b"last_updated_timestamp", "revision", b"revision"]) -> None: ... + +global___FeatureTableMeta = FeatureTableMeta diff --git a/sdk/python/feast/protos/feast/core/FeatureTable_pb2_grpc.py b/sdk/python/feast/protos/feast/core/FeatureTable_pb2_grpc.py new file mode 100644 index 0000000000..2daafffebf --- /dev/null +++ b/sdk/python/feast/protos/feast/core/FeatureTable_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + diff --git a/sdk/python/feast/protos/feast/core/FeatureViewProjection_pb2.py b/sdk/python/feast/protos/feast/core/FeatureViewProjection_pb2.py new file mode 100644 index 0000000000..286f511658 --- /dev/null +++ b/sdk/python/feast/protos/feast/core/FeatureViewProjection_pb2.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: feast/core/FeatureViewProjection.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from feast.protos.feast.core import Feature_pb2 as feast_dot_core_dot_Feature__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n&feast/core/FeatureViewProjection.proto\x12\nfeast.core\x1a\x18\x66\x65\x61st/core/Feature.proto\"\x83\x02\n\x15\x46\x65\x61tureViewProjection\x12\x19\n\x11\x66\x65\x61ture_view_name\x18\x01 \x01(\t\x12\x1f\n\x17\x66\x65\x61ture_view_name_alias\x18\x03 \x01(\t\x12\x32\n\x0f\x66\x65\x61ture_columns\x18\x02 \x03(\x0b\x32\x19.feast.core.FeatureSpecV2\x12G\n\x0cjoin_key_map\x18\x04 \x03(\x0b\x32\x31.feast.core.FeatureViewProjection.JoinKeyMapEntry\x1a\x31\n\x0fJoinKeyMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42Z\n\x10\x66\x65\x61st.proto.coreB\x15\x46\x65\x61tureReferenceProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'feast.core.FeatureViewProjection_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\020feast.proto.coreB\025FeatureReferenceProtoZ/github.com/feast-dev/feast/go/protos/feast/core' + _globals['_FEATUREVIEWPROJECTION_JOINKEYMAPENTRY']._options = None + _globals['_FEATUREVIEWPROJECTION_JOINKEYMAPENTRY']._serialized_options = b'8\001' + _globals['_FEATUREVIEWPROJECTION']._serialized_start=81 + _globals['_FEATUREVIEWPROJECTION']._serialized_end=340 + _globals['_FEATUREVIEWPROJECTION_JOINKEYMAPENTRY']._serialized_start=291 + _globals['_FEATUREVIEWPROJECTION_JOINKEYMAPENTRY']._serialized_end=340 +# @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/FeatureViewProjection_pb2.pyi b/sdk/python/feast/protos/feast/core/FeatureViewProjection_pb2.pyi new file mode 100644 index 0000000000..2c0a298e14 --- /dev/null +++ b/sdk/python/feast/protos/feast/core/FeatureViewProjection_pb2.pyi @@ -0,0 +1,66 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" +import builtins +import collections.abc +import feast.core.Feature_pb2 +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.message +import sys + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class FeatureViewProjection(google.protobuf.message.Message): + """A projection to be applied on top of a FeatureView. + Contains the modifications to a FeatureView such as the features subset to use. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class JoinKeyMapEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + FEATURE_VIEW_NAME_FIELD_NUMBER: builtins.int + FEATURE_VIEW_NAME_ALIAS_FIELD_NUMBER: builtins.int + FEATURE_COLUMNS_FIELD_NUMBER: builtins.int + JOIN_KEY_MAP_FIELD_NUMBER: builtins.int + feature_view_name: builtins.str + """The feature view name""" + feature_view_name_alias: builtins.str + """Alias for feature view name""" + @property + def feature_columns(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.Feature_pb2.FeatureSpecV2]: + """The features of the feature view that are a part of the feature reference.""" + @property + def join_key_map(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: + """Map for entity join_key overrides of feature data entity join_key to entity data join_key""" + def __init__( + self, + *, + feature_view_name: builtins.str = ..., + feature_view_name_alias: builtins.str = ..., + feature_columns: collections.abc.Iterable[feast.core.Feature_pb2.FeatureSpecV2] | None = ..., + join_key_map: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["feature_columns", b"feature_columns", "feature_view_name", b"feature_view_name", "feature_view_name_alias", b"feature_view_name_alias", "join_key_map", b"join_key_map"]) -> None: ... + +global___FeatureViewProjection = FeatureViewProjection diff --git a/sdk/python/feast/protos/feast/core/FeatureViewProjection_pb2_grpc.py b/sdk/python/feast/protos/feast/core/FeatureViewProjection_pb2_grpc.py new file mode 100644 index 0000000000..2daafffebf --- /dev/null +++ b/sdk/python/feast/protos/feast/core/FeatureViewProjection_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + diff --git a/sdk/python/feast/protos/feast/core/FeatureView_pb2.py b/sdk/python/feast/protos/feast/core/FeatureView_pb2.py new file mode 100644 index 0000000000..f1480593d9 --- /dev/null +++ b/sdk/python/feast/protos/feast/core/FeatureView_pb2.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: feast/core/FeatureView.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import duration_pb2 as google_dot_protobuf_dot_duration__pb2 +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 +from feast.protos.feast.core import DataSource_pb2 as feast_dot_core_dot_DataSource__pb2 +from feast.protos.feast.core import Feature_pb2 as feast_dot_core_dot_Feature__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1c\x66\x65\x61st/core/FeatureView.proto\x12\nfeast.core\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1b\x66\x65\x61st/core/DataSource.proto\x1a\x18\x66\x65\x61st/core/Feature.proto\"c\n\x0b\x46\x65\x61tureView\x12)\n\x04spec\x18\x01 \x01(\x0b\x32\x1b.feast.core.FeatureViewSpec\x12)\n\x04meta\x18\x02 \x01(\x0b\x32\x1b.feast.core.FeatureViewMeta\"\xbd\x03\n\x0f\x46\x65\x61tureViewSpec\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x10\n\x08\x65ntities\x18\x03 \x03(\t\x12+\n\x08\x66\x65\x61tures\x18\x04 \x03(\x0b\x32\x19.feast.core.FeatureSpecV2\x12\x31\n\x0e\x65ntity_columns\x18\x0c \x03(\x0b\x32\x19.feast.core.FeatureSpecV2\x12\x13\n\x0b\x64\x65scription\x18\n \x01(\t\x12\x33\n\x04tags\x18\x05 \x03(\x0b\x32%.feast.core.FeatureViewSpec.TagsEntry\x12\r\n\x05owner\x18\x0b \x01(\t\x12&\n\x03ttl\x18\x06 \x01(\x0b\x32\x19.google.protobuf.Duration\x12,\n\x0c\x62\x61tch_source\x18\x07 \x01(\x0b\x32\x16.feast.core.DataSource\x12-\n\rstream_source\x18\t \x01(\x0b\x32\x16.feast.core.DataSource\x12\x0e\n\x06online\x18\x08 \x01(\x08\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xcc\x01\n\x0f\x46\x65\x61tureViewMeta\x12\x35\n\x11\x63reated_timestamp\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12:\n\x16last_updated_timestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x46\n\x19materialization_intervals\x18\x03 \x03(\x0b\x32#.feast.core.MaterializationInterval\"w\n\x17MaterializationInterval\x12.\n\nstart_time\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12,\n\x08\x65nd_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.TimestampBU\n\x10\x66\x65\x61st.proto.coreB\x10\x46\x65\x61tureViewProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'feast.core.FeatureView_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\020feast.proto.coreB\020FeatureViewProtoZ/github.com/feast-dev/feast/go/protos/feast/core' + _globals['_FEATUREVIEWSPEC_TAGSENTRY']._options = None + _globals['_FEATUREVIEWSPEC_TAGSENTRY']._serialized_options = b'8\001' + _globals['_FEATUREVIEW']._serialized_start=164 + _globals['_FEATUREVIEW']._serialized_end=263 + _globals['_FEATUREVIEWSPEC']._serialized_start=266 + _globals['_FEATUREVIEWSPEC']._serialized_end=711 + _globals['_FEATUREVIEWSPEC_TAGSENTRY']._serialized_start=668 + _globals['_FEATUREVIEWSPEC_TAGSENTRY']._serialized_end=711 + _globals['_FEATUREVIEWMETA']._serialized_start=714 + _globals['_FEATUREVIEWMETA']._serialized_end=918 + _globals['_MATERIALIZATIONINTERVAL']._serialized_start=920 + _globals['_MATERIALIZATIONINTERVAL']._serialized_end=1039 +# @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/FeatureView_pb2.pyi b/sdk/python/feast/protos/feast/core/FeatureView_pb2.pyi new file mode 100644 index 0000000000..e1d4e2dfee --- /dev/null +++ b/sdk/python/feast/protos/feast/core/FeatureView_pb2.pyi @@ -0,0 +1,194 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file + +Copyright 2020 The Feast 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 + + https://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 builtins +import collections.abc +import feast.core.DataSource_pb2 +import feast.core.Feature_pb2 +import google.protobuf.descriptor +import google.protobuf.duration_pb2 +import google.protobuf.internal.containers +import google.protobuf.message +import google.protobuf.timestamp_pb2 +import sys + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class FeatureView(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + SPEC_FIELD_NUMBER: builtins.int + META_FIELD_NUMBER: builtins.int + @property + def spec(self) -> global___FeatureViewSpec: + """User-specified specifications of this feature view.""" + @property + def meta(self) -> global___FeatureViewMeta: + """System-populated metadata for this feature view.""" + def __init__( + self, + *, + spec: global___FeatureViewSpec | None = ..., + meta: global___FeatureViewMeta | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["meta", b"meta", "spec", b"spec"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["meta", b"meta", "spec", b"spec"]) -> None: ... + +global___FeatureView = FeatureView + +class FeatureViewSpec(google.protobuf.message.Message): + """Next available id: 13 + TODO(adchia): refactor common fields from this and ODFV into separate metadata proto + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class TagsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + NAME_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + ENTITIES_FIELD_NUMBER: builtins.int + FEATURES_FIELD_NUMBER: builtins.int + ENTITY_COLUMNS_FIELD_NUMBER: builtins.int + DESCRIPTION_FIELD_NUMBER: builtins.int + TAGS_FIELD_NUMBER: builtins.int + OWNER_FIELD_NUMBER: builtins.int + TTL_FIELD_NUMBER: builtins.int + BATCH_SOURCE_FIELD_NUMBER: builtins.int + STREAM_SOURCE_FIELD_NUMBER: builtins.int + ONLINE_FIELD_NUMBER: builtins.int + name: builtins.str + """Name of the feature view. Must be unique. Not updated.""" + project: builtins.str + """Name of Feast project that this feature view belongs to.""" + @property + def entities(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: + """List of names of entities associated with this feature view.""" + @property + def features(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.Feature_pb2.FeatureSpecV2]: + """List of specifications for each feature defined as part of this feature view.""" + @property + def entity_columns(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.Feature_pb2.FeatureSpecV2]: + """List of specifications for each entity defined as part of this feature view.""" + description: builtins.str + """Description of the feature view.""" + @property + def tags(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: + """User defined metadata""" + owner: builtins.str + """Owner of the feature view.""" + @property + def ttl(self) -> google.protobuf.duration_pb2.Duration: + """Features in this feature view can only be retrieved from online serving + younger than ttl. Ttl is measured as the duration of time between + the feature's event timestamp and when the feature is retrieved + Feature values outside ttl will be returned as unset values and indicated to end user + """ + @property + def batch_source(self) -> feast.core.DataSource_pb2.DataSource: + """Batch/Offline DataSource where this view can retrieve offline feature data.""" + @property + def stream_source(self) -> feast.core.DataSource_pb2.DataSource: + """Streaming DataSource from where this view can consume "online" feature data.""" + online: builtins.bool + """Whether these features should be served online or not""" + def __init__( + self, + *, + name: builtins.str = ..., + project: builtins.str = ..., + entities: collections.abc.Iterable[builtins.str] | None = ..., + features: collections.abc.Iterable[feast.core.Feature_pb2.FeatureSpecV2] | None = ..., + entity_columns: collections.abc.Iterable[feast.core.Feature_pb2.FeatureSpecV2] | None = ..., + description: builtins.str = ..., + tags: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + owner: builtins.str = ..., + ttl: google.protobuf.duration_pb2.Duration | None = ..., + batch_source: feast.core.DataSource_pb2.DataSource | None = ..., + stream_source: feast.core.DataSource_pb2.DataSource | None = ..., + online: builtins.bool = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["batch_source", b"batch_source", "stream_source", b"stream_source", "ttl", b"ttl"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["batch_source", b"batch_source", "description", b"description", "entities", b"entities", "entity_columns", b"entity_columns", "features", b"features", "name", b"name", "online", b"online", "owner", b"owner", "project", b"project", "stream_source", b"stream_source", "tags", b"tags", "ttl", b"ttl"]) -> None: ... + +global___FeatureViewSpec = FeatureViewSpec + +class FeatureViewMeta(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + CREATED_TIMESTAMP_FIELD_NUMBER: builtins.int + LAST_UPDATED_TIMESTAMP_FIELD_NUMBER: builtins.int + MATERIALIZATION_INTERVALS_FIELD_NUMBER: builtins.int + @property + def created_timestamp(self) -> google.protobuf.timestamp_pb2.Timestamp: + """Time where this Feature View is created""" + @property + def last_updated_timestamp(self) -> google.protobuf.timestamp_pb2.Timestamp: + """Time where this Feature View is last updated""" + @property + def materialization_intervals(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___MaterializationInterval]: + """List of pairs (start_time, end_time) for which this feature view has been materialized.""" + def __init__( + self, + *, + created_timestamp: google.protobuf.timestamp_pb2.Timestamp | None = ..., + last_updated_timestamp: google.protobuf.timestamp_pb2.Timestamp | None = ..., + materialization_intervals: collections.abc.Iterable[global___MaterializationInterval] | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["created_timestamp", b"created_timestamp", "last_updated_timestamp", b"last_updated_timestamp"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["created_timestamp", b"created_timestamp", "last_updated_timestamp", b"last_updated_timestamp", "materialization_intervals", b"materialization_intervals"]) -> None: ... + +global___FeatureViewMeta = FeatureViewMeta + +class MaterializationInterval(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + START_TIME_FIELD_NUMBER: builtins.int + END_TIME_FIELD_NUMBER: builtins.int + @property + def start_time(self) -> google.protobuf.timestamp_pb2.Timestamp: ... + @property + def end_time(self) -> google.protobuf.timestamp_pb2.Timestamp: ... + def __init__( + self, + *, + start_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + end_time: google.protobuf.timestamp_pb2.Timestamp | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["end_time", b"end_time", "start_time", b"start_time"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["end_time", b"end_time", "start_time", b"start_time"]) -> None: ... + +global___MaterializationInterval = MaterializationInterval diff --git a/sdk/python/feast/protos/feast/core/FeatureView_pb2_grpc.py b/sdk/python/feast/protos/feast/core/FeatureView_pb2_grpc.py new file mode 100644 index 0000000000..2daafffebf --- /dev/null +++ b/sdk/python/feast/protos/feast/core/FeatureView_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + diff --git a/sdk/python/feast/protos/feast/core/Feature_pb2.py b/sdk/python/feast/protos/feast/core/Feature_pb2.py new file mode 100644 index 0000000000..dd7c6008ef --- /dev/null +++ b/sdk/python/feast/protos/feast/core/Feature_pb2.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: feast/core/Feature.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from feast.protos.feast.types import Value_pb2 as feast_dot_types_dot_Value__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18\x66\x65\x61st/core/Feature.proto\x12\nfeast.core\x1a\x17\x66\x65\x61st/types/Value.proto\"\xc3\x01\n\rFeatureSpecV2\x12\x0c\n\x04name\x18\x01 \x01(\t\x12/\n\nvalue_type\x18\x02 \x01(\x0e\x32\x1b.feast.types.ValueType.Enum\x12\x31\n\x04tags\x18\x03 \x03(\x0b\x32#.feast.core.FeatureSpecV2.TagsEntry\x12\x13\n\x0b\x64\x65scription\x18\x04 \x01(\t\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42Q\n\x10\x66\x65\x61st.proto.coreB\x0c\x46\x65\x61tureProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'feast.core.Feature_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\020feast.proto.coreB\014FeatureProtoZ/github.com/feast-dev/feast/go/protos/feast/core' + _globals['_FEATURESPECV2_TAGSENTRY']._options = None + _globals['_FEATURESPECV2_TAGSENTRY']._serialized_options = b'8\001' + _globals['_FEATURESPECV2']._serialized_start=66 + _globals['_FEATURESPECV2']._serialized_end=261 + _globals['_FEATURESPECV2_TAGSENTRY']._serialized_start=218 + _globals['_FEATURESPECV2_TAGSENTRY']._serialized_end=261 +# @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/Feature_pb2.pyi b/sdk/python/feast/protos/feast/core/Feature_pb2.pyi new file mode 100644 index 0000000000..f4235b0965 --- /dev/null +++ b/sdk/python/feast/protos/feast/core/Feature_pb2.pyi @@ -0,0 +1,75 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file + +Copyright 2020 The Feast 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 + + https://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 builtins +import collections.abc +import feast.types.Value_pb2 +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.message +import sys + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class FeatureSpecV2(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class TagsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + NAME_FIELD_NUMBER: builtins.int + VALUE_TYPE_FIELD_NUMBER: builtins.int + TAGS_FIELD_NUMBER: builtins.int + DESCRIPTION_FIELD_NUMBER: builtins.int + name: builtins.str + """Name of the feature. Not updatable.""" + value_type: feast.types.Value_pb2.ValueType.Enum.ValueType + """Value type of the feature. Not updatable.""" + @property + def tags(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: + """Tags for user defined metadata on a feature""" + description: builtins.str + """Description of the feature.""" + def __init__( + self, + *, + name: builtins.str = ..., + value_type: feast.types.Value_pb2.ValueType.Enum.ValueType = ..., + tags: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + description: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["description", b"description", "name", b"name", "tags", b"tags", "value_type", b"value_type"]) -> None: ... + +global___FeatureSpecV2 = FeatureSpecV2 diff --git a/sdk/python/feast/protos/feast/core/Feature_pb2_grpc.py b/sdk/python/feast/protos/feast/core/Feature_pb2_grpc.py new file mode 100644 index 0000000000..2daafffebf --- /dev/null +++ b/sdk/python/feast/protos/feast/core/Feature_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + diff --git a/sdk/python/feast/protos/feast/core/InfraObject_pb2.py b/sdk/python/feast/protos/feast/core/InfraObject_pb2.py new file mode 100644 index 0000000000..0804aecbf6 --- /dev/null +++ b/sdk/python/feast/protos/feast/core/InfraObject_pb2.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: feast/core/InfraObject.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from feast.protos.feast.core import DatastoreTable_pb2 as feast_dot_core_dot_DatastoreTable__pb2 +from feast.protos.feast.core import DynamoDBTable_pb2 as feast_dot_core_dot_DynamoDBTable__pb2 +from feast.protos.feast.core import SqliteTable_pb2 as feast_dot_core_dot_SqliteTable__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1c\x66\x65\x61st/core/InfraObject.proto\x12\nfeast.core\x1a\x1f\x66\x65\x61st/core/DatastoreTable.proto\x1a\x1e\x66\x65\x61st/core/DynamoDBTable.proto\x1a\x1c\x66\x65\x61st/core/SqliteTable.proto\"7\n\x05Infra\x12.\n\rinfra_objects\x18\x01 \x03(\x0b\x32\x17.feast.core.InfraObject\"\xb6\x02\n\x0bInfraObject\x12\x1f\n\x17infra_object_class_type\x18\x01 \x01(\t\x12\x33\n\x0e\x64ynamodb_table\x18\x02 \x01(\x0b\x32\x19.feast.core.DynamoDBTableH\x00\x12\x35\n\x0f\x64\x61tastore_table\x18\x03 \x01(\x0b\x32\x1a.feast.core.DatastoreTableH\x00\x12/\n\x0csqlite_table\x18\x04 \x01(\x0b\x32\x17.feast.core.SqliteTableH\x00\x12;\n\x0c\x63ustom_infra\x18\x64 \x01(\x0b\x32#.feast.core.InfraObject.CustomInfraH\x00\x1a\x1c\n\x0b\x43ustomInfra\x12\r\n\x05\x66ield\x18\x01 \x01(\x0c\x42\x0e\n\x0cinfra_objectBU\n\x10\x66\x65\x61st.proto.coreB\x10InfraObjectProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'feast.core.InfraObject_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\020feast.proto.coreB\020InfraObjectProtoZ/github.com/feast-dev/feast/go/protos/feast/core' + _globals['_INFRA']._serialized_start=139 + _globals['_INFRA']._serialized_end=194 + _globals['_INFRAOBJECT']._serialized_start=197 + _globals['_INFRAOBJECT']._serialized_end=507 + _globals['_INFRAOBJECT_CUSTOMINFRA']._serialized_start=463 + _globals['_INFRAOBJECT_CUSTOMINFRA']._serialized_end=491 +# @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/InfraObject_pb2.pyi b/sdk/python/feast/protos/feast/core/InfraObject_pb2.pyi new file mode 100644 index 0000000000..38b31b7317 --- /dev/null +++ b/sdk/python/feast/protos/feast/core/InfraObject_pb2.pyi @@ -0,0 +1,101 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file + +* Copyright 2021 The Feast 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 +* +* https://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 builtins +import collections.abc +import feast.core.DatastoreTable_pb2 +import feast.core.DynamoDBTable_pb2 +import feast.core.SqliteTable_pb2 +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.message +import sys + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class Infra(google.protobuf.message.Message): + """Represents a set of infrastructure objects managed by Feast""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + INFRA_OBJECTS_FIELD_NUMBER: builtins.int + @property + def infra_objects(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___InfraObject]: + """List of infrastructure objects managed by Feast""" + def __init__( + self, + *, + infra_objects: collections.abc.Iterable[global___InfraObject] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["infra_objects", b"infra_objects"]) -> None: ... + +global___Infra = Infra + +class InfraObject(google.protobuf.message.Message): + """Represents a single infrastructure object managed by Feast""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class CustomInfra(google.protobuf.message.Message): + """Allows for custom infra objects to be added""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + FIELD_FIELD_NUMBER: builtins.int + field: builtins.bytes + def __init__( + self, + *, + field: builtins.bytes = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["field", b"field"]) -> None: ... + + INFRA_OBJECT_CLASS_TYPE_FIELD_NUMBER: builtins.int + DYNAMODB_TABLE_FIELD_NUMBER: builtins.int + DATASTORE_TABLE_FIELD_NUMBER: builtins.int + SQLITE_TABLE_FIELD_NUMBER: builtins.int + CUSTOM_INFRA_FIELD_NUMBER: builtins.int + infra_object_class_type: builtins.str + """Represents the Python class for the infrastructure object""" + @property + def dynamodb_table(self) -> feast.core.DynamoDBTable_pb2.DynamoDBTable: ... + @property + def datastore_table(self) -> feast.core.DatastoreTable_pb2.DatastoreTable: ... + @property + def sqlite_table(self) -> feast.core.SqliteTable_pb2.SqliteTable: ... + @property + def custom_infra(self) -> global___InfraObject.CustomInfra: ... + def __init__( + self, + *, + infra_object_class_type: builtins.str = ..., + dynamodb_table: feast.core.DynamoDBTable_pb2.DynamoDBTable | None = ..., + datastore_table: feast.core.DatastoreTable_pb2.DatastoreTable | None = ..., + sqlite_table: feast.core.SqliteTable_pb2.SqliteTable | None = ..., + custom_infra: global___InfraObject.CustomInfra | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["custom_infra", b"custom_infra", "datastore_table", b"datastore_table", "dynamodb_table", b"dynamodb_table", "infra_object", b"infra_object", "sqlite_table", b"sqlite_table"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["custom_infra", b"custom_infra", "datastore_table", b"datastore_table", "dynamodb_table", b"dynamodb_table", "infra_object", b"infra_object", "infra_object_class_type", b"infra_object_class_type", "sqlite_table", b"sqlite_table"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal["infra_object", b"infra_object"]) -> typing_extensions.Literal["dynamodb_table", "datastore_table", "sqlite_table", "custom_infra"] | None: ... + +global___InfraObject = InfraObject diff --git a/sdk/python/feast/protos/feast/core/InfraObject_pb2_grpc.py b/sdk/python/feast/protos/feast/core/InfraObject_pb2_grpc.py new file mode 100644 index 0000000000..2daafffebf --- /dev/null +++ b/sdk/python/feast/protos/feast/core/InfraObject_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + diff --git a/sdk/python/feast/protos/feast/core/OnDemandFeatureView_pb2.py b/sdk/python/feast/protos/feast/core/OnDemandFeatureView_pb2.py new file mode 100644 index 0000000000..4be551724c --- /dev/null +++ b/sdk/python/feast/protos/feast/core/OnDemandFeatureView_pb2.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: feast/core/OnDemandFeatureView.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 +from feast.protos.feast.core import FeatureView_pb2 as feast_dot_core_dot_FeatureView__pb2 +from feast.protos.feast.core import FeatureViewProjection_pb2 as feast_dot_core_dot_FeatureViewProjection__pb2 +from feast.protos.feast.core import Feature_pb2 as feast_dot_core_dot_Feature__pb2 +from feast.protos.feast.core import DataSource_pb2 as feast_dot_core_dot_DataSource__pb2 +from feast.protos.feast.core import Transformation_pb2 as feast_dot_core_dot_Transformation__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$feast/core/OnDemandFeatureView.proto\x12\nfeast.core\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1c\x66\x65\x61st/core/FeatureView.proto\x1a&feast/core/FeatureViewProjection.proto\x1a\x18\x66\x65\x61st/core/Feature.proto\x1a\x1b\x66\x65\x61st/core/DataSource.proto\x1a\x1f\x66\x65\x61st/core/Transformation.proto\"{\n\x13OnDemandFeatureView\x12\x31\n\x04spec\x18\x01 \x01(\x0b\x32#.feast.core.OnDemandFeatureViewSpec\x12\x31\n\x04meta\x18\x02 \x01(\x0b\x32#.feast.core.OnDemandFeatureViewMeta\"\x99\x04\n\x17OnDemandFeatureViewSpec\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12+\n\x08\x66\x65\x61tures\x18\x03 \x03(\x0b\x32\x19.feast.core.FeatureSpecV2\x12\x41\n\x07sources\x18\x04 \x03(\x0b\x32\x30.feast.core.OnDemandFeatureViewSpec.SourcesEntry\x12\x42\n\x15user_defined_function\x18\x05 \x01(\x0b\x32\x1f.feast.core.UserDefinedFunctionB\x02\x18\x01\x12\x43\n\x16\x66\x65\x61ture_transformation\x18\n \x01(\x0b\x32#.feast.core.FeatureTransformationV2\x12\x13\n\x0b\x64\x65scription\x18\x06 \x01(\t\x12;\n\x04tags\x18\x07 \x03(\x0b\x32-.feast.core.OnDemandFeatureViewSpec.TagsEntry\x12\r\n\x05owner\x18\x08 \x01(\t\x12\x0c\n\x04mode\x18\x0b \x01(\t\x1aJ\n\x0cSourcesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.feast.core.OnDemandSource:\x02\x38\x01\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x8c\x01\n\x17OnDemandFeatureViewMeta\x12\x35\n\x11\x63reated_timestamp\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12:\n\x16last_updated_timestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xc8\x01\n\x0eOnDemandSource\x12/\n\x0c\x66\x65\x61ture_view\x18\x01 \x01(\x0b\x32\x17.feast.core.FeatureViewH\x00\x12\x44\n\x17\x66\x65\x61ture_view_projection\x18\x03 \x01(\x0b\x32!.feast.core.FeatureViewProjectionH\x00\x12\x35\n\x13request_data_source\x18\x02 \x01(\x0b\x32\x16.feast.core.DataSourceH\x00\x42\x08\n\x06source\"H\n\x13UserDefinedFunction\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04\x62ody\x18\x02 \x01(\x0c\x12\x11\n\tbody_text\x18\x03 \x01(\t:\x02\x18\x01\x42]\n\x10\x66\x65\x61st.proto.coreB\x18OnDemandFeatureViewProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'feast.core.OnDemandFeatureView_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\020feast.proto.coreB\030OnDemandFeatureViewProtoZ/github.com/feast-dev/feast/go/protos/feast/core' + _globals['_ONDEMANDFEATUREVIEWSPEC_SOURCESENTRY']._options = None + _globals['_ONDEMANDFEATUREVIEWSPEC_SOURCESENTRY']._serialized_options = b'8\001' + _globals['_ONDEMANDFEATUREVIEWSPEC_TAGSENTRY']._options = None + _globals['_ONDEMANDFEATUREVIEWSPEC_TAGSENTRY']._serialized_options = b'8\001' + _globals['_ONDEMANDFEATUREVIEWSPEC'].fields_by_name['user_defined_function']._options = None + _globals['_ONDEMANDFEATUREVIEWSPEC'].fields_by_name['user_defined_function']._serialized_options = b'\030\001' + _globals['_USERDEFINEDFUNCTION']._options = None + _globals['_USERDEFINEDFUNCTION']._serialized_options = b'\030\001' + _globals['_ONDEMANDFEATUREVIEW']._serialized_start=243 + _globals['_ONDEMANDFEATUREVIEW']._serialized_end=366 + _globals['_ONDEMANDFEATUREVIEWSPEC']._serialized_start=369 + _globals['_ONDEMANDFEATUREVIEWSPEC']._serialized_end=906 + _globals['_ONDEMANDFEATUREVIEWSPEC_SOURCESENTRY']._serialized_start=787 + _globals['_ONDEMANDFEATUREVIEWSPEC_SOURCESENTRY']._serialized_end=861 + _globals['_ONDEMANDFEATUREVIEWSPEC_TAGSENTRY']._serialized_start=863 + _globals['_ONDEMANDFEATUREVIEWSPEC_TAGSENTRY']._serialized_end=906 + _globals['_ONDEMANDFEATUREVIEWMETA']._serialized_start=909 + _globals['_ONDEMANDFEATUREVIEWMETA']._serialized_end=1049 + _globals['_ONDEMANDSOURCE']._serialized_start=1052 + _globals['_ONDEMANDSOURCE']._serialized_end=1252 + _globals['_USERDEFINEDFUNCTION']._serialized_start=1254 + _globals['_USERDEFINEDFUNCTION']._serialized_end=1326 +# @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/OnDemandFeatureView_pb2.pyi b/sdk/python/feast/protos/feast/core/OnDemandFeatureView_pb2.pyi new file mode 100644 index 0000000000..d72a8f9862 --- /dev/null +++ b/sdk/python/feast/protos/feast/core/OnDemandFeatureView_pb2.pyi @@ -0,0 +1,219 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file + +Copyright 2020 The Feast 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 + + https://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 builtins +import collections.abc +import feast.core.DataSource_pb2 +import feast.core.FeatureViewProjection_pb2 +import feast.core.FeatureView_pb2 +import feast.core.Feature_pb2 +import feast.core.Transformation_pb2 +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.message +import google.protobuf.timestamp_pb2 +import sys + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class OnDemandFeatureView(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + SPEC_FIELD_NUMBER: builtins.int + META_FIELD_NUMBER: builtins.int + @property + def spec(self) -> global___OnDemandFeatureViewSpec: + """User-specified specifications of this feature view.""" + @property + def meta(self) -> global___OnDemandFeatureViewMeta: ... + def __init__( + self, + *, + spec: global___OnDemandFeatureViewSpec | None = ..., + meta: global___OnDemandFeatureViewMeta | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["meta", b"meta", "spec", b"spec"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["meta", b"meta", "spec", b"spec"]) -> None: ... + +global___OnDemandFeatureView = OnDemandFeatureView + +class OnDemandFeatureViewSpec(google.protobuf.message.Message): + """Next available id: 9""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class SourcesEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> global___OnDemandSource: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: global___OnDemandSource | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + class TagsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + NAME_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + FEATURES_FIELD_NUMBER: builtins.int + SOURCES_FIELD_NUMBER: builtins.int + USER_DEFINED_FUNCTION_FIELD_NUMBER: builtins.int + FEATURE_TRANSFORMATION_FIELD_NUMBER: builtins.int + DESCRIPTION_FIELD_NUMBER: builtins.int + TAGS_FIELD_NUMBER: builtins.int + OWNER_FIELD_NUMBER: builtins.int + MODE_FIELD_NUMBER: builtins.int + name: builtins.str + """Name of the feature view. Must be unique. Not updated.""" + project: builtins.str + """Name of Feast project that this feature view belongs to.""" + @property + def features(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.Feature_pb2.FeatureSpecV2]: + """List of features specifications for each feature defined with this feature view.""" + @property + def sources(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, global___OnDemandSource]: + """Map of sources for this feature view.""" + @property + def user_defined_function(self) -> global___UserDefinedFunction: ... + @property + def feature_transformation(self) -> feast.core.Transformation_pb2.FeatureTransformationV2: + """Oneof with {user_defined_function, on_demand_substrait_transformation}""" + description: builtins.str + """Description of the on demand feature view.""" + @property + def tags(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: + """User defined metadata.""" + owner: builtins.str + """Owner of the on demand feature view.""" + mode: builtins.str + def __init__( + self, + *, + name: builtins.str = ..., + project: builtins.str = ..., + features: collections.abc.Iterable[feast.core.Feature_pb2.FeatureSpecV2] | None = ..., + sources: collections.abc.Mapping[builtins.str, global___OnDemandSource] | None = ..., + user_defined_function: global___UserDefinedFunction | None = ..., + feature_transformation: feast.core.Transformation_pb2.FeatureTransformationV2 | None = ..., + description: builtins.str = ..., + tags: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + owner: builtins.str = ..., + mode: builtins.str = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["feature_transformation", b"feature_transformation", "user_defined_function", b"user_defined_function"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["description", b"description", "feature_transformation", b"feature_transformation", "features", b"features", "mode", b"mode", "name", b"name", "owner", b"owner", "project", b"project", "sources", b"sources", "tags", b"tags", "user_defined_function", b"user_defined_function"]) -> None: ... + +global___OnDemandFeatureViewSpec = OnDemandFeatureViewSpec + +class OnDemandFeatureViewMeta(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + CREATED_TIMESTAMP_FIELD_NUMBER: builtins.int + LAST_UPDATED_TIMESTAMP_FIELD_NUMBER: builtins.int + @property + def created_timestamp(self) -> google.protobuf.timestamp_pb2.Timestamp: + """Time where this Feature View is created""" + @property + def last_updated_timestamp(self) -> google.protobuf.timestamp_pb2.Timestamp: + """Time where this Feature View is last updated""" + def __init__( + self, + *, + created_timestamp: google.protobuf.timestamp_pb2.Timestamp | None = ..., + last_updated_timestamp: google.protobuf.timestamp_pb2.Timestamp | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["created_timestamp", b"created_timestamp", "last_updated_timestamp", b"last_updated_timestamp"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["created_timestamp", b"created_timestamp", "last_updated_timestamp", b"last_updated_timestamp"]) -> None: ... + +global___OnDemandFeatureViewMeta = OnDemandFeatureViewMeta + +class OnDemandSource(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + FEATURE_VIEW_FIELD_NUMBER: builtins.int + FEATURE_VIEW_PROJECTION_FIELD_NUMBER: builtins.int + REQUEST_DATA_SOURCE_FIELD_NUMBER: builtins.int + @property + def feature_view(self) -> feast.core.FeatureView_pb2.FeatureView: ... + @property + def feature_view_projection(self) -> feast.core.FeatureViewProjection_pb2.FeatureViewProjection: ... + @property + def request_data_source(self) -> feast.core.DataSource_pb2.DataSource: ... + def __init__( + self, + *, + feature_view: feast.core.FeatureView_pb2.FeatureView | None = ..., + feature_view_projection: feast.core.FeatureViewProjection_pb2.FeatureViewProjection | None = ..., + request_data_source: feast.core.DataSource_pb2.DataSource | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["feature_view", b"feature_view", "feature_view_projection", b"feature_view_projection", "request_data_source", b"request_data_source", "source", b"source"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["feature_view", b"feature_view", "feature_view_projection", b"feature_view_projection", "request_data_source", b"request_data_source", "source", b"source"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal["source", b"source"]) -> typing_extensions.Literal["feature_view", "feature_view_projection", "request_data_source"] | None: ... + +global___OnDemandSource = OnDemandSource + +class UserDefinedFunction(google.protobuf.message.Message): + """Serialized representation of python function.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + BODY_FIELD_NUMBER: builtins.int + BODY_TEXT_FIELD_NUMBER: builtins.int + name: builtins.str + """The function name""" + body: builtins.bytes + """The python-syntax function body (serialized by dill)""" + body_text: builtins.str + """The string representation of the udf""" + def __init__( + self, + *, + name: builtins.str = ..., + body: builtins.bytes = ..., + body_text: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["body", b"body", "body_text", b"body_text", "name", b"name"]) -> None: ... + +global___UserDefinedFunction = UserDefinedFunction diff --git a/sdk/python/feast/protos/feast/core/OnDemandFeatureView_pb2_grpc.py b/sdk/python/feast/protos/feast/core/OnDemandFeatureView_pb2_grpc.py new file mode 100644 index 0000000000..2daafffebf --- /dev/null +++ b/sdk/python/feast/protos/feast/core/OnDemandFeatureView_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + diff --git a/sdk/python/feast/protos/feast/core/Permission_pb2.py b/sdk/python/feast/protos/feast/core/Permission_pb2.py new file mode 100644 index 0000000000..822ad0261b --- /dev/null +++ b/sdk/python/feast/protos/feast/core/Permission_pb2.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: feast/core/Permission.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from feast.protos.feast.core import Policy_pb2 as feast_dot_core_dot_Policy__pb2 +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1b\x66\x65\x61st/core/Permission.proto\x12\nfeast.core\x1a\x17\x66\x65\x61st/core/Policy.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"`\n\nPermission\x12(\n\x04spec\x18\x01 \x01(\x0b\x32\x1a.feast.core.PermissionSpec\x12(\n\x04meta\x18\x02 \x01(\x0b\x32\x1a.feast.core.PermissionMeta\"\x9f\x06\n\x0ePermissionSpec\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12.\n\x05types\x18\x03 \x03(\x0e\x32\x1f.feast.core.PermissionSpec.Type\x12\x14\n\x0cname_pattern\x18\x04 \x01(\t\x12\x43\n\rrequired_tags\x18\x05 \x03(\x0b\x32,.feast.core.PermissionSpec.RequiredTagsEntry\x12\x39\n\x07\x61\x63tions\x18\x06 \x03(\x0e\x32(.feast.core.PermissionSpec.AuthzedAction\x12\"\n\x06policy\x18\x07 \x01(\x0b\x32\x12.feast.core.Policy\x12\x32\n\x04tags\x18\x08 \x03(\x0b\x32$.feast.core.PermissionSpec.TagsEntry\x1a\x33\n\x11RequiredTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x89\x01\n\rAuthzedAction\x12\n\n\x06\x43REATE\x10\x00\x12\x0c\n\x08\x44\x45SCRIBE\x10\x01\x12\n\n\x06UPDATE\x10\x02\x12\n\n\x06\x44\x45LETE\x10\x03\x12\x0f\n\x0bREAD_ONLINE\x10\x04\x12\x10\n\x0cREAD_OFFLINE\x10\x05\x12\x10\n\x0cWRITE_ONLINE\x10\x06\x12\x11\n\rWRITE_OFFLINE\x10\x07\"\xe1\x01\n\x04Type\x12\x10\n\x0c\x46\x45\x41TURE_VIEW\x10\x00\x12\x1a\n\x16ON_DEMAND_FEATURE_VIEW\x10\x01\x12\x16\n\x12\x42\x41TCH_FEATURE_VIEW\x10\x02\x12\x17\n\x13STREAM_FEATURE_VIEW\x10\x03\x12\n\n\x06\x45NTITY\x10\x04\x12\x13\n\x0f\x46\x45\x41TURE_SERVICE\x10\x05\x12\x0f\n\x0b\x44\x41TA_SOURCE\x10\x06\x12\x18\n\x14VALIDATION_REFERENCE\x10\x07\x12\x11\n\rSAVED_DATASET\x10\x08\x12\x0e\n\nPERMISSION\x10\t\x12\x0b\n\x07PROJECT\x10\n\"\x83\x01\n\x0ePermissionMeta\x12\x35\n\x11\x63reated_timestamp\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12:\n\x16last_updated_timestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.TimestampBT\n\x10\x66\x65\x61st.proto.coreB\x0fPermissionProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'feast.core.Permission_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\020feast.proto.coreB\017PermissionProtoZ/github.com/feast-dev/feast/go/protos/feast/core' + _globals['_PERMISSIONSPEC_REQUIREDTAGSENTRY']._options = None + _globals['_PERMISSIONSPEC_REQUIREDTAGSENTRY']._serialized_options = b'8\001' + _globals['_PERMISSIONSPEC_TAGSENTRY']._options = None + _globals['_PERMISSIONSPEC_TAGSENTRY']._serialized_options = b'8\001' + _globals['_PERMISSION']._serialized_start=101 + _globals['_PERMISSION']._serialized_end=197 + _globals['_PERMISSIONSPEC']._serialized_start=200 + _globals['_PERMISSIONSPEC']._serialized_end=999 + _globals['_PERMISSIONSPEC_REQUIREDTAGSENTRY']._serialized_start=535 + _globals['_PERMISSIONSPEC_REQUIREDTAGSENTRY']._serialized_end=586 + _globals['_PERMISSIONSPEC_TAGSENTRY']._serialized_start=588 + _globals['_PERMISSIONSPEC_TAGSENTRY']._serialized_end=631 + _globals['_PERMISSIONSPEC_AUTHZEDACTION']._serialized_start=634 + _globals['_PERMISSIONSPEC_AUTHZEDACTION']._serialized_end=771 + _globals['_PERMISSIONSPEC_TYPE']._serialized_start=774 + _globals['_PERMISSIONSPEC_TYPE']._serialized_end=999 + _globals['_PERMISSIONMETA']._serialized_start=1002 + _globals['_PERMISSIONMETA']._serialized_end=1133 +# @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/Permission_pb2.pyi b/sdk/python/feast/protos/feast/core/Permission_pb2.pyi new file mode 100644 index 0000000000..1155c13188 --- /dev/null +++ b/sdk/python/feast/protos/feast/core/Permission_pb2.pyi @@ -0,0 +1,195 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" +import builtins +import collections.abc +import feast.core.Policy_pb2 +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.internal.enum_type_wrapper +import google.protobuf.message +import google.protobuf.timestamp_pb2 +import sys +import typing + +if sys.version_info >= (3, 10): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class Permission(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + SPEC_FIELD_NUMBER: builtins.int + META_FIELD_NUMBER: builtins.int + @property + def spec(self) -> global___PermissionSpec: + """User-specified specifications of this permission.""" + @property + def meta(self) -> global___PermissionMeta: + """System-populated metadata for this permission.""" + def __init__( + self, + *, + spec: global___PermissionSpec | None = ..., + meta: global___PermissionMeta | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["meta", b"meta", "spec", b"spec"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["meta", b"meta", "spec", b"spec"]) -> None: ... + +global___Permission = Permission + +class PermissionSpec(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class _AuthzedAction: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _AuthzedActionEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[PermissionSpec._AuthzedAction.ValueType], builtins.type): # noqa: F821 + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + CREATE: PermissionSpec._AuthzedAction.ValueType # 0 + DESCRIBE: PermissionSpec._AuthzedAction.ValueType # 1 + UPDATE: PermissionSpec._AuthzedAction.ValueType # 2 + DELETE: PermissionSpec._AuthzedAction.ValueType # 3 + READ_ONLINE: PermissionSpec._AuthzedAction.ValueType # 4 + READ_OFFLINE: PermissionSpec._AuthzedAction.ValueType # 5 + WRITE_ONLINE: PermissionSpec._AuthzedAction.ValueType # 6 + WRITE_OFFLINE: PermissionSpec._AuthzedAction.ValueType # 7 + + class AuthzedAction(_AuthzedAction, metaclass=_AuthzedActionEnumTypeWrapper): ... + CREATE: PermissionSpec.AuthzedAction.ValueType # 0 + DESCRIBE: PermissionSpec.AuthzedAction.ValueType # 1 + UPDATE: PermissionSpec.AuthzedAction.ValueType # 2 + DELETE: PermissionSpec.AuthzedAction.ValueType # 3 + READ_ONLINE: PermissionSpec.AuthzedAction.ValueType # 4 + READ_OFFLINE: PermissionSpec.AuthzedAction.ValueType # 5 + WRITE_ONLINE: PermissionSpec.AuthzedAction.ValueType # 6 + WRITE_OFFLINE: PermissionSpec.AuthzedAction.ValueType # 7 + + class _Type: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _TypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[PermissionSpec._Type.ValueType], builtins.type): # noqa: F821 + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + FEATURE_VIEW: PermissionSpec._Type.ValueType # 0 + ON_DEMAND_FEATURE_VIEW: PermissionSpec._Type.ValueType # 1 + BATCH_FEATURE_VIEW: PermissionSpec._Type.ValueType # 2 + STREAM_FEATURE_VIEW: PermissionSpec._Type.ValueType # 3 + ENTITY: PermissionSpec._Type.ValueType # 4 + FEATURE_SERVICE: PermissionSpec._Type.ValueType # 5 + DATA_SOURCE: PermissionSpec._Type.ValueType # 6 + VALIDATION_REFERENCE: PermissionSpec._Type.ValueType # 7 + SAVED_DATASET: PermissionSpec._Type.ValueType # 8 + PERMISSION: PermissionSpec._Type.ValueType # 9 + PROJECT: PermissionSpec._Type.ValueType # 10 + + class Type(_Type, metaclass=_TypeEnumTypeWrapper): ... + FEATURE_VIEW: PermissionSpec.Type.ValueType # 0 + ON_DEMAND_FEATURE_VIEW: PermissionSpec.Type.ValueType # 1 + BATCH_FEATURE_VIEW: PermissionSpec.Type.ValueType # 2 + STREAM_FEATURE_VIEW: PermissionSpec.Type.ValueType # 3 + ENTITY: PermissionSpec.Type.ValueType # 4 + FEATURE_SERVICE: PermissionSpec.Type.ValueType # 5 + DATA_SOURCE: PermissionSpec.Type.ValueType # 6 + VALIDATION_REFERENCE: PermissionSpec.Type.ValueType # 7 + SAVED_DATASET: PermissionSpec.Type.ValueType # 8 + PERMISSION: PermissionSpec.Type.ValueType # 9 + PROJECT: PermissionSpec.Type.ValueType # 10 + + class RequiredTagsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + class TagsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + NAME_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + TYPES_FIELD_NUMBER: builtins.int + NAME_PATTERN_FIELD_NUMBER: builtins.int + REQUIRED_TAGS_FIELD_NUMBER: builtins.int + ACTIONS_FIELD_NUMBER: builtins.int + POLICY_FIELD_NUMBER: builtins.int + TAGS_FIELD_NUMBER: builtins.int + name: builtins.str + """Name of the permission. Must be unique. Not updated.""" + project: builtins.str + """Name of Feast project.""" + @property + def types(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[global___PermissionSpec.Type.ValueType]: ... + name_pattern: builtins.str + @property + def required_tags(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: ... + @property + def actions(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[global___PermissionSpec.AuthzedAction.ValueType]: + """List of actions.""" + @property + def policy(self) -> feast.core.Policy_pb2.Policy: + """the policy.""" + @property + def tags(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: + """User defined metadata""" + def __init__( + self, + *, + name: builtins.str = ..., + project: builtins.str = ..., + types: collections.abc.Iterable[global___PermissionSpec.Type.ValueType] | None = ..., + name_pattern: builtins.str = ..., + required_tags: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + actions: collections.abc.Iterable[global___PermissionSpec.AuthzedAction.ValueType] | None = ..., + policy: feast.core.Policy_pb2.Policy | None = ..., + tags: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["policy", b"policy"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["actions", b"actions", "name", b"name", "name_pattern", b"name_pattern", "policy", b"policy", "project", b"project", "required_tags", b"required_tags", "tags", b"tags", "types", b"types"]) -> None: ... + +global___PermissionSpec = PermissionSpec + +class PermissionMeta(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + CREATED_TIMESTAMP_FIELD_NUMBER: builtins.int + LAST_UPDATED_TIMESTAMP_FIELD_NUMBER: builtins.int + @property + def created_timestamp(self) -> google.protobuf.timestamp_pb2.Timestamp: ... + @property + def last_updated_timestamp(self) -> google.protobuf.timestamp_pb2.Timestamp: ... + def __init__( + self, + *, + created_timestamp: google.protobuf.timestamp_pb2.Timestamp | None = ..., + last_updated_timestamp: google.protobuf.timestamp_pb2.Timestamp | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["created_timestamp", b"created_timestamp", "last_updated_timestamp", b"last_updated_timestamp"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["created_timestamp", b"created_timestamp", "last_updated_timestamp", b"last_updated_timestamp"]) -> None: ... + +global___PermissionMeta = PermissionMeta diff --git a/sdk/python/feast/protos/feast/core/Permission_pb2_grpc.py b/sdk/python/feast/protos/feast/core/Permission_pb2_grpc.py new file mode 100644 index 0000000000..2daafffebf --- /dev/null +++ b/sdk/python/feast/protos/feast/core/Permission_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + diff --git a/sdk/python/feast/protos/feast/core/Policy_pb2.py b/sdk/python/feast/protos/feast/core/Policy_pb2.py new file mode 100644 index 0000000000..2fac866115 --- /dev/null +++ b/sdk/python/feast/protos/feast/core/Policy_pb2.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: feast/core/Policy.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17\x66\x65\x61st/core/Policy.proto\x12\nfeast.core\"p\n\x06Policy\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x38\n\x11role_based_policy\x18\x03 \x01(\x0b\x32\x1b.feast.core.RoleBasedPolicyH\x00\x42\r\n\x0bpolicy_type\" \n\x0fRoleBasedPolicy\x12\r\n\x05roles\x18\x01 \x03(\tBP\n\x10\x66\x65\x61st.proto.coreB\x0bPolicyProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'feast.core.Policy_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\020feast.proto.coreB\013PolicyProtoZ/github.com/feast-dev/feast/go/protos/feast/core' + _globals['_POLICY']._serialized_start=39 + _globals['_POLICY']._serialized_end=151 + _globals['_ROLEBASEDPOLICY']._serialized_start=153 + _globals['_ROLEBASEDPOLICY']._serialized_end=185 +# @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/Policy_pb2.pyi b/sdk/python/feast/protos/feast/core/Policy_pb2.pyi new file mode 100644 index 0000000000..f19b18fff4 --- /dev/null +++ b/sdk/python/feast/protos/feast/core/Policy_pb2.pyi @@ -0,0 +1,58 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" +import builtins +import collections.abc +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.message +import sys + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class Policy(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + ROLE_BASED_POLICY_FIELD_NUMBER: builtins.int + name: builtins.str + """Name of the policy.""" + project: builtins.str + """Name of Feast project.""" + @property + def role_based_policy(self) -> global___RoleBasedPolicy: ... + def __init__( + self, + *, + name: builtins.str = ..., + project: builtins.str = ..., + role_based_policy: global___RoleBasedPolicy | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["policy_type", b"policy_type", "role_based_policy", b"role_based_policy"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["name", b"name", "policy_type", b"policy_type", "project", b"project", "role_based_policy", b"role_based_policy"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal["policy_type", b"policy_type"]) -> typing_extensions.Literal["role_based_policy"] | None: ... + +global___Policy = Policy + +class RoleBasedPolicy(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + ROLES_FIELD_NUMBER: builtins.int + @property + def roles(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: + """List of roles in this policy.""" + def __init__( + self, + *, + roles: collections.abc.Iterable[builtins.str] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["roles", b"roles"]) -> None: ... + +global___RoleBasedPolicy = RoleBasedPolicy diff --git a/sdk/python/feast/protos/feast/core/Policy_pb2_grpc.py b/sdk/python/feast/protos/feast/core/Policy_pb2_grpc.py new file mode 100644 index 0000000000..2daafffebf --- /dev/null +++ b/sdk/python/feast/protos/feast/core/Policy_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + diff --git a/sdk/python/feast/protos/feast/core/Project_pb2.py b/sdk/python/feast/protos/feast/core/Project_pb2.py new file mode 100644 index 0000000000..cfbf122014 --- /dev/null +++ b/sdk/python/feast/protos/feast/core/Project_pb2.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: feast/core/Project.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18\x66\x65\x61st/core/Project.proto\x12\nfeast.core\x1a\x1fgoogle/protobuf/timestamp.proto\"W\n\x07Project\x12%\n\x04spec\x18\x01 \x01(\x0b\x32\x17.feast.core.ProjectSpec\x12%\n\x04meta\x18\x02 \x01(\x0b\x32\x17.feast.core.ProjectMeta\"\x9d\x01\n\x0bProjectSpec\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12/\n\x04tags\x18\x03 \x03(\x0b\x32!.feast.core.ProjectSpec.TagsEntry\x12\r\n\x05owner\x18\x04 \x01(\t\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x80\x01\n\x0bProjectMeta\x12\x35\n\x11\x63reated_timestamp\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12:\n\x16last_updated_timestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.TimestampBQ\n\x10\x66\x65\x61st.proto.coreB\x0cProjectProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'feast.core.Project_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\020feast.proto.coreB\014ProjectProtoZ/github.com/feast-dev/feast/go/protos/feast/core' + _globals['_PROJECTSPEC_TAGSENTRY']._options = None + _globals['_PROJECTSPEC_TAGSENTRY']._serialized_options = b'8\001' + _globals['_PROJECT']._serialized_start=73 + _globals['_PROJECT']._serialized_end=160 + _globals['_PROJECTSPEC']._serialized_start=163 + _globals['_PROJECTSPEC']._serialized_end=320 + _globals['_PROJECTSPEC_TAGSENTRY']._serialized_start=277 + _globals['_PROJECTSPEC_TAGSENTRY']._serialized_end=320 + _globals['_PROJECTMETA']._serialized_start=323 + _globals['_PROJECTMETA']._serialized_end=451 +# @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/Project_pb2.pyi b/sdk/python/feast/protos/feast/core/Project_pb2.pyi new file mode 100644 index 0000000000..e3cce2ec42 --- /dev/null +++ b/sdk/python/feast/protos/feast/core/Project_pb2.pyi @@ -0,0 +1,119 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file + +* Copyright 2020 The Feast 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 +* +* https://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 builtins +import collections.abc +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.message +import google.protobuf.timestamp_pb2 +import sys + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class Project(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + SPEC_FIELD_NUMBER: builtins.int + META_FIELD_NUMBER: builtins.int + @property + def spec(self) -> global___ProjectSpec: + """User-specified specifications of this entity.""" + @property + def meta(self) -> global___ProjectMeta: + """System-populated metadata for this entity.""" + def __init__( + self, + *, + spec: global___ProjectSpec | None = ..., + meta: global___ProjectMeta | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["meta", b"meta", "spec", b"spec"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["meta", b"meta", "spec", b"spec"]) -> None: ... + +global___Project = Project + +class ProjectSpec(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class TagsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + NAME_FIELD_NUMBER: builtins.int + DESCRIPTION_FIELD_NUMBER: builtins.int + TAGS_FIELD_NUMBER: builtins.int + OWNER_FIELD_NUMBER: builtins.int + name: builtins.str + """Name of the Project""" + description: builtins.str + """Description of the Project""" + @property + def tags(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: + """User defined metadata""" + owner: builtins.str + """Owner of the Project""" + def __init__( + self, + *, + name: builtins.str = ..., + description: builtins.str = ..., + tags: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + owner: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["description", b"description", "name", b"name", "owner", b"owner", "tags", b"tags"]) -> None: ... + +global___ProjectSpec = ProjectSpec + +class ProjectMeta(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + CREATED_TIMESTAMP_FIELD_NUMBER: builtins.int + LAST_UPDATED_TIMESTAMP_FIELD_NUMBER: builtins.int + @property + def created_timestamp(self) -> google.protobuf.timestamp_pb2.Timestamp: + """Time when the Project is created""" + @property + def last_updated_timestamp(self) -> google.protobuf.timestamp_pb2.Timestamp: + """Time when the Project is last updated with registry changes (Apply stage)""" + def __init__( + self, + *, + created_timestamp: google.protobuf.timestamp_pb2.Timestamp | None = ..., + last_updated_timestamp: google.protobuf.timestamp_pb2.Timestamp | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["created_timestamp", b"created_timestamp", "last_updated_timestamp", b"last_updated_timestamp"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["created_timestamp", b"created_timestamp", "last_updated_timestamp", b"last_updated_timestamp"]) -> None: ... + +global___ProjectMeta = ProjectMeta diff --git a/sdk/python/feast/protos/feast/core/Project_pb2_grpc.py b/sdk/python/feast/protos/feast/core/Project_pb2_grpc.py new file mode 100644 index 0000000000..2daafffebf --- /dev/null +++ b/sdk/python/feast/protos/feast/core/Project_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + diff --git a/sdk/python/feast/protos/feast/core/Registry_pb2.py b/sdk/python/feast/protos/feast/core/Registry_pb2.py new file mode 100644 index 0000000000..671958d80c --- /dev/null +++ b/sdk/python/feast/protos/feast/core/Registry_pb2.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: feast/core/Registry.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from feast.protos.feast.core import Entity_pb2 as feast_dot_core_dot_Entity__pb2 +from feast.protos.feast.core import FeatureService_pb2 as feast_dot_core_dot_FeatureService__pb2 +from feast.protos.feast.core import FeatureTable_pb2 as feast_dot_core_dot_FeatureTable__pb2 +from feast.protos.feast.core import FeatureView_pb2 as feast_dot_core_dot_FeatureView__pb2 +from feast.protos.feast.core import InfraObject_pb2 as feast_dot_core_dot_InfraObject__pb2 +from feast.protos.feast.core import OnDemandFeatureView_pb2 as feast_dot_core_dot_OnDemandFeatureView__pb2 +from feast.protos.feast.core import StreamFeatureView_pb2 as feast_dot_core_dot_StreamFeatureView__pb2 +from feast.protos.feast.core import DataSource_pb2 as feast_dot_core_dot_DataSource__pb2 +from feast.protos.feast.core import SavedDataset_pb2 as feast_dot_core_dot_SavedDataset__pb2 +from feast.protos.feast.core import ValidationProfile_pb2 as feast_dot_core_dot_ValidationProfile__pb2 +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 +from feast.protos.feast.core import Permission_pb2 as feast_dot_core_dot_Permission__pb2 +from feast.protos.feast.core import Project_pb2 as feast_dot_core_dot_Project__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x19\x66\x65\x61st/core/Registry.proto\x12\nfeast.core\x1a\x17\x66\x65\x61st/core/Entity.proto\x1a\x1f\x66\x65\x61st/core/FeatureService.proto\x1a\x1d\x66\x65\x61st/core/FeatureTable.proto\x1a\x1c\x66\x65\x61st/core/FeatureView.proto\x1a\x1c\x66\x65\x61st/core/InfraObject.proto\x1a$feast/core/OnDemandFeatureView.proto\x1a\"feast/core/StreamFeatureView.proto\x1a\x1b\x66\x65\x61st/core/DataSource.proto\x1a\x1d\x66\x65\x61st/core/SavedDataset.proto\x1a\"feast/core/ValidationProfile.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1b\x66\x65\x61st/core/Permission.proto\x1a\x18\x66\x65\x61st/core/Project.proto\"\xff\x05\n\x08Registry\x12$\n\x08\x65ntities\x18\x01 \x03(\x0b\x32\x12.feast.core.Entity\x12\x30\n\x0e\x66\x65\x61ture_tables\x18\x02 \x03(\x0b\x32\x18.feast.core.FeatureTable\x12.\n\rfeature_views\x18\x06 \x03(\x0b\x32\x17.feast.core.FeatureView\x12,\n\x0c\x64\x61ta_sources\x18\x0c \x03(\x0b\x32\x16.feast.core.DataSource\x12@\n\x17on_demand_feature_views\x18\x08 \x03(\x0b\x32\x1f.feast.core.OnDemandFeatureView\x12;\n\x14stream_feature_views\x18\x0e \x03(\x0b\x32\x1d.feast.core.StreamFeatureView\x12\x34\n\x10\x66\x65\x61ture_services\x18\x07 \x03(\x0b\x32\x1a.feast.core.FeatureService\x12\x30\n\x0esaved_datasets\x18\x0b \x03(\x0b\x32\x18.feast.core.SavedDataset\x12>\n\x15validation_references\x18\r \x03(\x0b\x32\x1f.feast.core.ValidationReference\x12 \n\x05infra\x18\n \x01(\x0b\x32\x11.feast.core.Infra\x12\x39\n\x10project_metadata\x18\x0f \x03(\x0b\x32\x1b.feast.core.ProjectMetadataB\x02\x18\x01\x12\x1f\n\x17registry_schema_version\x18\x03 \x01(\t\x12\x12\n\nversion_id\x18\x04 \x01(\t\x12\x30\n\x0clast_updated\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12+\n\x0bpermissions\x18\x10 \x03(\x0b\x32\x16.feast.core.Permission\x12%\n\x08projects\x18\x11 \x03(\x0b\x32\x13.feast.core.Project\"8\n\x0fProjectMetadata\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x14\n\x0cproject_uuid\x18\x02 \x01(\tBR\n\x10\x66\x65\x61st.proto.coreB\rRegistryProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'feast.core.Registry_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\020feast.proto.coreB\rRegistryProtoZ/github.com/feast-dev/feast/go/protos/feast/core' + _globals['_REGISTRY'].fields_by_name['project_metadata']._options = None + _globals['_REGISTRY'].fields_by_name['project_metadata']._serialized_options = b'\030\001' + _globals['_REGISTRY']._serialized_start=449 + _globals['_REGISTRY']._serialized_end=1216 + _globals['_PROJECTMETADATA']._serialized_start=1218 + _globals['_PROJECTMETADATA']._serialized_end=1274 +# @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/Registry_pb2.pyi b/sdk/python/feast/protos/feast/core/Registry_pb2.pyi new file mode 100644 index 0000000000..fca49c7548 --- /dev/null +++ b/sdk/python/feast/protos/feast/core/Registry_pb2.pyi @@ -0,0 +1,140 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file + +* Copyright 2020 The Feast 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 +* +* https://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 builtins +import collections.abc +import feast.core.DataSource_pb2 +import feast.core.Entity_pb2 +import feast.core.FeatureService_pb2 +import feast.core.FeatureTable_pb2 +import feast.core.FeatureView_pb2 +import feast.core.InfraObject_pb2 +import feast.core.OnDemandFeatureView_pb2 +import feast.core.Permission_pb2 +import feast.core.Project_pb2 +import feast.core.SavedDataset_pb2 +import feast.core.StreamFeatureView_pb2 +import feast.core.ValidationProfile_pb2 +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.message +import google.protobuf.timestamp_pb2 +import sys + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class Registry(google.protobuf.message.Message): + """Next id: 18""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + ENTITIES_FIELD_NUMBER: builtins.int + FEATURE_TABLES_FIELD_NUMBER: builtins.int + FEATURE_VIEWS_FIELD_NUMBER: builtins.int + DATA_SOURCES_FIELD_NUMBER: builtins.int + ON_DEMAND_FEATURE_VIEWS_FIELD_NUMBER: builtins.int + STREAM_FEATURE_VIEWS_FIELD_NUMBER: builtins.int + FEATURE_SERVICES_FIELD_NUMBER: builtins.int + SAVED_DATASETS_FIELD_NUMBER: builtins.int + VALIDATION_REFERENCES_FIELD_NUMBER: builtins.int + INFRA_FIELD_NUMBER: builtins.int + PROJECT_METADATA_FIELD_NUMBER: builtins.int + REGISTRY_SCHEMA_VERSION_FIELD_NUMBER: builtins.int + VERSION_ID_FIELD_NUMBER: builtins.int + LAST_UPDATED_FIELD_NUMBER: builtins.int + PERMISSIONS_FIELD_NUMBER: builtins.int + PROJECTS_FIELD_NUMBER: builtins.int + @property + def entities(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.Entity_pb2.Entity]: ... + @property + def feature_tables(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.FeatureTable_pb2.FeatureTable]: ... + @property + def feature_views(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.FeatureView_pb2.FeatureView]: ... + @property + def data_sources(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.DataSource_pb2.DataSource]: ... + @property + def on_demand_feature_views(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.OnDemandFeatureView_pb2.OnDemandFeatureView]: ... + @property + def stream_feature_views(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.StreamFeatureView_pb2.StreamFeatureView]: ... + @property + def feature_services(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.FeatureService_pb2.FeatureService]: ... + @property + def saved_datasets(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.SavedDataset_pb2.SavedDataset]: ... + @property + def validation_references(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.ValidationProfile_pb2.ValidationReference]: ... + @property + def infra(self) -> feast.core.InfraObject_pb2.Infra: ... + @property + def project_metadata(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ProjectMetadata]: + """Tracking metadata of Feast by project""" + registry_schema_version: builtins.str + """to support migrations; incremented when schema is changed""" + version_id: builtins.str + """version id, random string generated on each update of the data; now used only for debugging purposes""" + @property + def last_updated(self) -> google.protobuf.timestamp_pb2.Timestamp: ... + @property + def permissions(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.Permission_pb2.Permission]: ... + @property + def projects(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.Project_pb2.Project]: ... + def __init__( + self, + *, + entities: collections.abc.Iterable[feast.core.Entity_pb2.Entity] | None = ..., + feature_tables: collections.abc.Iterable[feast.core.FeatureTable_pb2.FeatureTable] | None = ..., + feature_views: collections.abc.Iterable[feast.core.FeatureView_pb2.FeatureView] | None = ..., + data_sources: collections.abc.Iterable[feast.core.DataSource_pb2.DataSource] | None = ..., + on_demand_feature_views: collections.abc.Iterable[feast.core.OnDemandFeatureView_pb2.OnDemandFeatureView] | None = ..., + stream_feature_views: collections.abc.Iterable[feast.core.StreamFeatureView_pb2.StreamFeatureView] | None = ..., + feature_services: collections.abc.Iterable[feast.core.FeatureService_pb2.FeatureService] | None = ..., + saved_datasets: collections.abc.Iterable[feast.core.SavedDataset_pb2.SavedDataset] | None = ..., + validation_references: collections.abc.Iterable[feast.core.ValidationProfile_pb2.ValidationReference] | None = ..., + infra: feast.core.InfraObject_pb2.Infra | None = ..., + project_metadata: collections.abc.Iterable[global___ProjectMetadata] | None = ..., + registry_schema_version: builtins.str = ..., + version_id: builtins.str = ..., + last_updated: google.protobuf.timestamp_pb2.Timestamp | None = ..., + permissions: collections.abc.Iterable[feast.core.Permission_pb2.Permission] | None = ..., + projects: collections.abc.Iterable[feast.core.Project_pb2.Project] | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["infra", b"infra", "last_updated", b"last_updated"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["data_sources", b"data_sources", "entities", b"entities", "feature_services", b"feature_services", "feature_tables", b"feature_tables", "feature_views", b"feature_views", "infra", b"infra", "last_updated", b"last_updated", "on_demand_feature_views", b"on_demand_feature_views", "permissions", b"permissions", "project_metadata", b"project_metadata", "projects", b"projects", "registry_schema_version", b"registry_schema_version", "saved_datasets", b"saved_datasets", "stream_feature_views", b"stream_feature_views", "validation_references", b"validation_references", "version_id", b"version_id"]) -> None: ... + +global___Registry = Registry + +class ProjectMetadata(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + PROJECT_FIELD_NUMBER: builtins.int + PROJECT_UUID_FIELD_NUMBER: builtins.int + project: builtins.str + project_uuid: builtins.str + def __init__( + self, + *, + project: builtins.str = ..., + project_uuid: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["project", b"project", "project_uuid", b"project_uuid"]) -> None: ... + +global___ProjectMetadata = ProjectMetadata diff --git a/sdk/python/feast/protos/feast/core/Registry_pb2_grpc.py b/sdk/python/feast/protos/feast/core/Registry_pb2_grpc.py new file mode 100644 index 0000000000..2daafffebf --- /dev/null +++ b/sdk/python/feast/protos/feast/core/Registry_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + diff --git a/sdk/python/feast/protos/feast/core/SavedDataset_pb2.py b/sdk/python/feast/protos/feast/core/SavedDataset_pb2.py new file mode 100644 index 0000000000..fe1e2d49ea --- /dev/null +++ b/sdk/python/feast/protos/feast/core/SavedDataset_pb2.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: feast/core/SavedDataset.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 +from feast.protos.feast.core import DataSource_pb2 as feast_dot_core_dot_DataSource__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1d\x66\x65\x61st/core/SavedDataset.proto\x12\nfeast.core\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1b\x66\x65\x61st/core/DataSource.proto\"\xa5\x02\n\x10SavedDatasetSpec\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x10\n\x08\x66\x65\x61tures\x18\x03 \x03(\t\x12\x11\n\tjoin_keys\x18\x04 \x03(\t\x12\x1a\n\x12\x66ull_feature_names\x18\x05 \x01(\x08\x12\x30\n\x07storage\x18\x06 \x01(\x0b\x32\x1f.feast.core.SavedDatasetStorage\x12\x1c\n\x14\x66\x65\x61ture_service_name\x18\x08 \x01(\t\x12\x34\n\x04tags\x18\x07 \x03(\x0b\x32&.feast.core.SavedDatasetSpec.TagsEntry\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xa9\x04\n\x13SavedDatasetStorage\x12:\n\x0c\x66ile_storage\x18\x04 \x01(\x0b\x32\".feast.core.DataSource.FileOptionsH\x00\x12\x42\n\x10\x62igquery_storage\x18\x05 \x01(\x0b\x32&.feast.core.DataSource.BigQueryOptionsH\x00\x12\x42\n\x10redshift_storage\x18\x06 \x01(\x0b\x32&.feast.core.DataSource.RedshiftOptionsH\x00\x12\x44\n\x11snowflake_storage\x18\x07 \x01(\x0b\x32\'.feast.core.DataSource.SnowflakeOptionsH\x00\x12<\n\rtrino_storage\x18\x08 \x01(\x0b\x32#.feast.core.DataSource.TrinoOptionsH\x00\x12<\n\rspark_storage\x18\t \x01(\x0b\x32#.feast.core.DataSource.SparkOptionsH\x00\x12\x44\n\x0e\x63ustom_storage\x18\n \x01(\x0b\x32*.feast.core.DataSource.CustomSourceOptionsH\x00\x12>\n\x0e\x61thena_storage\x18\x0b \x01(\x0b\x32$.feast.core.DataSource.AthenaOptionsH\x00\x42\x06\n\x04kind\"\xf7\x01\n\x10SavedDatasetMeta\x12\x35\n\x11\x63reated_timestamp\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12:\n\x16last_updated_timestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x37\n\x13min_event_timestamp\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x37\n\x13max_event_timestamp\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"f\n\x0cSavedDataset\x12*\n\x04spec\x18\x01 \x01(\x0b\x32\x1c.feast.core.SavedDatasetSpec\x12*\n\x04meta\x18\x02 \x01(\x0b\x32\x1c.feast.core.SavedDatasetMetaBV\n\x10\x66\x65\x61st.proto.coreB\x11SavedDatasetProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'feast.core.SavedDataset_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\020feast.proto.coreB\021SavedDatasetProtoZ/github.com/feast-dev/feast/go/protos/feast/core' + _globals['_SAVEDDATASETSPEC_TAGSENTRY']._options = None + _globals['_SAVEDDATASETSPEC_TAGSENTRY']._serialized_options = b'8\001' + _globals['_SAVEDDATASETSPEC']._serialized_start=108 + _globals['_SAVEDDATASETSPEC']._serialized_end=401 + _globals['_SAVEDDATASETSPEC_TAGSENTRY']._serialized_start=358 + _globals['_SAVEDDATASETSPEC_TAGSENTRY']._serialized_end=401 + _globals['_SAVEDDATASETSTORAGE']._serialized_start=404 + _globals['_SAVEDDATASETSTORAGE']._serialized_end=957 + _globals['_SAVEDDATASETMETA']._serialized_start=960 + _globals['_SAVEDDATASETMETA']._serialized_end=1207 + _globals['_SAVEDDATASET']._serialized_start=1209 + _globals['_SAVEDDATASET']._serialized_end=1311 +# @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/SavedDataset_pb2.pyi b/sdk/python/feast/protos/feast/core/SavedDataset_pb2.pyi new file mode 100644 index 0000000000..47525b64ed --- /dev/null +++ b/sdk/python/feast/protos/feast/core/SavedDataset_pb2.pyi @@ -0,0 +1,192 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file + +Copyright 2021 The Feast 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 + + https://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 builtins +import collections.abc +import feast.core.DataSource_pb2 +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.message +import google.protobuf.timestamp_pb2 +import sys + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class SavedDatasetSpec(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class TagsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + NAME_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + FEATURES_FIELD_NUMBER: builtins.int + JOIN_KEYS_FIELD_NUMBER: builtins.int + FULL_FEATURE_NAMES_FIELD_NUMBER: builtins.int + STORAGE_FIELD_NUMBER: builtins.int + FEATURE_SERVICE_NAME_FIELD_NUMBER: builtins.int + TAGS_FIELD_NUMBER: builtins.int + name: builtins.str + """Name of the dataset. Must be unique since it's possible to overwrite dataset by name""" + project: builtins.str + """Name of Feast project that this Dataset belongs to.""" + @property + def features(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: + """list of feature references with format ":" """ + @property + def join_keys(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: + """entity columns + request columns from all feature views used during retrieval""" + full_feature_names: builtins.bool + """Whether full feature names are used in stored data""" + @property + def storage(self) -> global___SavedDatasetStorage: ... + feature_service_name: builtins.str + """Optional and only populated if generated from a feature service fetch""" + @property + def tags(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: + """User defined metadata""" + def __init__( + self, + *, + name: builtins.str = ..., + project: builtins.str = ..., + features: collections.abc.Iterable[builtins.str] | None = ..., + join_keys: collections.abc.Iterable[builtins.str] | None = ..., + full_feature_names: builtins.bool = ..., + storage: global___SavedDatasetStorage | None = ..., + feature_service_name: builtins.str = ..., + tags: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["storage", b"storage"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["feature_service_name", b"feature_service_name", "features", b"features", "full_feature_names", b"full_feature_names", "join_keys", b"join_keys", "name", b"name", "project", b"project", "storage", b"storage", "tags", b"tags"]) -> None: ... + +global___SavedDatasetSpec = SavedDatasetSpec + +class SavedDatasetStorage(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + FILE_STORAGE_FIELD_NUMBER: builtins.int + BIGQUERY_STORAGE_FIELD_NUMBER: builtins.int + REDSHIFT_STORAGE_FIELD_NUMBER: builtins.int + SNOWFLAKE_STORAGE_FIELD_NUMBER: builtins.int + TRINO_STORAGE_FIELD_NUMBER: builtins.int + SPARK_STORAGE_FIELD_NUMBER: builtins.int + CUSTOM_STORAGE_FIELD_NUMBER: builtins.int + ATHENA_STORAGE_FIELD_NUMBER: builtins.int + @property + def file_storage(self) -> feast.core.DataSource_pb2.DataSource.FileOptions: ... + @property + def bigquery_storage(self) -> feast.core.DataSource_pb2.DataSource.BigQueryOptions: ... + @property + def redshift_storage(self) -> feast.core.DataSource_pb2.DataSource.RedshiftOptions: ... + @property + def snowflake_storage(self) -> feast.core.DataSource_pb2.DataSource.SnowflakeOptions: ... + @property + def trino_storage(self) -> feast.core.DataSource_pb2.DataSource.TrinoOptions: ... + @property + def spark_storage(self) -> feast.core.DataSource_pb2.DataSource.SparkOptions: ... + @property + def custom_storage(self) -> feast.core.DataSource_pb2.DataSource.CustomSourceOptions: ... + @property + def athena_storage(self) -> feast.core.DataSource_pb2.DataSource.AthenaOptions: ... + def __init__( + self, + *, + file_storage: feast.core.DataSource_pb2.DataSource.FileOptions | None = ..., + bigquery_storage: feast.core.DataSource_pb2.DataSource.BigQueryOptions | None = ..., + redshift_storage: feast.core.DataSource_pb2.DataSource.RedshiftOptions | None = ..., + snowflake_storage: feast.core.DataSource_pb2.DataSource.SnowflakeOptions | None = ..., + trino_storage: feast.core.DataSource_pb2.DataSource.TrinoOptions | None = ..., + spark_storage: feast.core.DataSource_pb2.DataSource.SparkOptions | None = ..., + custom_storage: feast.core.DataSource_pb2.DataSource.CustomSourceOptions | None = ..., + athena_storage: feast.core.DataSource_pb2.DataSource.AthenaOptions | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["athena_storage", b"athena_storage", "bigquery_storage", b"bigquery_storage", "custom_storage", b"custom_storage", "file_storage", b"file_storage", "kind", b"kind", "redshift_storage", b"redshift_storage", "snowflake_storage", b"snowflake_storage", "spark_storage", b"spark_storage", "trino_storage", b"trino_storage"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["athena_storage", b"athena_storage", "bigquery_storage", b"bigquery_storage", "custom_storage", b"custom_storage", "file_storage", b"file_storage", "kind", b"kind", "redshift_storage", b"redshift_storage", "snowflake_storage", b"snowflake_storage", "spark_storage", b"spark_storage", "trino_storage", b"trino_storage"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal["kind", b"kind"]) -> typing_extensions.Literal["file_storage", "bigquery_storage", "redshift_storage", "snowflake_storage", "trino_storage", "spark_storage", "custom_storage", "athena_storage"] | None: ... + +global___SavedDatasetStorage = SavedDatasetStorage + +class SavedDatasetMeta(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + CREATED_TIMESTAMP_FIELD_NUMBER: builtins.int + LAST_UPDATED_TIMESTAMP_FIELD_NUMBER: builtins.int + MIN_EVENT_TIMESTAMP_FIELD_NUMBER: builtins.int + MAX_EVENT_TIMESTAMP_FIELD_NUMBER: builtins.int + @property + def created_timestamp(self) -> google.protobuf.timestamp_pb2.Timestamp: + """Time when this saved dataset is created""" + @property + def last_updated_timestamp(self) -> google.protobuf.timestamp_pb2.Timestamp: + """Time when this saved dataset is last updated""" + @property + def min_event_timestamp(self) -> google.protobuf.timestamp_pb2.Timestamp: + """Min timestamp in the dataset (needed for retrieval)""" + @property + def max_event_timestamp(self) -> google.protobuf.timestamp_pb2.Timestamp: + """Max timestamp in the dataset (needed for retrieval)""" + def __init__( + self, + *, + created_timestamp: google.protobuf.timestamp_pb2.Timestamp | None = ..., + last_updated_timestamp: google.protobuf.timestamp_pb2.Timestamp | None = ..., + min_event_timestamp: google.protobuf.timestamp_pb2.Timestamp | None = ..., + max_event_timestamp: google.protobuf.timestamp_pb2.Timestamp | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["created_timestamp", b"created_timestamp", "last_updated_timestamp", b"last_updated_timestamp", "max_event_timestamp", b"max_event_timestamp", "min_event_timestamp", b"min_event_timestamp"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["created_timestamp", b"created_timestamp", "last_updated_timestamp", b"last_updated_timestamp", "max_event_timestamp", b"max_event_timestamp", "min_event_timestamp", b"min_event_timestamp"]) -> None: ... + +global___SavedDatasetMeta = SavedDatasetMeta + +class SavedDataset(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + SPEC_FIELD_NUMBER: builtins.int + META_FIELD_NUMBER: builtins.int + @property + def spec(self) -> global___SavedDatasetSpec: ... + @property + def meta(self) -> global___SavedDatasetMeta: ... + def __init__( + self, + *, + spec: global___SavedDatasetSpec | None = ..., + meta: global___SavedDatasetMeta | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["meta", b"meta", "spec", b"spec"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["meta", b"meta", "spec", b"spec"]) -> None: ... + +global___SavedDataset = SavedDataset diff --git a/sdk/python/feast/protos/feast/core/SavedDataset_pb2_grpc.py b/sdk/python/feast/protos/feast/core/SavedDataset_pb2_grpc.py new file mode 100644 index 0000000000..2daafffebf --- /dev/null +++ b/sdk/python/feast/protos/feast/core/SavedDataset_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + diff --git a/sdk/python/feast/protos/feast/core/SqliteTable_pb2.py b/sdk/python/feast/protos/feast/core/SqliteTable_pb2.py new file mode 100644 index 0000000000..8cc14781c7 --- /dev/null +++ b/sdk/python/feast/protos/feast/core/SqliteTable_pb2.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: feast/core/SqliteTable.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1c\x66\x65\x61st/core/SqliteTable.proto\x12\nfeast.core\")\n\x0bSqliteTable\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\tBU\n\x10\x66\x65\x61st.proto.coreB\x10SqliteTableProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'feast.core.SqliteTable_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\020feast.proto.coreB\020SqliteTableProtoZ/github.com/feast-dev/feast/go/protos/feast/core' + _globals['_SQLITETABLE']._serialized_start=44 + _globals['_SQLITETABLE']._serialized_end=85 +# @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/SqliteTable_pb2.pyi b/sdk/python/feast/protos/feast/core/SqliteTable_pb2.pyi new file mode 100644 index 0000000000..10ecebf362 --- /dev/null +++ b/sdk/python/feast/protos/feast/core/SqliteTable_pb2.pyi @@ -0,0 +1,50 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file + +* Copyright 2021 The Feast 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 +* +* https://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 builtins +import google.protobuf.descriptor +import google.protobuf.message +import sys + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class SqliteTable(google.protobuf.message.Message): + """Represents a Sqlite table""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + PATH_FIELD_NUMBER: builtins.int + NAME_FIELD_NUMBER: builtins.int + path: builtins.str + """Absolute path of the table""" + name: builtins.str + """Name of the table""" + def __init__( + self, + *, + path: builtins.str = ..., + name: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["name", b"name", "path", b"path"]) -> None: ... + +global___SqliteTable = SqliteTable diff --git a/sdk/python/feast/protos/feast/core/SqliteTable_pb2_grpc.py b/sdk/python/feast/protos/feast/core/SqliteTable_pb2_grpc.py new file mode 100644 index 0000000000..2daafffebf --- /dev/null +++ b/sdk/python/feast/protos/feast/core/SqliteTable_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + diff --git a/sdk/python/feast/protos/feast/core/Store_pb2.py b/sdk/python/feast/protos/feast/core/Store_pb2.py new file mode 100644 index 0000000000..7d24e11947 --- /dev/null +++ b/sdk/python/feast/protos/feast/core/Store_pb2.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: feast/core/Store.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16\x66\x65\x61st/core/Store.proto\x12\nfeast.core\"\xfd\x06\n\x05Store\x12\x0c\n\x04name\x18\x01 \x01(\t\x12)\n\x04type\x18\x02 \x01(\x0e\x32\x1b.feast.core.Store.StoreType\x12\x35\n\rsubscriptions\x18\x04 \x03(\x0b\x32\x1e.feast.core.Store.Subscription\x12\x35\n\x0credis_config\x18\x0b \x01(\x0b\x32\x1d.feast.core.Store.RedisConfigH\x00\x12\x44\n\x14redis_cluster_config\x18\x0e \x01(\x0b\x32$.feast.core.Store.RedisClusterConfigH\x00\x1a\x88\x01\n\x0bRedisConfig\x12\x0c\n\x04host\x18\x01 \x01(\t\x12\x0c\n\x04port\x18\x02 \x01(\x05\x12\x1a\n\x12initial_backoff_ms\x18\x03 \x01(\x05\x12\x13\n\x0bmax_retries\x18\x04 \x01(\x05\x12\x1f\n\x17\x66lush_frequency_seconds\x18\x05 \x01(\x05\x12\x0b\n\x03ssl\x18\x06 \x01(\x08\x1a\xdb\x02\n\x12RedisClusterConfig\x12\x19\n\x11\x63onnection_string\x18\x01 \x01(\t\x12\x1a\n\x12initial_backoff_ms\x18\x02 \x01(\x05\x12\x13\n\x0bmax_retries\x18\x03 \x01(\x05\x12\x1f\n\x17\x66lush_frequency_seconds\x18\x04 \x01(\x05\x12\x12\n\nkey_prefix\x18\x05 \x01(\t\x12\x17\n\x0f\x65nable_fallback\x18\x06 \x01(\x08\x12\x17\n\x0f\x66\x61llback_prefix\x18\x07 \x01(\t\x12@\n\tread_from\x18\x08 \x01(\x0e\x32-.feast.core.Store.RedisClusterConfig.ReadFrom\"P\n\x08ReadFrom\x12\n\n\x06MASTER\x10\x00\x12\x14\n\x10MASTER_PREFERRED\x10\x01\x12\x0b\n\x07REPLICA\x10\x02\x12\x15\n\x11REPLICA_PREFERRED\x10\x03\x1a\x44\n\x0cSubscription\x12\x0f\n\x07project\x18\x03 \x01(\t\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07\x65xclude\x18\x04 \x01(\x08J\x04\x08\x02\x10\x03\"N\n\tStoreType\x12\x0b\n\x07INVALID\x10\x00\x12\t\n\x05REDIS\x10\x01\x12\x11\n\rREDIS_CLUSTER\x10\x04\"\x04\x08\x02\x10\x02\"\x04\x08\x03\x10\x03\"\x04\x08\x0c\x10\x0c\"\x04\x08\r\x10\rB\x08\n\x06\x63onfigBO\n\x10\x66\x65\x61st.proto.coreB\nStoreProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'feast.core.Store_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\020feast.proto.coreB\nStoreProtoZ/github.com/feast-dev/feast/go/protos/feast/core' + _globals['_STORE']._serialized_start=39 + _globals['_STORE']._serialized_end=932 + _globals['_STORE_REDISCONFIG']._serialized_start=286 + _globals['_STORE_REDISCONFIG']._serialized_end=422 + _globals['_STORE_REDISCLUSTERCONFIG']._serialized_start=425 + _globals['_STORE_REDISCLUSTERCONFIG']._serialized_end=772 + _globals['_STORE_REDISCLUSTERCONFIG_READFROM']._serialized_start=692 + _globals['_STORE_REDISCLUSTERCONFIG_READFROM']._serialized_end=772 + _globals['_STORE_SUBSCRIPTION']._serialized_start=774 + _globals['_STORE_SUBSCRIPTION']._serialized_end=842 + _globals['_STORE_STORETYPE']._serialized_start=844 + _globals['_STORE_STORETYPE']._serialized_end=922 +# @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/Store_pb2.pyi b/sdk/python/feast/protos/feast/core/Store_pb2.pyi new file mode 100644 index 0000000000..5ee957d184 --- /dev/null +++ b/sdk/python/feast/protos/feast/core/Store_pb2.pyi @@ -0,0 +1,234 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file + +* Copyright 2019 The Feast 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 +* +* https://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 builtins +import collections.abc +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.internal.enum_type_wrapper +import google.protobuf.message +import sys +import typing + +if sys.version_info >= (3, 10): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class Store(google.protobuf.message.Message): + """Store provides a location where Feast reads and writes feature values. + Feature values will be written to the Store in the form of FeatureRow elements. + The way FeatureRow is encoded and decoded when it is written to and read from + the Store depends on the type of the Store. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class _StoreType: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _StoreTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[Store._StoreType.ValueType], builtins.type): # noqa: F821 + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + INVALID: Store._StoreType.ValueType # 0 + REDIS: Store._StoreType.ValueType # 1 + """Redis stores a FeatureRow element as a key, value pair. + + The Redis data types used (https://redis.io/topics/data-types): + - key: STRING + - value: STRING + + Encodings: + - key: byte array of RedisKey (refer to feast.storage.RedisKeyV2) + - value: Redis hashmap + """ + REDIS_CLUSTER: Store._StoreType.ValueType # 4 + + class StoreType(_StoreType, metaclass=_StoreTypeEnumTypeWrapper): ... + INVALID: Store.StoreType.ValueType # 0 + REDIS: Store.StoreType.ValueType # 1 + """Redis stores a FeatureRow element as a key, value pair. + + The Redis data types used (https://redis.io/topics/data-types): + - key: STRING + - value: STRING + + Encodings: + - key: byte array of RedisKey (refer to feast.storage.RedisKeyV2) + - value: Redis hashmap + """ + REDIS_CLUSTER: Store.StoreType.ValueType # 4 + + class RedisConfig(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + HOST_FIELD_NUMBER: builtins.int + PORT_FIELD_NUMBER: builtins.int + INITIAL_BACKOFF_MS_FIELD_NUMBER: builtins.int + MAX_RETRIES_FIELD_NUMBER: builtins.int + FLUSH_FREQUENCY_SECONDS_FIELD_NUMBER: builtins.int + SSL_FIELD_NUMBER: builtins.int + host: builtins.str + port: builtins.int + initial_backoff_ms: builtins.int + """Optional. The number of milliseconds to wait before retrying failed Redis connection. + By default, Feast uses exponential backoff policy and "initial_backoff_ms" sets the initial wait duration. + """ + max_retries: builtins.int + """Optional. Maximum total number of retries for connecting to Redis. Default to zero retries.""" + flush_frequency_seconds: builtins.int + """Optional. How often flush data to redis""" + ssl: builtins.bool + """Optional. Connect over SSL.""" + def __init__( + self, + *, + host: builtins.str = ..., + port: builtins.int = ..., + initial_backoff_ms: builtins.int = ..., + max_retries: builtins.int = ..., + flush_frequency_seconds: builtins.int = ..., + ssl: builtins.bool = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["flush_frequency_seconds", b"flush_frequency_seconds", "host", b"host", "initial_backoff_ms", b"initial_backoff_ms", "max_retries", b"max_retries", "port", b"port", "ssl", b"ssl"]) -> None: ... + + class RedisClusterConfig(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class _ReadFrom: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _ReadFromEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[Store.RedisClusterConfig._ReadFrom.ValueType], builtins.type): # noqa: F821 + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + MASTER: Store.RedisClusterConfig._ReadFrom.ValueType # 0 + MASTER_PREFERRED: Store.RedisClusterConfig._ReadFrom.ValueType # 1 + REPLICA: Store.RedisClusterConfig._ReadFrom.ValueType # 2 + REPLICA_PREFERRED: Store.RedisClusterConfig._ReadFrom.ValueType # 3 + + class ReadFrom(_ReadFrom, metaclass=_ReadFromEnumTypeWrapper): + """Optional. Priority of nodes when reading from cluster""" + + MASTER: Store.RedisClusterConfig.ReadFrom.ValueType # 0 + MASTER_PREFERRED: Store.RedisClusterConfig.ReadFrom.ValueType # 1 + REPLICA: Store.RedisClusterConfig.ReadFrom.ValueType # 2 + REPLICA_PREFERRED: Store.RedisClusterConfig.ReadFrom.ValueType # 3 + + CONNECTION_STRING_FIELD_NUMBER: builtins.int + INITIAL_BACKOFF_MS_FIELD_NUMBER: builtins.int + MAX_RETRIES_FIELD_NUMBER: builtins.int + FLUSH_FREQUENCY_SECONDS_FIELD_NUMBER: builtins.int + KEY_PREFIX_FIELD_NUMBER: builtins.int + ENABLE_FALLBACK_FIELD_NUMBER: builtins.int + FALLBACK_PREFIX_FIELD_NUMBER: builtins.int + READ_FROM_FIELD_NUMBER: builtins.int + connection_string: builtins.str + """List of Redis Uri for all the nodes in Redis Cluster, comma separated. Eg. host1:6379, host2:6379""" + initial_backoff_ms: builtins.int + max_retries: builtins.int + flush_frequency_seconds: builtins.int + """Optional. How often flush data to redis""" + key_prefix: builtins.str + """Optional. Append a prefix to the Redis Key""" + enable_fallback: builtins.bool + """Optional. Enable fallback to another key prefix if the original key is not present. + Useful for migrating key prefix without re-ingestion. Disabled by default. + """ + fallback_prefix: builtins.str + """Optional. This would be the fallback prefix to use if enable_fallback is true.""" + read_from: global___Store.RedisClusterConfig.ReadFrom.ValueType + def __init__( + self, + *, + connection_string: builtins.str = ..., + initial_backoff_ms: builtins.int = ..., + max_retries: builtins.int = ..., + flush_frequency_seconds: builtins.int = ..., + key_prefix: builtins.str = ..., + enable_fallback: builtins.bool = ..., + fallback_prefix: builtins.str = ..., + read_from: global___Store.RedisClusterConfig.ReadFrom.ValueType = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["connection_string", b"connection_string", "enable_fallback", b"enable_fallback", "fallback_prefix", b"fallback_prefix", "flush_frequency_seconds", b"flush_frequency_seconds", "initial_backoff_ms", b"initial_backoff_ms", "key_prefix", b"key_prefix", "max_retries", b"max_retries", "read_from", b"read_from"]) -> None: ... + + class Subscription(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + PROJECT_FIELD_NUMBER: builtins.int + NAME_FIELD_NUMBER: builtins.int + EXCLUDE_FIELD_NUMBER: builtins.int + project: builtins.str + """Name of project that the feature sets belongs to. This can be one of + - [project_name] + - * + If an asterisk is provided, filtering on projects will be disabled. All projects will + be matched. It is NOT possible to provide an asterisk with a string in order to do + pattern matching. + """ + name: builtins.str + """Name of the desired feature set. Asterisks can be used as wildcards in the name. + Matching on names is only permitted if a specific project is defined. It is disallowed + If the project name is set to "*" + e.g. + - * can be used to match all feature sets + - my-feature-set* can be used to match all features prefixed by "my-feature-set" + - my-feature-set-6 can be used to select a single feature set + """ + exclude: builtins.bool + """All matches with exclude enabled will be filtered out instead of added""" + def __init__( + self, + *, + project: builtins.str = ..., + name: builtins.str = ..., + exclude: builtins.bool = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["exclude", b"exclude", "name", b"name", "project", b"project"]) -> None: ... + + NAME_FIELD_NUMBER: builtins.int + TYPE_FIELD_NUMBER: builtins.int + SUBSCRIPTIONS_FIELD_NUMBER: builtins.int + REDIS_CONFIG_FIELD_NUMBER: builtins.int + REDIS_CLUSTER_CONFIG_FIELD_NUMBER: builtins.int + name: builtins.str + """Name of the store.""" + type: global___Store.StoreType.ValueType + """Type of store.""" + @property + def subscriptions(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Store.Subscription]: + """Feature sets to subscribe to.""" + @property + def redis_config(self) -> global___Store.RedisConfig: ... + @property + def redis_cluster_config(self) -> global___Store.RedisClusterConfig: ... + def __init__( + self, + *, + name: builtins.str = ..., + type: global___Store.StoreType.ValueType = ..., + subscriptions: collections.abc.Iterable[global___Store.Subscription] | None = ..., + redis_config: global___Store.RedisConfig | None = ..., + redis_cluster_config: global___Store.RedisClusterConfig | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["config", b"config", "redis_cluster_config", b"redis_cluster_config", "redis_config", b"redis_config"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["config", b"config", "name", b"name", "redis_cluster_config", b"redis_cluster_config", "redis_config", b"redis_config", "subscriptions", b"subscriptions", "type", b"type"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal["config", b"config"]) -> typing_extensions.Literal["redis_config", "redis_cluster_config"] | None: ... + +global___Store = Store diff --git a/sdk/python/feast/protos/feast/core/Store_pb2_grpc.py b/sdk/python/feast/protos/feast/core/Store_pb2_grpc.py new file mode 100644 index 0000000000..2daafffebf --- /dev/null +++ b/sdk/python/feast/protos/feast/core/Store_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + diff --git a/sdk/python/feast/protos/feast/core/StreamFeatureView_pb2.py b/sdk/python/feast/protos/feast/core/StreamFeatureView_pb2.py new file mode 100644 index 0000000000..ba19088edd --- /dev/null +++ b/sdk/python/feast/protos/feast/core/StreamFeatureView_pb2.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: feast/core/StreamFeatureView.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import duration_pb2 as google_dot_protobuf_dot_duration__pb2 +from feast.protos.feast.core import OnDemandFeatureView_pb2 as feast_dot_core_dot_OnDemandFeatureView__pb2 +from feast.protos.feast.core import FeatureView_pb2 as feast_dot_core_dot_FeatureView__pb2 +from feast.protos.feast.core import Feature_pb2 as feast_dot_core_dot_Feature__pb2 +from feast.protos.feast.core import DataSource_pb2 as feast_dot_core_dot_DataSource__pb2 +from feast.protos.feast.core import Aggregation_pb2 as feast_dot_core_dot_Aggregation__pb2 +from feast.protos.feast.core import Transformation_pb2 as feast_dot_core_dot_Transformation__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"feast/core/StreamFeatureView.proto\x12\nfeast.core\x1a\x1egoogle/protobuf/duration.proto\x1a$feast/core/OnDemandFeatureView.proto\x1a\x1c\x66\x65\x61st/core/FeatureView.proto\x1a\x18\x66\x65\x61st/core/Feature.proto\x1a\x1b\x66\x65\x61st/core/DataSource.proto\x1a\x1c\x66\x65\x61st/core/Aggregation.proto\x1a\x1f\x66\x65\x61st/core/Transformation.proto\"o\n\x11StreamFeatureView\x12/\n\x04spec\x18\x01 \x01(\x0b\x32!.feast.core.StreamFeatureViewSpec\x12)\n\x04meta\x18\x02 \x01(\x0b\x32\x1b.feast.core.FeatureViewMeta\"\xa8\x05\n\x15StreamFeatureViewSpec\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x10\n\x08\x65ntities\x18\x03 \x03(\t\x12+\n\x08\x66\x65\x61tures\x18\x04 \x03(\x0b\x32\x19.feast.core.FeatureSpecV2\x12\x31\n\x0e\x65ntity_columns\x18\x05 \x03(\x0b\x32\x19.feast.core.FeatureSpecV2\x12\x13\n\x0b\x64\x65scription\x18\x06 \x01(\t\x12\x39\n\x04tags\x18\x07 \x03(\x0b\x32+.feast.core.StreamFeatureViewSpec.TagsEntry\x12\r\n\x05owner\x18\x08 \x01(\t\x12&\n\x03ttl\x18\t \x01(\x0b\x32\x19.google.protobuf.Duration\x12,\n\x0c\x62\x61tch_source\x18\n \x01(\x0b\x32\x16.feast.core.DataSource\x12-\n\rstream_source\x18\x0b \x01(\x0b\x32\x16.feast.core.DataSource\x12\x0e\n\x06online\x18\x0c \x01(\x08\x12\x42\n\x15user_defined_function\x18\r \x01(\x0b\x32\x1f.feast.core.UserDefinedFunctionB\x02\x18\x01\x12\x0c\n\x04mode\x18\x0e \x01(\t\x12-\n\x0c\x61ggregations\x18\x0f \x03(\x0b\x32\x17.feast.core.Aggregation\x12\x17\n\x0ftimestamp_field\x18\x10 \x01(\t\x12\x43\n\x16\x66\x65\x61ture_transformation\x18\x11 \x01(\x0b\x32#.feast.core.FeatureTransformationV2\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42[\n\x10\x66\x65\x61st.proto.coreB\x16StreamFeatureViewProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'feast.core.StreamFeatureView_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\020feast.proto.coreB\026StreamFeatureViewProtoZ/github.com/feast-dev/feast/go/protos/feast/core' + _globals['_STREAMFEATUREVIEWSPEC_TAGSENTRY']._options = None + _globals['_STREAMFEATUREVIEWSPEC_TAGSENTRY']._serialized_options = b'8\001' + _globals['_STREAMFEATUREVIEWSPEC'].fields_by_name['user_defined_function']._options = None + _globals['_STREAMFEATUREVIEWSPEC'].fields_by_name['user_defined_function']._serialized_options = b'\030\001' + _globals['_STREAMFEATUREVIEW']._serialized_start=268 + _globals['_STREAMFEATUREVIEW']._serialized_end=379 + _globals['_STREAMFEATUREVIEWSPEC']._serialized_start=382 + _globals['_STREAMFEATUREVIEWSPEC']._serialized_end=1062 + _globals['_STREAMFEATUREVIEWSPEC_TAGSENTRY']._serialized_start=1019 + _globals['_STREAMFEATUREVIEWSPEC_TAGSENTRY']._serialized_end=1062 +# @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/StreamFeatureView_pb2.pyi b/sdk/python/feast/protos/feast/core/StreamFeatureView_pb2.pyi new file mode 100644 index 0000000000..70e897a2f2 --- /dev/null +++ b/sdk/python/feast/protos/feast/core/StreamFeatureView_pb2.pyi @@ -0,0 +1,170 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file + +Copyright 2020 The Feast 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 + + https://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 builtins +import collections.abc +import feast.core.Aggregation_pb2 +import feast.core.DataSource_pb2 +import feast.core.FeatureView_pb2 +import feast.core.Feature_pb2 +import feast.core.OnDemandFeatureView_pb2 +import feast.core.Transformation_pb2 +import google.protobuf.descriptor +import google.protobuf.duration_pb2 +import google.protobuf.internal.containers +import google.protobuf.message +import sys + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class StreamFeatureView(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + SPEC_FIELD_NUMBER: builtins.int + META_FIELD_NUMBER: builtins.int + @property + def spec(self) -> global___StreamFeatureViewSpec: + """User-specified specifications of this feature view.""" + @property + def meta(self) -> feast.core.FeatureView_pb2.FeatureViewMeta: ... + def __init__( + self, + *, + spec: global___StreamFeatureViewSpec | None = ..., + meta: feast.core.FeatureView_pb2.FeatureViewMeta | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["meta", b"meta", "spec", b"spec"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["meta", b"meta", "spec", b"spec"]) -> None: ... + +global___StreamFeatureView = StreamFeatureView + +class StreamFeatureViewSpec(google.protobuf.message.Message): + """Next available id: 17""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class TagsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + NAME_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + ENTITIES_FIELD_NUMBER: builtins.int + FEATURES_FIELD_NUMBER: builtins.int + ENTITY_COLUMNS_FIELD_NUMBER: builtins.int + DESCRIPTION_FIELD_NUMBER: builtins.int + TAGS_FIELD_NUMBER: builtins.int + OWNER_FIELD_NUMBER: builtins.int + TTL_FIELD_NUMBER: builtins.int + BATCH_SOURCE_FIELD_NUMBER: builtins.int + STREAM_SOURCE_FIELD_NUMBER: builtins.int + ONLINE_FIELD_NUMBER: builtins.int + USER_DEFINED_FUNCTION_FIELD_NUMBER: builtins.int + MODE_FIELD_NUMBER: builtins.int + AGGREGATIONS_FIELD_NUMBER: builtins.int + TIMESTAMP_FIELD_FIELD_NUMBER: builtins.int + FEATURE_TRANSFORMATION_FIELD_NUMBER: builtins.int + name: builtins.str + """Name of the feature view. Must be unique. Not updated.""" + project: builtins.str + """Name of Feast project that this feature view belongs to.""" + @property + def entities(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: + """List of names of entities associated with this feature view.""" + @property + def features(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.Feature_pb2.FeatureSpecV2]: + """List of specifications for each feature defined as part of this feature view.""" + @property + def entity_columns(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.Feature_pb2.FeatureSpecV2]: + """List of specifications for each entity defined as part of this feature view.""" + description: builtins.str + """Description of the feature view.""" + @property + def tags(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: + """User defined metadata""" + owner: builtins.str + """Owner of the feature view.""" + @property + def ttl(self) -> google.protobuf.duration_pb2.Duration: + """Features in this feature view can only be retrieved from online serving + younger than ttl. Ttl is measured as the duration of time between + the feature's event timestamp and when the feature is retrieved + Feature values outside ttl will be returned as unset values and indicated to end user + """ + @property + def batch_source(self) -> feast.core.DataSource_pb2.DataSource: + """Batch/Offline DataSource where this view can retrieve offline feature data.""" + @property + def stream_source(self) -> feast.core.DataSource_pb2.DataSource: + """Streaming DataSource from where this view can consume "online" feature data.""" + online: builtins.bool + """Whether these features should be served online or not""" + @property + def user_defined_function(self) -> feast.core.OnDemandFeatureView_pb2.UserDefinedFunction: + """Serialized function that is encoded in the streamfeatureview""" + mode: builtins.str + """Mode of execution""" + @property + def aggregations(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.Aggregation_pb2.Aggregation]: + """Aggregation definitions""" + timestamp_field: builtins.str + """Timestamp field for aggregation""" + @property + def feature_transformation(self) -> feast.core.Transformation_pb2.FeatureTransformationV2: + """Oneof with {user_defined_function, on_demand_substrait_transformation}""" + def __init__( + self, + *, + name: builtins.str = ..., + project: builtins.str = ..., + entities: collections.abc.Iterable[builtins.str] | None = ..., + features: collections.abc.Iterable[feast.core.Feature_pb2.FeatureSpecV2] | None = ..., + entity_columns: collections.abc.Iterable[feast.core.Feature_pb2.FeatureSpecV2] | None = ..., + description: builtins.str = ..., + tags: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + owner: builtins.str = ..., + ttl: google.protobuf.duration_pb2.Duration | None = ..., + batch_source: feast.core.DataSource_pb2.DataSource | None = ..., + stream_source: feast.core.DataSource_pb2.DataSource | None = ..., + online: builtins.bool = ..., + user_defined_function: feast.core.OnDemandFeatureView_pb2.UserDefinedFunction | None = ..., + mode: builtins.str = ..., + aggregations: collections.abc.Iterable[feast.core.Aggregation_pb2.Aggregation] | None = ..., + timestamp_field: builtins.str = ..., + feature_transformation: feast.core.Transformation_pb2.FeatureTransformationV2 | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["batch_source", b"batch_source", "feature_transformation", b"feature_transformation", "stream_source", b"stream_source", "ttl", b"ttl", "user_defined_function", b"user_defined_function"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["aggregations", b"aggregations", "batch_source", b"batch_source", "description", b"description", "entities", b"entities", "entity_columns", b"entity_columns", "feature_transformation", b"feature_transformation", "features", b"features", "mode", b"mode", "name", b"name", "online", b"online", "owner", b"owner", "project", b"project", "stream_source", b"stream_source", "tags", b"tags", "timestamp_field", b"timestamp_field", "ttl", b"ttl", "user_defined_function", b"user_defined_function"]) -> None: ... + +global___StreamFeatureViewSpec = StreamFeatureViewSpec diff --git a/sdk/python/feast/protos/feast/core/StreamFeatureView_pb2_grpc.py b/sdk/python/feast/protos/feast/core/StreamFeatureView_pb2_grpc.py new file mode 100644 index 0000000000..2daafffebf --- /dev/null +++ b/sdk/python/feast/protos/feast/core/StreamFeatureView_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + diff --git a/sdk/python/feast/protos/feast/core/Transformation_pb2.py b/sdk/python/feast/protos/feast/core/Transformation_pb2.py new file mode 100644 index 0000000000..9fd11d3026 --- /dev/null +++ b/sdk/python/feast/protos/feast/core/Transformation_pb2.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: feast/core/Transformation.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1f\x66\x65\x61st/core/Transformation.proto\x12\nfeast.core\"F\n\x15UserDefinedFunctionV2\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04\x62ody\x18\x02 \x01(\x0c\x12\x11\n\tbody_text\x18\x03 \x01(\t\"\xba\x01\n\x17\x46\x65\x61tureTransformationV2\x12\x42\n\x15user_defined_function\x18\x01 \x01(\x0b\x32!.feast.core.UserDefinedFunctionV2H\x00\x12I\n\x18substrait_transformation\x18\x02 \x01(\x0b\x32%.feast.core.SubstraitTransformationV2H\x00\x42\x10\n\x0etransformation\"J\n\x19SubstraitTransformationV2\x12\x16\n\x0esubstrait_plan\x18\x01 \x01(\x0c\x12\x15\n\ribis_function\x18\x02 \x01(\x0c\x42_\n\x10\x66\x65\x61st.proto.coreB\x1a\x46\x65\x61tureTransformationProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'feast.core.Transformation_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\020feast.proto.coreB\032FeatureTransformationProtoZ/github.com/feast-dev/feast/go/protos/feast/core' + _globals['_USERDEFINEDFUNCTIONV2']._serialized_start=47 + _globals['_USERDEFINEDFUNCTIONV2']._serialized_end=117 + _globals['_FEATURETRANSFORMATIONV2']._serialized_start=120 + _globals['_FEATURETRANSFORMATIONV2']._serialized_end=306 + _globals['_SUBSTRAITTRANSFORMATIONV2']._serialized_start=308 + _globals['_SUBSTRAITTRANSFORMATIONV2']._serialized_end=382 +# @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/Transformation_pb2.pyi b/sdk/python/feast/protos/feast/core/Transformation_pb2.pyi new file mode 100644 index 0000000000..1120c447e0 --- /dev/null +++ b/sdk/python/feast/protos/feast/core/Transformation_pb2.pyi @@ -0,0 +1,80 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" +import builtins +import google.protobuf.descriptor +import google.protobuf.message +import sys + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class UserDefinedFunctionV2(google.protobuf.message.Message): + """Serialized representation of python function.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + BODY_FIELD_NUMBER: builtins.int + BODY_TEXT_FIELD_NUMBER: builtins.int + name: builtins.str + """The function name""" + body: builtins.bytes + """The python-syntax function body (serialized by dill)""" + body_text: builtins.str + """The string representation of the udf""" + def __init__( + self, + *, + name: builtins.str = ..., + body: builtins.bytes = ..., + body_text: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["body", b"body", "body_text", b"body_text", "name", b"name"]) -> None: ... + +global___UserDefinedFunctionV2 = UserDefinedFunctionV2 + +class FeatureTransformationV2(google.protobuf.message.Message): + """A feature transformation executed as a user-defined function""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + USER_DEFINED_FUNCTION_FIELD_NUMBER: builtins.int + SUBSTRAIT_TRANSFORMATION_FIELD_NUMBER: builtins.int + @property + def user_defined_function(self) -> global___UserDefinedFunctionV2: ... + @property + def substrait_transformation(self) -> global___SubstraitTransformationV2: ... + def __init__( + self, + *, + user_defined_function: global___UserDefinedFunctionV2 | None = ..., + substrait_transformation: global___SubstraitTransformationV2 | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["substrait_transformation", b"substrait_transformation", "transformation", b"transformation", "user_defined_function", b"user_defined_function"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["substrait_transformation", b"substrait_transformation", "transformation", b"transformation", "user_defined_function", b"user_defined_function"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal["transformation", b"transformation"]) -> typing_extensions.Literal["user_defined_function", "substrait_transformation"] | None: ... + +global___FeatureTransformationV2 = FeatureTransformationV2 + +class SubstraitTransformationV2(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + SUBSTRAIT_PLAN_FIELD_NUMBER: builtins.int + IBIS_FUNCTION_FIELD_NUMBER: builtins.int + substrait_plan: builtins.bytes + ibis_function: builtins.bytes + def __init__( + self, + *, + substrait_plan: builtins.bytes = ..., + ibis_function: builtins.bytes = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["ibis_function", b"ibis_function", "substrait_plan", b"substrait_plan"]) -> None: ... + +global___SubstraitTransformationV2 = SubstraitTransformationV2 diff --git a/sdk/python/feast/protos/feast/core/Transformation_pb2_grpc.py b/sdk/python/feast/protos/feast/core/Transformation_pb2_grpc.py new file mode 100644 index 0000000000..2daafffebf --- /dev/null +++ b/sdk/python/feast/protos/feast/core/Transformation_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + diff --git a/sdk/python/feast/protos/feast/core/ValidationProfile_pb2.py b/sdk/python/feast/protos/feast/core/ValidationProfile_pb2.py new file mode 100644 index 0000000000..0fb27ceab1 --- /dev/null +++ b/sdk/python/feast/protos/feast/core/ValidationProfile_pb2.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: feast/core/ValidationProfile.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"feast/core/ValidationProfile.proto\x12\nfeast.core\"\x83\x01\n\x14GEValidationProfiler\x12\x46\n\x08profiler\x18\x01 \x01(\x0b\x32\x34.feast.core.GEValidationProfiler.UserDefinedProfiler\x1a#\n\x13UserDefinedProfiler\x12\x0c\n\x04\x62ody\x18\x01 \x01(\x0c\"0\n\x13GEValidationProfile\x12\x19\n\x11\x65xpectation_suite\x18\x01 \x01(\x0c\"\xdd\x02\n\x13ValidationReference\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x1e\n\x16reference_dataset_name\x18\x02 \x01(\t\x12\x0f\n\x07project\x18\x03 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x04 \x01(\t\x12\x37\n\x04tags\x18\x05 \x03(\x0b\x32).feast.core.ValidationReference.TagsEntry\x12\x37\n\x0bge_profiler\x18\x06 \x01(\x0b\x32 .feast.core.GEValidationProfilerH\x00\x12\x35\n\nge_profile\x18\x07 \x01(\x0b\x32\x1f.feast.core.GEValidationProfileH\x01\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\n\n\x08profilerB\x10\n\x0e\x63\x61\x63hed_profileBV\n\x10\x66\x65\x61st.proto.coreB\x11ValidationProfileZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'feast.core.ValidationProfile_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\020feast.proto.coreB\021ValidationProfileZ/github.com/feast-dev/feast/go/protos/feast/core' + _globals['_VALIDATIONREFERENCE_TAGSENTRY']._options = None + _globals['_VALIDATIONREFERENCE_TAGSENTRY']._serialized_options = b'8\001' + _globals['_GEVALIDATIONPROFILER']._serialized_start=51 + _globals['_GEVALIDATIONPROFILER']._serialized_end=182 + _globals['_GEVALIDATIONPROFILER_USERDEFINEDPROFILER']._serialized_start=147 + _globals['_GEVALIDATIONPROFILER_USERDEFINEDPROFILER']._serialized_end=182 + _globals['_GEVALIDATIONPROFILE']._serialized_start=184 + _globals['_GEVALIDATIONPROFILE']._serialized_end=232 + _globals['_VALIDATIONREFERENCE']._serialized_start=235 + _globals['_VALIDATIONREFERENCE']._serialized_end=584 + _globals['_VALIDATIONREFERENCE_TAGSENTRY']._serialized_start=511 + _globals['_VALIDATIONREFERENCE_TAGSENTRY']._serialized_end=554 +# @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/ValidationProfile_pb2.pyi b/sdk/python/feast/protos/feast/core/ValidationProfile_pb2.pyi new file mode 100644 index 0000000000..93da1e0f5e --- /dev/null +++ b/sdk/python/feast/protos/feast/core/ValidationProfile_pb2.pyi @@ -0,0 +1,136 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file + +Copyright 2021 The Feast 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 + + https://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 builtins +import collections.abc +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.message +import sys +import typing + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class GEValidationProfiler(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class UserDefinedProfiler(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + BODY_FIELD_NUMBER: builtins.int + body: builtins.bytes + """The python-syntax function body (serialized by dill)""" + def __init__( + self, + *, + body: builtins.bytes = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["body", b"body"]) -> None: ... + + PROFILER_FIELD_NUMBER: builtins.int + @property + def profiler(self) -> global___GEValidationProfiler.UserDefinedProfiler: ... + def __init__( + self, + *, + profiler: global___GEValidationProfiler.UserDefinedProfiler | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["profiler", b"profiler"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["profiler", b"profiler"]) -> None: ... + +global___GEValidationProfiler = GEValidationProfiler + +class GEValidationProfile(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + EXPECTATION_SUITE_FIELD_NUMBER: builtins.int + expectation_suite: builtins.bytes + """JSON-serialized ExpectationSuite object""" + def __init__( + self, + *, + expectation_suite: builtins.bytes = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["expectation_suite", b"expectation_suite"]) -> None: ... + +global___GEValidationProfile = GEValidationProfile + +class ValidationReference(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class TagsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + NAME_FIELD_NUMBER: builtins.int + REFERENCE_DATASET_NAME_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + DESCRIPTION_FIELD_NUMBER: builtins.int + TAGS_FIELD_NUMBER: builtins.int + GE_PROFILER_FIELD_NUMBER: builtins.int + GE_PROFILE_FIELD_NUMBER: builtins.int + name: builtins.str + """Unique name of validation reference within the project""" + reference_dataset_name: builtins.str + """Name of saved dataset used as reference dataset""" + project: builtins.str + """Name of Feast project that this object source belongs to""" + description: builtins.str + """Description of the validation reference""" + @property + def tags(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: + """User defined metadata""" + @property + def ge_profiler(self) -> global___GEValidationProfiler: ... + @property + def ge_profile(self) -> global___GEValidationProfile: ... + def __init__( + self, + *, + name: builtins.str = ..., + reference_dataset_name: builtins.str = ..., + project: builtins.str = ..., + description: builtins.str = ..., + tags: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + ge_profiler: global___GEValidationProfiler | None = ..., + ge_profile: global___GEValidationProfile | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["cached_profile", b"cached_profile", "ge_profile", b"ge_profile", "ge_profiler", b"ge_profiler", "profiler", b"profiler"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["cached_profile", b"cached_profile", "description", b"description", "ge_profile", b"ge_profile", "ge_profiler", b"ge_profiler", "name", b"name", "profiler", b"profiler", "project", b"project", "reference_dataset_name", b"reference_dataset_name", "tags", b"tags"]) -> None: ... + @typing.overload + def WhichOneof(self, oneof_group: typing_extensions.Literal["cached_profile", b"cached_profile"]) -> typing_extensions.Literal["ge_profile"] | None: ... + @typing.overload + def WhichOneof(self, oneof_group: typing_extensions.Literal["profiler", b"profiler"]) -> typing_extensions.Literal["ge_profiler"] | None: ... + +global___ValidationReference = ValidationReference diff --git a/sdk/python/feast/protos/feast/core/ValidationProfile_pb2_grpc.py b/sdk/python/feast/protos/feast/core/ValidationProfile_pb2_grpc.py new file mode 100644 index 0000000000..2daafffebf --- /dev/null +++ b/sdk/python/feast/protos/feast/core/ValidationProfile_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + diff --git a/sdk/python/feast/protos/feast/core/__init__.py b/sdk/python/feast/protos/feast/core/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sdk/python/feast/protos/feast/registry/RegistryServer_pb2.py b/sdk/python/feast/protos/feast/registry/RegistryServer_pb2.py new file mode 100644 index 0000000000..e0cae3da4b --- /dev/null +++ b/sdk/python/feast/protos/feast/registry/RegistryServer_pb2.py @@ -0,0 +1,198 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: feast/registry/RegistryServer.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2 +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 +from feast.protos.feast.core import Registry_pb2 as feast_dot_core_dot_Registry__pb2 +from feast.protos.feast.core import Entity_pb2 as feast_dot_core_dot_Entity__pb2 +from feast.protos.feast.core import DataSource_pb2 as feast_dot_core_dot_DataSource__pb2 +from feast.protos.feast.core import FeatureView_pb2 as feast_dot_core_dot_FeatureView__pb2 +from feast.protos.feast.core import StreamFeatureView_pb2 as feast_dot_core_dot_StreamFeatureView__pb2 +from feast.protos.feast.core import OnDemandFeatureView_pb2 as feast_dot_core_dot_OnDemandFeatureView__pb2 +from feast.protos.feast.core import FeatureService_pb2 as feast_dot_core_dot_FeatureService__pb2 +from feast.protos.feast.core import SavedDataset_pb2 as feast_dot_core_dot_SavedDataset__pb2 +from feast.protos.feast.core import ValidationProfile_pb2 as feast_dot_core_dot_ValidationProfile__pb2 +from feast.protos.feast.core import InfraObject_pb2 as feast_dot_core_dot_InfraObject__pb2 +from feast.protos.feast.core import Permission_pb2 as feast_dot_core_dot_Permission__pb2 +from feast.protos.feast.core import Project_pb2 as feast_dot_core_dot_Project__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n#feast/registry/RegistryServer.proto\x12\x0e\x66\x65\x61st.registry\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x19\x66\x65\x61st/core/Registry.proto\x1a\x17\x66\x65\x61st/core/Entity.proto\x1a\x1b\x66\x65\x61st/core/DataSource.proto\x1a\x1c\x66\x65\x61st/core/FeatureView.proto\x1a\"feast/core/StreamFeatureView.proto\x1a$feast/core/OnDemandFeatureView.proto\x1a\x1f\x66\x65\x61st/core/FeatureService.proto\x1a\x1d\x66\x65\x61st/core/SavedDataset.proto\x1a\"feast/core/ValidationProfile.proto\x1a\x1c\x66\x65\x61st/core/InfraObject.proto\x1a\x1b\x66\x65\x61st/core/Permission.proto\x1a\x18\x66\x65\x61st/core/Project.proto\"!\n\x0eRefreshRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\"W\n\x12UpdateInfraRequest\x12 \n\x05infra\x18\x01 \x01(\x0b\x32\x11.feast.core.Infra\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"7\n\x0fGetInfraRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\"B\n\x1aListProjectMetadataRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\"T\n\x1bListProjectMetadataResponse\x12\x35\n\x10project_metadata\x18\x01 \x03(\x0b\x32\x1b.feast.core.ProjectMetadata\"\xcb\x01\n\x1b\x41pplyMaterializationRequest\x12-\n\x0c\x66\x65\x61ture_view\x18\x01 \x01(\x0b\x32\x17.feast.core.FeatureView\x12\x0f\n\x07project\x18\x02 \x01(\t\x12.\n\nstart_date\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12,\n\x08\x65nd_date\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0e\n\x06\x63ommit\x18\x05 \x01(\x08\"Y\n\x12\x41pplyEntityRequest\x12\"\n\x06\x65ntity\x18\x01 \x01(\x0b\x32\x12.feast.core.Entity\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"F\n\x10GetEntityRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\xa5\x01\n\x13ListEntitiesRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12;\n\x04tags\x18\x03 \x03(\x0b\x32-.feast.registry.ListEntitiesRequest.TagsEntry\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"<\n\x14ListEntitiesResponse\x12$\n\x08\x65ntities\x18\x01 \x03(\x0b\x32\x12.feast.core.Entity\"D\n\x13\x44\x65leteEntityRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"f\n\x16\x41pplyDataSourceRequest\x12+\n\x0b\x64\x61ta_source\x18\x01 \x01(\x0b\x32\x16.feast.core.DataSource\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"J\n\x14GetDataSourceRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\xab\x01\n\x16ListDataSourcesRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12>\n\x04tags\x18\x03 \x03(\x0b\x32\x30.feast.registry.ListDataSourcesRequest.TagsEntry\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"G\n\x17ListDataSourcesResponse\x12,\n\x0c\x64\x61ta_sources\x18\x01 \x03(\x0b\x32\x16.feast.core.DataSource\"H\n\x17\x44\x65leteDataSourceRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"\x81\x02\n\x17\x41pplyFeatureViewRequest\x12/\n\x0c\x66\x65\x61ture_view\x18\x01 \x01(\x0b\x32\x17.feast.core.FeatureViewH\x00\x12\x41\n\x16on_demand_feature_view\x18\x02 \x01(\x0b\x32\x1f.feast.core.OnDemandFeatureViewH\x00\x12<\n\x13stream_feature_view\x18\x03 \x01(\x0b\x32\x1d.feast.core.StreamFeatureViewH\x00\x12\x0f\n\x07project\x18\x04 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x05 \x01(\x08\x42\x13\n\x11\x62\x61se_feature_view\"K\n\x15GetFeatureViewRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\xad\x01\n\x17ListFeatureViewsRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12?\n\x04tags\x18\x03 \x03(\x0b\x32\x31.feast.registry.ListFeatureViewsRequest.TagsEntry\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"J\n\x18ListFeatureViewsResponse\x12.\n\rfeature_views\x18\x01 \x03(\x0b\x32\x17.feast.core.FeatureView\"I\n\x18\x44\x65leteFeatureViewRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"\xd6\x01\n\x0e\x41nyFeatureView\x12/\n\x0c\x66\x65\x61ture_view\x18\x01 \x01(\x0b\x32\x17.feast.core.FeatureViewH\x00\x12\x41\n\x16on_demand_feature_view\x18\x02 \x01(\x0b\x32\x1f.feast.core.OnDemandFeatureViewH\x00\x12<\n\x13stream_feature_view\x18\x03 \x01(\x0b\x32\x1d.feast.core.StreamFeatureViewH\x00\x42\x12\n\x10\x61ny_feature_view\"N\n\x18GetAnyFeatureViewRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"U\n\x19GetAnyFeatureViewResponse\x12\x38\n\x10\x61ny_feature_view\x18\x01 \x01(\x0b\x32\x1e.feast.registry.AnyFeatureView\"\xb3\x01\n\x1aListAllFeatureViewsRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12\x42\n\x04tags\x18\x03 \x03(\x0b\x32\x34.feast.registry.ListAllFeatureViewsRequest.TagsEntry\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"T\n\x1bListAllFeatureViewsResponse\x12\x35\n\rfeature_views\x18\x01 \x03(\x0b\x32\x1e.feast.registry.AnyFeatureView\"Q\n\x1bGetStreamFeatureViewRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\xb9\x01\n\x1dListStreamFeatureViewsRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12\x45\n\x04tags\x18\x03 \x03(\x0b\x32\x37.feast.registry.ListStreamFeatureViewsRequest.TagsEntry\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"]\n\x1eListStreamFeatureViewsResponse\x12;\n\x14stream_feature_views\x18\x01 \x03(\x0b\x32\x1d.feast.core.StreamFeatureView\"S\n\x1dGetOnDemandFeatureViewRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\xbd\x01\n\x1fListOnDemandFeatureViewsRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12G\n\x04tags\x18\x03 \x03(\x0b\x32\x39.feast.registry.ListOnDemandFeatureViewsRequest.TagsEntry\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"d\n ListOnDemandFeatureViewsResponse\x12@\n\x17on_demand_feature_views\x18\x01 \x03(\x0b\x32\x1f.feast.core.OnDemandFeatureView\"r\n\x1a\x41pplyFeatureServiceRequest\x12\x33\n\x0f\x66\x65\x61ture_service\x18\x01 \x01(\x0b\x32\x1a.feast.core.FeatureService\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"N\n\x18GetFeatureServiceRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\xb3\x01\n\x1aListFeatureServicesRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12\x42\n\x04tags\x18\x03 \x03(\x0b\x32\x34.feast.registry.ListFeatureServicesRequest.TagsEntry\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"S\n\x1bListFeatureServicesResponse\x12\x34\n\x10\x66\x65\x61ture_services\x18\x01 \x03(\x0b\x32\x1a.feast.core.FeatureService\"L\n\x1b\x44\x65leteFeatureServiceRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"l\n\x18\x41pplySavedDatasetRequest\x12/\n\rsaved_dataset\x18\x01 \x01(\x0b\x32\x18.feast.core.SavedDataset\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"L\n\x16GetSavedDatasetRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\xaf\x01\n\x18ListSavedDatasetsRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12@\n\x04tags\x18\x03 \x03(\x0b\x32\x32.feast.registry.ListSavedDatasetsRequest.TagsEntry\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"M\n\x19ListSavedDatasetsResponse\x12\x30\n\x0esaved_datasets\x18\x01 \x03(\x0b\x32\x18.feast.core.SavedDataset\"J\n\x19\x44\x65leteSavedDatasetRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"\x81\x01\n\x1f\x41pplyValidationReferenceRequest\x12=\n\x14validation_reference\x18\x01 \x01(\x0b\x32\x1f.feast.core.ValidationReference\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"S\n\x1dGetValidationReferenceRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\xbd\x01\n\x1fListValidationReferencesRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12G\n\x04tags\x18\x03 \x03(\x0b\x32\x39.feast.registry.ListValidationReferencesRequest.TagsEntry\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"b\n ListValidationReferencesResponse\x12>\n\x15validation_references\x18\x01 \x03(\x0b\x32\x1f.feast.core.ValidationReference\"Q\n DeleteValidationReferenceRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"e\n\x16\x41pplyPermissionRequest\x12*\n\npermission\x18\x01 \x01(\x0b\x32\x16.feast.core.Permission\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"J\n\x14GetPermissionRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\xab\x01\n\x16ListPermissionsRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12>\n\x04tags\x18\x03 \x03(\x0b\x32\x30.feast.registry.ListPermissionsRequest.TagsEntry\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"F\n\x17ListPermissionsResponse\x12+\n\x0bpermissions\x18\x01 \x03(\x0b\x32\x16.feast.core.Permission\"H\n\x17\x44\x65letePermissionRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"K\n\x13\x41pplyProjectRequest\x12$\n\x07project\x18\x01 \x01(\x0b\x32\x13.feast.core.Project\x12\x0e\n\x06\x63ommit\x18\x02 \x01(\x08\"6\n\x11GetProjectRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\"\x94\x01\n\x13ListProjectsRequest\x12\x13\n\x0b\x61llow_cache\x18\x01 \x01(\x08\x12;\n\x04tags\x18\x02 \x03(\x0b\x32-.feast.registry.ListProjectsRequest.TagsEntry\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"=\n\x14ListProjectsResponse\x12%\n\x08projects\x18\x01 \x03(\x0b\x32\x13.feast.core.Project\"4\n\x14\x44\x65leteProjectRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x02 \x01(\x08\x32\xcb \n\x0eRegistryServer\x12K\n\x0b\x41pplyEntity\x12\".feast.registry.ApplyEntityRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x43\n\tGetEntity\x12 .feast.registry.GetEntityRequest\x1a\x12.feast.core.Entity\"\x00\x12[\n\x0cListEntities\x12#.feast.registry.ListEntitiesRequest\x1a$.feast.registry.ListEntitiesResponse\"\x00\x12M\n\x0c\x44\x65leteEntity\x12#.feast.registry.DeleteEntityRequest\x1a\x16.google.protobuf.Empty\"\x00\x12S\n\x0f\x41pplyDataSource\x12&.feast.registry.ApplyDataSourceRequest\x1a\x16.google.protobuf.Empty\"\x00\x12O\n\rGetDataSource\x12$.feast.registry.GetDataSourceRequest\x1a\x16.feast.core.DataSource\"\x00\x12\x64\n\x0fListDataSources\x12&.feast.registry.ListDataSourcesRequest\x1a\'.feast.registry.ListDataSourcesResponse\"\x00\x12U\n\x10\x44\x65leteDataSource\x12\'.feast.registry.DeleteDataSourceRequest\x1a\x16.google.protobuf.Empty\"\x00\x12U\n\x10\x41pplyFeatureView\x12\'.feast.registry.ApplyFeatureViewRequest\x1a\x16.google.protobuf.Empty\"\x00\x12W\n\x11\x44\x65leteFeatureView\x12(.feast.registry.DeleteFeatureViewRequest\x1a\x16.google.protobuf.Empty\"\x00\x12j\n\x11GetAnyFeatureView\x12(.feast.registry.GetAnyFeatureViewRequest\x1a).feast.registry.GetAnyFeatureViewResponse\"\x00\x12p\n\x13ListAllFeatureViews\x12*.feast.registry.ListAllFeatureViewsRequest\x1a+.feast.registry.ListAllFeatureViewsResponse\"\x00\x12R\n\x0eGetFeatureView\x12%.feast.registry.GetFeatureViewRequest\x1a\x17.feast.core.FeatureView\"\x00\x12g\n\x10ListFeatureViews\x12\'.feast.registry.ListFeatureViewsRequest\x1a(.feast.registry.ListFeatureViewsResponse\"\x00\x12\x64\n\x14GetStreamFeatureView\x12+.feast.registry.GetStreamFeatureViewRequest\x1a\x1d.feast.core.StreamFeatureView\"\x00\x12y\n\x16ListStreamFeatureViews\x12-.feast.registry.ListStreamFeatureViewsRequest\x1a..feast.registry.ListStreamFeatureViewsResponse\"\x00\x12j\n\x16GetOnDemandFeatureView\x12-.feast.registry.GetOnDemandFeatureViewRequest\x1a\x1f.feast.core.OnDemandFeatureView\"\x00\x12\x7f\n\x18ListOnDemandFeatureViews\x12/.feast.registry.ListOnDemandFeatureViewsRequest\x1a\x30.feast.registry.ListOnDemandFeatureViewsResponse\"\x00\x12[\n\x13\x41pplyFeatureService\x12*.feast.registry.ApplyFeatureServiceRequest\x1a\x16.google.protobuf.Empty\"\x00\x12[\n\x11GetFeatureService\x12(.feast.registry.GetFeatureServiceRequest\x1a\x1a.feast.core.FeatureService\"\x00\x12p\n\x13ListFeatureServices\x12*.feast.registry.ListFeatureServicesRequest\x1a+.feast.registry.ListFeatureServicesResponse\"\x00\x12]\n\x14\x44\x65leteFeatureService\x12+.feast.registry.DeleteFeatureServiceRequest\x1a\x16.google.protobuf.Empty\"\x00\x12W\n\x11\x41pplySavedDataset\x12(.feast.registry.ApplySavedDatasetRequest\x1a\x16.google.protobuf.Empty\"\x00\x12U\n\x0fGetSavedDataset\x12&.feast.registry.GetSavedDatasetRequest\x1a\x18.feast.core.SavedDataset\"\x00\x12j\n\x11ListSavedDatasets\x12(.feast.registry.ListSavedDatasetsRequest\x1a).feast.registry.ListSavedDatasetsResponse\"\x00\x12Y\n\x12\x44\x65leteSavedDataset\x12).feast.registry.DeleteSavedDatasetRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x65\n\x18\x41pplyValidationReference\x12/.feast.registry.ApplyValidationReferenceRequest\x1a\x16.google.protobuf.Empty\"\x00\x12j\n\x16GetValidationReference\x12-.feast.registry.GetValidationReferenceRequest\x1a\x1f.feast.core.ValidationReference\"\x00\x12\x7f\n\x18ListValidationReferences\x12/.feast.registry.ListValidationReferencesRequest\x1a\x30.feast.registry.ListValidationReferencesResponse\"\x00\x12g\n\x19\x44\x65leteValidationReference\x12\x30.feast.registry.DeleteValidationReferenceRequest\x1a\x16.google.protobuf.Empty\"\x00\x12S\n\x0f\x41pplyPermission\x12&.feast.registry.ApplyPermissionRequest\x1a\x16.google.protobuf.Empty\"\x00\x12O\n\rGetPermission\x12$.feast.registry.GetPermissionRequest\x1a\x16.feast.core.Permission\"\x00\x12\x64\n\x0fListPermissions\x12&.feast.registry.ListPermissionsRequest\x1a\'.feast.registry.ListPermissionsResponse\"\x00\x12U\n\x10\x44\x65letePermission\x12\'.feast.registry.DeletePermissionRequest\x1a\x16.google.protobuf.Empty\"\x00\x12M\n\x0c\x41pplyProject\x12#.feast.registry.ApplyProjectRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x46\n\nGetProject\x12!.feast.registry.GetProjectRequest\x1a\x13.feast.core.Project\"\x00\x12[\n\x0cListProjects\x12#.feast.registry.ListProjectsRequest\x1a$.feast.registry.ListProjectsResponse\"\x00\x12O\n\rDeleteProject\x12$.feast.registry.DeleteProjectRequest\x1a\x16.google.protobuf.Empty\"\x00\x12]\n\x14\x41pplyMaterialization\x12+.feast.registry.ApplyMaterializationRequest\x1a\x16.google.protobuf.Empty\"\x00\x12p\n\x13ListProjectMetadata\x12*.feast.registry.ListProjectMetadataRequest\x1a+.feast.registry.ListProjectMetadataResponse\"\x00\x12K\n\x0bUpdateInfra\x12\".feast.registry.UpdateInfraRequest\x1a\x16.google.protobuf.Empty\"\x00\x12@\n\x08GetInfra\x12\x1f.feast.registry.GetInfraRequest\x1a\x11.feast.core.Infra\"\x00\x12:\n\x06\x43ommit\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12\x43\n\x07Refresh\x12\x1e.feast.registry.RefreshRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x37\n\x05Proto\x12\x16.google.protobuf.Empty\x1a\x14.feast.core.Registry\"\x00\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'feast.registry.RegistryServer_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_LISTENTITIESREQUEST_TAGSENTRY']._options = None + _globals['_LISTENTITIESREQUEST_TAGSENTRY']._serialized_options = b'8\001' + _globals['_LISTDATASOURCESREQUEST_TAGSENTRY']._options = None + _globals['_LISTDATASOURCESREQUEST_TAGSENTRY']._serialized_options = b'8\001' + _globals['_LISTFEATUREVIEWSREQUEST_TAGSENTRY']._options = None + _globals['_LISTFEATUREVIEWSREQUEST_TAGSENTRY']._serialized_options = b'8\001' + _globals['_LISTALLFEATUREVIEWSREQUEST_TAGSENTRY']._options = None + _globals['_LISTALLFEATUREVIEWSREQUEST_TAGSENTRY']._serialized_options = b'8\001' + _globals['_LISTSTREAMFEATUREVIEWSREQUEST_TAGSENTRY']._options = None + _globals['_LISTSTREAMFEATUREVIEWSREQUEST_TAGSENTRY']._serialized_options = b'8\001' + _globals['_LISTONDEMANDFEATUREVIEWSREQUEST_TAGSENTRY']._options = None + _globals['_LISTONDEMANDFEATUREVIEWSREQUEST_TAGSENTRY']._serialized_options = b'8\001' + _globals['_LISTFEATURESERVICESREQUEST_TAGSENTRY']._options = None + _globals['_LISTFEATURESERVICESREQUEST_TAGSENTRY']._serialized_options = b'8\001' + _globals['_LISTSAVEDDATASETSREQUEST_TAGSENTRY']._options = None + _globals['_LISTSAVEDDATASETSREQUEST_TAGSENTRY']._serialized_options = b'8\001' + _globals['_LISTVALIDATIONREFERENCESREQUEST_TAGSENTRY']._options = None + _globals['_LISTVALIDATIONREFERENCESREQUEST_TAGSENTRY']._serialized_options = b'8\001' + _globals['_LISTPERMISSIONSREQUEST_TAGSENTRY']._options = None + _globals['_LISTPERMISSIONSREQUEST_TAGSENTRY']._serialized_options = b'8\001' + _globals['_LISTPROJECTSREQUEST_TAGSENTRY']._options = None + _globals['_LISTPROJECTSREQUEST_TAGSENTRY']._serialized_options = b'8\001' + _globals['_REFRESHREQUEST']._serialized_start=487 + _globals['_REFRESHREQUEST']._serialized_end=520 + _globals['_UPDATEINFRAREQUEST']._serialized_start=522 + _globals['_UPDATEINFRAREQUEST']._serialized_end=609 + _globals['_GETINFRAREQUEST']._serialized_start=611 + _globals['_GETINFRAREQUEST']._serialized_end=666 + _globals['_LISTPROJECTMETADATAREQUEST']._serialized_start=668 + _globals['_LISTPROJECTMETADATAREQUEST']._serialized_end=734 + _globals['_LISTPROJECTMETADATARESPONSE']._serialized_start=736 + _globals['_LISTPROJECTMETADATARESPONSE']._serialized_end=820 + _globals['_APPLYMATERIALIZATIONREQUEST']._serialized_start=823 + _globals['_APPLYMATERIALIZATIONREQUEST']._serialized_end=1026 + _globals['_APPLYENTITYREQUEST']._serialized_start=1028 + _globals['_APPLYENTITYREQUEST']._serialized_end=1117 + _globals['_GETENTITYREQUEST']._serialized_start=1119 + _globals['_GETENTITYREQUEST']._serialized_end=1189 + _globals['_LISTENTITIESREQUEST']._serialized_start=1192 + _globals['_LISTENTITIESREQUEST']._serialized_end=1357 + _globals['_LISTENTITIESREQUEST_TAGSENTRY']._serialized_start=1314 + _globals['_LISTENTITIESREQUEST_TAGSENTRY']._serialized_end=1357 + _globals['_LISTENTITIESRESPONSE']._serialized_start=1359 + _globals['_LISTENTITIESRESPONSE']._serialized_end=1419 + _globals['_DELETEENTITYREQUEST']._serialized_start=1421 + _globals['_DELETEENTITYREQUEST']._serialized_end=1489 + _globals['_APPLYDATASOURCEREQUEST']._serialized_start=1491 + _globals['_APPLYDATASOURCEREQUEST']._serialized_end=1593 + _globals['_GETDATASOURCEREQUEST']._serialized_start=1595 + _globals['_GETDATASOURCEREQUEST']._serialized_end=1669 + _globals['_LISTDATASOURCESREQUEST']._serialized_start=1672 + _globals['_LISTDATASOURCESREQUEST']._serialized_end=1843 + _globals['_LISTDATASOURCESREQUEST_TAGSENTRY']._serialized_start=1314 + _globals['_LISTDATASOURCESREQUEST_TAGSENTRY']._serialized_end=1357 + _globals['_LISTDATASOURCESRESPONSE']._serialized_start=1845 + _globals['_LISTDATASOURCESRESPONSE']._serialized_end=1916 + _globals['_DELETEDATASOURCEREQUEST']._serialized_start=1918 + _globals['_DELETEDATASOURCEREQUEST']._serialized_end=1990 + _globals['_APPLYFEATUREVIEWREQUEST']._serialized_start=1993 + _globals['_APPLYFEATUREVIEWREQUEST']._serialized_end=2250 + _globals['_GETFEATUREVIEWREQUEST']._serialized_start=2252 + _globals['_GETFEATUREVIEWREQUEST']._serialized_end=2327 + _globals['_LISTFEATUREVIEWSREQUEST']._serialized_start=2330 + _globals['_LISTFEATUREVIEWSREQUEST']._serialized_end=2503 + _globals['_LISTFEATUREVIEWSREQUEST_TAGSENTRY']._serialized_start=1314 + _globals['_LISTFEATUREVIEWSREQUEST_TAGSENTRY']._serialized_end=1357 + _globals['_LISTFEATUREVIEWSRESPONSE']._serialized_start=2505 + _globals['_LISTFEATUREVIEWSRESPONSE']._serialized_end=2579 + _globals['_DELETEFEATUREVIEWREQUEST']._serialized_start=2581 + _globals['_DELETEFEATUREVIEWREQUEST']._serialized_end=2654 + _globals['_ANYFEATUREVIEW']._serialized_start=2657 + _globals['_ANYFEATUREVIEW']._serialized_end=2871 + _globals['_GETANYFEATUREVIEWREQUEST']._serialized_start=2873 + _globals['_GETANYFEATUREVIEWREQUEST']._serialized_end=2951 + _globals['_GETANYFEATUREVIEWRESPONSE']._serialized_start=2953 + _globals['_GETANYFEATUREVIEWRESPONSE']._serialized_end=3038 + _globals['_LISTALLFEATUREVIEWSREQUEST']._serialized_start=3041 + _globals['_LISTALLFEATUREVIEWSREQUEST']._serialized_end=3220 + _globals['_LISTALLFEATUREVIEWSREQUEST_TAGSENTRY']._serialized_start=1314 + _globals['_LISTALLFEATUREVIEWSREQUEST_TAGSENTRY']._serialized_end=1357 + _globals['_LISTALLFEATUREVIEWSRESPONSE']._serialized_start=3222 + _globals['_LISTALLFEATUREVIEWSRESPONSE']._serialized_end=3306 + _globals['_GETSTREAMFEATUREVIEWREQUEST']._serialized_start=3308 + _globals['_GETSTREAMFEATUREVIEWREQUEST']._serialized_end=3389 + _globals['_LISTSTREAMFEATUREVIEWSREQUEST']._serialized_start=3392 + _globals['_LISTSTREAMFEATUREVIEWSREQUEST']._serialized_end=3577 + _globals['_LISTSTREAMFEATUREVIEWSREQUEST_TAGSENTRY']._serialized_start=1314 + _globals['_LISTSTREAMFEATUREVIEWSREQUEST_TAGSENTRY']._serialized_end=1357 + _globals['_LISTSTREAMFEATUREVIEWSRESPONSE']._serialized_start=3579 + _globals['_LISTSTREAMFEATUREVIEWSRESPONSE']._serialized_end=3672 + _globals['_GETONDEMANDFEATUREVIEWREQUEST']._serialized_start=3674 + _globals['_GETONDEMANDFEATUREVIEWREQUEST']._serialized_end=3757 + _globals['_LISTONDEMANDFEATUREVIEWSREQUEST']._serialized_start=3760 + _globals['_LISTONDEMANDFEATUREVIEWSREQUEST']._serialized_end=3949 + _globals['_LISTONDEMANDFEATUREVIEWSREQUEST_TAGSENTRY']._serialized_start=1314 + _globals['_LISTONDEMANDFEATUREVIEWSREQUEST_TAGSENTRY']._serialized_end=1357 + _globals['_LISTONDEMANDFEATUREVIEWSRESPONSE']._serialized_start=3951 + _globals['_LISTONDEMANDFEATUREVIEWSRESPONSE']._serialized_end=4051 + _globals['_APPLYFEATURESERVICEREQUEST']._serialized_start=4053 + _globals['_APPLYFEATURESERVICEREQUEST']._serialized_end=4167 + _globals['_GETFEATURESERVICEREQUEST']._serialized_start=4169 + _globals['_GETFEATURESERVICEREQUEST']._serialized_end=4247 + _globals['_LISTFEATURESERVICESREQUEST']._serialized_start=4250 + _globals['_LISTFEATURESERVICESREQUEST']._serialized_end=4429 + _globals['_LISTFEATURESERVICESREQUEST_TAGSENTRY']._serialized_start=1314 + _globals['_LISTFEATURESERVICESREQUEST_TAGSENTRY']._serialized_end=1357 + _globals['_LISTFEATURESERVICESRESPONSE']._serialized_start=4431 + _globals['_LISTFEATURESERVICESRESPONSE']._serialized_end=4514 + _globals['_DELETEFEATURESERVICEREQUEST']._serialized_start=4516 + _globals['_DELETEFEATURESERVICEREQUEST']._serialized_end=4592 + _globals['_APPLYSAVEDDATASETREQUEST']._serialized_start=4594 + _globals['_APPLYSAVEDDATASETREQUEST']._serialized_end=4702 + _globals['_GETSAVEDDATASETREQUEST']._serialized_start=4704 + _globals['_GETSAVEDDATASETREQUEST']._serialized_end=4780 + _globals['_LISTSAVEDDATASETSREQUEST']._serialized_start=4783 + _globals['_LISTSAVEDDATASETSREQUEST']._serialized_end=4958 + _globals['_LISTSAVEDDATASETSREQUEST_TAGSENTRY']._serialized_start=1314 + _globals['_LISTSAVEDDATASETSREQUEST_TAGSENTRY']._serialized_end=1357 + _globals['_LISTSAVEDDATASETSRESPONSE']._serialized_start=4960 + _globals['_LISTSAVEDDATASETSRESPONSE']._serialized_end=5037 + _globals['_DELETESAVEDDATASETREQUEST']._serialized_start=5039 + _globals['_DELETESAVEDDATASETREQUEST']._serialized_end=5113 + _globals['_APPLYVALIDATIONREFERENCEREQUEST']._serialized_start=5116 + _globals['_APPLYVALIDATIONREFERENCEREQUEST']._serialized_end=5245 + _globals['_GETVALIDATIONREFERENCEREQUEST']._serialized_start=5247 + _globals['_GETVALIDATIONREFERENCEREQUEST']._serialized_end=5330 + _globals['_LISTVALIDATIONREFERENCESREQUEST']._serialized_start=5333 + _globals['_LISTVALIDATIONREFERENCESREQUEST']._serialized_end=5522 + _globals['_LISTVALIDATIONREFERENCESREQUEST_TAGSENTRY']._serialized_start=1314 + _globals['_LISTVALIDATIONREFERENCESREQUEST_TAGSENTRY']._serialized_end=1357 + _globals['_LISTVALIDATIONREFERENCESRESPONSE']._serialized_start=5524 + _globals['_LISTVALIDATIONREFERENCESRESPONSE']._serialized_end=5622 + _globals['_DELETEVALIDATIONREFERENCEREQUEST']._serialized_start=5624 + _globals['_DELETEVALIDATIONREFERENCEREQUEST']._serialized_end=5705 + _globals['_APPLYPERMISSIONREQUEST']._serialized_start=5707 + _globals['_APPLYPERMISSIONREQUEST']._serialized_end=5808 + _globals['_GETPERMISSIONREQUEST']._serialized_start=5810 + _globals['_GETPERMISSIONREQUEST']._serialized_end=5884 + _globals['_LISTPERMISSIONSREQUEST']._serialized_start=5887 + _globals['_LISTPERMISSIONSREQUEST']._serialized_end=6058 + _globals['_LISTPERMISSIONSREQUEST_TAGSENTRY']._serialized_start=1314 + _globals['_LISTPERMISSIONSREQUEST_TAGSENTRY']._serialized_end=1357 + _globals['_LISTPERMISSIONSRESPONSE']._serialized_start=6060 + _globals['_LISTPERMISSIONSRESPONSE']._serialized_end=6130 + _globals['_DELETEPERMISSIONREQUEST']._serialized_start=6132 + _globals['_DELETEPERMISSIONREQUEST']._serialized_end=6204 + _globals['_APPLYPROJECTREQUEST']._serialized_start=6206 + _globals['_APPLYPROJECTREQUEST']._serialized_end=6281 + _globals['_GETPROJECTREQUEST']._serialized_start=6283 + _globals['_GETPROJECTREQUEST']._serialized_end=6337 + _globals['_LISTPROJECTSREQUEST']._serialized_start=6340 + _globals['_LISTPROJECTSREQUEST']._serialized_end=6488 + _globals['_LISTPROJECTSREQUEST_TAGSENTRY']._serialized_start=1314 + _globals['_LISTPROJECTSREQUEST_TAGSENTRY']._serialized_end=1357 + _globals['_LISTPROJECTSRESPONSE']._serialized_start=6490 + _globals['_LISTPROJECTSRESPONSE']._serialized_end=6551 + _globals['_DELETEPROJECTREQUEST']._serialized_start=6553 + _globals['_DELETEPROJECTREQUEST']._serialized_end=6605 + _globals['_REGISTRYSERVER']._serialized_start=6608 + _globals['_REGISTRYSERVER']._serialized_end=10779 +# @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/registry/RegistryServer_pb2.pyi b/sdk/python/feast/protos/feast/registry/RegistryServer_pb2.pyi new file mode 100644 index 0000000000..f4507c02e2 --- /dev/null +++ b/sdk/python/feast/protos/feast/registry/RegistryServer_pb2.pyi @@ -0,0 +1,1318 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" +import builtins +import collections.abc +import feast.core.DataSource_pb2 +import feast.core.Entity_pb2 +import feast.core.FeatureService_pb2 +import feast.core.FeatureView_pb2 +import feast.core.InfraObject_pb2 +import feast.core.OnDemandFeatureView_pb2 +import feast.core.Permission_pb2 +import feast.core.Project_pb2 +import feast.core.Registry_pb2 +import feast.core.SavedDataset_pb2 +import feast.core.StreamFeatureView_pb2 +import feast.core.ValidationProfile_pb2 +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.message +import google.protobuf.timestamp_pb2 +import sys + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class RefreshRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + PROJECT_FIELD_NUMBER: builtins.int + project: builtins.str + def __init__( + self, + *, + project: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["project", b"project"]) -> None: ... + +global___RefreshRequest = RefreshRequest + +class UpdateInfraRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + INFRA_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + COMMIT_FIELD_NUMBER: builtins.int + @property + def infra(self) -> feast.core.InfraObject_pb2.Infra: ... + project: builtins.str + commit: builtins.bool + def __init__( + self, + *, + infra: feast.core.InfraObject_pb2.Infra | None = ..., + project: builtins.str = ..., + commit: builtins.bool = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["infra", b"infra"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["commit", b"commit", "infra", b"infra", "project", b"project"]) -> None: ... + +global___UpdateInfraRequest = UpdateInfraRequest + +class GetInfraRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + PROJECT_FIELD_NUMBER: builtins.int + ALLOW_CACHE_FIELD_NUMBER: builtins.int + project: builtins.str + allow_cache: builtins.bool + def __init__( + self, + *, + project: builtins.str = ..., + allow_cache: builtins.bool = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["allow_cache", b"allow_cache", "project", b"project"]) -> None: ... + +global___GetInfraRequest = GetInfraRequest + +class ListProjectMetadataRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + PROJECT_FIELD_NUMBER: builtins.int + ALLOW_CACHE_FIELD_NUMBER: builtins.int + project: builtins.str + allow_cache: builtins.bool + def __init__( + self, + *, + project: builtins.str = ..., + allow_cache: builtins.bool = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["allow_cache", b"allow_cache", "project", b"project"]) -> None: ... + +global___ListProjectMetadataRequest = ListProjectMetadataRequest + +class ListProjectMetadataResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + PROJECT_METADATA_FIELD_NUMBER: builtins.int + @property + def project_metadata(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.Registry_pb2.ProjectMetadata]: ... + def __init__( + self, + *, + project_metadata: collections.abc.Iterable[feast.core.Registry_pb2.ProjectMetadata] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["project_metadata", b"project_metadata"]) -> None: ... + +global___ListProjectMetadataResponse = ListProjectMetadataResponse + +class ApplyMaterializationRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + FEATURE_VIEW_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + START_DATE_FIELD_NUMBER: builtins.int + END_DATE_FIELD_NUMBER: builtins.int + COMMIT_FIELD_NUMBER: builtins.int + @property + def feature_view(self) -> feast.core.FeatureView_pb2.FeatureView: ... + project: builtins.str + @property + def start_date(self) -> google.protobuf.timestamp_pb2.Timestamp: ... + @property + def end_date(self) -> google.protobuf.timestamp_pb2.Timestamp: ... + commit: builtins.bool + def __init__( + self, + *, + feature_view: feast.core.FeatureView_pb2.FeatureView | None = ..., + project: builtins.str = ..., + start_date: google.protobuf.timestamp_pb2.Timestamp | None = ..., + end_date: google.protobuf.timestamp_pb2.Timestamp | None = ..., + commit: builtins.bool = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["end_date", b"end_date", "feature_view", b"feature_view", "start_date", b"start_date"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["commit", b"commit", "end_date", b"end_date", "feature_view", b"feature_view", "project", b"project", "start_date", b"start_date"]) -> None: ... + +global___ApplyMaterializationRequest = ApplyMaterializationRequest + +class ApplyEntityRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + ENTITY_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + COMMIT_FIELD_NUMBER: builtins.int + @property + def entity(self) -> feast.core.Entity_pb2.Entity: ... + project: builtins.str + commit: builtins.bool + def __init__( + self, + *, + entity: feast.core.Entity_pb2.Entity | None = ..., + project: builtins.str = ..., + commit: builtins.bool = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["entity", b"entity"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["commit", b"commit", "entity", b"entity", "project", b"project"]) -> None: ... + +global___ApplyEntityRequest = ApplyEntityRequest + +class GetEntityRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + ALLOW_CACHE_FIELD_NUMBER: builtins.int + name: builtins.str + project: builtins.str + allow_cache: builtins.bool + def __init__( + self, + *, + name: builtins.str = ..., + project: builtins.str = ..., + allow_cache: builtins.bool = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["allow_cache", b"allow_cache", "name", b"name", "project", b"project"]) -> None: ... + +global___GetEntityRequest = GetEntityRequest + +class ListEntitiesRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class TagsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + PROJECT_FIELD_NUMBER: builtins.int + ALLOW_CACHE_FIELD_NUMBER: builtins.int + TAGS_FIELD_NUMBER: builtins.int + project: builtins.str + allow_cache: builtins.bool + @property + def tags(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: ... + def __init__( + self, + *, + project: builtins.str = ..., + allow_cache: builtins.bool = ..., + tags: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["allow_cache", b"allow_cache", "project", b"project", "tags", b"tags"]) -> None: ... + +global___ListEntitiesRequest = ListEntitiesRequest + +class ListEntitiesResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + ENTITIES_FIELD_NUMBER: builtins.int + @property + def entities(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.Entity_pb2.Entity]: ... + def __init__( + self, + *, + entities: collections.abc.Iterable[feast.core.Entity_pb2.Entity] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["entities", b"entities"]) -> None: ... + +global___ListEntitiesResponse = ListEntitiesResponse + +class DeleteEntityRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + COMMIT_FIELD_NUMBER: builtins.int + name: builtins.str + project: builtins.str + commit: builtins.bool + def __init__( + self, + *, + name: builtins.str = ..., + project: builtins.str = ..., + commit: builtins.bool = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["commit", b"commit", "name", b"name", "project", b"project"]) -> None: ... + +global___DeleteEntityRequest = DeleteEntityRequest + +class ApplyDataSourceRequest(google.protobuf.message.Message): + """DataSources""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + DATA_SOURCE_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + COMMIT_FIELD_NUMBER: builtins.int + @property + def data_source(self) -> feast.core.DataSource_pb2.DataSource: ... + project: builtins.str + commit: builtins.bool + def __init__( + self, + *, + data_source: feast.core.DataSource_pb2.DataSource | None = ..., + project: builtins.str = ..., + commit: builtins.bool = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["data_source", b"data_source"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["commit", b"commit", "data_source", b"data_source", "project", b"project"]) -> None: ... + +global___ApplyDataSourceRequest = ApplyDataSourceRequest + +class GetDataSourceRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + ALLOW_CACHE_FIELD_NUMBER: builtins.int + name: builtins.str + project: builtins.str + allow_cache: builtins.bool + def __init__( + self, + *, + name: builtins.str = ..., + project: builtins.str = ..., + allow_cache: builtins.bool = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["allow_cache", b"allow_cache", "name", b"name", "project", b"project"]) -> None: ... + +global___GetDataSourceRequest = GetDataSourceRequest + +class ListDataSourcesRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class TagsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + PROJECT_FIELD_NUMBER: builtins.int + ALLOW_CACHE_FIELD_NUMBER: builtins.int + TAGS_FIELD_NUMBER: builtins.int + project: builtins.str + allow_cache: builtins.bool + @property + def tags(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: ... + def __init__( + self, + *, + project: builtins.str = ..., + allow_cache: builtins.bool = ..., + tags: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["allow_cache", b"allow_cache", "project", b"project", "tags", b"tags"]) -> None: ... + +global___ListDataSourcesRequest = ListDataSourcesRequest + +class ListDataSourcesResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + DATA_SOURCES_FIELD_NUMBER: builtins.int + @property + def data_sources(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.DataSource_pb2.DataSource]: ... + def __init__( + self, + *, + data_sources: collections.abc.Iterable[feast.core.DataSource_pb2.DataSource] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["data_sources", b"data_sources"]) -> None: ... + +global___ListDataSourcesResponse = ListDataSourcesResponse + +class DeleteDataSourceRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + COMMIT_FIELD_NUMBER: builtins.int + name: builtins.str + project: builtins.str + commit: builtins.bool + def __init__( + self, + *, + name: builtins.str = ..., + project: builtins.str = ..., + commit: builtins.bool = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["commit", b"commit", "name", b"name", "project", b"project"]) -> None: ... + +global___DeleteDataSourceRequest = DeleteDataSourceRequest + +class ApplyFeatureViewRequest(google.protobuf.message.Message): + """FeatureViews""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + FEATURE_VIEW_FIELD_NUMBER: builtins.int + ON_DEMAND_FEATURE_VIEW_FIELD_NUMBER: builtins.int + STREAM_FEATURE_VIEW_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + COMMIT_FIELD_NUMBER: builtins.int + @property + def feature_view(self) -> feast.core.FeatureView_pb2.FeatureView: ... + @property + def on_demand_feature_view(self) -> feast.core.OnDemandFeatureView_pb2.OnDemandFeatureView: ... + @property + def stream_feature_view(self) -> feast.core.StreamFeatureView_pb2.StreamFeatureView: ... + project: builtins.str + commit: builtins.bool + def __init__( + self, + *, + feature_view: feast.core.FeatureView_pb2.FeatureView | None = ..., + on_demand_feature_view: feast.core.OnDemandFeatureView_pb2.OnDemandFeatureView | None = ..., + stream_feature_view: feast.core.StreamFeatureView_pb2.StreamFeatureView | None = ..., + project: builtins.str = ..., + commit: builtins.bool = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["base_feature_view", b"base_feature_view", "feature_view", b"feature_view", "on_demand_feature_view", b"on_demand_feature_view", "stream_feature_view", b"stream_feature_view"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["base_feature_view", b"base_feature_view", "commit", b"commit", "feature_view", b"feature_view", "on_demand_feature_view", b"on_demand_feature_view", "project", b"project", "stream_feature_view", b"stream_feature_view"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal["base_feature_view", b"base_feature_view"]) -> typing_extensions.Literal["feature_view", "on_demand_feature_view", "stream_feature_view"] | None: ... + +global___ApplyFeatureViewRequest = ApplyFeatureViewRequest + +class GetFeatureViewRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + ALLOW_CACHE_FIELD_NUMBER: builtins.int + name: builtins.str + project: builtins.str + allow_cache: builtins.bool + def __init__( + self, + *, + name: builtins.str = ..., + project: builtins.str = ..., + allow_cache: builtins.bool = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["allow_cache", b"allow_cache", "name", b"name", "project", b"project"]) -> None: ... + +global___GetFeatureViewRequest = GetFeatureViewRequest + +class ListFeatureViewsRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class TagsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + PROJECT_FIELD_NUMBER: builtins.int + ALLOW_CACHE_FIELD_NUMBER: builtins.int + TAGS_FIELD_NUMBER: builtins.int + project: builtins.str + allow_cache: builtins.bool + @property + def tags(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: ... + def __init__( + self, + *, + project: builtins.str = ..., + allow_cache: builtins.bool = ..., + tags: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["allow_cache", b"allow_cache", "project", b"project", "tags", b"tags"]) -> None: ... + +global___ListFeatureViewsRequest = ListFeatureViewsRequest + +class ListFeatureViewsResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + FEATURE_VIEWS_FIELD_NUMBER: builtins.int + @property + def feature_views(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.FeatureView_pb2.FeatureView]: ... + def __init__( + self, + *, + feature_views: collections.abc.Iterable[feast.core.FeatureView_pb2.FeatureView] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["feature_views", b"feature_views"]) -> None: ... + +global___ListFeatureViewsResponse = ListFeatureViewsResponse + +class DeleteFeatureViewRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + COMMIT_FIELD_NUMBER: builtins.int + name: builtins.str + project: builtins.str + commit: builtins.bool + def __init__( + self, + *, + name: builtins.str = ..., + project: builtins.str = ..., + commit: builtins.bool = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["commit", b"commit", "name", b"name", "project", b"project"]) -> None: ... + +global___DeleteFeatureViewRequest = DeleteFeatureViewRequest + +class AnyFeatureView(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + FEATURE_VIEW_FIELD_NUMBER: builtins.int + ON_DEMAND_FEATURE_VIEW_FIELD_NUMBER: builtins.int + STREAM_FEATURE_VIEW_FIELD_NUMBER: builtins.int + @property + def feature_view(self) -> feast.core.FeatureView_pb2.FeatureView: ... + @property + def on_demand_feature_view(self) -> feast.core.OnDemandFeatureView_pb2.OnDemandFeatureView: ... + @property + def stream_feature_view(self) -> feast.core.StreamFeatureView_pb2.StreamFeatureView: ... + def __init__( + self, + *, + feature_view: feast.core.FeatureView_pb2.FeatureView | None = ..., + on_demand_feature_view: feast.core.OnDemandFeatureView_pb2.OnDemandFeatureView | None = ..., + stream_feature_view: feast.core.StreamFeatureView_pb2.StreamFeatureView | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["any_feature_view", b"any_feature_view", "feature_view", b"feature_view", "on_demand_feature_view", b"on_demand_feature_view", "stream_feature_view", b"stream_feature_view"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["any_feature_view", b"any_feature_view", "feature_view", b"feature_view", "on_demand_feature_view", b"on_demand_feature_view", "stream_feature_view", b"stream_feature_view"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal["any_feature_view", b"any_feature_view"]) -> typing_extensions.Literal["feature_view", "on_demand_feature_view", "stream_feature_view"] | None: ... + +global___AnyFeatureView = AnyFeatureView + +class GetAnyFeatureViewRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + ALLOW_CACHE_FIELD_NUMBER: builtins.int + name: builtins.str + project: builtins.str + allow_cache: builtins.bool + def __init__( + self, + *, + name: builtins.str = ..., + project: builtins.str = ..., + allow_cache: builtins.bool = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["allow_cache", b"allow_cache", "name", b"name", "project", b"project"]) -> None: ... + +global___GetAnyFeatureViewRequest = GetAnyFeatureViewRequest + +class GetAnyFeatureViewResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + ANY_FEATURE_VIEW_FIELD_NUMBER: builtins.int + @property + def any_feature_view(self) -> global___AnyFeatureView: ... + def __init__( + self, + *, + any_feature_view: global___AnyFeatureView | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["any_feature_view", b"any_feature_view"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["any_feature_view", b"any_feature_view"]) -> None: ... + +global___GetAnyFeatureViewResponse = GetAnyFeatureViewResponse + +class ListAllFeatureViewsRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class TagsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + PROJECT_FIELD_NUMBER: builtins.int + ALLOW_CACHE_FIELD_NUMBER: builtins.int + TAGS_FIELD_NUMBER: builtins.int + project: builtins.str + allow_cache: builtins.bool + @property + def tags(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: ... + def __init__( + self, + *, + project: builtins.str = ..., + allow_cache: builtins.bool = ..., + tags: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["allow_cache", b"allow_cache", "project", b"project", "tags", b"tags"]) -> None: ... + +global___ListAllFeatureViewsRequest = ListAllFeatureViewsRequest + +class ListAllFeatureViewsResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + FEATURE_VIEWS_FIELD_NUMBER: builtins.int + @property + def feature_views(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___AnyFeatureView]: ... + def __init__( + self, + *, + feature_views: collections.abc.Iterable[global___AnyFeatureView] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["feature_views", b"feature_views"]) -> None: ... + +global___ListAllFeatureViewsResponse = ListAllFeatureViewsResponse + +class GetStreamFeatureViewRequest(google.protobuf.message.Message): + """StreamFeatureView""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + ALLOW_CACHE_FIELD_NUMBER: builtins.int + name: builtins.str + project: builtins.str + allow_cache: builtins.bool + def __init__( + self, + *, + name: builtins.str = ..., + project: builtins.str = ..., + allow_cache: builtins.bool = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["allow_cache", b"allow_cache", "name", b"name", "project", b"project"]) -> None: ... + +global___GetStreamFeatureViewRequest = GetStreamFeatureViewRequest + +class ListStreamFeatureViewsRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class TagsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + PROJECT_FIELD_NUMBER: builtins.int + ALLOW_CACHE_FIELD_NUMBER: builtins.int + TAGS_FIELD_NUMBER: builtins.int + project: builtins.str + allow_cache: builtins.bool + @property + def tags(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: ... + def __init__( + self, + *, + project: builtins.str = ..., + allow_cache: builtins.bool = ..., + tags: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["allow_cache", b"allow_cache", "project", b"project", "tags", b"tags"]) -> None: ... + +global___ListStreamFeatureViewsRequest = ListStreamFeatureViewsRequest + +class ListStreamFeatureViewsResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + STREAM_FEATURE_VIEWS_FIELD_NUMBER: builtins.int + @property + def stream_feature_views(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.StreamFeatureView_pb2.StreamFeatureView]: ... + def __init__( + self, + *, + stream_feature_views: collections.abc.Iterable[feast.core.StreamFeatureView_pb2.StreamFeatureView] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["stream_feature_views", b"stream_feature_views"]) -> None: ... + +global___ListStreamFeatureViewsResponse = ListStreamFeatureViewsResponse + +class GetOnDemandFeatureViewRequest(google.protobuf.message.Message): + """OnDemandFeatureView""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + ALLOW_CACHE_FIELD_NUMBER: builtins.int + name: builtins.str + project: builtins.str + allow_cache: builtins.bool + def __init__( + self, + *, + name: builtins.str = ..., + project: builtins.str = ..., + allow_cache: builtins.bool = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["allow_cache", b"allow_cache", "name", b"name", "project", b"project"]) -> None: ... + +global___GetOnDemandFeatureViewRequest = GetOnDemandFeatureViewRequest + +class ListOnDemandFeatureViewsRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class TagsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + PROJECT_FIELD_NUMBER: builtins.int + ALLOW_CACHE_FIELD_NUMBER: builtins.int + TAGS_FIELD_NUMBER: builtins.int + project: builtins.str + allow_cache: builtins.bool + @property + def tags(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: ... + def __init__( + self, + *, + project: builtins.str = ..., + allow_cache: builtins.bool = ..., + tags: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["allow_cache", b"allow_cache", "project", b"project", "tags", b"tags"]) -> None: ... + +global___ListOnDemandFeatureViewsRequest = ListOnDemandFeatureViewsRequest + +class ListOnDemandFeatureViewsResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + ON_DEMAND_FEATURE_VIEWS_FIELD_NUMBER: builtins.int + @property + def on_demand_feature_views(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.OnDemandFeatureView_pb2.OnDemandFeatureView]: ... + def __init__( + self, + *, + on_demand_feature_views: collections.abc.Iterable[feast.core.OnDemandFeatureView_pb2.OnDemandFeatureView] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["on_demand_feature_views", b"on_demand_feature_views"]) -> None: ... + +global___ListOnDemandFeatureViewsResponse = ListOnDemandFeatureViewsResponse + +class ApplyFeatureServiceRequest(google.protobuf.message.Message): + """FeatureServices""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + FEATURE_SERVICE_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + COMMIT_FIELD_NUMBER: builtins.int + @property + def feature_service(self) -> feast.core.FeatureService_pb2.FeatureService: ... + project: builtins.str + commit: builtins.bool + def __init__( + self, + *, + feature_service: feast.core.FeatureService_pb2.FeatureService | None = ..., + project: builtins.str = ..., + commit: builtins.bool = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["feature_service", b"feature_service"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["commit", b"commit", "feature_service", b"feature_service", "project", b"project"]) -> None: ... + +global___ApplyFeatureServiceRequest = ApplyFeatureServiceRequest + +class GetFeatureServiceRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + ALLOW_CACHE_FIELD_NUMBER: builtins.int + name: builtins.str + project: builtins.str + allow_cache: builtins.bool + def __init__( + self, + *, + name: builtins.str = ..., + project: builtins.str = ..., + allow_cache: builtins.bool = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["allow_cache", b"allow_cache", "name", b"name", "project", b"project"]) -> None: ... + +global___GetFeatureServiceRequest = GetFeatureServiceRequest + +class ListFeatureServicesRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class TagsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + PROJECT_FIELD_NUMBER: builtins.int + ALLOW_CACHE_FIELD_NUMBER: builtins.int + TAGS_FIELD_NUMBER: builtins.int + project: builtins.str + allow_cache: builtins.bool + @property + def tags(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: ... + def __init__( + self, + *, + project: builtins.str = ..., + allow_cache: builtins.bool = ..., + tags: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["allow_cache", b"allow_cache", "project", b"project", "tags", b"tags"]) -> None: ... + +global___ListFeatureServicesRequest = ListFeatureServicesRequest + +class ListFeatureServicesResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + FEATURE_SERVICES_FIELD_NUMBER: builtins.int + @property + def feature_services(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.FeatureService_pb2.FeatureService]: ... + def __init__( + self, + *, + feature_services: collections.abc.Iterable[feast.core.FeatureService_pb2.FeatureService] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["feature_services", b"feature_services"]) -> None: ... + +global___ListFeatureServicesResponse = ListFeatureServicesResponse + +class DeleteFeatureServiceRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + COMMIT_FIELD_NUMBER: builtins.int + name: builtins.str + project: builtins.str + commit: builtins.bool + def __init__( + self, + *, + name: builtins.str = ..., + project: builtins.str = ..., + commit: builtins.bool = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["commit", b"commit", "name", b"name", "project", b"project"]) -> None: ... + +global___DeleteFeatureServiceRequest = DeleteFeatureServiceRequest + +class ApplySavedDatasetRequest(google.protobuf.message.Message): + """SavedDataset""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + SAVED_DATASET_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + COMMIT_FIELD_NUMBER: builtins.int + @property + def saved_dataset(self) -> feast.core.SavedDataset_pb2.SavedDataset: ... + project: builtins.str + commit: builtins.bool + def __init__( + self, + *, + saved_dataset: feast.core.SavedDataset_pb2.SavedDataset | None = ..., + project: builtins.str = ..., + commit: builtins.bool = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["saved_dataset", b"saved_dataset"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["commit", b"commit", "project", b"project", "saved_dataset", b"saved_dataset"]) -> None: ... + +global___ApplySavedDatasetRequest = ApplySavedDatasetRequest + +class GetSavedDatasetRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + ALLOW_CACHE_FIELD_NUMBER: builtins.int + name: builtins.str + project: builtins.str + allow_cache: builtins.bool + def __init__( + self, + *, + name: builtins.str = ..., + project: builtins.str = ..., + allow_cache: builtins.bool = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["allow_cache", b"allow_cache", "name", b"name", "project", b"project"]) -> None: ... + +global___GetSavedDatasetRequest = GetSavedDatasetRequest + +class ListSavedDatasetsRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class TagsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + PROJECT_FIELD_NUMBER: builtins.int + ALLOW_CACHE_FIELD_NUMBER: builtins.int + TAGS_FIELD_NUMBER: builtins.int + project: builtins.str + allow_cache: builtins.bool + @property + def tags(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: ... + def __init__( + self, + *, + project: builtins.str = ..., + allow_cache: builtins.bool = ..., + tags: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["allow_cache", b"allow_cache", "project", b"project", "tags", b"tags"]) -> None: ... + +global___ListSavedDatasetsRequest = ListSavedDatasetsRequest + +class ListSavedDatasetsResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + SAVED_DATASETS_FIELD_NUMBER: builtins.int + @property + def saved_datasets(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.SavedDataset_pb2.SavedDataset]: ... + def __init__( + self, + *, + saved_datasets: collections.abc.Iterable[feast.core.SavedDataset_pb2.SavedDataset] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["saved_datasets", b"saved_datasets"]) -> None: ... + +global___ListSavedDatasetsResponse = ListSavedDatasetsResponse + +class DeleteSavedDatasetRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + COMMIT_FIELD_NUMBER: builtins.int + name: builtins.str + project: builtins.str + commit: builtins.bool + def __init__( + self, + *, + name: builtins.str = ..., + project: builtins.str = ..., + commit: builtins.bool = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["commit", b"commit", "name", b"name", "project", b"project"]) -> None: ... + +global___DeleteSavedDatasetRequest = DeleteSavedDatasetRequest + +class ApplyValidationReferenceRequest(google.protobuf.message.Message): + """ValidationReference""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + VALIDATION_REFERENCE_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + COMMIT_FIELD_NUMBER: builtins.int + @property + def validation_reference(self) -> feast.core.ValidationProfile_pb2.ValidationReference: ... + project: builtins.str + commit: builtins.bool + def __init__( + self, + *, + validation_reference: feast.core.ValidationProfile_pb2.ValidationReference | None = ..., + project: builtins.str = ..., + commit: builtins.bool = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["validation_reference", b"validation_reference"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["commit", b"commit", "project", b"project", "validation_reference", b"validation_reference"]) -> None: ... + +global___ApplyValidationReferenceRequest = ApplyValidationReferenceRequest + +class GetValidationReferenceRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + ALLOW_CACHE_FIELD_NUMBER: builtins.int + name: builtins.str + project: builtins.str + allow_cache: builtins.bool + def __init__( + self, + *, + name: builtins.str = ..., + project: builtins.str = ..., + allow_cache: builtins.bool = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["allow_cache", b"allow_cache", "name", b"name", "project", b"project"]) -> None: ... + +global___GetValidationReferenceRequest = GetValidationReferenceRequest + +class ListValidationReferencesRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class TagsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + PROJECT_FIELD_NUMBER: builtins.int + ALLOW_CACHE_FIELD_NUMBER: builtins.int + TAGS_FIELD_NUMBER: builtins.int + project: builtins.str + allow_cache: builtins.bool + @property + def tags(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: ... + def __init__( + self, + *, + project: builtins.str = ..., + allow_cache: builtins.bool = ..., + tags: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["allow_cache", b"allow_cache", "project", b"project", "tags", b"tags"]) -> None: ... + +global___ListValidationReferencesRequest = ListValidationReferencesRequest + +class ListValidationReferencesResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + VALIDATION_REFERENCES_FIELD_NUMBER: builtins.int + @property + def validation_references(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.ValidationProfile_pb2.ValidationReference]: ... + def __init__( + self, + *, + validation_references: collections.abc.Iterable[feast.core.ValidationProfile_pb2.ValidationReference] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["validation_references", b"validation_references"]) -> None: ... + +global___ListValidationReferencesResponse = ListValidationReferencesResponse + +class DeleteValidationReferenceRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + COMMIT_FIELD_NUMBER: builtins.int + name: builtins.str + project: builtins.str + commit: builtins.bool + def __init__( + self, + *, + name: builtins.str = ..., + project: builtins.str = ..., + commit: builtins.bool = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["commit", b"commit", "name", b"name", "project", b"project"]) -> None: ... + +global___DeleteValidationReferenceRequest = DeleteValidationReferenceRequest + +class ApplyPermissionRequest(google.protobuf.message.Message): + """Permissions""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + PERMISSION_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + COMMIT_FIELD_NUMBER: builtins.int + @property + def permission(self) -> feast.core.Permission_pb2.Permission: ... + project: builtins.str + commit: builtins.bool + def __init__( + self, + *, + permission: feast.core.Permission_pb2.Permission | None = ..., + project: builtins.str = ..., + commit: builtins.bool = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["permission", b"permission"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["commit", b"commit", "permission", b"permission", "project", b"project"]) -> None: ... + +global___ApplyPermissionRequest = ApplyPermissionRequest + +class GetPermissionRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + ALLOW_CACHE_FIELD_NUMBER: builtins.int + name: builtins.str + project: builtins.str + allow_cache: builtins.bool + def __init__( + self, + *, + name: builtins.str = ..., + project: builtins.str = ..., + allow_cache: builtins.bool = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["allow_cache", b"allow_cache", "name", b"name", "project", b"project"]) -> None: ... + +global___GetPermissionRequest = GetPermissionRequest + +class ListPermissionsRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class TagsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + PROJECT_FIELD_NUMBER: builtins.int + ALLOW_CACHE_FIELD_NUMBER: builtins.int + TAGS_FIELD_NUMBER: builtins.int + project: builtins.str + allow_cache: builtins.bool + @property + def tags(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: ... + def __init__( + self, + *, + project: builtins.str = ..., + allow_cache: builtins.bool = ..., + tags: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["allow_cache", b"allow_cache", "project", b"project", "tags", b"tags"]) -> None: ... + +global___ListPermissionsRequest = ListPermissionsRequest + +class ListPermissionsResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + PERMISSIONS_FIELD_NUMBER: builtins.int + @property + def permissions(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.Permission_pb2.Permission]: ... + def __init__( + self, + *, + permissions: collections.abc.Iterable[feast.core.Permission_pb2.Permission] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["permissions", b"permissions"]) -> None: ... + +global___ListPermissionsResponse = ListPermissionsResponse + +class DeletePermissionRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + COMMIT_FIELD_NUMBER: builtins.int + name: builtins.str + project: builtins.str + commit: builtins.bool + def __init__( + self, + *, + name: builtins.str = ..., + project: builtins.str = ..., + commit: builtins.bool = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["commit", b"commit", "name", b"name", "project", b"project"]) -> None: ... + +global___DeletePermissionRequest = DeletePermissionRequest + +class ApplyProjectRequest(google.protobuf.message.Message): + """Projects""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + PROJECT_FIELD_NUMBER: builtins.int + COMMIT_FIELD_NUMBER: builtins.int + @property + def project(self) -> feast.core.Project_pb2.Project: ... + commit: builtins.bool + def __init__( + self, + *, + project: feast.core.Project_pb2.Project | None = ..., + commit: builtins.bool = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["project", b"project"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["commit", b"commit", "project", b"project"]) -> None: ... + +global___ApplyProjectRequest = ApplyProjectRequest + +class GetProjectRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + ALLOW_CACHE_FIELD_NUMBER: builtins.int + name: builtins.str + allow_cache: builtins.bool + def __init__( + self, + *, + name: builtins.str = ..., + allow_cache: builtins.bool = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["allow_cache", b"allow_cache", "name", b"name"]) -> None: ... + +global___GetProjectRequest = GetProjectRequest + +class ListProjectsRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class TagsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + ALLOW_CACHE_FIELD_NUMBER: builtins.int + TAGS_FIELD_NUMBER: builtins.int + allow_cache: builtins.bool + @property + def tags(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: ... + def __init__( + self, + *, + allow_cache: builtins.bool = ..., + tags: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["allow_cache", b"allow_cache", "tags", b"tags"]) -> None: ... + +global___ListProjectsRequest = ListProjectsRequest + +class ListProjectsResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + PROJECTS_FIELD_NUMBER: builtins.int + @property + def projects(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.Project_pb2.Project]: ... + def __init__( + self, + *, + projects: collections.abc.Iterable[feast.core.Project_pb2.Project] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["projects", b"projects"]) -> None: ... + +global___ListProjectsResponse = ListProjectsResponse + +class DeleteProjectRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + COMMIT_FIELD_NUMBER: builtins.int + name: builtins.str + commit: builtins.bool + def __init__( + self, + *, + name: builtins.str = ..., + commit: builtins.bool = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["commit", b"commit", "name", b"name"]) -> None: ... + +global___DeleteProjectRequest = DeleteProjectRequest diff --git a/sdk/python/feast/protos/feast/registry/RegistryServer_pb2_grpc.py b/sdk/python/feast/protos/feast/registry/RegistryServer_pb2_grpc.py new file mode 100644 index 0000000000..bab23c4394 --- /dev/null +++ b/sdk/python/feast/protos/feast/registry/RegistryServer_pb2_grpc.py @@ -0,0 +1,1542 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + +from feast.protos.feast.core import DataSource_pb2 as feast_dot_core_dot_DataSource__pb2 +from feast.protos.feast.core import Entity_pb2 as feast_dot_core_dot_Entity__pb2 +from feast.protos.feast.core import FeatureService_pb2 as feast_dot_core_dot_FeatureService__pb2 +from feast.protos.feast.core import FeatureView_pb2 as feast_dot_core_dot_FeatureView__pb2 +from feast.protos.feast.core import InfraObject_pb2 as feast_dot_core_dot_InfraObject__pb2 +from feast.protos.feast.core import OnDemandFeatureView_pb2 as feast_dot_core_dot_OnDemandFeatureView__pb2 +from feast.protos.feast.core import Permission_pb2 as feast_dot_core_dot_Permission__pb2 +from feast.protos.feast.core import Project_pb2 as feast_dot_core_dot_Project__pb2 +from feast.protos.feast.core import Registry_pb2 as feast_dot_core_dot_Registry__pb2 +from feast.protos.feast.core import SavedDataset_pb2 as feast_dot_core_dot_SavedDataset__pb2 +from feast.protos.feast.core import StreamFeatureView_pb2 as feast_dot_core_dot_StreamFeatureView__pb2 +from feast.protos.feast.core import ValidationProfile_pb2 as feast_dot_core_dot_ValidationProfile__pb2 +from feast.protos.feast.registry import RegistryServer_pb2 as feast_dot_registry_dot_RegistryServer__pb2 +from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2 + + +class RegistryServerStub(object): + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.ApplyEntity = channel.unary_unary( + '/feast.registry.RegistryServer/ApplyEntity', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.ApplyEntityRequest.SerializeToString, + response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, + ) + self.GetEntity = channel.unary_unary( + '/feast.registry.RegistryServer/GetEntity', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.GetEntityRequest.SerializeToString, + response_deserializer=feast_dot_core_dot_Entity__pb2.Entity.FromString, + ) + self.ListEntities = channel.unary_unary( + '/feast.registry.RegistryServer/ListEntities', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.ListEntitiesRequest.SerializeToString, + response_deserializer=feast_dot_registry_dot_RegistryServer__pb2.ListEntitiesResponse.FromString, + ) + self.DeleteEntity = channel.unary_unary( + '/feast.registry.RegistryServer/DeleteEntity', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.DeleteEntityRequest.SerializeToString, + response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, + ) + self.ApplyDataSource = channel.unary_unary( + '/feast.registry.RegistryServer/ApplyDataSource', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.ApplyDataSourceRequest.SerializeToString, + response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, + ) + self.GetDataSource = channel.unary_unary( + '/feast.registry.RegistryServer/GetDataSource', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.GetDataSourceRequest.SerializeToString, + response_deserializer=feast_dot_core_dot_DataSource__pb2.DataSource.FromString, + ) + self.ListDataSources = channel.unary_unary( + '/feast.registry.RegistryServer/ListDataSources', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.ListDataSourcesRequest.SerializeToString, + response_deserializer=feast_dot_registry_dot_RegistryServer__pb2.ListDataSourcesResponse.FromString, + ) + self.DeleteDataSource = channel.unary_unary( + '/feast.registry.RegistryServer/DeleteDataSource', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.DeleteDataSourceRequest.SerializeToString, + response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, + ) + self.ApplyFeatureView = channel.unary_unary( + '/feast.registry.RegistryServer/ApplyFeatureView', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.ApplyFeatureViewRequest.SerializeToString, + response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, + ) + self.DeleteFeatureView = channel.unary_unary( + '/feast.registry.RegistryServer/DeleteFeatureView', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.DeleteFeatureViewRequest.SerializeToString, + response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, + ) + self.GetAnyFeatureView = channel.unary_unary( + '/feast.registry.RegistryServer/GetAnyFeatureView', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.GetAnyFeatureViewRequest.SerializeToString, + response_deserializer=feast_dot_registry_dot_RegistryServer__pb2.GetAnyFeatureViewResponse.FromString, + ) + self.ListAllFeatureViews = channel.unary_unary( + '/feast.registry.RegistryServer/ListAllFeatureViews', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.ListAllFeatureViewsRequest.SerializeToString, + response_deserializer=feast_dot_registry_dot_RegistryServer__pb2.ListAllFeatureViewsResponse.FromString, + ) + self.GetFeatureView = channel.unary_unary( + '/feast.registry.RegistryServer/GetFeatureView', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.GetFeatureViewRequest.SerializeToString, + response_deserializer=feast_dot_core_dot_FeatureView__pb2.FeatureView.FromString, + ) + self.ListFeatureViews = channel.unary_unary( + '/feast.registry.RegistryServer/ListFeatureViews', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.ListFeatureViewsRequest.SerializeToString, + response_deserializer=feast_dot_registry_dot_RegistryServer__pb2.ListFeatureViewsResponse.FromString, + ) + self.GetStreamFeatureView = channel.unary_unary( + '/feast.registry.RegistryServer/GetStreamFeatureView', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.GetStreamFeatureViewRequest.SerializeToString, + response_deserializer=feast_dot_core_dot_StreamFeatureView__pb2.StreamFeatureView.FromString, + ) + self.ListStreamFeatureViews = channel.unary_unary( + '/feast.registry.RegistryServer/ListStreamFeatureViews', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.ListStreamFeatureViewsRequest.SerializeToString, + response_deserializer=feast_dot_registry_dot_RegistryServer__pb2.ListStreamFeatureViewsResponse.FromString, + ) + self.GetOnDemandFeatureView = channel.unary_unary( + '/feast.registry.RegistryServer/GetOnDemandFeatureView', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.GetOnDemandFeatureViewRequest.SerializeToString, + response_deserializer=feast_dot_core_dot_OnDemandFeatureView__pb2.OnDemandFeatureView.FromString, + ) + self.ListOnDemandFeatureViews = channel.unary_unary( + '/feast.registry.RegistryServer/ListOnDemandFeatureViews', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.ListOnDemandFeatureViewsRequest.SerializeToString, + response_deserializer=feast_dot_registry_dot_RegistryServer__pb2.ListOnDemandFeatureViewsResponse.FromString, + ) + self.ApplyFeatureService = channel.unary_unary( + '/feast.registry.RegistryServer/ApplyFeatureService', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.ApplyFeatureServiceRequest.SerializeToString, + response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, + ) + self.GetFeatureService = channel.unary_unary( + '/feast.registry.RegistryServer/GetFeatureService', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.GetFeatureServiceRequest.SerializeToString, + response_deserializer=feast_dot_core_dot_FeatureService__pb2.FeatureService.FromString, + ) + self.ListFeatureServices = channel.unary_unary( + '/feast.registry.RegistryServer/ListFeatureServices', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.ListFeatureServicesRequest.SerializeToString, + response_deserializer=feast_dot_registry_dot_RegistryServer__pb2.ListFeatureServicesResponse.FromString, + ) + self.DeleteFeatureService = channel.unary_unary( + '/feast.registry.RegistryServer/DeleteFeatureService', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.DeleteFeatureServiceRequest.SerializeToString, + response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, + ) + self.ApplySavedDataset = channel.unary_unary( + '/feast.registry.RegistryServer/ApplySavedDataset', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.ApplySavedDatasetRequest.SerializeToString, + response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, + ) + self.GetSavedDataset = channel.unary_unary( + '/feast.registry.RegistryServer/GetSavedDataset', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.GetSavedDatasetRequest.SerializeToString, + response_deserializer=feast_dot_core_dot_SavedDataset__pb2.SavedDataset.FromString, + ) + self.ListSavedDatasets = channel.unary_unary( + '/feast.registry.RegistryServer/ListSavedDatasets', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.ListSavedDatasetsRequest.SerializeToString, + response_deserializer=feast_dot_registry_dot_RegistryServer__pb2.ListSavedDatasetsResponse.FromString, + ) + self.DeleteSavedDataset = channel.unary_unary( + '/feast.registry.RegistryServer/DeleteSavedDataset', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.DeleteSavedDatasetRequest.SerializeToString, + response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, + ) + self.ApplyValidationReference = channel.unary_unary( + '/feast.registry.RegistryServer/ApplyValidationReference', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.ApplyValidationReferenceRequest.SerializeToString, + response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, + ) + self.GetValidationReference = channel.unary_unary( + '/feast.registry.RegistryServer/GetValidationReference', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.GetValidationReferenceRequest.SerializeToString, + response_deserializer=feast_dot_core_dot_ValidationProfile__pb2.ValidationReference.FromString, + ) + self.ListValidationReferences = channel.unary_unary( + '/feast.registry.RegistryServer/ListValidationReferences', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.ListValidationReferencesRequest.SerializeToString, + response_deserializer=feast_dot_registry_dot_RegistryServer__pb2.ListValidationReferencesResponse.FromString, + ) + self.DeleteValidationReference = channel.unary_unary( + '/feast.registry.RegistryServer/DeleteValidationReference', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.DeleteValidationReferenceRequest.SerializeToString, + response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, + ) + self.ApplyPermission = channel.unary_unary( + '/feast.registry.RegistryServer/ApplyPermission', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.ApplyPermissionRequest.SerializeToString, + response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, + ) + self.GetPermission = channel.unary_unary( + '/feast.registry.RegistryServer/GetPermission', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.GetPermissionRequest.SerializeToString, + response_deserializer=feast_dot_core_dot_Permission__pb2.Permission.FromString, + ) + self.ListPermissions = channel.unary_unary( + '/feast.registry.RegistryServer/ListPermissions', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.ListPermissionsRequest.SerializeToString, + response_deserializer=feast_dot_registry_dot_RegistryServer__pb2.ListPermissionsResponse.FromString, + ) + self.DeletePermission = channel.unary_unary( + '/feast.registry.RegistryServer/DeletePermission', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.DeletePermissionRequest.SerializeToString, + response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, + ) + self.ApplyProject = channel.unary_unary( + '/feast.registry.RegistryServer/ApplyProject', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.ApplyProjectRequest.SerializeToString, + response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, + ) + self.GetProject = channel.unary_unary( + '/feast.registry.RegistryServer/GetProject', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.GetProjectRequest.SerializeToString, + response_deserializer=feast_dot_core_dot_Project__pb2.Project.FromString, + ) + self.ListProjects = channel.unary_unary( + '/feast.registry.RegistryServer/ListProjects', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.ListProjectsRequest.SerializeToString, + response_deserializer=feast_dot_registry_dot_RegistryServer__pb2.ListProjectsResponse.FromString, + ) + self.DeleteProject = channel.unary_unary( + '/feast.registry.RegistryServer/DeleteProject', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.DeleteProjectRequest.SerializeToString, + response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, + ) + self.ApplyMaterialization = channel.unary_unary( + '/feast.registry.RegistryServer/ApplyMaterialization', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.ApplyMaterializationRequest.SerializeToString, + response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, + ) + self.ListProjectMetadata = channel.unary_unary( + '/feast.registry.RegistryServer/ListProjectMetadata', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.ListProjectMetadataRequest.SerializeToString, + response_deserializer=feast_dot_registry_dot_RegistryServer__pb2.ListProjectMetadataResponse.FromString, + ) + self.UpdateInfra = channel.unary_unary( + '/feast.registry.RegistryServer/UpdateInfra', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.UpdateInfraRequest.SerializeToString, + response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, + ) + self.GetInfra = channel.unary_unary( + '/feast.registry.RegistryServer/GetInfra', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.GetInfraRequest.SerializeToString, + response_deserializer=feast_dot_core_dot_InfraObject__pb2.Infra.FromString, + ) + self.Commit = channel.unary_unary( + '/feast.registry.RegistryServer/Commit', + request_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, + response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, + ) + self.Refresh = channel.unary_unary( + '/feast.registry.RegistryServer/Refresh', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.RefreshRequest.SerializeToString, + response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, + ) + self.Proto = channel.unary_unary( + '/feast.registry.RegistryServer/Proto', + request_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, + response_deserializer=feast_dot_core_dot_Registry__pb2.Registry.FromString, + ) + + +class RegistryServerServicer(object): + """Missing associated documentation comment in .proto file.""" + + def ApplyEntity(self, request, context): + """Entity RPCs + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetEntity(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def ListEntities(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def DeleteEntity(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def ApplyDataSource(self, request, context): + """DataSource RPCs + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetDataSource(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def ListDataSources(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def DeleteDataSource(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def ApplyFeatureView(self, request, context): + """FeatureView RPCs + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def DeleteFeatureView(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetAnyFeatureView(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def ListAllFeatureViews(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetFeatureView(self, request, context): + """plain FeatureView RPCs + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def ListFeatureViews(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetStreamFeatureView(self, request, context): + """StreamFeatureView RPCs + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def ListStreamFeatureViews(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetOnDemandFeatureView(self, request, context): + """OnDemandFeatureView RPCs + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def ListOnDemandFeatureViews(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def ApplyFeatureService(self, request, context): + """FeatureService RPCs + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetFeatureService(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def ListFeatureServices(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def DeleteFeatureService(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def ApplySavedDataset(self, request, context): + """SavedDataset RPCs + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetSavedDataset(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def ListSavedDatasets(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def DeleteSavedDataset(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def ApplyValidationReference(self, request, context): + """ValidationReference RPCs + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetValidationReference(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def ListValidationReferences(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def DeleteValidationReference(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def ApplyPermission(self, request, context): + """Permission RPCs + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetPermission(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def ListPermissions(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def DeletePermission(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def ApplyProject(self, request, context): + """Project RPCs + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetProject(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def ListProjects(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def DeleteProject(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def ApplyMaterialization(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def ListProjectMetadata(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def UpdateInfra(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetInfra(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def Commit(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def Refresh(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def Proto(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_RegistryServerServicer_to_server(servicer, server): + rpc_method_handlers = { + 'ApplyEntity': grpc.unary_unary_rpc_method_handler( + servicer.ApplyEntity, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.ApplyEntityRequest.FromString, + response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, + ), + 'GetEntity': grpc.unary_unary_rpc_method_handler( + servicer.GetEntity, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.GetEntityRequest.FromString, + response_serializer=feast_dot_core_dot_Entity__pb2.Entity.SerializeToString, + ), + 'ListEntities': grpc.unary_unary_rpc_method_handler( + servicer.ListEntities, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.ListEntitiesRequest.FromString, + response_serializer=feast_dot_registry_dot_RegistryServer__pb2.ListEntitiesResponse.SerializeToString, + ), + 'DeleteEntity': grpc.unary_unary_rpc_method_handler( + servicer.DeleteEntity, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.DeleteEntityRequest.FromString, + response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, + ), + 'ApplyDataSource': grpc.unary_unary_rpc_method_handler( + servicer.ApplyDataSource, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.ApplyDataSourceRequest.FromString, + response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, + ), + 'GetDataSource': grpc.unary_unary_rpc_method_handler( + servicer.GetDataSource, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.GetDataSourceRequest.FromString, + response_serializer=feast_dot_core_dot_DataSource__pb2.DataSource.SerializeToString, + ), + 'ListDataSources': grpc.unary_unary_rpc_method_handler( + servicer.ListDataSources, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.ListDataSourcesRequest.FromString, + response_serializer=feast_dot_registry_dot_RegistryServer__pb2.ListDataSourcesResponse.SerializeToString, + ), + 'DeleteDataSource': grpc.unary_unary_rpc_method_handler( + servicer.DeleteDataSource, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.DeleteDataSourceRequest.FromString, + response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, + ), + 'ApplyFeatureView': grpc.unary_unary_rpc_method_handler( + servicer.ApplyFeatureView, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.ApplyFeatureViewRequest.FromString, + response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, + ), + 'DeleteFeatureView': grpc.unary_unary_rpc_method_handler( + servicer.DeleteFeatureView, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.DeleteFeatureViewRequest.FromString, + response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, + ), + 'GetAnyFeatureView': grpc.unary_unary_rpc_method_handler( + servicer.GetAnyFeatureView, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.GetAnyFeatureViewRequest.FromString, + response_serializer=feast_dot_registry_dot_RegistryServer__pb2.GetAnyFeatureViewResponse.SerializeToString, + ), + 'ListAllFeatureViews': grpc.unary_unary_rpc_method_handler( + servicer.ListAllFeatureViews, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.ListAllFeatureViewsRequest.FromString, + response_serializer=feast_dot_registry_dot_RegistryServer__pb2.ListAllFeatureViewsResponse.SerializeToString, + ), + 'GetFeatureView': grpc.unary_unary_rpc_method_handler( + servicer.GetFeatureView, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.GetFeatureViewRequest.FromString, + response_serializer=feast_dot_core_dot_FeatureView__pb2.FeatureView.SerializeToString, + ), + 'ListFeatureViews': grpc.unary_unary_rpc_method_handler( + servicer.ListFeatureViews, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.ListFeatureViewsRequest.FromString, + response_serializer=feast_dot_registry_dot_RegistryServer__pb2.ListFeatureViewsResponse.SerializeToString, + ), + 'GetStreamFeatureView': grpc.unary_unary_rpc_method_handler( + servicer.GetStreamFeatureView, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.GetStreamFeatureViewRequest.FromString, + response_serializer=feast_dot_core_dot_StreamFeatureView__pb2.StreamFeatureView.SerializeToString, + ), + 'ListStreamFeatureViews': grpc.unary_unary_rpc_method_handler( + servicer.ListStreamFeatureViews, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.ListStreamFeatureViewsRequest.FromString, + response_serializer=feast_dot_registry_dot_RegistryServer__pb2.ListStreamFeatureViewsResponse.SerializeToString, + ), + 'GetOnDemandFeatureView': grpc.unary_unary_rpc_method_handler( + servicer.GetOnDemandFeatureView, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.GetOnDemandFeatureViewRequest.FromString, + response_serializer=feast_dot_core_dot_OnDemandFeatureView__pb2.OnDemandFeatureView.SerializeToString, + ), + 'ListOnDemandFeatureViews': grpc.unary_unary_rpc_method_handler( + servicer.ListOnDemandFeatureViews, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.ListOnDemandFeatureViewsRequest.FromString, + response_serializer=feast_dot_registry_dot_RegistryServer__pb2.ListOnDemandFeatureViewsResponse.SerializeToString, + ), + 'ApplyFeatureService': grpc.unary_unary_rpc_method_handler( + servicer.ApplyFeatureService, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.ApplyFeatureServiceRequest.FromString, + response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, + ), + 'GetFeatureService': grpc.unary_unary_rpc_method_handler( + servicer.GetFeatureService, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.GetFeatureServiceRequest.FromString, + response_serializer=feast_dot_core_dot_FeatureService__pb2.FeatureService.SerializeToString, + ), + 'ListFeatureServices': grpc.unary_unary_rpc_method_handler( + servicer.ListFeatureServices, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.ListFeatureServicesRequest.FromString, + response_serializer=feast_dot_registry_dot_RegistryServer__pb2.ListFeatureServicesResponse.SerializeToString, + ), + 'DeleteFeatureService': grpc.unary_unary_rpc_method_handler( + servicer.DeleteFeatureService, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.DeleteFeatureServiceRequest.FromString, + response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, + ), + 'ApplySavedDataset': grpc.unary_unary_rpc_method_handler( + servicer.ApplySavedDataset, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.ApplySavedDatasetRequest.FromString, + response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, + ), + 'GetSavedDataset': grpc.unary_unary_rpc_method_handler( + servicer.GetSavedDataset, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.GetSavedDatasetRequest.FromString, + response_serializer=feast_dot_core_dot_SavedDataset__pb2.SavedDataset.SerializeToString, + ), + 'ListSavedDatasets': grpc.unary_unary_rpc_method_handler( + servicer.ListSavedDatasets, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.ListSavedDatasetsRequest.FromString, + response_serializer=feast_dot_registry_dot_RegistryServer__pb2.ListSavedDatasetsResponse.SerializeToString, + ), + 'DeleteSavedDataset': grpc.unary_unary_rpc_method_handler( + servicer.DeleteSavedDataset, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.DeleteSavedDatasetRequest.FromString, + response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, + ), + 'ApplyValidationReference': grpc.unary_unary_rpc_method_handler( + servicer.ApplyValidationReference, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.ApplyValidationReferenceRequest.FromString, + response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, + ), + 'GetValidationReference': grpc.unary_unary_rpc_method_handler( + servicer.GetValidationReference, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.GetValidationReferenceRequest.FromString, + response_serializer=feast_dot_core_dot_ValidationProfile__pb2.ValidationReference.SerializeToString, + ), + 'ListValidationReferences': grpc.unary_unary_rpc_method_handler( + servicer.ListValidationReferences, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.ListValidationReferencesRequest.FromString, + response_serializer=feast_dot_registry_dot_RegistryServer__pb2.ListValidationReferencesResponse.SerializeToString, + ), + 'DeleteValidationReference': grpc.unary_unary_rpc_method_handler( + servicer.DeleteValidationReference, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.DeleteValidationReferenceRequest.FromString, + response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, + ), + 'ApplyPermission': grpc.unary_unary_rpc_method_handler( + servicer.ApplyPermission, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.ApplyPermissionRequest.FromString, + response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, + ), + 'GetPermission': grpc.unary_unary_rpc_method_handler( + servicer.GetPermission, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.GetPermissionRequest.FromString, + response_serializer=feast_dot_core_dot_Permission__pb2.Permission.SerializeToString, + ), + 'ListPermissions': grpc.unary_unary_rpc_method_handler( + servicer.ListPermissions, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.ListPermissionsRequest.FromString, + response_serializer=feast_dot_registry_dot_RegistryServer__pb2.ListPermissionsResponse.SerializeToString, + ), + 'DeletePermission': grpc.unary_unary_rpc_method_handler( + servicer.DeletePermission, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.DeletePermissionRequest.FromString, + response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, + ), + 'ApplyProject': grpc.unary_unary_rpc_method_handler( + servicer.ApplyProject, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.ApplyProjectRequest.FromString, + response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, + ), + 'GetProject': grpc.unary_unary_rpc_method_handler( + servicer.GetProject, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.GetProjectRequest.FromString, + response_serializer=feast_dot_core_dot_Project__pb2.Project.SerializeToString, + ), + 'ListProjects': grpc.unary_unary_rpc_method_handler( + servicer.ListProjects, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.ListProjectsRequest.FromString, + response_serializer=feast_dot_registry_dot_RegistryServer__pb2.ListProjectsResponse.SerializeToString, + ), + 'DeleteProject': grpc.unary_unary_rpc_method_handler( + servicer.DeleteProject, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.DeleteProjectRequest.FromString, + response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, + ), + 'ApplyMaterialization': grpc.unary_unary_rpc_method_handler( + servicer.ApplyMaterialization, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.ApplyMaterializationRequest.FromString, + response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, + ), + 'ListProjectMetadata': grpc.unary_unary_rpc_method_handler( + servicer.ListProjectMetadata, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.ListProjectMetadataRequest.FromString, + response_serializer=feast_dot_registry_dot_RegistryServer__pb2.ListProjectMetadataResponse.SerializeToString, + ), + 'UpdateInfra': grpc.unary_unary_rpc_method_handler( + servicer.UpdateInfra, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.UpdateInfraRequest.FromString, + response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, + ), + 'GetInfra': grpc.unary_unary_rpc_method_handler( + servicer.GetInfra, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.GetInfraRequest.FromString, + response_serializer=feast_dot_core_dot_InfraObject__pb2.Infra.SerializeToString, + ), + 'Commit': grpc.unary_unary_rpc_method_handler( + servicer.Commit, + request_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, + response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, + ), + 'Refresh': grpc.unary_unary_rpc_method_handler( + servicer.Refresh, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.RefreshRequest.FromString, + response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, + ), + 'Proto': grpc.unary_unary_rpc_method_handler( + servicer.Proto, + request_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, + response_serializer=feast_dot_core_dot_Registry__pb2.Registry.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'feast.registry.RegistryServer', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + + + # This class is part of an EXPERIMENTAL API. +class RegistryServer(object): + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def ApplyEntity(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/ApplyEntity', + feast_dot_registry_dot_RegistryServer__pb2.ApplyEntityRequest.SerializeToString, + google_dot_protobuf_dot_empty__pb2.Empty.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def GetEntity(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/GetEntity', + feast_dot_registry_dot_RegistryServer__pb2.GetEntityRequest.SerializeToString, + feast_dot_core_dot_Entity__pb2.Entity.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def ListEntities(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/ListEntities', + feast_dot_registry_dot_RegistryServer__pb2.ListEntitiesRequest.SerializeToString, + feast_dot_registry_dot_RegistryServer__pb2.ListEntitiesResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def DeleteEntity(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/DeleteEntity', + feast_dot_registry_dot_RegistryServer__pb2.DeleteEntityRequest.SerializeToString, + google_dot_protobuf_dot_empty__pb2.Empty.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def ApplyDataSource(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/ApplyDataSource', + feast_dot_registry_dot_RegistryServer__pb2.ApplyDataSourceRequest.SerializeToString, + google_dot_protobuf_dot_empty__pb2.Empty.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def GetDataSource(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/GetDataSource', + feast_dot_registry_dot_RegistryServer__pb2.GetDataSourceRequest.SerializeToString, + feast_dot_core_dot_DataSource__pb2.DataSource.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def ListDataSources(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/ListDataSources', + feast_dot_registry_dot_RegistryServer__pb2.ListDataSourcesRequest.SerializeToString, + feast_dot_registry_dot_RegistryServer__pb2.ListDataSourcesResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def DeleteDataSource(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/DeleteDataSource', + feast_dot_registry_dot_RegistryServer__pb2.DeleteDataSourceRequest.SerializeToString, + google_dot_protobuf_dot_empty__pb2.Empty.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def ApplyFeatureView(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/ApplyFeatureView', + feast_dot_registry_dot_RegistryServer__pb2.ApplyFeatureViewRequest.SerializeToString, + google_dot_protobuf_dot_empty__pb2.Empty.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def DeleteFeatureView(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/DeleteFeatureView', + feast_dot_registry_dot_RegistryServer__pb2.DeleteFeatureViewRequest.SerializeToString, + google_dot_protobuf_dot_empty__pb2.Empty.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def GetAnyFeatureView(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/GetAnyFeatureView', + feast_dot_registry_dot_RegistryServer__pb2.GetAnyFeatureViewRequest.SerializeToString, + feast_dot_registry_dot_RegistryServer__pb2.GetAnyFeatureViewResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def ListAllFeatureViews(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/ListAllFeatureViews', + feast_dot_registry_dot_RegistryServer__pb2.ListAllFeatureViewsRequest.SerializeToString, + feast_dot_registry_dot_RegistryServer__pb2.ListAllFeatureViewsResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def GetFeatureView(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/GetFeatureView', + feast_dot_registry_dot_RegistryServer__pb2.GetFeatureViewRequest.SerializeToString, + feast_dot_core_dot_FeatureView__pb2.FeatureView.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def ListFeatureViews(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/ListFeatureViews', + feast_dot_registry_dot_RegistryServer__pb2.ListFeatureViewsRequest.SerializeToString, + feast_dot_registry_dot_RegistryServer__pb2.ListFeatureViewsResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def GetStreamFeatureView(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/GetStreamFeatureView', + feast_dot_registry_dot_RegistryServer__pb2.GetStreamFeatureViewRequest.SerializeToString, + feast_dot_core_dot_StreamFeatureView__pb2.StreamFeatureView.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def ListStreamFeatureViews(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/ListStreamFeatureViews', + feast_dot_registry_dot_RegistryServer__pb2.ListStreamFeatureViewsRequest.SerializeToString, + feast_dot_registry_dot_RegistryServer__pb2.ListStreamFeatureViewsResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def GetOnDemandFeatureView(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/GetOnDemandFeatureView', + feast_dot_registry_dot_RegistryServer__pb2.GetOnDemandFeatureViewRequest.SerializeToString, + feast_dot_core_dot_OnDemandFeatureView__pb2.OnDemandFeatureView.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def ListOnDemandFeatureViews(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/ListOnDemandFeatureViews', + feast_dot_registry_dot_RegistryServer__pb2.ListOnDemandFeatureViewsRequest.SerializeToString, + feast_dot_registry_dot_RegistryServer__pb2.ListOnDemandFeatureViewsResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def ApplyFeatureService(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/ApplyFeatureService', + feast_dot_registry_dot_RegistryServer__pb2.ApplyFeatureServiceRequest.SerializeToString, + google_dot_protobuf_dot_empty__pb2.Empty.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def GetFeatureService(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/GetFeatureService', + feast_dot_registry_dot_RegistryServer__pb2.GetFeatureServiceRequest.SerializeToString, + feast_dot_core_dot_FeatureService__pb2.FeatureService.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def ListFeatureServices(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/ListFeatureServices', + feast_dot_registry_dot_RegistryServer__pb2.ListFeatureServicesRequest.SerializeToString, + feast_dot_registry_dot_RegistryServer__pb2.ListFeatureServicesResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def DeleteFeatureService(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/DeleteFeatureService', + feast_dot_registry_dot_RegistryServer__pb2.DeleteFeatureServiceRequest.SerializeToString, + google_dot_protobuf_dot_empty__pb2.Empty.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def ApplySavedDataset(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/ApplySavedDataset', + feast_dot_registry_dot_RegistryServer__pb2.ApplySavedDatasetRequest.SerializeToString, + google_dot_protobuf_dot_empty__pb2.Empty.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def GetSavedDataset(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/GetSavedDataset', + feast_dot_registry_dot_RegistryServer__pb2.GetSavedDatasetRequest.SerializeToString, + feast_dot_core_dot_SavedDataset__pb2.SavedDataset.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def ListSavedDatasets(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/ListSavedDatasets', + feast_dot_registry_dot_RegistryServer__pb2.ListSavedDatasetsRequest.SerializeToString, + feast_dot_registry_dot_RegistryServer__pb2.ListSavedDatasetsResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def DeleteSavedDataset(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/DeleteSavedDataset', + feast_dot_registry_dot_RegistryServer__pb2.DeleteSavedDatasetRequest.SerializeToString, + google_dot_protobuf_dot_empty__pb2.Empty.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def ApplyValidationReference(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/ApplyValidationReference', + feast_dot_registry_dot_RegistryServer__pb2.ApplyValidationReferenceRequest.SerializeToString, + google_dot_protobuf_dot_empty__pb2.Empty.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def GetValidationReference(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/GetValidationReference', + feast_dot_registry_dot_RegistryServer__pb2.GetValidationReferenceRequest.SerializeToString, + feast_dot_core_dot_ValidationProfile__pb2.ValidationReference.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def ListValidationReferences(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/ListValidationReferences', + feast_dot_registry_dot_RegistryServer__pb2.ListValidationReferencesRequest.SerializeToString, + feast_dot_registry_dot_RegistryServer__pb2.ListValidationReferencesResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def DeleteValidationReference(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/DeleteValidationReference', + feast_dot_registry_dot_RegistryServer__pb2.DeleteValidationReferenceRequest.SerializeToString, + google_dot_protobuf_dot_empty__pb2.Empty.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def ApplyPermission(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/ApplyPermission', + feast_dot_registry_dot_RegistryServer__pb2.ApplyPermissionRequest.SerializeToString, + google_dot_protobuf_dot_empty__pb2.Empty.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def GetPermission(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/GetPermission', + feast_dot_registry_dot_RegistryServer__pb2.GetPermissionRequest.SerializeToString, + feast_dot_core_dot_Permission__pb2.Permission.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def ListPermissions(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/ListPermissions', + feast_dot_registry_dot_RegistryServer__pb2.ListPermissionsRequest.SerializeToString, + feast_dot_registry_dot_RegistryServer__pb2.ListPermissionsResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def DeletePermission(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/DeletePermission', + feast_dot_registry_dot_RegistryServer__pb2.DeletePermissionRequest.SerializeToString, + google_dot_protobuf_dot_empty__pb2.Empty.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def ApplyProject(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/ApplyProject', + feast_dot_registry_dot_RegistryServer__pb2.ApplyProjectRequest.SerializeToString, + google_dot_protobuf_dot_empty__pb2.Empty.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def GetProject(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/GetProject', + feast_dot_registry_dot_RegistryServer__pb2.GetProjectRequest.SerializeToString, + feast_dot_core_dot_Project__pb2.Project.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def ListProjects(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/ListProjects', + feast_dot_registry_dot_RegistryServer__pb2.ListProjectsRequest.SerializeToString, + feast_dot_registry_dot_RegistryServer__pb2.ListProjectsResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def DeleteProject(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/DeleteProject', + feast_dot_registry_dot_RegistryServer__pb2.DeleteProjectRequest.SerializeToString, + google_dot_protobuf_dot_empty__pb2.Empty.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def ApplyMaterialization(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/ApplyMaterialization', + feast_dot_registry_dot_RegistryServer__pb2.ApplyMaterializationRequest.SerializeToString, + google_dot_protobuf_dot_empty__pb2.Empty.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def ListProjectMetadata(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/ListProjectMetadata', + feast_dot_registry_dot_RegistryServer__pb2.ListProjectMetadataRequest.SerializeToString, + feast_dot_registry_dot_RegistryServer__pb2.ListProjectMetadataResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def UpdateInfra(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/UpdateInfra', + feast_dot_registry_dot_RegistryServer__pb2.UpdateInfraRequest.SerializeToString, + google_dot_protobuf_dot_empty__pb2.Empty.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def GetInfra(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/GetInfra', + feast_dot_registry_dot_RegistryServer__pb2.GetInfraRequest.SerializeToString, + feast_dot_core_dot_InfraObject__pb2.Infra.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def Commit(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/Commit', + google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, + google_dot_protobuf_dot_empty__pb2.Empty.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def Refresh(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/Refresh', + feast_dot_registry_dot_RegistryServer__pb2.RefreshRequest.SerializeToString, + google_dot_protobuf_dot_empty__pb2.Empty.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def Proto(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/Proto', + google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, + feast_dot_core_dot_Registry__pb2.Registry.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/sdk/python/feast/protos/feast/registry/__init__.py b/sdk/python/feast/protos/feast/registry/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sdk/python/feast/protos/feast/serving/Connector_pb2.py b/sdk/python/feast/protos/feast/serving/Connector_pb2.py new file mode 100644 index 0000000000..b38471dea8 --- /dev/null +++ b/sdk/python/feast/protos/feast/serving/Connector_pb2.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: feast/serving/Connector.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 +from feast.protos.feast.types import Value_pb2 as feast_dot_types_dot_Value__pb2 +from feast.protos.feast.types import EntityKey_pb2 as feast_dot_types_dot_EntityKey__pb2 +from feast.protos.feast.serving import ServingService_pb2 as feast_dot_serving_dot_ServingService__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1d\x66\x65\x61st/serving/Connector.proto\x12\x0egrpc.connector\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x17\x66\x65\x61st/types/Value.proto\x1a\x1b\x66\x65\x61st/types/EntityKey.proto\x1a\"feast/serving/ServingService.proto\"\x9a\x01\n\x10\x43onnectorFeature\x12\x34\n\treference\x18\x01 \x01(\x0b\x32!.feast.serving.FeatureReferenceV2\x12-\n\ttimestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12!\n\x05value\x18\x03 \x01(\x0b\x32\x12.feast.types.Value\"M\n\x14\x43onnectorFeatureList\x12\x35\n\x0b\x66\x65\x61tureList\x18\x01 \x03(\x0b\x32 .grpc.connector.ConnectorFeature\"_\n\x11OnlineReadRequest\x12*\n\nentityKeys\x18\x01 \x03(\x0b\x32\x16.feast.types.EntityKey\x12\x0c\n\x04view\x18\x02 \x01(\t\x12\x10\n\x08\x66\x65\x61tures\x18\x03 \x03(\t\"K\n\x12OnlineReadResponse\x12\x35\n\x07results\x18\x01 \x03(\x0b\x32$.grpc.connector.ConnectorFeatureList2b\n\x0bOnlineStore\x12S\n\nOnlineRead\x12!.grpc.connector.OnlineReadRequest\x1a\".grpc.connector.OnlineReadResponseB4Z2github.com/feast-dev/feast/go/protos/feast/servingb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'feast.serving.Connector_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'Z2github.com/feast-dev/feast/go/protos/feast/serving' + _globals['_CONNECTORFEATURE']._serialized_start=173 + _globals['_CONNECTORFEATURE']._serialized_end=327 + _globals['_CONNECTORFEATURELIST']._serialized_start=329 + _globals['_CONNECTORFEATURELIST']._serialized_end=406 + _globals['_ONLINEREADREQUEST']._serialized_start=408 + _globals['_ONLINEREADREQUEST']._serialized_end=503 + _globals['_ONLINEREADRESPONSE']._serialized_start=505 + _globals['_ONLINEREADRESPONSE']._serialized_end=580 + _globals['_ONLINESTORE']._serialized_start=582 + _globals['_ONLINESTORE']._serialized_end=680 +# @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/serving/Connector_pb2.pyi b/sdk/python/feast/protos/feast/serving/Connector_pb2.pyi new file mode 100644 index 0000000000..f87109e0fa --- /dev/null +++ b/sdk/python/feast/protos/feast/serving/Connector_pb2.pyi @@ -0,0 +1,97 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" +import builtins +import collections.abc +import feast.serving.ServingService_pb2 +import feast.types.EntityKey_pb2 +import feast.types.Value_pb2 +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.message +import google.protobuf.timestamp_pb2 +import sys + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class ConnectorFeature(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + REFERENCE_FIELD_NUMBER: builtins.int + TIMESTAMP_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + @property + def reference(self) -> feast.serving.ServingService_pb2.FeatureReferenceV2: ... + @property + def timestamp(self) -> google.protobuf.timestamp_pb2.Timestamp: ... + @property + def value(self) -> feast.types.Value_pb2.Value: ... + def __init__( + self, + *, + reference: feast.serving.ServingService_pb2.FeatureReferenceV2 | None = ..., + timestamp: google.protobuf.timestamp_pb2.Timestamp | None = ..., + value: feast.types.Value_pb2.Value | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["reference", b"reference", "timestamp", b"timestamp", "value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["reference", b"reference", "timestamp", b"timestamp", "value", b"value"]) -> None: ... + +global___ConnectorFeature = ConnectorFeature + +class ConnectorFeatureList(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + FEATURELIST_FIELD_NUMBER: builtins.int + @property + def featureList(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ConnectorFeature]: ... + def __init__( + self, + *, + featureList: collections.abc.Iterable[global___ConnectorFeature] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["featureList", b"featureList"]) -> None: ... + +global___ConnectorFeatureList = ConnectorFeatureList + +class OnlineReadRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + ENTITYKEYS_FIELD_NUMBER: builtins.int + VIEW_FIELD_NUMBER: builtins.int + FEATURES_FIELD_NUMBER: builtins.int + @property + def entityKeys(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.types.EntityKey_pb2.EntityKey]: ... + view: builtins.str + @property + def features(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: ... + def __init__( + self, + *, + entityKeys: collections.abc.Iterable[feast.types.EntityKey_pb2.EntityKey] | None = ..., + view: builtins.str = ..., + features: collections.abc.Iterable[builtins.str] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["entityKeys", b"entityKeys", "features", b"features", "view", b"view"]) -> None: ... + +global___OnlineReadRequest = OnlineReadRequest + +class OnlineReadResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + RESULTS_FIELD_NUMBER: builtins.int + @property + def results(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ConnectorFeatureList]: ... + def __init__( + self, + *, + results: collections.abc.Iterable[global___ConnectorFeatureList] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["results", b"results"]) -> None: ... + +global___OnlineReadResponse = OnlineReadResponse diff --git a/sdk/python/feast/protos/feast/serving/Connector_pb2_grpc.py b/sdk/python/feast/protos/feast/serving/Connector_pb2_grpc.py new file mode 100644 index 0000000000..dfadf982dd --- /dev/null +++ b/sdk/python/feast/protos/feast/serving/Connector_pb2_grpc.py @@ -0,0 +1,66 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + +from feast.protos.feast.serving import Connector_pb2 as feast_dot_serving_dot_Connector__pb2 + + +class OnlineStoreStub(object): + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.OnlineRead = channel.unary_unary( + '/grpc.connector.OnlineStore/OnlineRead', + request_serializer=feast_dot_serving_dot_Connector__pb2.OnlineReadRequest.SerializeToString, + response_deserializer=feast_dot_serving_dot_Connector__pb2.OnlineReadResponse.FromString, + ) + + +class OnlineStoreServicer(object): + """Missing associated documentation comment in .proto file.""" + + def OnlineRead(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_OnlineStoreServicer_to_server(servicer, server): + rpc_method_handlers = { + 'OnlineRead': grpc.unary_unary_rpc_method_handler( + servicer.OnlineRead, + request_deserializer=feast_dot_serving_dot_Connector__pb2.OnlineReadRequest.FromString, + response_serializer=feast_dot_serving_dot_Connector__pb2.OnlineReadResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'grpc.connector.OnlineStore', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + + + # This class is part of an EXPERIMENTAL API. +class OnlineStore(object): + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def OnlineRead(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/grpc.connector.OnlineStore/OnlineRead', + feast_dot_serving_dot_Connector__pb2.OnlineReadRequest.SerializeToString, + feast_dot_serving_dot_Connector__pb2.OnlineReadResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/sdk/python/feast/protos/feast/serving/GrpcServer_pb2.py b/sdk/python/feast/protos/feast/serving/GrpcServer_pb2.py new file mode 100644 index 0000000000..8e40630cff --- /dev/null +++ b/sdk/python/feast/protos/feast/serving/GrpcServer_pb2.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: feast/serving/GrpcServer.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from feast.protos.feast.serving import ServingService_pb2 as feast_dot_serving_dot_ServingService__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1e\x66\x65\x61st/serving/GrpcServer.proto\x1a\"feast/serving/ServingService.proto\"\xb3\x01\n\x0bPushRequest\x12,\n\x08\x66\x65\x61tures\x18\x01 \x03(\x0b\x32\x1a.PushRequest.FeaturesEntry\x12\x1b\n\x13stream_feature_view\x18\x02 \x01(\t\x12\x1c\n\x14\x61llow_registry_cache\x18\x03 \x01(\x08\x12\n\n\x02to\x18\x04 \x01(\t\x1a/\n\rFeaturesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x1e\n\x0cPushResponse\x12\x0e\n\x06status\x18\x01 \x01(\x08\"\xc1\x01\n\x19WriteToOnlineStoreRequest\x12:\n\x08\x66\x65\x61tures\x18\x01 \x03(\x0b\x32(.WriteToOnlineStoreRequest.FeaturesEntry\x12\x19\n\x11\x66\x65\x61ture_view_name\x18\x02 \x01(\t\x12\x1c\n\x14\x61llow_registry_cache\x18\x03 \x01(\x08\x1a/\n\rFeaturesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\",\n\x1aWriteToOnlineStoreResponse\x12\x0e\n\x06status\x18\x01 \x01(\x08\x32\xf1\x01\n\x11GrpcFeatureServer\x12%\n\x04Push\x12\x0c.PushRequest\x1a\r.PushResponse\"\x00\x12M\n\x12WriteToOnlineStore\x12\x1a.WriteToOnlineStoreRequest\x1a\x1b.WriteToOnlineStoreResponse\x12\x66\n\x11GetOnlineFeatures\x12\'.feast.serving.GetOnlineFeaturesRequest\x1a(.feast.serving.GetOnlineFeaturesResponseb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'feast.serving.GrpcServer_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_PUSHREQUEST_FEATURESENTRY']._options = None + _globals['_PUSHREQUEST_FEATURESENTRY']._serialized_options = b'8\001' + _globals['_WRITETOONLINESTOREREQUEST_FEATURESENTRY']._options = None + _globals['_WRITETOONLINESTOREREQUEST_FEATURESENTRY']._serialized_options = b'8\001' + _globals['_PUSHREQUEST']._serialized_start=71 + _globals['_PUSHREQUEST']._serialized_end=250 + _globals['_PUSHREQUEST_FEATURESENTRY']._serialized_start=203 + _globals['_PUSHREQUEST_FEATURESENTRY']._serialized_end=250 + _globals['_PUSHRESPONSE']._serialized_start=252 + _globals['_PUSHRESPONSE']._serialized_end=282 + _globals['_WRITETOONLINESTOREREQUEST']._serialized_start=285 + _globals['_WRITETOONLINESTOREREQUEST']._serialized_end=478 + _globals['_WRITETOONLINESTOREREQUEST_FEATURESENTRY']._serialized_start=203 + _globals['_WRITETOONLINESTOREREQUEST_FEATURESENTRY']._serialized_end=250 + _globals['_WRITETOONLINESTORERESPONSE']._serialized_start=480 + _globals['_WRITETOONLINESTORERESPONSE']._serialized_end=524 + _globals['_GRPCFEATURESERVER']._serialized_start=527 + _globals['_GRPCFEATURESERVER']._serialized_end=768 +# @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/serving/GrpcServer_pb2.pyi b/sdk/python/feast/protos/feast/serving/GrpcServer_pb2.pyi new file mode 100644 index 0000000000..54964f46e5 --- /dev/null +++ b/sdk/python/feast/protos/feast/serving/GrpcServer_pb2.pyi @@ -0,0 +1,120 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" +import builtins +import collections.abc +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.message +import sys + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class PushRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class FeaturesEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + FEATURES_FIELD_NUMBER: builtins.int + STREAM_FEATURE_VIEW_FIELD_NUMBER: builtins.int + ALLOW_REGISTRY_CACHE_FIELD_NUMBER: builtins.int + TO_FIELD_NUMBER: builtins.int + @property + def features(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: ... + stream_feature_view: builtins.str + allow_registry_cache: builtins.bool + to: builtins.str + def __init__( + self, + *, + features: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + stream_feature_view: builtins.str = ..., + allow_registry_cache: builtins.bool = ..., + to: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["allow_registry_cache", b"allow_registry_cache", "features", b"features", "stream_feature_view", b"stream_feature_view", "to", b"to"]) -> None: ... + +global___PushRequest = PushRequest + +class PushResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + STATUS_FIELD_NUMBER: builtins.int + status: builtins.bool + def __init__( + self, + *, + status: builtins.bool = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["status", b"status"]) -> None: ... + +global___PushResponse = PushResponse + +class WriteToOnlineStoreRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class FeaturesEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + FEATURES_FIELD_NUMBER: builtins.int + FEATURE_VIEW_NAME_FIELD_NUMBER: builtins.int + ALLOW_REGISTRY_CACHE_FIELD_NUMBER: builtins.int + @property + def features(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: ... + feature_view_name: builtins.str + allow_registry_cache: builtins.bool + def __init__( + self, + *, + features: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + feature_view_name: builtins.str = ..., + allow_registry_cache: builtins.bool = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["allow_registry_cache", b"allow_registry_cache", "feature_view_name", b"feature_view_name", "features", b"features"]) -> None: ... + +global___WriteToOnlineStoreRequest = WriteToOnlineStoreRequest + +class WriteToOnlineStoreResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + STATUS_FIELD_NUMBER: builtins.int + status: builtins.bool + def __init__( + self, + *, + status: builtins.bool = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["status", b"status"]) -> None: ... + +global___WriteToOnlineStoreResponse = WriteToOnlineStoreResponse diff --git a/sdk/python/feast/protos/feast/serving/GrpcServer_pb2_grpc.py b/sdk/python/feast/protos/feast/serving/GrpcServer_pb2_grpc.py new file mode 100644 index 0000000000..b381cc0f41 --- /dev/null +++ b/sdk/python/feast/protos/feast/serving/GrpcServer_pb2_grpc.py @@ -0,0 +1,133 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + +from feast.protos.feast.serving import GrpcServer_pb2 as feast_dot_serving_dot_GrpcServer__pb2 +from feast.protos.feast.serving import ServingService_pb2 as feast_dot_serving_dot_ServingService__pb2 + + +class GrpcFeatureServerStub(object): + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.Push = channel.unary_unary( + '/GrpcFeatureServer/Push', + request_serializer=feast_dot_serving_dot_GrpcServer__pb2.PushRequest.SerializeToString, + response_deserializer=feast_dot_serving_dot_GrpcServer__pb2.PushResponse.FromString, + ) + self.WriteToOnlineStore = channel.unary_unary( + '/GrpcFeatureServer/WriteToOnlineStore', + request_serializer=feast_dot_serving_dot_GrpcServer__pb2.WriteToOnlineStoreRequest.SerializeToString, + response_deserializer=feast_dot_serving_dot_GrpcServer__pb2.WriteToOnlineStoreResponse.FromString, + ) + self.GetOnlineFeatures = channel.unary_unary( + '/GrpcFeatureServer/GetOnlineFeatures', + request_serializer=feast_dot_serving_dot_ServingService__pb2.GetOnlineFeaturesRequest.SerializeToString, + response_deserializer=feast_dot_serving_dot_ServingService__pb2.GetOnlineFeaturesResponse.FromString, + ) + + +class GrpcFeatureServerServicer(object): + """Missing associated documentation comment in .proto file.""" + + def Push(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def WriteToOnlineStore(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetOnlineFeatures(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_GrpcFeatureServerServicer_to_server(servicer, server): + rpc_method_handlers = { + 'Push': grpc.unary_unary_rpc_method_handler( + servicer.Push, + request_deserializer=feast_dot_serving_dot_GrpcServer__pb2.PushRequest.FromString, + response_serializer=feast_dot_serving_dot_GrpcServer__pb2.PushResponse.SerializeToString, + ), + 'WriteToOnlineStore': grpc.unary_unary_rpc_method_handler( + servicer.WriteToOnlineStore, + request_deserializer=feast_dot_serving_dot_GrpcServer__pb2.WriteToOnlineStoreRequest.FromString, + response_serializer=feast_dot_serving_dot_GrpcServer__pb2.WriteToOnlineStoreResponse.SerializeToString, + ), + 'GetOnlineFeatures': grpc.unary_unary_rpc_method_handler( + servicer.GetOnlineFeatures, + request_deserializer=feast_dot_serving_dot_ServingService__pb2.GetOnlineFeaturesRequest.FromString, + response_serializer=feast_dot_serving_dot_ServingService__pb2.GetOnlineFeaturesResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'GrpcFeatureServer', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + + + # This class is part of an EXPERIMENTAL API. +class GrpcFeatureServer(object): + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def Push(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/GrpcFeatureServer/Push', + feast_dot_serving_dot_GrpcServer__pb2.PushRequest.SerializeToString, + feast_dot_serving_dot_GrpcServer__pb2.PushResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def WriteToOnlineStore(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/GrpcFeatureServer/WriteToOnlineStore', + feast_dot_serving_dot_GrpcServer__pb2.WriteToOnlineStoreRequest.SerializeToString, + feast_dot_serving_dot_GrpcServer__pb2.WriteToOnlineStoreResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def GetOnlineFeatures(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/GrpcFeatureServer/GetOnlineFeatures', + feast_dot_serving_dot_ServingService__pb2.GetOnlineFeaturesRequest.SerializeToString, + feast_dot_serving_dot_ServingService__pb2.GetOnlineFeaturesResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/sdk/python/feast/protos/feast/serving/ServingService_pb2.py b/sdk/python/feast/protos/feast/serving/ServingService_pb2.py new file mode 100644 index 0000000000..fa86664057 --- /dev/null +++ b/sdk/python/feast/protos/feast/serving/ServingService_pb2.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: feast/serving/ServingService.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 +from feast.protos.feast.types import Value_pb2 as feast_dot_types_dot_Value__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"feast/serving/ServingService.proto\x12\rfeast.serving\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x17\x66\x65\x61st/types/Value.proto\"\x1c\n\x1aGetFeastServingInfoRequest\".\n\x1bGetFeastServingInfoResponse\x12\x0f\n\x07version\x18\x01 \x01(\t\"E\n\x12\x46\x65\x61tureReferenceV2\x12\x19\n\x11\x66\x65\x61ture_view_name\x18\x01 \x01(\t\x12\x14\n\x0c\x66\x65\x61ture_name\x18\x02 \x01(\t\"\xfd\x02\n\x1aGetOnlineFeaturesRequestV2\x12\x33\n\x08\x66\x65\x61tures\x18\x04 \x03(\x0b\x32!.feast.serving.FeatureReferenceV2\x12H\n\x0b\x65ntity_rows\x18\x02 \x03(\x0b\x32\x33.feast.serving.GetOnlineFeaturesRequestV2.EntityRow\x12\x0f\n\x07project\x18\x05 \x01(\t\x1a\xce\x01\n\tEntityRow\x12-\n\ttimestamp\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12O\n\x06\x66ields\x18\x02 \x03(\x0b\x32?.feast.serving.GetOnlineFeaturesRequestV2.EntityRow.FieldsEntry\x1a\x41\n\x0b\x46ieldsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12!\n\x05value\x18\x02 \x01(\x0b\x32\x12.feast.types.Value:\x02\x38\x01\"\x1a\n\x0b\x46\x65\x61tureList\x12\x0b\n\x03val\x18\x01 \x03(\t\"\xc8\x03\n\x18GetOnlineFeaturesRequest\x12\x19\n\x0f\x66\x65\x61ture_service\x18\x01 \x01(\tH\x00\x12.\n\x08\x66\x65\x61tures\x18\x02 \x01(\x0b\x32\x1a.feast.serving.FeatureListH\x00\x12G\n\x08\x65ntities\x18\x03 \x03(\x0b\x32\x35.feast.serving.GetOnlineFeaturesRequest.EntitiesEntry\x12\x1a\n\x12\x66ull_feature_names\x18\x04 \x01(\x08\x12T\n\x0frequest_context\x18\x05 \x03(\x0b\x32;.feast.serving.GetOnlineFeaturesRequest.RequestContextEntry\x1aK\n\rEntitiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.feast.types.RepeatedValue:\x02\x38\x01\x1aQ\n\x13RequestContextEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.feast.types.RepeatedValue:\x02\x38\x01\x42\x06\n\x04kind\"\xd2\x02\n\x19GetOnlineFeaturesResponse\x12\x42\n\x08metadata\x18\x01 \x01(\x0b\x32\x30.feast.serving.GetOnlineFeaturesResponseMetadata\x12G\n\x07results\x18\x02 \x03(\x0b\x32\x36.feast.serving.GetOnlineFeaturesResponse.FeatureVector\x12\x0e\n\x06status\x18\x03 \x01(\x08\x1a\x97\x01\n\rFeatureVector\x12\"\n\x06values\x18\x01 \x03(\x0b\x32\x12.feast.types.Value\x12,\n\x08statuses\x18\x02 \x03(\x0e\x32\x1a.feast.serving.FieldStatus\x12\x34\n\x10\x65vent_timestamps\x18\x03 \x03(\x0b\x32\x1a.google.protobuf.Timestamp\"V\n!GetOnlineFeaturesResponseMetadata\x12\x31\n\rfeature_names\x18\x01 \x01(\x0b\x32\x1a.feast.serving.FeatureList*[\n\x0b\x46ieldStatus\x12\x0b\n\x07INVALID\x10\x00\x12\x0b\n\x07PRESENT\x10\x01\x12\x0e\n\nNULL_VALUE\x10\x02\x12\r\n\tNOT_FOUND\x10\x03\x12\x13\n\x0fOUTSIDE_MAX_AGE\x10\x04\x32\xe6\x01\n\x0eServingService\x12l\n\x13GetFeastServingInfo\x12).feast.serving.GetFeastServingInfoRequest\x1a*.feast.serving.GetFeastServingInfoResponse\x12\x66\n\x11GetOnlineFeatures\x12\'.feast.serving.GetOnlineFeaturesRequest\x1a(.feast.serving.GetOnlineFeaturesResponseBZ\n\x13\x66\x65\x61st.proto.servingB\x0fServingAPIProtoZ2github.com/feast-dev/feast/go/protos/feast/servingb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'feast.serving.ServingService_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\023feast.proto.servingB\017ServingAPIProtoZ2github.com/feast-dev/feast/go/protos/feast/serving' + _globals['_GETONLINEFEATURESREQUESTV2_ENTITYROW_FIELDSENTRY']._options = None + _globals['_GETONLINEFEATURESREQUESTV2_ENTITYROW_FIELDSENTRY']._serialized_options = b'8\001' + _globals['_GETONLINEFEATURESREQUEST_ENTITIESENTRY']._options = None + _globals['_GETONLINEFEATURESREQUEST_ENTITIESENTRY']._serialized_options = b'8\001' + _globals['_GETONLINEFEATURESREQUEST_REQUESTCONTEXTENTRY']._options = None + _globals['_GETONLINEFEATURESREQUEST_REQUESTCONTEXTENTRY']._serialized_options = b'8\001' + _globals['_FIELDSTATUS']._serialized_start=1560 + _globals['_FIELDSTATUS']._serialized_end=1651 + _globals['_GETFEASTSERVINGINFOREQUEST']._serialized_start=111 + _globals['_GETFEASTSERVINGINFOREQUEST']._serialized_end=139 + _globals['_GETFEASTSERVINGINFORESPONSE']._serialized_start=141 + _globals['_GETFEASTSERVINGINFORESPONSE']._serialized_end=187 + _globals['_FEATUREREFERENCEV2']._serialized_start=189 + _globals['_FEATUREREFERENCEV2']._serialized_end=258 + _globals['_GETONLINEFEATURESREQUESTV2']._serialized_start=261 + _globals['_GETONLINEFEATURESREQUESTV2']._serialized_end=642 + _globals['_GETONLINEFEATURESREQUESTV2_ENTITYROW']._serialized_start=436 + _globals['_GETONLINEFEATURESREQUESTV2_ENTITYROW']._serialized_end=642 + _globals['_GETONLINEFEATURESREQUESTV2_ENTITYROW_FIELDSENTRY']._serialized_start=577 + _globals['_GETONLINEFEATURESREQUESTV2_ENTITYROW_FIELDSENTRY']._serialized_end=642 + _globals['_FEATURELIST']._serialized_start=644 + _globals['_FEATURELIST']._serialized_end=670 + _globals['_GETONLINEFEATURESREQUEST']._serialized_start=673 + _globals['_GETONLINEFEATURESREQUEST']._serialized_end=1129 + _globals['_GETONLINEFEATURESREQUEST_ENTITIESENTRY']._serialized_start=963 + _globals['_GETONLINEFEATURESREQUEST_ENTITIESENTRY']._serialized_end=1038 + _globals['_GETONLINEFEATURESREQUEST_REQUESTCONTEXTENTRY']._serialized_start=1040 + _globals['_GETONLINEFEATURESREQUEST_REQUESTCONTEXTENTRY']._serialized_end=1121 + _globals['_GETONLINEFEATURESRESPONSE']._serialized_start=1132 + _globals['_GETONLINEFEATURESRESPONSE']._serialized_end=1470 + _globals['_GETONLINEFEATURESRESPONSE_FEATUREVECTOR']._serialized_start=1319 + _globals['_GETONLINEFEATURESRESPONSE_FEATUREVECTOR']._serialized_end=1470 + _globals['_GETONLINEFEATURESRESPONSEMETADATA']._serialized_start=1472 + _globals['_GETONLINEFEATURESRESPONSEMETADATA']._serialized_end=1558 + _globals['_SERVINGSERVICE']._serialized_start=1654 + _globals['_SERVINGSERVICE']._serialized_end=1884 +# @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/serving/ServingService_pb2.pyi b/sdk/python/feast/protos/feast/serving/ServingService_pb2.pyi new file mode 100644 index 0000000000..3c5e57ae45 --- /dev/null +++ b/sdk/python/feast/protos/feast/serving/ServingService_pb2.pyi @@ -0,0 +1,347 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file + +Copyright 2018 The Feast 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 + + https://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 builtins +import collections.abc +import feast.types.Value_pb2 +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.internal.enum_type_wrapper +import google.protobuf.message +import google.protobuf.timestamp_pb2 +import sys +import typing + +if sys.version_info >= (3, 10): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class _FieldStatus: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _FieldStatusEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_FieldStatus.ValueType], builtins.type): # noqa: F821 + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + INVALID: _FieldStatus.ValueType # 0 + """Status is unset for this field.""" + PRESENT: _FieldStatus.ValueType # 1 + """Field value is present for this field and age is within max age.""" + NULL_VALUE: _FieldStatus.ValueType # 2 + """Values could be found for entity key and age is within max age, but + this field value is not assigned a value on ingestion into feast. + """ + NOT_FOUND: _FieldStatus.ValueType # 3 + """Entity key did not return any values as they do not exist in Feast. + This could suggest that the feature values have not yet been ingested + into feast or the ingestion failed. + """ + OUTSIDE_MAX_AGE: _FieldStatus.ValueType # 4 + """Values could be found for entity key, but field values are outside the maximum + allowable range. + """ + +class FieldStatus(_FieldStatus, metaclass=_FieldStatusEnumTypeWrapper): ... + +INVALID: FieldStatus.ValueType # 0 +"""Status is unset for this field.""" +PRESENT: FieldStatus.ValueType # 1 +"""Field value is present for this field and age is within max age.""" +NULL_VALUE: FieldStatus.ValueType # 2 +"""Values could be found for entity key and age is within max age, but +this field value is not assigned a value on ingestion into feast. +""" +NOT_FOUND: FieldStatus.ValueType # 3 +"""Entity key did not return any values as they do not exist in Feast. +This could suggest that the feature values have not yet been ingested +into feast or the ingestion failed. +""" +OUTSIDE_MAX_AGE: FieldStatus.ValueType # 4 +"""Values could be found for entity key, but field values are outside the maximum +allowable range. +""" +global___FieldStatus = FieldStatus + +class GetFeastServingInfoRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + +global___GetFeastServingInfoRequest = GetFeastServingInfoRequest + +class GetFeastServingInfoResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + VERSION_FIELD_NUMBER: builtins.int + version: builtins.str + """Feast version of this serving deployment.""" + def __init__( + self, + *, + version: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["version", b"version"]) -> None: ... + +global___GetFeastServingInfoResponse = GetFeastServingInfoResponse + +class FeatureReferenceV2(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + FEATURE_VIEW_NAME_FIELD_NUMBER: builtins.int + FEATURE_NAME_FIELD_NUMBER: builtins.int + feature_view_name: builtins.str + """Name of the Feature View to retrieve the feature from.""" + feature_name: builtins.str + """Name of the Feature to retrieve the feature from.""" + def __init__( + self, + *, + feature_view_name: builtins.str = ..., + feature_name: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["feature_name", b"feature_name", "feature_view_name", b"feature_view_name"]) -> None: ... + +global___FeatureReferenceV2 = FeatureReferenceV2 + +class GetOnlineFeaturesRequestV2(google.protobuf.message.Message): + """ToDo (oleksii): remove this message (since it's not used) and move EntityRow on package level""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class EntityRow(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class FieldsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> feast.types.Value_pb2.Value: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: feast.types.Value_pb2.Value | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + TIMESTAMP_FIELD_NUMBER: builtins.int + FIELDS_FIELD_NUMBER: builtins.int + @property + def timestamp(self) -> google.protobuf.timestamp_pb2.Timestamp: + """Request timestamp of this row. This value will be used, + together with maxAge, to determine feature staleness. + """ + @property + def fields(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, feast.types.Value_pb2.Value]: + """Map containing mapping of entity name to entity value.""" + def __init__( + self, + *, + timestamp: google.protobuf.timestamp_pb2.Timestamp | None = ..., + fields: collections.abc.Mapping[builtins.str, feast.types.Value_pb2.Value] | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["timestamp", b"timestamp"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["fields", b"fields", "timestamp", b"timestamp"]) -> None: ... + + FEATURES_FIELD_NUMBER: builtins.int + ENTITY_ROWS_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + @property + def features(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___FeatureReferenceV2]: + """List of features that are being retrieved""" + @property + def entity_rows(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___GetOnlineFeaturesRequestV2.EntityRow]: + """List of entity rows, containing entity id and timestamp data. + Used during retrieval of feature rows and for joining feature + rows into a final dataset + """ + project: builtins.str + """Optional field to specify project name override. If specified, uses the + given project for retrieval. Overrides the projects specified in + Feature References if both are specified. + """ + def __init__( + self, + *, + features: collections.abc.Iterable[global___FeatureReferenceV2] | None = ..., + entity_rows: collections.abc.Iterable[global___GetOnlineFeaturesRequestV2.EntityRow] | None = ..., + project: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["entity_rows", b"entity_rows", "features", b"features", "project", b"project"]) -> None: ... + +global___GetOnlineFeaturesRequestV2 = GetOnlineFeaturesRequestV2 + +class FeatureList(google.protobuf.message.Message): + """In JSON "val" field can be omitted""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + VAL_FIELD_NUMBER: builtins.int + @property + def val(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: ... + def __init__( + self, + *, + val: collections.abc.Iterable[builtins.str] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["val", b"val"]) -> None: ... + +global___FeatureList = FeatureList + +class GetOnlineFeaturesRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class EntitiesEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> feast.types.Value_pb2.RepeatedValue: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: feast.types.Value_pb2.RepeatedValue | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + class RequestContextEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> feast.types.Value_pb2.RepeatedValue: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: feast.types.Value_pb2.RepeatedValue | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + FEATURE_SERVICE_FIELD_NUMBER: builtins.int + FEATURES_FIELD_NUMBER: builtins.int + ENTITIES_FIELD_NUMBER: builtins.int + FULL_FEATURE_NAMES_FIELD_NUMBER: builtins.int + REQUEST_CONTEXT_FIELD_NUMBER: builtins.int + feature_service: builtins.str + @property + def features(self) -> global___FeatureList: ... + @property + def entities(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, feast.types.Value_pb2.RepeatedValue]: + """The entity data is specified in a columnar format + A map of entity name -> list of values + """ + full_feature_names: builtins.bool + @property + def request_context(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, feast.types.Value_pb2.RepeatedValue]: + """Context for OnDemand Feature Transformation + (was moved to dedicated parameter to avoid unnecessary separation logic on serving side) + A map of variable name -> list of values + """ + def __init__( + self, + *, + feature_service: builtins.str = ..., + features: global___FeatureList | None = ..., + entities: collections.abc.Mapping[builtins.str, feast.types.Value_pb2.RepeatedValue] | None = ..., + full_feature_names: builtins.bool = ..., + request_context: collections.abc.Mapping[builtins.str, feast.types.Value_pb2.RepeatedValue] | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["feature_service", b"feature_service", "features", b"features", "kind", b"kind"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["entities", b"entities", "feature_service", b"feature_service", "features", b"features", "full_feature_names", b"full_feature_names", "kind", b"kind", "request_context", b"request_context"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal["kind", b"kind"]) -> typing_extensions.Literal["feature_service", "features"] | None: ... + +global___GetOnlineFeaturesRequest = GetOnlineFeaturesRequest + +class GetOnlineFeaturesResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class FeatureVector(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + VALUES_FIELD_NUMBER: builtins.int + STATUSES_FIELD_NUMBER: builtins.int + EVENT_TIMESTAMPS_FIELD_NUMBER: builtins.int + @property + def values(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.types.Value_pb2.Value]: ... + @property + def statuses(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[global___FieldStatus.ValueType]: ... + @property + def event_timestamps(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[google.protobuf.timestamp_pb2.Timestamp]: ... + def __init__( + self, + *, + values: collections.abc.Iterable[feast.types.Value_pb2.Value] | None = ..., + statuses: collections.abc.Iterable[global___FieldStatus.ValueType] | None = ..., + event_timestamps: collections.abc.Iterable[google.protobuf.timestamp_pb2.Timestamp] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["event_timestamps", b"event_timestamps", "statuses", b"statuses", "values", b"values"]) -> None: ... + + METADATA_FIELD_NUMBER: builtins.int + RESULTS_FIELD_NUMBER: builtins.int + STATUS_FIELD_NUMBER: builtins.int + @property + def metadata(self) -> global___GetOnlineFeaturesResponseMetadata: ... + @property + def results(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___GetOnlineFeaturesResponse.FeatureVector]: + """Length of "results" array should match length of requested features. + We also preserve the same order of features here as in metadata.feature_names + """ + status: builtins.bool + def __init__( + self, + *, + metadata: global___GetOnlineFeaturesResponseMetadata | None = ..., + results: collections.abc.Iterable[global___GetOnlineFeaturesResponse.FeatureVector] | None = ..., + status: builtins.bool = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["metadata", b"metadata"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["metadata", b"metadata", "results", b"results", "status", b"status"]) -> None: ... + +global___GetOnlineFeaturesResponse = GetOnlineFeaturesResponse + +class GetOnlineFeaturesResponseMetadata(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + FEATURE_NAMES_FIELD_NUMBER: builtins.int + @property + def feature_names(self) -> global___FeatureList: ... + def __init__( + self, + *, + feature_names: global___FeatureList | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["feature_names", b"feature_names"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["feature_names", b"feature_names"]) -> None: ... + +global___GetOnlineFeaturesResponseMetadata = GetOnlineFeaturesResponseMetadata diff --git a/sdk/python/feast/protos/feast/serving/ServingService_pb2_grpc.py b/sdk/python/feast/protos/feast/serving/ServingService_pb2_grpc.py new file mode 100644 index 0000000000..d3cd055f66 --- /dev/null +++ b/sdk/python/feast/protos/feast/serving/ServingService_pb2_grpc.py @@ -0,0 +1,101 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + +from feast.protos.feast.serving import ServingService_pb2 as feast_dot_serving_dot_ServingService__pb2 + + +class ServingServiceStub(object): + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.GetFeastServingInfo = channel.unary_unary( + '/feast.serving.ServingService/GetFeastServingInfo', + request_serializer=feast_dot_serving_dot_ServingService__pb2.GetFeastServingInfoRequest.SerializeToString, + response_deserializer=feast_dot_serving_dot_ServingService__pb2.GetFeastServingInfoResponse.FromString, + ) + self.GetOnlineFeatures = channel.unary_unary( + '/feast.serving.ServingService/GetOnlineFeatures', + request_serializer=feast_dot_serving_dot_ServingService__pb2.GetOnlineFeaturesRequest.SerializeToString, + response_deserializer=feast_dot_serving_dot_ServingService__pb2.GetOnlineFeaturesResponse.FromString, + ) + + +class ServingServiceServicer(object): + """Missing associated documentation comment in .proto file.""" + + def GetFeastServingInfo(self, request, context): + """Get information about this Feast serving. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetOnlineFeatures(self, request, context): + """Get online features synchronously. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_ServingServiceServicer_to_server(servicer, server): + rpc_method_handlers = { + 'GetFeastServingInfo': grpc.unary_unary_rpc_method_handler( + servicer.GetFeastServingInfo, + request_deserializer=feast_dot_serving_dot_ServingService__pb2.GetFeastServingInfoRequest.FromString, + response_serializer=feast_dot_serving_dot_ServingService__pb2.GetFeastServingInfoResponse.SerializeToString, + ), + 'GetOnlineFeatures': grpc.unary_unary_rpc_method_handler( + servicer.GetOnlineFeatures, + request_deserializer=feast_dot_serving_dot_ServingService__pb2.GetOnlineFeaturesRequest.FromString, + response_serializer=feast_dot_serving_dot_ServingService__pb2.GetOnlineFeaturesResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'feast.serving.ServingService', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + + + # This class is part of an EXPERIMENTAL API. +class ServingService(object): + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def GetFeastServingInfo(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.serving.ServingService/GetFeastServingInfo', + feast_dot_serving_dot_ServingService__pb2.GetFeastServingInfoRequest.SerializeToString, + feast_dot_serving_dot_ServingService__pb2.GetFeastServingInfoResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def GetOnlineFeatures(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.serving.ServingService/GetOnlineFeatures', + feast_dot_serving_dot_ServingService__pb2.GetOnlineFeaturesRequest.SerializeToString, + feast_dot_serving_dot_ServingService__pb2.GetOnlineFeaturesResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/sdk/python/feast/protos/feast/serving/TransformationService_pb2.py b/sdk/python/feast/protos/feast/serving/TransformationService_pb2.py new file mode 100644 index 0000000000..bc060e9a77 --- /dev/null +++ b/sdk/python/feast/protos/feast/serving/TransformationService_pb2.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: feast/serving/TransformationService.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n)feast/serving/TransformationService.proto\x12\rfeast.serving\"+\n\tValueType\x12\x15\n\x0b\x61rrow_value\x18\x01 \x01(\x0cH\x00\x42\x07\n\x05value\"%\n#GetTransformationServiceInfoRequest\"\x9c\x01\n$GetTransformationServiceInfoResponse\x12\x0f\n\x07version\x18\x01 \x01(\t\x12\x36\n\x04type\x18\x02 \x01(\x0e\x32(.feast.serving.TransformationServiceType\x12+\n#transformation_service_type_details\x18\x03 \x01(\t\"\x88\x01\n\x18TransformFeaturesRequest\x12#\n\x1bon_demand_feature_view_name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x36\n\x14transformation_input\x18\x03 \x01(\x0b\x32\x18.feast.serving.ValueType\"T\n\x19TransformFeaturesResponse\x12\x37\n\x15transformation_output\x18\x03 \x01(\x0b\x32\x18.feast.serving.ValueType*\x94\x01\n\x19TransformationServiceType\x12\'\n#TRANSFORMATION_SERVICE_TYPE_INVALID\x10\x00\x12&\n\"TRANSFORMATION_SERVICE_TYPE_PYTHON\x10\x01\x12&\n\"TRANSFORMATION_SERVICE_TYPE_CUSTOM\x10\x64\x32\x89\x02\n\x15TransformationService\x12\x87\x01\n\x1cGetTransformationServiceInfo\x12\x32.feast.serving.GetTransformationServiceInfoRequest\x1a\x33.feast.serving.GetTransformationServiceInfoResponse\x12\x66\n\x11TransformFeatures\x12\'.feast.serving.TransformFeaturesRequest\x1a(.feast.serving.TransformFeaturesResponseBh\n\x13\x66\x65\x61st.proto.servingB\x1dTransformationServiceAPIProtoZ2github.com/feast-dev/feast/go/protos/feast/servingb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'feast.serving.TransformationService_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\023feast.proto.servingB\035TransformationServiceAPIProtoZ2github.com/feast-dev/feast/go/protos/feast/serving' + _globals['_TRANSFORMATIONSERVICETYPE']._serialized_start=529 + _globals['_TRANSFORMATIONSERVICETYPE']._serialized_end=677 + _globals['_VALUETYPE']._serialized_start=60 + _globals['_VALUETYPE']._serialized_end=103 + _globals['_GETTRANSFORMATIONSERVICEINFOREQUEST']._serialized_start=105 + _globals['_GETTRANSFORMATIONSERVICEINFOREQUEST']._serialized_end=142 + _globals['_GETTRANSFORMATIONSERVICEINFORESPONSE']._serialized_start=145 + _globals['_GETTRANSFORMATIONSERVICEINFORESPONSE']._serialized_end=301 + _globals['_TRANSFORMFEATURESREQUEST']._serialized_start=304 + _globals['_TRANSFORMFEATURESREQUEST']._serialized_end=440 + _globals['_TRANSFORMFEATURESRESPONSE']._serialized_start=442 + _globals['_TRANSFORMFEATURESRESPONSE']._serialized_end=526 + _globals['_TRANSFORMATIONSERVICE']._serialized_start=680 + _globals['_TRANSFORMATIONSERVICE']._serialized_end=945 +# @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/serving/TransformationService_pb2.pyi b/sdk/python/feast/protos/feast/serving/TransformationService_pb2.pyi new file mode 100644 index 0000000000..3e0752b7bd --- /dev/null +++ b/sdk/python/feast/protos/feast/serving/TransformationService_pb2.pyi @@ -0,0 +1,136 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file + +Copyright 2021 The Feast 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 + + https://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 builtins +import google.protobuf.descriptor +import google.protobuf.internal.enum_type_wrapper +import google.protobuf.message +import sys +import typing + +if sys.version_info >= (3, 10): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class _TransformationServiceType: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _TransformationServiceTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_TransformationServiceType.ValueType], builtins.type): # noqa: F821 + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + TRANSFORMATION_SERVICE_TYPE_INVALID: _TransformationServiceType.ValueType # 0 + TRANSFORMATION_SERVICE_TYPE_PYTHON: _TransformationServiceType.ValueType # 1 + TRANSFORMATION_SERVICE_TYPE_CUSTOM: _TransformationServiceType.ValueType # 100 + +class TransformationServiceType(_TransformationServiceType, metaclass=_TransformationServiceTypeEnumTypeWrapper): ... + +TRANSFORMATION_SERVICE_TYPE_INVALID: TransformationServiceType.ValueType # 0 +TRANSFORMATION_SERVICE_TYPE_PYTHON: TransformationServiceType.ValueType # 1 +TRANSFORMATION_SERVICE_TYPE_CUSTOM: TransformationServiceType.ValueType # 100 +global___TransformationServiceType = TransformationServiceType + +class ValueType(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + ARROW_VALUE_FIELD_NUMBER: builtins.int + arrow_value: builtins.bytes + """Having a oneOf provides forward compatibility if we need to support compound types + that are not supported by arrow natively. + """ + def __init__( + self, + *, + arrow_value: builtins.bytes = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["arrow_value", b"arrow_value", "value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["arrow_value", b"arrow_value", "value", b"value"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal["value", b"value"]) -> typing_extensions.Literal["arrow_value"] | None: ... + +global___ValueType = ValueType + +class GetTransformationServiceInfoRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + +global___GetTransformationServiceInfoRequest = GetTransformationServiceInfoRequest + +class GetTransformationServiceInfoResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + VERSION_FIELD_NUMBER: builtins.int + TYPE_FIELD_NUMBER: builtins.int + TRANSFORMATION_SERVICE_TYPE_DETAILS_FIELD_NUMBER: builtins.int + version: builtins.str + """Feast version of this transformation service deployment.""" + type: global___TransformationServiceType.ValueType + """Type of transformation service deployment. This is either Python, or custom""" + transformation_service_type_details: builtins.str + def __init__( + self, + *, + version: builtins.str = ..., + type: global___TransformationServiceType.ValueType = ..., + transformation_service_type_details: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["transformation_service_type_details", b"transformation_service_type_details", "type", b"type", "version", b"version"]) -> None: ... + +global___GetTransformationServiceInfoResponse = GetTransformationServiceInfoResponse + +class TransformFeaturesRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + ON_DEMAND_FEATURE_VIEW_NAME_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + TRANSFORMATION_INPUT_FIELD_NUMBER: builtins.int + on_demand_feature_view_name: builtins.str + project: builtins.str + @property + def transformation_input(self) -> global___ValueType: ... + def __init__( + self, + *, + on_demand_feature_view_name: builtins.str = ..., + project: builtins.str = ..., + transformation_input: global___ValueType | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["transformation_input", b"transformation_input"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["on_demand_feature_view_name", b"on_demand_feature_view_name", "project", b"project", "transformation_input", b"transformation_input"]) -> None: ... + +global___TransformFeaturesRequest = TransformFeaturesRequest + +class TransformFeaturesResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + TRANSFORMATION_OUTPUT_FIELD_NUMBER: builtins.int + @property + def transformation_output(self) -> global___ValueType: ... + def __init__( + self, + *, + transformation_output: global___ValueType | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["transformation_output", b"transformation_output"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["transformation_output", b"transformation_output"]) -> None: ... + +global___TransformFeaturesResponse = TransformFeaturesResponse diff --git a/sdk/python/feast/protos/feast/serving/TransformationService_pb2_grpc.py b/sdk/python/feast/protos/feast/serving/TransformationService_pb2_grpc.py new file mode 100644 index 0000000000..30099e39ca --- /dev/null +++ b/sdk/python/feast/protos/feast/serving/TransformationService_pb2_grpc.py @@ -0,0 +1,99 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + +from feast.protos.feast.serving import TransformationService_pb2 as feast_dot_serving_dot_TransformationService__pb2 + + +class TransformationServiceStub(object): + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.GetTransformationServiceInfo = channel.unary_unary( + '/feast.serving.TransformationService/GetTransformationServiceInfo', + request_serializer=feast_dot_serving_dot_TransformationService__pb2.GetTransformationServiceInfoRequest.SerializeToString, + response_deserializer=feast_dot_serving_dot_TransformationService__pb2.GetTransformationServiceInfoResponse.FromString, + ) + self.TransformFeatures = channel.unary_unary( + '/feast.serving.TransformationService/TransformFeatures', + request_serializer=feast_dot_serving_dot_TransformationService__pb2.TransformFeaturesRequest.SerializeToString, + response_deserializer=feast_dot_serving_dot_TransformationService__pb2.TransformFeaturesResponse.FromString, + ) + + +class TransformationServiceServicer(object): + """Missing associated documentation comment in .proto file.""" + + def GetTransformationServiceInfo(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def TransformFeatures(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_TransformationServiceServicer_to_server(servicer, server): + rpc_method_handlers = { + 'GetTransformationServiceInfo': grpc.unary_unary_rpc_method_handler( + servicer.GetTransformationServiceInfo, + request_deserializer=feast_dot_serving_dot_TransformationService__pb2.GetTransformationServiceInfoRequest.FromString, + response_serializer=feast_dot_serving_dot_TransformationService__pb2.GetTransformationServiceInfoResponse.SerializeToString, + ), + 'TransformFeatures': grpc.unary_unary_rpc_method_handler( + servicer.TransformFeatures, + request_deserializer=feast_dot_serving_dot_TransformationService__pb2.TransformFeaturesRequest.FromString, + response_serializer=feast_dot_serving_dot_TransformationService__pb2.TransformFeaturesResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'feast.serving.TransformationService', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + + + # This class is part of an EXPERIMENTAL API. +class TransformationService(object): + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def GetTransformationServiceInfo(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.serving.TransformationService/GetTransformationServiceInfo', + feast_dot_serving_dot_TransformationService__pb2.GetTransformationServiceInfoRequest.SerializeToString, + feast_dot_serving_dot_TransformationService__pb2.GetTransformationServiceInfoResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def TransformFeatures(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.serving.TransformationService/TransformFeatures', + feast_dot_serving_dot_TransformationService__pb2.TransformFeaturesRequest.SerializeToString, + feast_dot_serving_dot_TransformationService__pb2.TransformFeaturesResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/sdk/python/feast/protos/feast/serving/__init__.py b/sdk/python/feast/protos/feast/serving/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sdk/python/feast/protos/feast/storage/Redis_pb2.py b/sdk/python/feast/protos/feast/storage/Redis_pb2.py new file mode 100644 index 0000000000..37d59c9df5 --- /dev/null +++ b/sdk/python/feast/protos/feast/storage/Redis_pb2.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: feast/storage/Redis.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from feast.protos.feast.types import Value_pb2 as feast_dot_types_dot_Value__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x19\x66\x65\x61st/storage/Redis.proto\x12\rfeast.storage\x1a\x17\x66\x65\x61st/types/Value.proto\"^\n\nRedisKeyV2\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x14\n\x0c\x65ntity_names\x18\x02 \x03(\t\x12)\n\rentity_values\x18\x03 \x03(\x0b\x32\x12.feast.types.ValueBU\n\x13\x66\x65\x61st.proto.storageB\nRedisProtoZ2github.com/feast-dev/feast/go/protos/feast/storageb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'feast.storage.Redis_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\023feast.proto.storageB\nRedisProtoZ2github.com/feast-dev/feast/go/protos/feast/storage' + _globals['_REDISKEYV2']._serialized_start=69 + _globals['_REDISKEYV2']._serialized_end=163 +# @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/storage/Redis_pb2.pyi b/sdk/python/feast/protos/feast/storage/Redis_pb2.pyi new file mode 100644 index 0000000000..74cc2b07f0 --- /dev/null +++ b/sdk/python/feast/protos/feast/storage/Redis_pb2.pyi @@ -0,0 +1,54 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file + +Copyright 2019 The Feast 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 + + https://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 builtins +import collections.abc +import feast.types.Value_pb2 +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.message +import sys + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class RedisKeyV2(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + PROJECT_FIELD_NUMBER: builtins.int + ENTITY_NAMES_FIELD_NUMBER: builtins.int + ENTITY_VALUES_FIELD_NUMBER: builtins.int + project: builtins.str + @property + def entity_names(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: ... + @property + def entity_values(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.types.Value_pb2.Value]: ... + def __init__( + self, + *, + project: builtins.str = ..., + entity_names: collections.abc.Iterable[builtins.str] | None = ..., + entity_values: collections.abc.Iterable[feast.types.Value_pb2.Value] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["entity_names", b"entity_names", "entity_values", b"entity_values", "project", b"project"]) -> None: ... + +global___RedisKeyV2 = RedisKeyV2 diff --git a/sdk/python/feast/protos/feast/storage/Redis_pb2_grpc.py b/sdk/python/feast/protos/feast/storage/Redis_pb2_grpc.py new file mode 100644 index 0000000000..2daafffebf --- /dev/null +++ b/sdk/python/feast/protos/feast/storage/Redis_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + diff --git a/sdk/python/feast/protos/feast/storage/__init__.py b/sdk/python/feast/protos/feast/storage/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sdk/python/feast/protos/feast/types/EntityKey_pb2.py b/sdk/python/feast/protos/feast/types/EntityKey_pb2.py new file mode 100644 index 0000000000..a6e1abf730 --- /dev/null +++ b/sdk/python/feast/protos/feast/types/EntityKey_pb2.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: feast/types/EntityKey.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from feast.protos.feast.types import Value_pb2 as feast_dot_types_dot_Value__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1b\x66\x65\x61st/types/EntityKey.proto\x12\x0b\x66\x65\x61st.types\x1a\x17\x66\x65\x61st/types/Value.proto\"I\n\tEntityKey\x12\x11\n\tjoin_keys\x18\x01 \x03(\t\x12)\n\rentity_values\x18\x02 \x03(\x0b\x32\x12.feast.types.ValueBU\n\x11\x66\x65\x61st.proto.typesB\x0e\x45ntityKeyProtoZ0github.com/feast-dev/feast/go/protos/feast/typesb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'feast.types.EntityKey_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\021feast.proto.typesB\016EntityKeyProtoZ0github.com/feast-dev/feast/go/protos/feast/types' + _globals['_ENTITYKEY']._serialized_start=69 + _globals['_ENTITYKEY']._serialized_end=142 +# @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/types/EntityKey_pb2.pyi b/sdk/python/feast/protos/feast/types/EntityKey_pb2.pyi new file mode 100644 index 0000000000..fe65e0c1b3 --- /dev/null +++ b/sdk/python/feast/protos/feast/types/EntityKey_pb2.pyi @@ -0,0 +1,51 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file + +Copyright 2018 The Feast 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 + + https://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 builtins +import collections.abc +import feast.types.Value_pb2 +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.message +import sys + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class EntityKey(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + JOIN_KEYS_FIELD_NUMBER: builtins.int + ENTITY_VALUES_FIELD_NUMBER: builtins.int + @property + def join_keys(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: ... + @property + def entity_values(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.types.Value_pb2.Value]: ... + def __init__( + self, + *, + join_keys: collections.abc.Iterable[builtins.str] | None = ..., + entity_values: collections.abc.Iterable[feast.types.Value_pb2.Value] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["entity_values", b"entity_values", "join_keys", b"join_keys"]) -> None: ... + +global___EntityKey = EntityKey diff --git a/sdk/python/feast/protos/feast/types/EntityKey_pb2_grpc.py b/sdk/python/feast/protos/feast/types/EntityKey_pb2_grpc.py new file mode 100644 index 0000000000..2daafffebf --- /dev/null +++ b/sdk/python/feast/protos/feast/types/EntityKey_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + diff --git a/sdk/python/feast/protos/feast/types/Field_pb2.py b/sdk/python/feast/protos/feast/types/Field_pb2.py new file mode 100644 index 0000000000..973fdc6cde --- /dev/null +++ b/sdk/python/feast/protos/feast/types/Field_pb2.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: feast/types/Field.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from feast.protos.feast.types import Value_pb2 as feast_dot_types_dot_Value__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17\x66\x65\x61st/types/Field.proto\x12\x0b\x66\x65\x61st.types\x1a\x17\x66\x65\x61st/types/Value.proto\"\xaf\x01\n\x05\x46ield\x12\x0c\n\x04name\x18\x01 \x01(\t\x12*\n\x05value\x18\x02 \x01(\x0e\x32\x1b.feast.types.ValueType.Enum\x12*\n\x04tags\x18\x03 \x03(\x0b\x32\x1c.feast.types.Field.TagsEntry\x12\x13\n\x0b\x64\x65scription\x18\x04 \x01(\t\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42Q\n\x11\x66\x65\x61st.proto.typesB\nFieldProtoZ0github.com/feast-dev/feast/go/protos/feast/typesb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'feast.types.Field_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\021feast.proto.typesB\nFieldProtoZ0github.com/feast-dev/feast/go/protos/feast/types' + _globals['_FIELD_TAGSENTRY']._options = None + _globals['_FIELD_TAGSENTRY']._serialized_options = b'8\001' + _globals['_FIELD']._serialized_start=66 + _globals['_FIELD']._serialized_end=241 + _globals['_FIELD_TAGSENTRY']._serialized_start=198 + _globals['_FIELD_TAGSENTRY']._serialized_end=241 +# @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/types/Field_pb2.pyi b/sdk/python/feast/protos/feast/types/Field_pb2.pyi new file mode 100644 index 0000000000..28a2194237 --- /dev/null +++ b/sdk/python/feast/protos/feast/types/Field_pb2.pyi @@ -0,0 +1,73 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file + +Copyright 2018 The Feast 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 + + https://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 builtins +import collections.abc +import feast.types.Value_pb2 +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.message +import sys + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class Field(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class TagsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + NAME_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + TAGS_FIELD_NUMBER: builtins.int + DESCRIPTION_FIELD_NUMBER: builtins.int + name: builtins.str + value: feast.types.Value_pb2.ValueType.Enum.ValueType + @property + def tags(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: + """Tags for user defined metadata on a field""" + description: builtins.str + """Description of the field.""" + def __init__( + self, + *, + name: builtins.str = ..., + value: feast.types.Value_pb2.ValueType.Enum.ValueType = ..., + tags: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + description: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["description", b"description", "name", b"name", "tags", b"tags", "value", b"value"]) -> None: ... + +global___Field = Field diff --git a/sdk/python/feast/protos/feast/types/Field_pb2_grpc.py b/sdk/python/feast/protos/feast/types/Field_pb2_grpc.py new file mode 100644 index 0000000000..2daafffebf --- /dev/null +++ b/sdk/python/feast/protos/feast/types/Field_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + diff --git a/sdk/python/feast/protos/feast/types/Value_pb2.py b/sdk/python/feast/protos/feast/types/Value_pb2.py new file mode 100644 index 0000000000..18ee331180 --- /dev/null +++ b/sdk/python/feast/protos/feast/types/Value_pb2.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: feast/types/Value.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17\x66\x65\x61st/types/Value.proto\x12\x0b\x66\x65\x61st.types\"\x97\x02\n\tValueType\"\x89\x02\n\x04\x45num\x12\x0b\n\x07INVALID\x10\x00\x12\t\n\x05\x42YTES\x10\x01\x12\n\n\x06STRING\x10\x02\x12\t\n\x05INT32\x10\x03\x12\t\n\x05INT64\x10\x04\x12\n\n\x06\x44OUBLE\x10\x05\x12\t\n\x05\x46LOAT\x10\x06\x12\x08\n\x04\x42OOL\x10\x07\x12\x12\n\x0eUNIX_TIMESTAMP\x10\x08\x12\x0e\n\nBYTES_LIST\x10\x0b\x12\x0f\n\x0bSTRING_LIST\x10\x0c\x12\x0e\n\nINT32_LIST\x10\r\x12\x0e\n\nINT64_LIST\x10\x0e\x12\x0f\n\x0b\x44OUBLE_LIST\x10\x0f\x12\x0e\n\nFLOAT_LIST\x10\x10\x12\r\n\tBOOL_LIST\x10\x11\x12\x17\n\x13UNIX_TIMESTAMP_LIST\x10\x12\x12\x08\n\x04NULL\x10\x13\"\x82\x05\n\x05Value\x12\x13\n\tbytes_val\x18\x01 \x01(\x0cH\x00\x12\x14\n\nstring_val\x18\x02 \x01(\tH\x00\x12\x13\n\tint32_val\x18\x03 \x01(\x05H\x00\x12\x13\n\tint64_val\x18\x04 \x01(\x03H\x00\x12\x14\n\ndouble_val\x18\x05 \x01(\x01H\x00\x12\x13\n\tfloat_val\x18\x06 \x01(\x02H\x00\x12\x12\n\x08\x62ool_val\x18\x07 \x01(\x08H\x00\x12\x1c\n\x12unix_timestamp_val\x18\x08 \x01(\x03H\x00\x12\x30\n\x0e\x62ytes_list_val\x18\x0b \x01(\x0b\x32\x16.feast.types.BytesListH\x00\x12\x32\n\x0fstring_list_val\x18\x0c \x01(\x0b\x32\x17.feast.types.StringListH\x00\x12\x30\n\x0eint32_list_val\x18\r \x01(\x0b\x32\x16.feast.types.Int32ListH\x00\x12\x30\n\x0eint64_list_val\x18\x0e \x01(\x0b\x32\x16.feast.types.Int64ListH\x00\x12\x32\n\x0f\x64ouble_list_val\x18\x0f \x01(\x0b\x32\x17.feast.types.DoubleListH\x00\x12\x30\n\x0e\x66loat_list_val\x18\x10 \x01(\x0b\x32\x16.feast.types.FloatListH\x00\x12.\n\rbool_list_val\x18\x11 \x01(\x0b\x32\x15.feast.types.BoolListH\x00\x12\x39\n\x17unix_timestamp_list_val\x18\x12 \x01(\x0b\x32\x16.feast.types.Int64ListH\x00\x12%\n\x08null_val\x18\x13 \x01(\x0e\x32\x11.feast.types.NullH\x00\x42\x05\n\x03val\"\x18\n\tBytesList\x12\x0b\n\x03val\x18\x01 \x03(\x0c\"\x19\n\nStringList\x12\x0b\n\x03val\x18\x01 \x03(\t\"\x18\n\tInt32List\x12\x0b\n\x03val\x18\x01 \x03(\x05\"\x18\n\tInt64List\x12\x0b\n\x03val\x18\x01 \x03(\x03\"\x19\n\nDoubleList\x12\x0b\n\x03val\x18\x01 \x03(\x01\"\x18\n\tFloatList\x12\x0b\n\x03val\x18\x01 \x03(\x02\"\x17\n\x08\x42oolList\x12\x0b\n\x03val\x18\x01 \x03(\x08\"0\n\rRepeatedValue\x12\x1f\n\x03val\x18\x01 \x03(\x0b\x32\x12.feast.types.Value*\x10\n\x04Null\x12\x08\n\x04NULL\x10\x00\x42Q\n\x11\x66\x65\x61st.proto.typesB\nValueProtoZ0github.com/feast-dev/feast/go/protos/feast/typesb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'feast.types.Value_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\021feast.proto.typesB\nValueProtoZ0github.com/feast-dev/feast/go/protos/feast/types' + _globals['_NULL']._serialized_start=1200 + _globals['_NULL']._serialized_end=1216 + _globals['_VALUETYPE']._serialized_start=41 + _globals['_VALUETYPE']._serialized_end=320 + _globals['_VALUETYPE_ENUM']._serialized_start=55 + _globals['_VALUETYPE_ENUM']._serialized_end=320 + _globals['_VALUE']._serialized_start=323 + _globals['_VALUE']._serialized_end=965 + _globals['_BYTESLIST']._serialized_start=967 + _globals['_BYTESLIST']._serialized_end=991 + _globals['_STRINGLIST']._serialized_start=993 + _globals['_STRINGLIST']._serialized_end=1018 + _globals['_INT32LIST']._serialized_start=1020 + _globals['_INT32LIST']._serialized_end=1044 + _globals['_INT64LIST']._serialized_start=1046 + _globals['_INT64LIST']._serialized_end=1070 + _globals['_DOUBLELIST']._serialized_start=1072 + _globals['_DOUBLELIST']._serialized_end=1097 + _globals['_FLOATLIST']._serialized_start=1099 + _globals['_FLOATLIST']._serialized_end=1123 + _globals['_BOOLLIST']._serialized_start=1125 + _globals['_BOOLLIST']._serialized_end=1148 + _globals['_REPEATEDVALUE']._serialized_start=1150 + _globals['_REPEATEDVALUE']._serialized_end=1198 +# @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/types/Value_pb2.pyi b/sdk/python/feast/protos/feast/types/Value_pb2.pyi new file mode 100644 index 0000000000..15e4870e6a --- /dev/null +++ b/sdk/python/feast/protos/feast/types/Value_pb2.pyi @@ -0,0 +1,296 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file + +Copyright 2018 The Feast 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 + + https://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 builtins +import collections.abc +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.internal.enum_type_wrapper +import google.protobuf.message +import sys +import typing + +if sys.version_info >= (3, 10): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class _Null: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _NullEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_Null.ValueType], builtins.type): # noqa: F821 + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + NULL: _Null.ValueType # 0 + +class Null(_Null, metaclass=_NullEnumTypeWrapper): ... + +NULL: Null.ValueType # 0 +global___Null = Null + +class ValueType(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class _Enum: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _EnumEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[ValueType._Enum.ValueType], builtins.type): # noqa: F821 + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + INVALID: ValueType._Enum.ValueType # 0 + BYTES: ValueType._Enum.ValueType # 1 + STRING: ValueType._Enum.ValueType # 2 + INT32: ValueType._Enum.ValueType # 3 + INT64: ValueType._Enum.ValueType # 4 + DOUBLE: ValueType._Enum.ValueType # 5 + FLOAT: ValueType._Enum.ValueType # 6 + BOOL: ValueType._Enum.ValueType # 7 + UNIX_TIMESTAMP: ValueType._Enum.ValueType # 8 + BYTES_LIST: ValueType._Enum.ValueType # 11 + STRING_LIST: ValueType._Enum.ValueType # 12 + INT32_LIST: ValueType._Enum.ValueType # 13 + INT64_LIST: ValueType._Enum.ValueType # 14 + DOUBLE_LIST: ValueType._Enum.ValueType # 15 + FLOAT_LIST: ValueType._Enum.ValueType # 16 + BOOL_LIST: ValueType._Enum.ValueType # 17 + UNIX_TIMESTAMP_LIST: ValueType._Enum.ValueType # 18 + NULL: ValueType._Enum.ValueType # 19 + + class Enum(_Enum, metaclass=_EnumEnumTypeWrapper): ... + INVALID: ValueType.Enum.ValueType # 0 + BYTES: ValueType.Enum.ValueType # 1 + STRING: ValueType.Enum.ValueType # 2 + INT32: ValueType.Enum.ValueType # 3 + INT64: ValueType.Enum.ValueType # 4 + DOUBLE: ValueType.Enum.ValueType # 5 + FLOAT: ValueType.Enum.ValueType # 6 + BOOL: ValueType.Enum.ValueType # 7 + UNIX_TIMESTAMP: ValueType.Enum.ValueType # 8 + BYTES_LIST: ValueType.Enum.ValueType # 11 + STRING_LIST: ValueType.Enum.ValueType # 12 + INT32_LIST: ValueType.Enum.ValueType # 13 + INT64_LIST: ValueType.Enum.ValueType # 14 + DOUBLE_LIST: ValueType.Enum.ValueType # 15 + FLOAT_LIST: ValueType.Enum.ValueType # 16 + BOOL_LIST: ValueType.Enum.ValueType # 17 + UNIX_TIMESTAMP_LIST: ValueType.Enum.ValueType # 18 + NULL: ValueType.Enum.ValueType # 19 + + def __init__( + self, + ) -> None: ... + +global___ValueType = ValueType + +class Value(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + BYTES_VAL_FIELD_NUMBER: builtins.int + STRING_VAL_FIELD_NUMBER: builtins.int + INT32_VAL_FIELD_NUMBER: builtins.int + INT64_VAL_FIELD_NUMBER: builtins.int + DOUBLE_VAL_FIELD_NUMBER: builtins.int + FLOAT_VAL_FIELD_NUMBER: builtins.int + BOOL_VAL_FIELD_NUMBER: builtins.int + UNIX_TIMESTAMP_VAL_FIELD_NUMBER: builtins.int + BYTES_LIST_VAL_FIELD_NUMBER: builtins.int + STRING_LIST_VAL_FIELD_NUMBER: builtins.int + INT32_LIST_VAL_FIELD_NUMBER: builtins.int + INT64_LIST_VAL_FIELD_NUMBER: builtins.int + DOUBLE_LIST_VAL_FIELD_NUMBER: builtins.int + FLOAT_LIST_VAL_FIELD_NUMBER: builtins.int + BOOL_LIST_VAL_FIELD_NUMBER: builtins.int + UNIX_TIMESTAMP_LIST_VAL_FIELD_NUMBER: builtins.int + NULL_VAL_FIELD_NUMBER: builtins.int + bytes_val: builtins.bytes + string_val: builtins.str + int32_val: builtins.int + int64_val: builtins.int + double_val: builtins.float + float_val: builtins.float + bool_val: builtins.bool + unix_timestamp_val: builtins.int + @property + def bytes_list_val(self) -> global___BytesList: ... + @property + def string_list_val(self) -> global___StringList: ... + @property + def int32_list_val(self) -> global___Int32List: ... + @property + def int64_list_val(self) -> global___Int64List: ... + @property + def double_list_val(self) -> global___DoubleList: ... + @property + def float_list_val(self) -> global___FloatList: ... + @property + def bool_list_val(self) -> global___BoolList: ... + @property + def unix_timestamp_list_val(self) -> global___Int64List: ... + null_val: global___Null.ValueType + def __init__( + self, + *, + bytes_val: builtins.bytes = ..., + string_val: builtins.str = ..., + int32_val: builtins.int = ..., + int64_val: builtins.int = ..., + double_val: builtins.float = ..., + float_val: builtins.float = ..., + bool_val: builtins.bool = ..., + unix_timestamp_val: builtins.int = ..., + bytes_list_val: global___BytesList | None = ..., + string_list_val: global___StringList | None = ..., + int32_list_val: global___Int32List | None = ..., + int64_list_val: global___Int64List | None = ..., + double_list_val: global___DoubleList | None = ..., + float_list_val: global___FloatList | None = ..., + bool_list_val: global___BoolList | None = ..., + unix_timestamp_list_val: global___Int64List | None = ..., + null_val: global___Null.ValueType = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["bool_list_val", b"bool_list_val", "bool_val", b"bool_val", "bytes_list_val", b"bytes_list_val", "bytes_val", b"bytes_val", "double_list_val", b"double_list_val", "double_val", b"double_val", "float_list_val", b"float_list_val", "float_val", b"float_val", "int32_list_val", b"int32_list_val", "int32_val", b"int32_val", "int64_list_val", b"int64_list_val", "int64_val", b"int64_val", "null_val", b"null_val", "string_list_val", b"string_list_val", "string_val", b"string_val", "unix_timestamp_list_val", b"unix_timestamp_list_val", "unix_timestamp_val", b"unix_timestamp_val", "val", b"val"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["bool_list_val", b"bool_list_val", "bool_val", b"bool_val", "bytes_list_val", b"bytes_list_val", "bytes_val", b"bytes_val", "double_list_val", b"double_list_val", "double_val", b"double_val", "float_list_val", b"float_list_val", "float_val", b"float_val", "int32_list_val", b"int32_list_val", "int32_val", b"int32_val", "int64_list_val", b"int64_list_val", "int64_val", b"int64_val", "null_val", b"null_val", "string_list_val", b"string_list_val", "string_val", b"string_val", "unix_timestamp_list_val", b"unix_timestamp_list_val", "unix_timestamp_val", b"unix_timestamp_val", "val", b"val"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal["val", b"val"]) -> typing_extensions.Literal["bytes_val", "string_val", "int32_val", "int64_val", "double_val", "float_val", "bool_val", "unix_timestamp_val", "bytes_list_val", "string_list_val", "int32_list_val", "int64_list_val", "double_list_val", "float_list_val", "bool_list_val", "unix_timestamp_list_val", "null_val"] | None: ... + +global___Value = Value + +class BytesList(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + VAL_FIELD_NUMBER: builtins.int + @property + def val(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.bytes]: ... + def __init__( + self, + *, + val: collections.abc.Iterable[builtins.bytes] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["val", b"val"]) -> None: ... + +global___BytesList = BytesList + +class StringList(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + VAL_FIELD_NUMBER: builtins.int + @property + def val(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: ... + def __init__( + self, + *, + val: collections.abc.Iterable[builtins.str] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["val", b"val"]) -> None: ... + +global___StringList = StringList + +class Int32List(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + VAL_FIELD_NUMBER: builtins.int + @property + def val(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: ... + def __init__( + self, + *, + val: collections.abc.Iterable[builtins.int] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["val", b"val"]) -> None: ... + +global___Int32List = Int32List + +class Int64List(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + VAL_FIELD_NUMBER: builtins.int + @property + def val(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: ... + def __init__( + self, + *, + val: collections.abc.Iterable[builtins.int] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["val", b"val"]) -> None: ... + +global___Int64List = Int64List + +class DoubleList(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + VAL_FIELD_NUMBER: builtins.int + @property + def val(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.float]: ... + def __init__( + self, + *, + val: collections.abc.Iterable[builtins.float] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["val", b"val"]) -> None: ... + +global___DoubleList = DoubleList + +class FloatList(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + VAL_FIELD_NUMBER: builtins.int + @property + def val(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.float]: ... + def __init__( + self, + *, + val: collections.abc.Iterable[builtins.float] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["val", b"val"]) -> None: ... + +global___FloatList = FloatList + +class BoolList(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + VAL_FIELD_NUMBER: builtins.int + @property + def val(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.bool]: ... + def __init__( + self, + *, + val: collections.abc.Iterable[builtins.bool] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["val", b"val"]) -> None: ... + +global___BoolList = BoolList + +class RepeatedValue(google.protobuf.message.Message): + """This is to avoid an issue of being unable to specify `repeated value` in oneofs or maps + In JSON "val" field can be omitted + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + VAL_FIELD_NUMBER: builtins.int + @property + def val(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Value]: ... + def __init__( + self, + *, + val: collections.abc.Iterable[global___Value] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["val", b"val"]) -> None: ... + +global___RepeatedValue = RepeatedValue diff --git a/sdk/python/feast/protos/feast/types/Value_pb2_grpc.py b/sdk/python/feast/protos/feast/types/Value_pb2_grpc.py new file mode 100644 index 0000000000..2daafffebf --- /dev/null +++ b/sdk/python/feast/protos/feast/types/Value_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + diff --git a/sdk/python/feast/protos/feast/types/__init__.py b/sdk/python/feast/protos/feast/types/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sdk/python/requirements/py3.10-ci-requirements.txt b/sdk/python/requirements/py3.10-ci-requirements.txt index 59e799ebab..0a46400007 100644 --- a/sdk/python/requirements/py3.10-ci-requirements.txt +++ b/sdk/python/requirements/py3.10-ci-requirements.txt @@ -1,7 +1,6 @@ # This file was autogenerated by uv via the following command: # uv pip compile -p 3.10 --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.10-ci-requirements.txt -aiobotocore==2.15.0 - # via feast (setup.py) +aiobotocore==2.15.1 aiohappyeyeballs==2.4.0 # via aiohttp aiohttp==3.10.5 @@ -22,8 +21,6 @@ anyio==4.5.0 # jupyter-server # starlette # watchfiles -appnope==0.1.4 - # via ipykernel argon2-cffi==23.1.0 # via jupyter-server argon2-cffi-bindings==21.2.0 @@ -33,7 +30,6 @@ arrow==1.3.0 asn1crypto==1.5.1 # via snowflake-connector-python assertpy==1.1 - # via feast (setup.py) asttokens==2.4.1 # via stack-data async-lru==2.0.4 @@ -55,10 +51,8 @@ azure-core==1.31.0 # via # azure-identity # azure-storage-blob -azure-identity==1.17.1 - # via feast (setup.py) +azure-identity==1.18.0 azure-storage-blob==12.23.0 - # via feast (setup.py) babel==2.16.0 # via # jupyterlab-server @@ -66,14 +60,11 @@ babel==2.16.0 beautifulsoup4==4.12.3 # via nbconvert bigtree==0.21.1 - # via feast (setup.py) bleach==6.1.0 # via nbconvert -boto3==1.35.16 - # via - # feast (setup.py) - # moto -botocore==1.35.16 +boto3==1.35.23 + # via moto +botocore==1.35.23 # via # aiobotocore # boto3 @@ -81,13 +72,11 @@ botocore==1.35.16 # s3transfer build==1.2.2 # via - # feast (setup.py) # pip-tools # singlestoredb cachetools==5.5.0 # via google-auth cassandra-driver==3.29.2 - # via feast (setup.py) certifi==2024.8.30 # via # elastic-transport @@ -110,7 +99,6 @@ charset-normalizer==3.3.2 # snowflake-connector-python click==8.1.7 # via - # feast (setup.py) # dask # geomet # great-expectations @@ -119,9 +107,7 @@ click==8.1.7 cloudpickle==3.0.0 # via dask colorama==0.4.6 - # via - # feast (setup.py) - # great-expectations + # via great-expectations comm==0.2.2 # via # ipykernel @@ -130,7 +116,6 @@ coverage[toml]==7.6.1 # via pytest-cov cryptography==42.0.8 # via - # feast (setup.py) # azure-identity # azure-storage-blob # great-expectations @@ -145,9 +130,7 @@ cryptography==42.0.8 cython==3.0.11 # via thriftpy2 dask[dataframe]==2024.9.0 - # via - # feast (setup.py) - # dask-expr + # via dask-expr dask-expr==1.1.14 # via dask db-dtypes==1.3.0 @@ -159,11 +142,9 @@ decorator==5.1.1 defusedxml==0.7.1 # via nbconvert deltalake==0.20.0 - # via feast (setup.py) deprecation==2.1.0 # via python-keycloak dill==0.3.8 - # via feast (setup.py) distlib==0.3.8 # via virtualenv docker==7.1.0 @@ -175,7 +156,6 @@ duckdb==1.1.0 elastic-transport==8.15.0 # via elasticsearch elasticsearch==8.15.1 - # via feast (setup.py) entrypoints==0.4 # via altair exceptiongroup==1.2.2 @@ -188,7 +168,6 @@ execnet==2.1.1 executing==2.1.0 # via stack-data fastapi==0.115.0 - # via feast (setup.py) fastjsonschema==2.20.0 # via nbformat filelock==3.16.1 @@ -202,21 +181,18 @@ frozenlist==1.4.1 # aiohttp # aiosignal fsspec==2024.9.0 - # via - # feast (setup.py) - # dask + # via dask geomet==0.2.1.post1 # via cassandra-driver -google-api-core[grpc]==2.19.2 +google-api-core[grpc]==2.20.0 # via - # feast (setup.py) # google-cloud-bigquery # google-cloud-bigquery-storage # google-cloud-bigtable # google-cloud-core # google-cloud-datastore # google-cloud-storage -google-auth==2.34.0 +google-auth==2.35.0 # via # google-api-core # google-cloud-bigquery @@ -227,11 +203,8 @@ google-auth==2.34.0 # google-cloud-storage # kubernetes google-cloud-bigquery[pandas]==3.25.0 - # via feast (setup.py) google-cloud-bigquery-storage==2.26.0 - # via feast (setup.py) google-cloud-bigtable==2.26.0 - # via feast (setup.py) google-cloud-core==2.4.1 # via # google-cloud-bigquery @@ -239,9 +212,7 @@ google-cloud-core==2.4.1 # google-cloud-datastore # google-cloud-storage google-cloud-datastore==2.20.1 - # via feast (setup.py) google-cloud-storage==2.18.2 - # via feast (setup.py) google-crc32c==1.6.0 # via # google-cloud-storage @@ -252,17 +223,16 @@ google-resumable-media==2.7.2 # google-cloud-storage googleapis-common-protos[grpc]==1.65.0 # via - # feast (setup.py) # google-api-core # grpc-google-iam-v1 # grpcio-status great-expectations==0.18.21 - # via feast (setup.py) +greenlet==3.1.0 + # via sqlalchemy grpc-google-iam-v1==0.13.1 # via google-cloud-bigtable grpcio==1.66.1 # via - # feast (setup.py) # google-api-core # googleapis-common-protos # grpc-google-iam-v1 @@ -272,42 +242,30 @@ grpcio==1.66.1 # grpcio-testing # grpcio-tools grpcio-health-checking==1.62.3 - # via feast (setup.py) grpcio-reflection==1.62.3 - # via feast (setup.py) grpcio-status==1.62.3 # via google-api-core grpcio-testing==1.62.3 - # via feast (setup.py) grpcio-tools==1.62.3 - # via feast (setup.py) gunicorn==23.0.0 - # via feast (setup.py) h11==0.14.0 # via # httpcore # uvicorn happybase==1.2.0 - # via feast (setup.py) hazelcast-python-client==5.5.0 - # via feast (setup.py) hiredis==2.4.0 - # via feast (setup.py) httpcore==1.0.5 # via httpx httptools==0.6.1 # via uvicorn httpx==0.27.2 # via - # feast (setup.py) # jupyterlab # python-keycloak ibis-framework[duckdb]==9.5.0 - # via - # feast (setup.py) - # ibis-substrait + # via ibis-substrait ibis-substrait==4.0.1 - # via feast (setup.py) identify==2.6.1 # via pre-commit idna==3.10 @@ -343,7 +301,6 @@ jedi==0.19.1 # via ipython jinja2==3.1.4 # via - # feast (setup.py) # altair # great-expectations # jupyter-server @@ -366,7 +323,6 @@ jsonpointer==3.0.0 # jsonschema jsonschema[format-nongpl]==4.23.0 # via - # feast (setup.py) # altair # great-expectations # jupyter-events @@ -414,7 +370,6 @@ jupyterlab-widgets==3.0.13 jwcrypto==1.5.6 # via python-keycloak kubernetes==20.13.0 - # via feast (setup.py) locket==1.0.0 # via partd makefun==1.15.4 @@ -435,17 +390,13 @@ matplotlib-inline==0.1.7 mdurl==0.1.2 # via markdown-it-py minio==7.1.0 - # via feast (setup.py) mistune==3.0.2 # via # great-expectations # nbconvert mmh3==5.0.0 - # via feast (setup.py) mock==2.0.0 - # via feast (setup.py) moto==4.2.14 - # via feast (setup.py) msal==1.31.0 # via # azure-identity @@ -457,13 +408,10 @@ multidict==6.1.0 # aiohttp # yarl mypy==1.11.2 - # via - # feast (setup.py) - # sqlalchemy + # via sqlalchemy mypy-extensions==1.0.0 # via mypy mypy-protobuf==3.3.0 - # via feast (setup.py) nbclient==0.10.0 # via nbconvert nbconvert==7.16.4 @@ -486,7 +434,6 @@ notebook-shim==0.2.4 # notebook numpy==1.26.4 # via - # feast (setup.py) # altair # dask # db-dtypes @@ -521,7 +468,6 @@ packaging==24.1 # sphinx pandas==2.2.2 # via - # feast (setup.py) # altair # dask # dask-expr @@ -547,7 +493,6 @@ pexpect==4.9.0 pip==24.2 # via pip-tools pip-tools==7.4.1 - # via feast (setup.py) platformdirs==3.11.0 # via # jupyter-core @@ -560,11 +505,8 @@ ply==3.11 portalocker==2.10.1 # via msal-extensions pre-commit==3.3.1 - # via feast (setup.py) prometheus-client==0.20.0 - # via - # feast (setup.py) - # jupyter-server + # via jupyter-server prompt-toolkit==3.0.47 # via ipython proto-plus==1.24.0 @@ -575,7 +517,6 @@ proto-plus==1.24.0 # google-cloud-datastore protobuf==4.25.5 # via - # feast (setup.py) # google-api-core # google-cloud-bigquery-storage # google-cloud-bigtable @@ -591,11 +532,8 @@ protobuf==4.25.5 # proto-plus # substrait psutil==5.9.0 - # via - # feast (setup.py) - # ipykernel + # via ipykernel psycopg[binary, pool]==3.2.2 - # via feast (setup.py) psycopg-binary==3.2.2 # via psycopg psycopg-pool==3.2.3 @@ -607,14 +545,12 @@ ptyprocess==0.7.0 pure-eval==0.2.3 # via stack-data py==1.11.0 - # via feast (setup.py) py-cpuinfo==9.0.0 # via pytest-benchmark py4j==0.10.9.7 # via pyspark pyarrow==17.0.0 # via - # feast (setup.py) # dask-expr # db-dtypes # deltalake @@ -630,35 +566,28 @@ pyasn1==0.6.1 pyasn1-modules==0.4.1 # via google-auth pybindgen==0.22.1 - # via feast (setup.py) pycparser==2.22 # via cffi pydantic==2.9.2 # via - # feast (setup.py) # fastapi # great-expectations pydantic-core==2.23.4 # via pydantic pygments==2.18.0 # via - # feast (setup.py) # ipython # nbconvert # rich # sphinx pyjwt[crypto]==2.9.0 # via - # feast (setup.py) # msal # singlestoredb # snowflake-connector-python pymssql==2.3.1 - # via feast (setup.py) pymysql==1.1.1 - # via feast (setup.py) pyodbc==5.1.0 - # via feast (setup.py) pyopenssl==24.2.1 # via snowflake-connector-python pyparsing==3.1.4 @@ -668,10 +597,8 @@ pyproject-hooks==1.1.0 # build # pip-tools pyspark==3.5.2 - # via feast (setup.py) pytest==7.4.4 # via - # feast (setup.py) # pytest-benchmark # pytest-cov # pytest-env @@ -681,21 +608,13 @@ pytest==7.4.4 # pytest-timeout # pytest-xdist pytest-benchmark==3.4.1 - # via feast (setup.py) pytest-cov==5.0.0 - # via feast (setup.py) pytest-env==1.1.3 - # via feast (setup.py) pytest-lazy-fixture==0.6.3 - # via feast (setup.py) pytest-mock==1.10.4 - # via feast (setup.py) pytest-ordering==0.6 - # via feast (setup.py) pytest-timeout==1.4.2 - # via feast (setup.py) pytest-xdist==3.6.1 - # via feast (setup.py) python-dateutil==2.9.0.post0 # via # arrow @@ -713,7 +632,6 @@ python-dotenv==1.0.1 python-json-logger==2.0.7 # via jupyter-events python-keycloak==4.2.2 - # via feast (setup.py) pytz==2024.2 # via # great-expectations @@ -723,7 +641,6 @@ pytz==2024.2 # trino pyyaml==6.0.2 # via - # feast (setup.py) # dask # ibis-substrait # jupyter-events @@ -737,19 +654,15 @@ pyzmq==26.2.0 # jupyter-client # jupyter-server redis==4.6.0 - # via feast (setup.py) referencing==0.35.1 # via # jsonschema # jsonschema-specifications # jupyter-events regex==2024.9.11 - # via - # feast (setup.py) - # parsimonious + # via parsimonious requests==2.32.3 # via - # feast (setup.py) # azure-core # docker # google-api-core @@ -794,8 +707,7 @@ ruamel-yaml==0.17.40 # via great-expectations ruamel-yaml-clib==0.2.8 # via ruamel-yaml -ruff==0.6.5 - # via feast (setup.py) +ruff==0.6.6 s3transfer==0.10.2 # via boto3 scipy==1.14.1 @@ -810,7 +722,6 @@ setuptools==75.1.0 # pip-tools # singlestoredb singlestoredb==1.6.3 - # via feast (setup.py) six==1.16.0 # via # asttokens @@ -831,13 +742,11 @@ sniffio==1.3.1 snowballstemmer==2.2.0 # via sphinx snowflake-connector-python[pandas]==3.12.2 - # via feast (setup.py) sortedcontainers==2.4.0 # via snowflake-connector-python soupsieve==2.6 # via beautifulsoup4 sphinx==6.2.1 - # via feast (setup.py) sphinxcontrib-applehelp==2.0.0 # via sphinx sphinxcontrib-devhelp==2.0.0 @@ -851,11 +760,9 @@ sphinxcontrib-qthelp==2.0.0 sphinxcontrib-serializinghtml==2.0.0 # via sphinx sqlalchemy[mypy]==2.0.35 - # via feast (setup.py) sqlglot==25.20.1 # via ibis-framework sqlite-vec==0.1.1 - # via feast (setup.py) sqlparams==6.1.0 # via singlestoredb stack-data==0.6.3 @@ -865,21 +772,17 @@ starlette==0.38.5 substrait==0.23.0 # via ibis-substrait tabulate==0.9.0 - # via feast (setup.py) tenacity==8.5.0 - # via feast (setup.py) terminado==0.18.1 # via # jupyter-server # jupyter-server-terminals testcontainers==4.4.0 - # via feast (setup.py) thriftpy2==0.5.2 # via happybase tinycss2==1.3.0 # via nbconvert toml==0.10.2 - # via feast (setup.py) tomli==2.0.1 # via # build @@ -907,9 +810,7 @@ tornado==6.4.1 # notebook # terminado tqdm==4.66.5 - # via - # feast (setup.py) - # great-expectations + # via great-expectations traitlets==5.14.3 # via # comm @@ -926,37 +827,23 @@ traitlets==5.14.3 # nbconvert # nbformat trino==0.329.0 - # via feast (setup.py) typeguard==4.3.0 - # via feast (setup.py) types-cffi==1.16.0.20240331 # via types-pyopenssl types-protobuf==3.19.22 - # via - # feast (setup.py) - # mypy-protobuf + # via mypy-protobuf types-pymysql==1.1.0.20240524 - # via feast (setup.py) types-pyopenssl==24.1.0.20240722 # via types-redis types-python-dateutil==2.9.0.20240906 - # via - # feast (setup.py) - # arrow + # via arrow types-pytz==2024.2.0.20240913 - # via feast (setup.py) types-pyyaml==6.0.12.20240917 - # via feast (setup.py) types-redis==4.6.0.20240903 - # via feast (setup.py) types-requests==2.30.0.0 - # via feast (setup.py) types-setuptools==75.1.0.20240917 - # via - # feast (setup.py) - # types-cffi + # via types-cffi types-tabulate==0.9.0.20240106 - # via feast (setup.py) types-urllib3==1.26.25.14 # via types-requests typing-extensions==4.12.2 @@ -992,7 +879,6 @@ uri-template==1.3.0 # via jsonschema urllib3==2.2.3 # via - # feast (setup.py) # botocore # docker # elastic-transport @@ -1003,13 +889,10 @@ urllib3==2.2.3 # responses # testcontainers uvicorn[standard]==0.30.6 - # via feast (setup.py) uvloop==0.20.0 # via uvicorn virtualenv==20.23.0 - # via - # feast (setup.py) - # pre-commit + # via pre-commit watchfiles==0.24.0 # via uvicorn wcwidth==0.2.13 diff --git a/sdk/python/requirements/py3.10-requirements.txt b/sdk/python/requirements/py3.10-requirements.txt index 26eeca3529..9e5eb0be72 100644 --- a/sdk/python/requirements/py3.10-requirements.txt +++ b/sdk/python/requirements/py3.10-requirements.txt @@ -11,36 +11,30 @@ attrs==24.2.0 # jsonschema # referencing bigtree==0.21.1 - # via feast (setup.py) certifi==2024.8.30 # via requests charset-normalizer==3.3.2 # via requests click==8.1.7 # via - # feast (setup.py) # dask # uvicorn cloudpickle==3.0.0 # via dask colorama==0.4.6 - # via feast (setup.py) dask[dataframe]==2024.9.0 - # via - # feast (setup.py) - # dask-expr + # via dask-expr dask-expr==1.1.14 # via dask dill==0.3.8 - # via feast (setup.py) exceptiongroup==1.2.2 # via anyio fastapi==0.115.0 - # via feast (setup.py) fsspec==2024.9.0 # via dask +greenlet==3.1.0 + # via sqlalchemy gunicorn==23.0.0 - # via feast (setup.py) h11==0.14.0 # via uvicorn httptools==0.6.1 @@ -52,9 +46,7 @@ idna==3.10 importlib-metadata==8.5.0 # via dask jinja2==3.1.4 - # via feast (setup.py) jsonschema==4.23.0 - # via feast (setup.py) jsonschema-specifications==2023.12.1 # via jsonschema locket==1.0.0 @@ -62,14 +54,12 @@ locket==1.0.0 markupsafe==2.1.5 # via jinja2 mmh3==5.0.0 - # via feast (setup.py) mypy==1.11.2 # via sqlalchemy mypy-extensions==1.0.0 # via mypy numpy==1.26.4 # via - # feast (setup.py) # dask # pandas # pyarrow @@ -79,31 +69,21 @@ packaging==24.1 # gunicorn pandas==2.2.2 # via - # feast (setup.py) # dask # dask-expr partd==1.4.2 # via dask prometheus-client==0.20.0 - # via feast (setup.py) protobuf==4.25.5 - # via feast (setup.py) psutil==6.0.0 - # via feast (setup.py) pyarrow==17.0.0 - # via - # feast (setup.py) - # dask-expr + # via dask-expr pydantic==2.9.2 - # via - # feast (setup.py) - # fastapi + # via fastapi pydantic-core==2.23.4 # via pydantic pygments==2.18.0 - # via feast (setup.py) pyjwt==2.9.0 - # via feast (setup.py) python-dateutil==2.9.0.post0 # via pandas python-dotenv==1.0.1 @@ -112,7 +92,6 @@ pytz==2024.2 # via pandas pyyaml==6.0.2 # via - # feast (setup.py) # dask # uvicorn referencing==0.35.1 @@ -120,7 +99,6 @@ referencing==0.35.1 # jsonschema # jsonschema-specifications requests==2.32.3 - # via feast (setup.py) rpds-py==0.20.0 # via # jsonschema @@ -130,15 +108,11 @@ six==1.16.0 sniffio==1.3.1 # via anyio sqlalchemy[mypy]==2.0.35 - # via feast (setup.py) starlette==0.38.5 # via fastapi tabulate==0.9.0 - # via feast (setup.py) tenacity==8.5.0 - # via feast (setup.py) toml==0.10.2 - # via feast (setup.py) tomli==2.0.1 # via mypy toolz==0.12.1 @@ -146,9 +120,7 @@ toolz==0.12.1 # dask # partd tqdm==4.66.5 - # via feast (setup.py) typeguard==4.3.0 - # via feast (setup.py) typing-extensions==4.12.2 # via # anyio @@ -164,7 +136,6 @@ tzdata==2024.1 urllib3==2.2.3 # via requests uvicorn[standard]==0.30.6 - # via feast (setup.py) uvloop==0.20.0 # via uvicorn watchfiles==0.24.0 diff --git a/sdk/python/requirements/py3.11-ci-requirements.txt b/sdk/python/requirements/py3.11-ci-requirements.txt index 9f57ecd841..26ced829c6 100644 --- a/sdk/python/requirements/py3.11-ci-requirements.txt +++ b/sdk/python/requirements/py3.11-ci-requirements.txt @@ -1,7 +1,6 @@ # This file was autogenerated by uv via the following command: # uv pip compile -p 3.11 --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.11-ci-requirements.txt -aiobotocore==2.15.0 - # via feast (setup.py) +aiobotocore==2.15.1 aiohappyeyeballs==2.4.0 # via aiohttp aiohttp==3.10.5 @@ -22,8 +21,6 @@ anyio==4.5.0 # jupyter-server # starlette # watchfiles -appnope==0.1.4 - # via ipykernel argon2-cffi==23.1.0 # via jupyter-server argon2-cffi-bindings==21.2.0 @@ -33,7 +30,6 @@ arrow==1.3.0 asn1crypto==1.5.1 # via snowflake-connector-python assertpy==1.1 - # via feast (setup.py) asttokens==2.4.1 # via stack-data async-lru==2.0.4 @@ -53,10 +49,8 @@ azure-core==1.31.0 # via # azure-identity # azure-storage-blob -azure-identity==1.17.1 - # via feast (setup.py) +azure-identity==1.18.0 azure-storage-blob==12.23.0 - # via feast (setup.py) babel==2.16.0 # via # jupyterlab-server @@ -64,14 +58,11 @@ babel==2.16.0 beautifulsoup4==4.12.3 # via nbconvert bigtree==0.21.1 - # via feast (setup.py) bleach==6.1.0 # via nbconvert -boto3==1.35.16 - # via - # feast (setup.py) - # moto -botocore==1.35.16 +boto3==1.35.23 + # via moto +botocore==1.35.23 # via # aiobotocore # boto3 @@ -79,13 +70,11 @@ botocore==1.35.16 # s3transfer build==1.2.2 # via - # feast (setup.py) # pip-tools # singlestoredb cachetools==5.5.0 # via google-auth cassandra-driver==3.29.2 - # via feast (setup.py) certifi==2024.8.30 # via # elastic-transport @@ -108,7 +97,6 @@ charset-normalizer==3.3.2 # snowflake-connector-python click==8.1.7 # via - # feast (setup.py) # dask # geomet # great-expectations @@ -117,9 +105,7 @@ click==8.1.7 cloudpickle==3.0.0 # via dask colorama==0.4.6 - # via - # feast (setup.py) - # great-expectations + # via great-expectations comm==0.2.2 # via # ipykernel @@ -128,7 +114,6 @@ coverage[toml]==7.6.1 # via pytest-cov cryptography==42.0.8 # via - # feast (setup.py) # azure-identity # azure-storage-blob # great-expectations @@ -143,9 +128,7 @@ cryptography==42.0.8 cython==3.0.11 # via thriftpy2 dask[dataframe]==2024.9.0 - # via - # feast (setup.py) - # dask-expr + # via dask-expr dask-expr==1.1.14 # via dask db-dtypes==1.3.0 @@ -157,11 +140,9 @@ decorator==5.1.1 defusedxml==0.7.1 # via nbconvert deltalake==0.20.0 - # via feast (setup.py) deprecation==2.1.0 # via python-keycloak dill==0.3.8 - # via feast (setup.py) distlib==0.3.8 # via virtualenv docker==7.1.0 @@ -173,7 +154,6 @@ duckdb==1.1.0 elastic-transport==8.15.0 # via elasticsearch elasticsearch==8.15.1 - # via feast (setup.py) entrypoints==0.4 # via altair execnet==2.1.1 @@ -181,7 +161,6 @@ execnet==2.1.1 executing==2.1.0 # via stack-data fastapi==0.115.0 - # via feast (setup.py) fastjsonschema==2.20.0 # via nbformat filelock==3.16.1 @@ -195,21 +174,18 @@ frozenlist==1.4.1 # aiohttp # aiosignal fsspec==2024.9.0 - # via - # feast (setup.py) - # dask + # via dask geomet==0.2.1.post1 # via cassandra-driver -google-api-core[grpc]==2.19.2 +google-api-core[grpc]==2.20.0 # via - # feast (setup.py) # google-cloud-bigquery # google-cloud-bigquery-storage # google-cloud-bigtable # google-cloud-core # google-cloud-datastore # google-cloud-storage -google-auth==2.34.0 +google-auth==2.35.0 # via # google-api-core # google-cloud-bigquery @@ -220,11 +196,8 @@ google-auth==2.34.0 # google-cloud-storage # kubernetes google-cloud-bigquery[pandas]==3.25.0 - # via feast (setup.py) google-cloud-bigquery-storage==2.26.0 - # via feast (setup.py) google-cloud-bigtable==2.26.0 - # via feast (setup.py) google-cloud-core==2.4.1 # via # google-cloud-bigquery @@ -232,9 +205,7 @@ google-cloud-core==2.4.1 # google-cloud-datastore # google-cloud-storage google-cloud-datastore==2.20.1 - # via feast (setup.py) google-cloud-storage==2.18.2 - # via feast (setup.py) google-crc32c==1.6.0 # via # google-cloud-storage @@ -245,17 +216,16 @@ google-resumable-media==2.7.2 # google-cloud-storage googleapis-common-protos[grpc]==1.65.0 # via - # feast (setup.py) # google-api-core # grpc-google-iam-v1 # grpcio-status great-expectations==0.18.21 - # via feast (setup.py) +greenlet==3.1.0 + # via sqlalchemy grpc-google-iam-v1==0.13.1 # via google-cloud-bigtable grpcio==1.66.1 # via - # feast (setup.py) # google-api-core # googleapis-common-protos # grpc-google-iam-v1 @@ -265,42 +235,30 @@ grpcio==1.66.1 # grpcio-testing # grpcio-tools grpcio-health-checking==1.62.3 - # via feast (setup.py) grpcio-reflection==1.62.3 - # via feast (setup.py) grpcio-status==1.62.3 # via google-api-core grpcio-testing==1.62.3 - # via feast (setup.py) grpcio-tools==1.62.3 - # via feast (setup.py) gunicorn==23.0.0 - # via feast (setup.py) h11==0.14.0 # via # httpcore # uvicorn happybase==1.2.0 - # via feast (setup.py) hazelcast-python-client==5.5.0 - # via feast (setup.py) hiredis==2.4.0 - # via feast (setup.py) httpcore==1.0.5 # via httpx httptools==0.6.1 # via uvicorn httpx==0.27.2 # via - # feast (setup.py) # jupyterlab # python-keycloak ibis-framework[duckdb]==9.5.0 - # via - # feast (setup.py) - # ibis-substrait + # via ibis-substrait ibis-substrait==4.0.1 - # via feast (setup.py) identify==2.6.1 # via pre-commit idna==3.10 @@ -334,7 +292,6 @@ jedi==0.19.1 # via ipython jinja2==3.1.4 # via - # feast (setup.py) # altair # great-expectations # jupyter-server @@ -357,7 +314,6 @@ jsonpointer==3.0.0 # jsonschema jsonschema[format-nongpl]==4.23.0 # via - # feast (setup.py) # altair # great-expectations # jupyter-events @@ -405,7 +361,6 @@ jupyterlab-widgets==3.0.13 jwcrypto==1.5.6 # via python-keycloak kubernetes==20.13.0 - # via feast (setup.py) locket==1.0.0 # via partd makefun==1.15.4 @@ -426,17 +381,13 @@ matplotlib-inline==0.1.7 mdurl==0.1.2 # via markdown-it-py minio==7.1.0 - # via feast (setup.py) mistune==3.0.2 # via # great-expectations # nbconvert mmh3==5.0.0 - # via feast (setup.py) mock==2.0.0 - # via feast (setup.py) moto==4.2.14 - # via feast (setup.py) msal==1.31.0 # via # azure-identity @@ -448,13 +399,10 @@ multidict==6.1.0 # aiohttp # yarl mypy==1.11.2 - # via - # feast (setup.py) - # sqlalchemy + # via sqlalchemy mypy-extensions==1.0.0 # via mypy mypy-protobuf==3.3.0 - # via feast (setup.py) nbclient==0.10.0 # via nbconvert nbconvert==7.16.4 @@ -477,7 +425,6 @@ notebook-shim==0.2.4 # notebook numpy==1.26.4 # via - # feast (setup.py) # altair # dask # db-dtypes @@ -512,7 +459,6 @@ packaging==24.1 # sphinx pandas==2.2.2 # via - # feast (setup.py) # altair # dask # dask-expr @@ -538,7 +484,6 @@ pexpect==4.9.0 pip==24.2 # via pip-tools pip-tools==7.4.1 - # via feast (setup.py) platformdirs==3.11.0 # via # jupyter-core @@ -551,11 +496,8 @@ ply==3.11 portalocker==2.10.1 # via msal-extensions pre-commit==3.3.1 - # via feast (setup.py) prometheus-client==0.20.0 - # via - # feast (setup.py) - # jupyter-server + # via jupyter-server prompt-toolkit==3.0.47 # via ipython proto-plus==1.24.0 @@ -566,7 +508,6 @@ proto-plus==1.24.0 # google-cloud-datastore protobuf==4.25.5 # via - # feast (setup.py) # google-api-core # google-cloud-bigquery-storage # google-cloud-bigtable @@ -582,11 +523,8 @@ protobuf==4.25.5 # proto-plus # substrait psutil==5.9.0 - # via - # feast (setup.py) - # ipykernel + # via ipykernel psycopg[binary, pool]==3.2.2 - # via feast (setup.py) psycopg-binary==3.2.2 # via psycopg psycopg-pool==3.2.3 @@ -598,14 +536,12 @@ ptyprocess==0.7.0 pure-eval==0.2.3 # via stack-data py==1.11.0 - # via feast (setup.py) py-cpuinfo==9.0.0 # via pytest-benchmark py4j==0.10.9.7 # via pyspark pyarrow==17.0.0 # via - # feast (setup.py) # dask-expr # db-dtypes # deltalake @@ -621,35 +557,28 @@ pyasn1==0.6.1 pyasn1-modules==0.4.1 # via google-auth pybindgen==0.22.1 - # via feast (setup.py) pycparser==2.22 # via cffi pydantic==2.9.2 # via - # feast (setup.py) # fastapi # great-expectations pydantic-core==2.23.4 # via pydantic pygments==2.18.0 # via - # feast (setup.py) # ipython # nbconvert # rich # sphinx pyjwt[crypto]==2.9.0 # via - # feast (setup.py) # msal # singlestoredb # snowflake-connector-python pymssql==2.3.1 - # via feast (setup.py) pymysql==1.1.1 - # via feast (setup.py) pyodbc==5.1.0 - # via feast (setup.py) pyopenssl==24.2.1 # via snowflake-connector-python pyparsing==3.1.4 @@ -659,10 +588,8 @@ pyproject-hooks==1.1.0 # build # pip-tools pyspark==3.5.2 - # via feast (setup.py) pytest==7.4.4 # via - # feast (setup.py) # pytest-benchmark # pytest-cov # pytest-env @@ -672,21 +599,13 @@ pytest==7.4.4 # pytest-timeout # pytest-xdist pytest-benchmark==3.4.1 - # via feast (setup.py) pytest-cov==5.0.0 - # via feast (setup.py) pytest-env==1.1.3 - # via feast (setup.py) pytest-lazy-fixture==0.6.3 - # via feast (setup.py) pytest-mock==1.10.4 - # via feast (setup.py) pytest-ordering==0.6 - # via feast (setup.py) pytest-timeout==1.4.2 - # via feast (setup.py) pytest-xdist==3.6.1 - # via feast (setup.py) python-dateutil==2.9.0.post0 # via # arrow @@ -704,7 +623,6 @@ python-dotenv==1.0.1 python-json-logger==2.0.7 # via jupyter-events python-keycloak==4.2.2 - # via feast (setup.py) pytz==2024.2 # via # great-expectations @@ -714,7 +632,6 @@ pytz==2024.2 # trino pyyaml==6.0.2 # via - # feast (setup.py) # dask # ibis-substrait # jupyter-events @@ -728,19 +645,15 @@ pyzmq==26.2.0 # jupyter-client # jupyter-server redis==4.6.0 - # via feast (setup.py) referencing==0.35.1 # via # jsonschema # jsonschema-specifications # jupyter-events regex==2024.9.11 - # via - # feast (setup.py) - # parsimonious + # via parsimonious requests==2.32.3 # via - # feast (setup.py) # azure-core # docker # google-api-core @@ -785,8 +698,7 @@ ruamel-yaml==0.17.40 # via great-expectations ruamel-yaml-clib==0.2.8 # via ruamel-yaml -ruff==0.6.5 - # via feast (setup.py) +ruff==0.6.6 s3transfer==0.10.2 # via boto3 scipy==1.14.1 @@ -801,7 +713,6 @@ setuptools==75.1.0 # pip-tools # singlestoredb singlestoredb==1.6.3 - # via feast (setup.py) six==1.16.0 # via # asttokens @@ -822,13 +733,11 @@ sniffio==1.3.1 snowballstemmer==2.2.0 # via sphinx snowflake-connector-python[pandas]==3.12.2 - # via feast (setup.py) sortedcontainers==2.4.0 # via snowflake-connector-python soupsieve==2.6 # via beautifulsoup4 sphinx==6.2.1 - # via feast (setup.py) sphinxcontrib-applehelp==2.0.0 # via sphinx sphinxcontrib-devhelp==2.0.0 @@ -842,11 +751,9 @@ sphinxcontrib-qthelp==2.0.0 sphinxcontrib-serializinghtml==2.0.0 # via sphinx sqlalchemy[mypy]==2.0.35 - # via feast (setup.py) sqlglot==25.20.1 # via ibis-framework sqlite-vec==0.1.1 - # via feast (setup.py) sqlparams==6.1.0 # via singlestoredb stack-data==0.6.3 @@ -856,23 +763,17 @@ starlette==0.38.5 substrait==0.23.0 # via ibis-substrait tabulate==0.9.0 - # via feast (setup.py) tenacity==8.5.0 - # via feast (setup.py) terminado==0.18.1 # via # jupyter-server # jupyter-server-terminals testcontainers==4.4.0 - # via feast (setup.py) thriftpy2==0.5.2 # via happybase tinycss2==1.3.0 # via nbconvert toml==0.10.2 - # via feast (setup.py) -tomli==2.0.1 - # via coverage tomlkit==0.13.2 # via snowflake-connector-python toolz==0.12.1 @@ -890,9 +791,7 @@ tornado==6.4.1 # notebook # terminado tqdm==4.66.5 - # via - # feast (setup.py) - # great-expectations + # via great-expectations traitlets==5.14.3 # via # comm @@ -909,37 +808,23 @@ traitlets==5.14.3 # nbconvert # nbformat trino==0.329.0 - # via feast (setup.py) typeguard==4.3.0 - # via feast (setup.py) types-cffi==1.16.0.20240331 # via types-pyopenssl types-protobuf==3.19.22 - # via - # feast (setup.py) - # mypy-protobuf + # via mypy-protobuf types-pymysql==1.1.0.20240524 - # via feast (setup.py) types-pyopenssl==24.1.0.20240722 # via types-redis types-python-dateutil==2.9.0.20240906 - # via - # feast (setup.py) - # arrow + # via arrow types-pytz==2024.2.0.20240913 - # via feast (setup.py) types-pyyaml==6.0.12.20240917 - # via feast (setup.py) types-redis==4.6.0.20240903 - # via feast (setup.py) types-requests==2.30.0.0 - # via feast (setup.py) types-setuptools==75.1.0.20240917 - # via - # feast (setup.py) - # types-cffi + # via types-cffi types-tabulate==0.9.0.20240106 - # via feast (setup.py) types-urllib3==1.26.25.14 # via types-requests typing-extensions==4.12.2 @@ -971,7 +856,6 @@ uri-template==1.3.0 # via jsonschema urllib3==2.2.3 # via - # feast (setup.py) # botocore # docker # elastic-transport @@ -982,13 +866,10 @@ urllib3==2.2.3 # responses # testcontainers uvicorn[standard]==0.30.6 - # via feast (setup.py) uvloop==0.20.0 # via uvicorn virtualenv==20.23.0 - # via - # feast (setup.py) - # pre-commit + # via pre-commit watchfiles==0.24.0 # via uvicorn wcwidth==0.2.13 diff --git a/sdk/python/requirements/py3.11-requirements.txt b/sdk/python/requirements/py3.11-requirements.txt index 5c20e45f07..1ce25e7d5b 100644 --- a/sdk/python/requirements/py3.11-requirements.txt +++ b/sdk/python/requirements/py3.11-requirements.txt @@ -11,34 +11,28 @@ attrs==24.2.0 # jsonschema # referencing bigtree==0.21.1 - # via feast (setup.py) certifi==2024.8.30 # via requests charset-normalizer==3.3.2 # via requests click==8.1.7 # via - # feast (setup.py) # dask # uvicorn cloudpickle==3.0.0 # via dask colorama==0.4.6 - # via feast (setup.py) dask[dataframe]==2024.9.0 - # via - # feast (setup.py) - # dask-expr + # via dask-expr dask-expr==1.1.14 # via dask dill==0.3.8 - # via feast (setup.py) fastapi==0.115.0 - # via feast (setup.py) fsspec==2024.9.0 # via dask +greenlet==3.1.0 + # via sqlalchemy gunicorn==23.0.0 - # via feast (setup.py) h11==0.14.0 # via uvicorn httptools==0.6.1 @@ -50,9 +44,7 @@ idna==3.10 importlib-metadata==8.5.0 # via dask jinja2==3.1.4 - # via feast (setup.py) jsonschema==4.23.0 - # via feast (setup.py) jsonschema-specifications==2023.12.1 # via jsonschema locket==1.0.0 @@ -60,14 +52,12 @@ locket==1.0.0 markupsafe==2.1.5 # via jinja2 mmh3==5.0.0 - # via feast (setup.py) mypy==1.11.2 # via sqlalchemy mypy-extensions==1.0.0 # via mypy numpy==1.26.4 # via - # feast (setup.py) # dask # pandas # pyarrow @@ -77,31 +67,21 @@ packaging==24.1 # gunicorn pandas==2.2.2 # via - # feast (setup.py) # dask # dask-expr partd==1.4.2 # via dask prometheus-client==0.20.0 - # via feast (setup.py) protobuf==4.25.5 - # via feast (setup.py) psutil==6.0.0 - # via feast (setup.py) pyarrow==17.0.0 - # via - # feast (setup.py) - # dask-expr + # via dask-expr pydantic==2.9.2 - # via - # feast (setup.py) - # fastapi + # via fastapi pydantic-core==2.23.4 # via pydantic pygments==2.18.0 - # via feast (setup.py) pyjwt==2.9.0 - # via feast (setup.py) python-dateutil==2.9.0.post0 # via pandas python-dotenv==1.0.1 @@ -110,7 +90,6 @@ pytz==2024.2 # via pandas pyyaml==6.0.2 # via - # feast (setup.py) # dask # uvicorn referencing==0.35.1 @@ -118,7 +97,6 @@ referencing==0.35.1 # jsonschema # jsonschema-specifications requests==2.32.3 - # via feast (setup.py) rpds-py==0.20.0 # via # jsonschema @@ -128,23 +106,17 @@ six==1.16.0 sniffio==1.3.1 # via anyio sqlalchemy[mypy]==2.0.35 - # via feast (setup.py) starlette==0.38.5 # via fastapi tabulate==0.9.0 - # via feast (setup.py) tenacity==8.5.0 - # via feast (setup.py) toml==0.10.2 - # via feast (setup.py) toolz==0.12.1 # via # dask # partd tqdm==4.66.5 - # via feast (setup.py) typeguard==4.3.0 - # via feast (setup.py) typing-extensions==4.12.2 # via # fastapi @@ -158,7 +130,6 @@ tzdata==2024.1 urllib3==2.2.3 # via requests uvicorn[standard]==0.30.6 - # via feast (setup.py) uvloop==0.20.0 # via uvicorn watchfiles==0.24.0 diff --git a/sdk/python/requirements/py3.9-ci-requirements.txt b/sdk/python/requirements/py3.9-ci-requirements.txt index bbdca890b6..c8c92969fc 100644 --- a/sdk/python/requirements/py3.9-ci-requirements.txt +++ b/sdk/python/requirements/py3.9-ci-requirements.txt @@ -1,7 +1,6 @@ # This file was autogenerated by uv via the following command: # uv pip compile -p 3.9 --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.9-ci-requirements.txt -aiobotocore==2.15.0 - # via feast (setup.py) +aiobotocore==2.15.1 aiohappyeyeballs==2.4.0 # via aiohttp aiohttp==3.10.5 @@ -22,8 +21,6 @@ anyio==4.5.0 # jupyter-server # starlette # watchfiles -appnope==0.1.4 - # via ipykernel argon2-cffi==23.1.0 # via jupyter-server argon2-cffi-bindings==21.2.0 @@ -33,7 +30,6 @@ arrow==1.3.0 asn1crypto==1.5.1 # via snowflake-connector-python assertpy==1.1 - # via feast (setup.py) asttokens==2.4.1 # via stack-data async-lru==2.0.4 @@ -55,10 +51,8 @@ azure-core==1.31.0 # via # azure-identity # azure-storage-blob -azure-identity==1.17.1 - # via feast (setup.py) +azure-identity==1.18.0 azure-storage-blob==12.23.0 - # via feast (setup.py) babel==2.16.0 # via # jupyterlab-server @@ -68,14 +62,11 @@ beautifulsoup4==4.12.3 bidict==0.23.1 # via ibis-framework bigtree==0.21.1 - # via feast (setup.py) bleach==6.1.0 # via nbconvert -boto3==1.35.16 - # via - # feast (setup.py) - # moto -botocore==1.35.16 +boto3==1.35.23 + # via moto +botocore==1.35.23 # via # aiobotocore # boto3 @@ -83,13 +74,11 @@ botocore==1.35.16 # s3transfer build==1.2.2 # via - # feast (setup.py) # pip-tools # singlestoredb cachetools==5.5.0 # via google-auth cassandra-driver==3.29.2 - # via feast (setup.py) certifi==2024.8.30 # via # elastic-transport @@ -112,7 +101,6 @@ charset-normalizer==3.3.2 # snowflake-connector-python click==8.1.7 # via - # feast (setup.py) # dask # geomet # great-expectations @@ -121,9 +109,7 @@ click==8.1.7 cloudpickle==3.0.0 # via dask colorama==0.4.6 - # via - # feast (setup.py) - # great-expectations + # via great-expectations comm==0.2.2 # via # ipykernel @@ -132,7 +118,6 @@ coverage[toml]==7.6.1 # via pytest-cov cryptography==42.0.8 # via - # feast (setup.py) # azure-identity # azure-storage-blob # great-expectations @@ -147,9 +132,7 @@ cryptography==42.0.8 cython==3.0.11 # via thriftpy2 dask[dataframe]==2024.8.0 - # via - # feast (setup.py) - # dask-expr + # via dask-expr dask-expr==1.1.10 # via dask db-dtypes==1.3.0 @@ -161,11 +144,9 @@ decorator==5.1.1 defusedxml==0.7.1 # via nbconvert deltalake==0.20.0 - # via feast (setup.py) deprecation==2.1.0 # via python-keycloak dill==0.3.8 - # via feast (setup.py) distlib==0.3.8 # via virtualenv docker==7.1.0 @@ -177,7 +158,6 @@ duckdb==0.10.3 elastic-transport==8.15.0 # via elasticsearch elasticsearch==8.15.1 - # via feast (setup.py) entrypoints==0.4 # via altair exceptiongroup==1.2.2 @@ -190,7 +170,6 @@ execnet==2.1.1 executing==2.1.0 # via stack-data fastapi==0.115.0 - # via feast (setup.py) fastjsonschema==2.20.0 # via nbformat filelock==3.16.1 @@ -204,21 +183,18 @@ frozenlist==1.4.1 # aiohttp # aiosignal fsspec==2024.9.0 - # via - # feast (setup.py) - # dask + # via dask geomet==0.2.1.post1 # via cassandra-driver -google-api-core[grpc]==2.19.2 +google-api-core[grpc]==2.20.0 # via - # feast (setup.py) # google-cloud-bigquery # google-cloud-bigquery-storage # google-cloud-bigtable # google-cloud-core # google-cloud-datastore # google-cloud-storage -google-auth==2.34.0 +google-auth==2.35.0 # via # google-api-core # google-cloud-bigquery @@ -229,11 +205,8 @@ google-auth==2.34.0 # google-cloud-storage # kubernetes google-cloud-bigquery[pandas]==3.25.0 - # via feast (setup.py) google-cloud-bigquery-storage==2.26.0 - # via feast (setup.py) google-cloud-bigtable==2.26.0 - # via feast (setup.py) google-cloud-core==2.4.1 # via # google-cloud-bigquery @@ -241,9 +214,7 @@ google-cloud-core==2.4.1 # google-cloud-datastore # google-cloud-storage google-cloud-datastore==2.20.1 - # via feast (setup.py) google-cloud-storage==2.18.2 - # via feast (setup.py) google-crc32c==1.6.0 # via # google-cloud-storage @@ -254,17 +225,16 @@ google-resumable-media==2.7.2 # google-cloud-storage googleapis-common-protos[grpc]==1.65.0 # via - # feast (setup.py) # google-api-core # grpc-google-iam-v1 # grpcio-status great-expectations==0.18.21 - # via feast (setup.py) +greenlet==3.1.0 + # via sqlalchemy grpc-google-iam-v1==0.13.1 # via google-cloud-bigtable grpcio==1.66.1 # via - # feast (setup.py) # google-api-core # googleapis-common-protos # grpc-google-iam-v1 @@ -274,42 +244,30 @@ grpcio==1.66.1 # grpcio-testing # grpcio-tools grpcio-health-checking==1.62.3 - # via feast (setup.py) grpcio-reflection==1.62.3 - # via feast (setup.py) grpcio-status==1.62.3 # via google-api-core grpcio-testing==1.62.3 - # via feast (setup.py) grpcio-tools==1.62.3 - # via feast (setup.py) gunicorn==23.0.0 - # via feast (setup.py) h11==0.14.0 # via # httpcore # uvicorn happybase==1.2.0 - # via feast (setup.py) hazelcast-python-client==5.5.0 - # via feast (setup.py) hiredis==2.4.0 - # via feast (setup.py) httpcore==1.0.5 # via httpx httptools==0.6.1 # via uvicorn httpx==0.27.2 # via - # feast (setup.py) # jupyterlab # python-keycloak ibis-framework[duckdb]==9.0.0 - # via - # feast (setup.py) - # ibis-substrait + # via ibis-substrait ibis-substrait==4.0.1 - # via feast (setup.py) identify==2.6.1 # via pre-commit idna==3.10 @@ -352,7 +310,6 @@ jedi==0.19.1 # via ipython jinja2==3.1.4 # via - # feast (setup.py) # altair # great-expectations # jupyter-server @@ -375,7 +332,6 @@ jsonpointer==3.0.0 # jsonschema jsonschema[format-nongpl]==4.23.0 # via - # feast (setup.py) # altair # great-expectations # jupyter-events @@ -423,7 +379,6 @@ jupyterlab-widgets==3.0.13 jwcrypto==1.5.6 # via python-keycloak kubernetes==20.13.0 - # via feast (setup.py) locket==1.0.0 # via partd makefun==1.15.4 @@ -444,17 +399,13 @@ matplotlib-inline==0.1.7 mdurl==0.1.2 # via markdown-it-py minio==7.1.0 - # via feast (setup.py) mistune==3.0.2 # via # great-expectations # nbconvert mmh3==5.0.0 - # via feast (setup.py) mock==2.0.0 - # via feast (setup.py) moto==4.2.14 - # via feast (setup.py) msal==1.31.0 # via # azure-identity @@ -466,13 +417,10 @@ multidict==6.1.0 # aiohttp # yarl mypy==1.11.2 - # via - # feast (setup.py) - # sqlalchemy + # via sqlalchemy mypy-extensions==1.0.0 # via mypy mypy-protobuf==3.3.0 - # via feast (setup.py) nbclient==0.10.0 # via nbconvert nbconvert==7.16.4 @@ -495,7 +443,6 @@ notebook-shim==0.2.4 # notebook numpy==1.26.4 # via - # feast (setup.py) # altair # dask # db-dtypes @@ -529,7 +476,6 @@ packaging==24.1 # sphinx pandas==2.2.2 # via - # feast (setup.py) # altair # dask # dask-expr @@ -555,7 +501,6 @@ pexpect==4.9.0 pip==24.2 # via pip-tools pip-tools==7.4.1 - # via feast (setup.py) platformdirs==3.11.0 # via # jupyter-core @@ -568,11 +513,8 @@ ply==3.11 portalocker==2.10.1 # via msal-extensions pre-commit==3.3.1 - # via feast (setup.py) prometheus-client==0.20.0 - # via - # feast (setup.py) - # jupyter-server + # via jupyter-server prompt-toolkit==3.0.47 # via ipython proto-plus==1.24.0 @@ -583,7 +525,6 @@ proto-plus==1.24.0 # google-cloud-datastore protobuf==4.25.5 # via - # feast (setup.py) # google-api-core # google-cloud-bigquery-storage # google-cloud-bigtable @@ -599,12 +540,9 @@ protobuf==4.25.5 # proto-plus # substrait psutil==5.9.0 - # via - # feast (setup.py) - # ipykernel -psycopg[binary, pool]==3.1.18 - # via feast (setup.py) -psycopg-binary==3.1.18 + # via ipykernel +psycopg[binary, pool]==3.2.2 +psycopg-binary==3.2.2 # via psycopg psycopg-pool==3.2.3 # via psycopg @@ -615,14 +553,12 @@ ptyprocess==0.7.0 pure-eval==0.2.3 # via stack-data py==1.11.0 - # via feast (setup.py) py-cpuinfo==9.0.0 # via pytest-benchmark py4j==0.10.9.7 # via pyspark pyarrow==16.1.0 # via - # feast (setup.py) # dask-expr # db-dtypes # deltalake @@ -638,35 +574,28 @@ pyasn1==0.6.1 pyasn1-modules==0.4.1 # via google-auth pybindgen==0.22.1 - # via feast (setup.py) pycparser==2.22 # via cffi pydantic==2.9.2 # via - # feast (setup.py) # fastapi # great-expectations pydantic-core==2.23.4 # via pydantic pygments==2.18.0 # via - # feast (setup.py) # ipython # nbconvert # rich # sphinx pyjwt[crypto]==2.9.0 # via - # feast (setup.py) # msal # singlestoredb # snowflake-connector-python pymssql==2.3.1 - # via feast (setup.py) pymysql==1.1.1 - # via feast (setup.py) pyodbc==5.1.0 - # via feast (setup.py) pyopenssl==24.2.1 # via snowflake-connector-python pyparsing==3.1.4 @@ -676,10 +605,8 @@ pyproject-hooks==1.1.0 # build # pip-tools pyspark==3.5.2 - # via feast (setup.py) pytest==7.4.4 # via - # feast (setup.py) # pytest-benchmark # pytest-cov # pytest-env @@ -689,21 +616,13 @@ pytest==7.4.4 # pytest-timeout # pytest-xdist pytest-benchmark==3.4.1 - # via feast (setup.py) pytest-cov==5.0.0 - # via feast (setup.py) pytest-env==1.1.3 - # via feast (setup.py) pytest-lazy-fixture==0.6.3 - # via feast (setup.py) pytest-mock==1.10.4 - # via feast (setup.py) pytest-ordering==0.6 - # via feast (setup.py) pytest-timeout==1.4.2 - # via feast (setup.py) pytest-xdist==3.6.1 - # via feast (setup.py) python-dateutil==2.9.0.post0 # via # arrow @@ -721,7 +640,6 @@ python-dotenv==1.0.1 python-json-logger==2.0.7 # via jupyter-events python-keycloak==4.2.2 - # via feast (setup.py) pytz==2024.2 # via # great-expectations @@ -731,7 +649,6 @@ pytz==2024.2 # trino pyyaml==6.0.2 # via - # feast (setup.py) # dask # ibis-substrait # jupyter-events @@ -745,19 +662,15 @@ pyzmq==26.2.0 # jupyter-client # jupyter-server redis==4.6.0 - # via feast (setup.py) referencing==0.35.1 # via # jsonschema # jsonschema-specifications # jupyter-events regex==2024.9.11 - # via - # feast (setup.py) - # parsimonious + # via parsimonious requests==2.32.3 # via - # feast (setup.py) # azure-core # docker # google-api-core @@ -802,8 +715,7 @@ ruamel-yaml==0.17.40 # via great-expectations ruamel-yaml-clib==0.2.8 # via ruamel-yaml -ruff==0.6.5 - # via feast (setup.py) +ruff==0.6.6 s3transfer==0.10.2 # via boto3 scipy==1.13.1 @@ -818,7 +730,6 @@ setuptools==75.1.0 # pip-tools # singlestoredb singlestoredb==1.6.3 - # via feast (setup.py) six==1.16.0 # via # asttokens @@ -839,13 +750,11 @@ sniffio==1.3.1 snowballstemmer==2.2.0 # via sphinx snowflake-connector-python[pandas]==3.12.2 - # via feast (setup.py) sortedcontainers==2.4.0 # via snowflake-connector-python soupsieve==2.6 # via beautifulsoup4 sphinx==6.2.1 - # via feast (setup.py) sphinxcontrib-applehelp==2.0.0 # via sphinx sphinxcontrib-devhelp==2.0.0 @@ -859,11 +768,9 @@ sphinxcontrib-qthelp==2.0.0 sphinxcontrib-serializinghtml==2.0.0 # via sphinx sqlalchemy[mypy]==2.0.35 - # via feast (setup.py) sqlglot==23.12.2 # via ibis-framework sqlite-vec==0.1.1 - # via feast (setup.py) sqlparams==6.1.0 # via singlestoredb stack-data==0.6.3 @@ -873,21 +780,17 @@ starlette==0.38.5 substrait==0.23.0 # via ibis-substrait tabulate==0.9.0 - # via feast (setup.py) tenacity==8.5.0 - # via feast (setup.py) terminado==0.18.1 # via # jupyter-server # jupyter-server-terminals testcontainers==4.4.0 - # via feast (setup.py) thriftpy2==0.5.2 # via happybase tinycss2==1.3.0 # via nbconvert toml==0.10.2 - # via feast (setup.py) tomli==2.0.1 # via # build @@ -915,9 +818,7 @@ tornado==6.4.1 # notebook # terminado tqdm==4.66.5 - # via - # feast (setup.py) - # great-expectations + # via great-expectations traitlets==5.14.3 # via # comm @@ -934,37 +835,23 @@ traitlets==5.14.3 # nbconvert # nbformat trino==0.329.0 - # via feast (setup.py) typeguard==4.3.0 - # via feast (setup.py) types-cffi==1.16.0.20240331 # via types-pyopenssl types-protobuf==3.19.22 - # via - # feast (setup.py) - # mypy-protobuf + # via mypy-protobuf types-pymysql==1.1.0.20240524 - # via feast (setup.py) types-pyopenssl==24.1.0.20240722 # via types-redis types-python-dateutil==2.9.0.20240906 - # via - # feast (setup.py) - # arrow + # via arrow types-pytz==2024.2.0.20240913 - # via feast (setup.py) types-pyyaml==6.0.12.20240917 - # via feast (setup.py) types-redis==4.6.0.20240903 - # via feast (setup.py) types-requests==2.30.0.0 - # via feast (setup.py) types-setuptools==75.1.0.20240917 - # via - # feast (setup.py) - # types-cffi + # via types-cffi types-tabulate==0.9.0.20240106 - # via feast (setup.py) types-urllib3==1.26.25.14 # via types-requests typing-extensions==4.12.2 @@ -1002,7 +889,6 @@ uri-template==1.3.0 # via jsonschema urllib3==1.26.20 # via - # feast (setup.py) # botocore # docker # elastic-transport @@ -1014,13 +900,10 @@ urllib3==1.26.20 # snowflake-connector-python # testcontainers uvicorn[standard]==0.30.6 - # via feast (setup.py) uvloop==0.20.0 # via uvicorn virtualenv==20.23.0 - # via - # feast (setup.py) - # pre-commit + # via pre-commit watchfiles==0.24.0 # via uvicorn wcwidth==0.2.13 diff --git a/sdk/python/requirements/py3.9-requirements.txt b/sdk/python/requirements/py3.9-requirements.txt index 7ffef84b23..857d7d72bf 100644 --- a/sdk/python/requirements/py3.9-requirements.txt +++ b/sdk/python/requirements/py3.9-requirements.txt @@ -11,36 +11,30 @@ attrs==24.2.0 # jsonschema # referencing bigtree==0.21.1 - # via feast (setup.py) certifi==2024.8.30 # via requests charset-normalizer==3.3.2 # via requests click==8.1.7 # via - # feast (setup.py) # dask # uvicorn cloudpickle==3.0.0 # via dask colorama==0.4.6 - # via feast (setup.py) dask[dataframe]==2024.8.0 - # via - # feast (setup.py) - # dask-expr + # via dask-expr dask-expr==1.1.10 # via dask dill==0.3.8 - # via feast (setup.py) exceptiongroup==1.2.2 # via anyio fastapi==0.115.0 - # via feast (setup.py) fsspec==2024.9.0 # via dask +greenlet==3.1.0 + # via sqlalchemy gunicorn==23.0.0 - # via feast (setup.py) h11==0.14.0 # via uvicorn httptools==0.6.1 @@ -54,9 +48,7 @@ importlib-metadata==8.5.0 # dask # typeguard jinja2==3.1.4 - # via feast (setup.py) jsonschema==4.23.0 - # via feast (setup.py) jsonschema-specifications==2023.12.1 # via jsonschema locket==1.0.0 @@ -64,14 +56,12 @@ locket==1.0.0 markupsafe==2.1.5 # via jinja2 mmh3==5.0.0 - # via feast (setup.py) mypy==1.11.2 # via sqlalchemy mypy-extensions==1.0.0 # via mypy numpy==1.26.4 # via - # feast (setup.py) # dask # pandas # pyarrow @@ -81,31 +71,21 @@ packaging==24.1 # gunicorn pandas==2.2.2 # via - # feast (setup.py) # dask # dask-expr partd==1.4.2 # via dask prometheus-client==0.20.0 - # via feast (setup.py) protobuf==4.25.5 - # via feast (setup.py) psutil==6.0.0 - # via feast (setup.py) pyarrow==17.0.0 - # via - # feast (setup.py) - # dask-expr + # via dask-expr pydantic==2.9.2 - # via - # feast (setup.py) - # fastapi + # via fastapi pydantic-core==2.23.4 # via pydantic pygments==2.18.0 - # via feast (setup.py) pyjwt==2.9.0 - # via feast (setup.py) python-dateutil==2.9.0.post0 # via pandas python-dotenv==1.0.1 @@ -114,7 +94,6 @@ pytz==2024.2 # via pandas pyyaml==6.0.2 # via - # feast (setup.py) # dask # uvicorn referencing==0.35.1 @@ -122,7 +101,6 @@ referencing==0.35.1 # jsonschema # jsonschema-specifications requests==2.32.3 - # via feast (setup.py) rpds-py==0.20.0 # via # jsonschema @@ -132,15 +110,11 @@ six==1.16.0 sniffio==1.3.1 # via anyio sqlalchemy[mypy]==2.0.35 - # via feast (setup.py) starlette==0.38.5 # via fastapi tabulate==0.9.0 - # via feast (setup.py) tenacity==8.5.0 - # via feast (setup.py) toml==0.10.2 - # via feast (setup.py) tomli==2.0.1 # via mypy toolz==0.12.1 @@ -148,9 +122,7 @@ toolz==0.12.1 # dask # partd tqdm==4.66.5 - # via feast (setup.py) typeguard==4.3.0 - # via feast (setup.py) typing-extensions==4.12.2 # via # anyio @@ -167,7 +139,6 @@ tzdata==2024.1 urllib3==2.2.3 # via requests uvicorn[standard]==0.30.6 - # via feast (setup.py) uvloop==0.20.0 # via uvicorn watchfiles==0.24.0 diff --git a/setup.py b/setup.py index 5a6f18db35..c62fb8c50f 100644 --- a/setup.py +++ b/setup.py @@ -11,20 +11,12 @@ # 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 glob import os import pathlib import re import shutil -import subprocess -import sys -from pathlib import Path -from setuptools import Command, find_packages, setup -from setuptools.command.build_ext import build_ext as _build_ext -from setuptools.command.build_py import build_py -from setuptools.command.develop import develop -from setuptools.command.install import install +from setuptools import find_packages, setup NAME = "feast" DESCRIPTION = "Python SDK for Feast" @@ -157,7 +149,6 @@ "virtualenv==20.23.0", "cryptography>=35.0,<43", "ruff>=0.3.3", - "protobuf<5", "mypy-protobuf>=3.1", "grpcio-tools>=1.56.2,<2", "grpcio-testing>=1.56.2,<2", @@ -244,107 +235,8 @@ else: use_scm_version = None -PROTO_SUBDIRS = ["core", "registry", "serving", "types", "storage"] PYTHON_CODE_PREFIX = "sdk/python" - -class BuildPythonProtosCommand(Command): - description = "Builds the proto files into Python files." - user_options = [ - ("inplace", "i", "Write generated proto files to source directory."), - ] - - def initialize_options(self): - self.python_protoc = [ - sys.executable, - "-m", - "grpc_tools.protoc", - ] # find_executable("protoc") - self.proto_folder = os.path.join(repo_root, "protos") - self.sub_folders = PROTO_SUBDIRS - self.build_lib = None - self.inplace = 0 - - def finalize_options(self): - self.set_undefined_options("build", ("build_lib", "build_lib")) - - @property - def python_folder(self): - if self.inplace: - return os.path.join( - os.path.dirname(__file__) or os.getcwd(), "sdk/python/feast/protos" - ) - - return os.path.join(self.build_lib, "feast/protos") - - def _generate_python_protos(self, path: str): - proto_files = glob.glob(os.path.join(self.proto_folder, path)) - Path(self.python_folder).mkdir(parents=True, exist_ok=True) - subprocess.check_call( - self.python_protoc - + [ - "-I", - self.proto_folder, - "--python_out", - self.python_folder, - "--grpc_python_out", - self.python_folder, - "--mypy_out", - self.python_folder, - ] - + proto_files - ) - - def run(self): - for sub_folder in self.sub_folders: - self._generate_python_protos(f"feast/{sub_folder}/*.proto") - # We need the __init__ files for each of the generated subdirs - # so that they are regular packages, and don't need the `--namespace-packages` flags - # when being typechecked using mypy. - with open(f"{self.python_folder}/feast/{sub_folder}/__init__.py", "w"): - pass - - with open(f"{self.python_folder}/__init__.py", "w"): - pass - with open(f"{self.python_folder}/feast/__init__.py", "w"): - pass - - for path in Path(self.python_folder).rglob("*.py"): - for folder in self.sub_folders: - # Read in the file - with open(path, "r") as file: - filedata = file.read() - - # Replace the target string - filedata = filedata.replace( - f"from feast.{folder}", f"from feast.protos.feast.{folder}" - ) - - # Write the file out again - with open(path, "w") as file: - file.write(filedata) - - -class BuildCommand(build_py): - """Custom build command.""" - - def run(self): - self.run_command("build_python_protos") - - self.run_command("build_ext") - build_py.run(self) - - -class DevelopCommand(develop): - """Custom develop command.""" - - def run(self): - self.reinitialize_command("build_python_protos", inplace=1) - self.run_command("build_python_protos") - - develop.run(self) - - setup( name=NAME, author=AUTHOR, @@ -358,8 +250,6 @@ def run(self): ), package_dir={"": PYTHON_CODE_PREFIX}, install_requires=REQUIRED, - # https://stackoverflow.com/questions/28509965/setuptools-development-requirements - # Install dev requirements with: pip install -e .[dev] extras_require={ "dev": DEV_REQUIRED, "ci": CI_REQUIRED, @@ -402,17 +292,7 @@ def run(self): entry_points={"console_scripts": ["feast=feast.cli:cli"]}, use_scm_version=use_scm_version, setup_requires=[ - # snowflake udf packages refer to conda packages, not pypi libraries. Conda stack is still on protobuf 4 - # So we are adding protobuf<5 as a requirement - "protobuf<5", - "grpcio-tools>=1.56.2,<2", - "mypy-protobuf>=3.1", - "pybindgen==0.22.0", - "setuptools_scm>=6.2", - ], - cmdclass={ - "build_python_protos": BuildPythonProtosCommand, - "build_py": BuildCommand, - "develop": DevelopCommand, - }, + "pybindgen==0.22.0", #TODO do we need this? + "setuptools_scm>=6.2", #TODO do we need this? + ] ) From f308572715d0593951f71bb3da5c5be6de29a2f9 Mon Sep 17 00:00:00 2001 From: Tornike Gurgenidze Date: Sun, 22 Sep 2024 22:12:23 +0400 Subject: [PATCH 092/185] fix: Import grpc only for type checking in errors.py (#4533) * fix: grpc import error Signed-off-by: tokoko * fix: loosen protobuf build requirement Signed-off-by: tokoko * fix: pin grpcio-tools version Signed-off-by: tokoko * fix: revert build-system in pyproject Signed-off-by: tokoko * fix: add manual install of setuptools and grpcio-tools Signed-off-by: tokoko * fix: remove incorrect pixi call Signed-off-by: tokoko * fix: add in-function imports in errors.py Signed-off-by: tokoko * fix: formatting Signed-off-by: tokoko * fix: merge changes from master Signed-off-by: tokoko * fix: add line endings Signed-off-by: tokoko * fix: add line endings Signed-off-by: tokoko * chore: remove proto generation from make commands Signed-off-by: tokoko --------- Signed-off-by: tokoko --- .devcontainer/devcontainer.json | 4 ++-- .github/workflows/smoke_tests.yml | 38 +++++++++++++++++++++++++++++++ Makefile | 8 +++++++ pyproject.toml | 1 - sdk/python/feast/errors.py | 18 +++++++++++---- 5 files changed, 61 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/smoke_tests.yml diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 04fcbb00aa..1b15dcf882 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -17,13 +17,13 @@ "ghcr.io/devcontainers-contrib/features/maven-sdkman:2": { "jdkVersion": "11.0.24-amzn" } - }, + } // Use 'forwardPorts' to make a list of ports inside the container available locally. // "forwardPorts": [], // Uncomment the next line to run commands after the container is created. - "postCreateCommand": "make install-python-ci-dependencies-uv-venv" + // "postCreateCommand": "make install-python-ci-dependencies-uv-venv" // Configure tool-specific properties. // "customizations": {}, diff --git a/.github/workflows/smoke_tests.yml b/.github/workflows/smoke_tests.yml new file mode 100644 index 0000000000..782f8b3f51 --- /dev/null +++ b/.github/workflows/smoke_tests.yml @@ -0,0 +1,38 @@ +name: smoke-tests + +on: [pull_request] +jobs: + unit-test-python: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + python-version: [ "3.9", "3.10", "3.11"] + os: [ ubuntu-latest ] + env: + OS: ${{ matrix.os }} + PYTHON: ${{ matrix.python-version }} + steps: + - uses: actions/checkout@v4 + - name: Setup Python + id: setup-python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + architecture: x64 + - name: Install uv + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + - name: Get uv cache dir + id: uv-cache + run: | + echo "::set-output name=dir::$(uv cache dir)" + - name: uv cache + uses: actions/cache@v4 + with: + path: ${{ steps.uv-cache.outputs.dir }} + key: ${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-uv-${{ hashFiles(format('**/py{0}-ci-requirements.txt', env.PYTHON)) }} + - name: Install dependencies + run: make install-python-dependencies-uv + - name: Test Imports + run: python -c "from feast import cli" \ No newline at end of file diff --git a/Makefile b/Makefile index f4b34124f7..6831a58337 100644 --- a/Makefile +++ b/Makefile @@ -37,6 +37,14 @@ build: protos build-java build-docker # Python SDK +install-python-dependencies-uv: + uv pip sync --system sdk/python/requirements/py$(PYTHON_VERSION)-requirements.txt + uv pip install --system --no-deps . + +install-python-dependencies-uv-venv: + uv pip sync sdk/python/requirements/py$(PYTHON_VERSION)-requirements.txt + uv pip install --no-deps . + install-python-ci-dependencies: python -m piptools sync sdk/python/requirements/py$(PYTHON_VERSION)-ci-requirements.txt pip install --no-deps -e . diff --git a/pyproject.toml b/pyproject.toml index d772bab9ea..2a051231e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,6 @@ requires = [ "sphinx!=4.0.0", "wheel", ] -build-backend = "setuptools.build_meta" [tool.setuptools_scm] # Including this section is comparable to supplying use_scm_version=True in setup.py. diff --git a/sdk/python/feast/errors.py b/sdk/python/feast/errors.py index 4dbb220c1e..11ce9ebc62 100644 --- a/sdk/python/feast/errors.py +++ b/sdk/python/feast/errors.py @@ -1,11 +1,13 @@ import importlib import json import logging -from typing import Any, List, Optional, Set +from typing import TYPE_CHECKING, Any, List, Optional, Set from colorama import Fore, Style from fastapi import status as HttpStatusCode -from grpc import StatusCode as GrpcStatusCode + +if TYPE_CHECKING: + from grpc import StatusCode as GrpcStatusCode from feast.field import Field @@ -15,7 +17,9 @@ class FeastError(Exception): pass - def grpc_status_code(self) -> GrpcStatusCode: + def grpc_status_code(self) -> "GrpcStatusCode": + from grpc import StatusCode as GrpcStatusCode + return GrpcStatusCode.INTERNAL def http_status_code(self) -> int: @@ -89,7 +93,9 @@ def __init__(self, ds_name: str): class FeastObjectNotFoundException(FeastError): pass - def grpc_status_code(self) -> GrpcStatusCode: + def grpc_status_code(self) -> "GrpcStatusCode": + from grpc import StatusCode as GrpcStatusCode + return GrpcStatusCode.NOT_FOUND def http_status_code(self) -> int: @@ -504,7 +510,9 @@ class FeastPermissionError(FeastError, PermissionError): def __init__(self, details: str): super().__init__(f"Permission error:\n{details}") - def grpc_status_code(self) -> GrpcStatusCode: + def grpc_status_code(self) -> "GrpcStatusCode": + from grpc import StatusCode as GrpcStatusCode + return GrpcStatusCode.PERMISSION_DENIED def http_status_code(self) -> int: From 42936084a7d214d65faea5359ae70eefda8d23ad Mon Sep 17 00:00:00 2001 From: Harri Lehtola <1781172+peruukki@users.noreply.github.com> Date: Sun, 22 Sep 2024 21:21:25 +0300 Subject: [PATCH 093/185] fix: Update react-router-dom to 6.3.0 and restrict its version in Feast UI (#4556) As noted in #3794, Feast UI is not compatible with latest react-router-dom versions, more precisely from 6.4.0 onwards. Limit react-router-dom version to a compatible range to avoid the runtime errors mentioned in the issue when installing peer dependencies without specifying exact versions. After setting the restricted versions, `yarn install` updated `react-router-dom` to the latest compatible version 6.3.0. It should be a minor upgrade (https://github.com/remix-run/react-router/blob/main/CHANGELOG.md#v630), and I didn't notice anything not working in the UI after it. Signed-off-by: Harri Lehtola --- ui/package.json | 4 ++-- ui/yarn.lock | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/ui/package.json b/ui/package.json index 978be97b88..bc2a71378a 100644 --- a/ui/package.json +++ b/ui/package.json @@ -20,7 +20,7 @@ "react": "^17.0.2", "react-dom": "^17.0.2", "react-query": "^3.34.12", - "react-router-dom": "6", + "react-router-dom": "<6.4.0", "react-scripts": "^5.0.0", "use-query-params": "^1.2.3", "zod": "^3.11.6" @@ -37,7 +37,7 @@ "query-string": "^7.1.1", "react-code-blocks": "^0.0.9-0", "react-query": "^3.34.12", - "react-router-dom": "6", + "react-router-dom": "<6.4.0", "react-scripts": "^5.0.0", "tslib": "^2.3.1", "use-query-params": "^1.2.3", diff --git a/ui/yarn.lock b/ui/yarn.lock index 26c833fa11..02f6634914 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -9345,18 +9345,18 @@ react-remove-scroll@^2.5.2: use-callback-ref "^1.3.0" use-sidecar "^1.1.2" -react-router-dom@6: - version "6.2.1" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.2.1.tgz#32ec81829152fbb8a7b045bf593a22eadf019bec" - integrity sha512-I6Zax+/TH/cZMDpj3/4Fl2eaNdcvoxxHoH1tYOREsQ22OKDYofGebrNm6CTPUcvLvZm63NL/vzCYdjf9CUhqmA== +react-router-dom@<6.4.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.3.0.tgz#a0216da813454e521905b5fa55e0e5176123f43d" + integrity sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw== dependencies: history "^5.2.0" - react-router "6.2.1" + react-router "6.3.0" -react-router@6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.2.1.tgz#be2a97a6006ce1d9123c28934e604faef51448a3" - integrity sha512-2fG0udBtxou9lXtK97eJeET2ki5//UWfQSl1rlJ7quwe6jrktK9FCCc8dQb5QY6jAv3jua8bBQRhhDOM/kVRsg== +react-router@6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557" + integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ== dependencies: history "^5.2.0" From 6dee6887418d7f9a9bc22f045a087c048e63eb37 Mon Sep 17 00:00:00 2001 From: Harri Lehtola <1781172+peruukki@users.noreply.github.com> Date: Sun, 22 Sep 2024 21:21:44 +0300 Subject: [PATCH 094/185] docs: Fix small typos in Feast UI projects list documentation (#4555) I initially created a `project-list.json` file, then found out it should be `projects-list.json`. Then spotted another tiny typo, so fixed it too. Signed-off-by: Harri Lehtola --- docs/reference/alpha-web-ui.md | 2 +- ui/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/alpha-web-ui.md b/docs/reference/alpha-web-ui.md index 398c8de0ae..2caeed9e2a 100644 --- a/docs/reference/alpha-web-ui.md +++ b/docs/reference/alpha-web-ui.md @@ -70,7 +70,7 @@ ReactDOM.render( ); ``` -When you start the React app, it will look for `project-list.json` to find a list of your projects. The JSON should looks something like this. +When you start the React app, it will look for `projects-list.json` to find a list of your projects. The JSON should look something like this. ```json { diff --git a/ui/README.md b/ui/README.md index 12aacd329e..852bddc296 100644 --- a/ui/README.md +++ b/ui/README.md @@ -46,7 +46,7 @@ ReactDOM.render( ); ``` -When you start the React app, it will look for `projects-list.json` to find a list of your projects. The JSON should looks something like this. +When you start the React app, it will look for `projects-list.json` to find a list of your projects. The JSON should look something like this. ```json { From 8ffb97ad2d368fb1403a6dbba9c5b4012bb69752 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 22 Sep 2024 22:23:16 +0400 Subject: [PATCH 095/185] chore: Bump webpack from 5.76.1 to 5.94.0 in /ui (#4492) Bumps [webpack](https://github.com/webpack/webpack) from 5.76.1 to 5.94.0. - [Release notes](https://github.com/webpack/webpack/releases) - [Commits](https://github.com/webpack/webpack/compare/v5.76.1...v5.94.0) --- updated-dependencies: - dependency-name: webpack dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- ui/yarn.lock | 440 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 281 insertions(+), 159 deletions(-) diff --git a/ui/yarn.lock b/ui/yarn.lock index 02f6634914..1f36143b67 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -1627,16 +1627,35 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.9" +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" + integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.24" + "@jridgewell/resolve-uri@3.1.0", "@jridgewell/resolve-uri@^3.0.3": version "3.1.0" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + "@jridgewell/set-array@^1.0.1": version "1.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + "@jridgewell/source-map@^0.3.2": version "0.3.2" resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb" @@ -1645,11 +1664,24 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" +"@jridgewell/source-map@^0.3.3": + version "0.3.6" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.6.tgz#9d71ca886e32502eb9362c9a74a46787c36df81a" + integrity sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + "@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10": version "1.4.14" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== +"@jridgewell/sourcemap-codec@^1.4.14": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + "@jridgewell/trace-mapping@^0.3.0": version "0.3.4" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.4.tgz#f6a0832dffd5b8a6aaa633b7d9f8e8e94c83a0c3" @@ -1666,6 +1698,14 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" +"@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + "@jridgewell/trace-mapping@^0.3.9": version "0.3.14" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz#b231a081d8f66796e475ad588a1ef473112701ed" @@ -2362,22 +2402,6 @@ "@types/d3-transition" "*" "@types/d3-zoom" "*" -"@types/eslint-scope@^3.7.3": - version "3.7.4" - resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16" - integrity sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA== - dependencies: - "@types/eslint" "*" - "@types/estree" "*" - -"@types/eslint@*": - version "8.4.1" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.4.1.tgz#c48251553e8759db9e656de3efc846954ac32304" - integrity sha512-GE44+DNEyxxh2Kc6ro/VkIj+9ma0pO0bwv9+uHSyBrikYOHr8zYcdPvnBOp1aw8s+CjRvuSx7CyWqRrNFQ59mA== - dependencies: - "@types/estree" "*" - "@types/json-schema" "*" - "@types/eslint@^7.28.2": version "7.29.0" resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.29.0.tgz#e56ddc8e542815272720bb0b4ccc2aff9c3e1c78" @@ -2396,10 +2420,10 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== -"@types/estree@^0.0.51": - version "0.0.51" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" - integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== +"@types/estree@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" + integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== "@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.18": version "4.17.28" @@ -2873,125 +2897,125 @@ "@typescript-eslint/types" "5.10.1" eslint-visitor-keys "^3.0.0" -"@webassemblyjs/ast@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7" - integrity sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw== +"@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.12.1.tgz#bb16a0e8b1914f979f45864c23819cc3e3f0d4bb" + integrity sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg== dependencies: - "@webassemblyjs/helper-numbers" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/helper-numbers" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" -"@webassemblyjs/floating-point-hex-parser@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz#f6c61a705f0fd7a6aecaa4e8198f23d9dc179e4f" - integrity sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ== +"@webassemblyjs/floating-point-hex-parser@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz#dacbcb95aff135c8260f77fa3b4c5fea600a6431" + integrity sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw== -"@webassemblyjs/helper-api-error@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz#1a63192d8788e5c012800ba6a7a46c705288fd16" - integrity sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg== +"@webassemblyjs/helper-api-error@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" + integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== -"@webassemblyjs/helper-buffer@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz#832a900eb444884cde9a7cad467f81500f5e5ab5" - integrity sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA== +"@webassemblyjs/helper-buffer@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz#6df20d272ea5439bf20ab3492b7fb70e9bfcb3f6" + integrity sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw== -"@webassemblyjs/helper-numbers@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz#64d81da219fbbba1e3bd1bfc74f6e8c4e10a62ae" - integrity sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ== +"@webassemblyjs/helper-numbers@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz#cbce5e7e0c1bd32cf4905ae444ef64cea919f1b5" + integrity sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g== dependencies: - "@webassemblyjs/floating-point-hex-parser" "1.11.1" - "@webassemblyjs/helper-api-error" "1.11.1" + "@webassemblyjs/floating-point-hex-parser" "1.11.6" + "@webassemblyjs/helper-api-error" "1.11.6" "@xtuc/long" "4.2.2" -"@webassemblyjs/helper-wasm-bytecode@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz#f328241e41e7b199d0b20c18e88429c4433295e1" - integrity sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q== +"@webassemblyjs/helper-wasm-bytecode@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" + integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== -"@webassemblyjs/helper-wasm-section@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz#21ee065a7b635f319e738f0dd73bfbda281c097a" - integrity sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg== +"@webassemblyjs/helper-wasm-section@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz#3da623233ae1a60409b509a52ade9bc22a37f7bf" + integrity sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g== dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-buffer" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/wasm-gen" "1.11.1" + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/wasm-gen" "1.12.1" -"@webassemblyjs/ieee754@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz#963929e9bbd05709e7e12243a099180812992614" - integrity sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ== +"@webassemblyjs/ieee754@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz#bb665c91d0b14fffceb0e38298c329af043c6e3a" + integrity sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg== dependencies: "@xtuc/ieee754" "^1.2.0" -"@webassemblyjs/leb128@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.1.tgz#ce814b45574e93d76bae1fb2644ab9cdd9527aa5" - integrity sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw== +"@webassemblyjs/leb128@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz#70e60e5e82f9ac81118bc25381a0b283893240d7" + integrity sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ== dependencies: "@xtuc/long" "4.2.2" -"@webassemblyjs/utf8@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.1.tgz#d1f8b764369e7c6e6bae350e854dec9a59f0a3ff" - integrity sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ== - -"@webassemblyjs/wasm-edit@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz#ad206ebf4bf95a058ce9880a8c092c5dec8193d6" - integrity sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-buffer" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/helper-wasm-section" "1.11.1" - "@webassemblyjs/wasm-gen" "1.11.1" - "@webassemblyjs/wasm-opt" "1.11.1" - "@webassemblyjs/wasm-parser" "1.11.1" - "@webassemblyjs/wast-printer" "1.11.1" - -"@webassemblyjs/wasm-gen@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz#86c5ea304849759b7d88c47a32f4f039ae3c8f76" - integrity sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/ieee754" "1.11.1" - "@webassemblyjs/leb128" "1.11.1" - "@webassemblyjs/utf8" "1.11.1" - -"@webassemblyjs/wasm-opt@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz#657b4c2202f4cf3b345f8a4c6461c8c2418985f2" - integrity sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-buffer" "1.11.1" - "@webassemblyjs/wasm-gen" "1.11.1" - "@webassemblyjs/wasm-parser" "1.11.1" - -"@webassemblyjs/wasm-parser@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz#86ca734534f417e9bd3c67c7a1c75d8be41fb199" - integrity sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-api-error" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/ieee754" "1.11.1" - "@webassemblyjs/leb128" "1.11.1" - "@webassemblyjs/utf8" "1.11.1" - -"@webassemblyjs/wast-printer@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz#d0c73beda8eec5426f10ae8ef55cee5e7084c2f0" - integrity sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg== - dependencies: - "@webassemblyjs/ast" "1.11.1" +"@webassemblyjs/utf8@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" + integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== + +"@webassemblyjs/wasm-edit@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz#9f9f3ff52a14c980939be0ef9d5df9ebc678ae3b" + integrity sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/helper-wasm-section" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-opt" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" + "@webassemblyjs/wast-printer" "1.12.1" + +"@webassemblyjs/wasm-gen@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz#a6520601da1b5700448273666a71ad0a45d78547" + integrity sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wasm-opt@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz#9e6e81475dfcfb62dab574ac2dda38226c232bc5" + integrity sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" + +"@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz#c47acb90e6f083391e3fa61d113650eea1e95937" + integrity sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-api-error" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wast-printer@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz#bcecf661d7d1abdaf989d8341a4833e33e2b31ac" + integrity sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA== + dependencies: + "@webassemblyjs/ast" "1.12.1" "@xtuc/long" "4.2.2" "@xmldom/xmldom@^0.7.2": @@ -3030,10 +3054,10 @@ acorn-globals@^6.0.0: acorn "^7.1.1" acorn-walk "^7.1.1" -acorn-import-assertions@^1.7.6: - version "1.8.0" - resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz#ba2b5939ce62c238db6d93d81c9b111b29b855e9" - integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw== +acorn-import-attributes@^1.9.5: + version "1.9.5" + resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef" + integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ== acorn-jsx@^5.3.1, acorn-jsx@^5.3.2: version "5.3.2" @@ -3074,6 +3098,11 @@ acorn@^8.8.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w== +acorn@^8.8.2: + version "8.12.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" + integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== + address@^1.0.1, address@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/address/-/address-1.1.2.tgz#bf1116c9c758c51b7a933d296b72c221ed9428b6" @@ -3629,7 +3658,7 @@ browser-process-hrtime@^1.0.0: resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== -browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.16.6, browserslist@^4.17.5, browserslist@^4.18.1, browserslist@^4.19.1: +browserslist@^4.0.0, browserslist@^4.16.6, browserslist@^4.17.5, browserslist@^4.18.1, browserslist@^4.19.1: version "4.19.1" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.19.1.tgz#4ac0435b35ab655896c31d53018b6dd5e9e4c9a3" integrity sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A== @@ -3640,6 +3669,16 @@ browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.16.6, browserslist@^4 node-releases "^2.0.1" picocolors "^1.0.0" +browserslist@^4.21.10: + version "4.23.3" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.3.tgz#debb029d3c93ebc97ffbc8d9cbb03403e227c800" + integrity sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA== + dependencies: + caniuse-lite "^1.0.30001646" + electron-to-chromium "^1.5.4" + node-releases "^2.0.18" + update-browserslist-db "^1.1.0" + bser@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" @@ -3736,6 +3775,11 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001286, caniuse-lite@^1.0.30001297, can resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001416.tgz" integrity sha512-06wzzdAkCPZO+Qm4e/eNghZBDfVNDsCgw33T27OwBH9unE9S478OYw//Q2L7Npf/zBzs7rjZOszIFQkwQKAEqA== +caniuse-lite@^1.0.30001646: + version "1.0.30001657" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001657.tgz#29fd504bffca719d1c6b63a1f6f840be1973a660" + integrity sha512-DPbJAlP8/BAXy3IgiWmZKItubb3TYGP0WscQQlVGIfT4s/YlFYVuJgyOsQNP7rJRChx/qdMeLJQJP0Sgg2yjNA== + case-sensitive-paths-webpack-plugin@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz#db64066c6422eed2e08cc14b986ca43796dbc6d4" @@ -4997,6 +5041,11 @@ electron-to-chromium@^1.4.17: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.57.tgz#2b2766df76ac8dbc0a1d41249bc5684a31849892" integrity sha512-FNC+P5K1n6pF+M0zIK+gFCoXcJhhzDViL3DRIGy2Fv5PohuSES1JHR7T+GlwxSxlzx4yYbsuzCZvHxcBSRCIOw== +electron-to-chromium@^1.5.4: + version "1.5.15" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.15.tgz#3c969a29b03682db7a3032283ec8be6e75effe50" + integrity sha512-Z4rIDoImwEJW+YYKnPul4DzqsWVqYetYVN3XqDmRpgV0mjz0hYTaeeh+8/9CL1bk3AHYmF4freW/NTiVoXA2gA== + emittery@^0.8.1: version "0.8.1" resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.8.1.tgz#bb23cc86d03b30aa75a7f734819dee2e1ba70860" @@ -5027,10 +5076,10 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== -enhanced-resolve@^5.10.0: - version "5.12.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz#300e1c90228f5b570c4d35babf263f6da7155634" - integrity sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ== +enhanced-resolve@^5.17.1: + version "5.17.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" + integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" @@ -5085,10 +5134,10 @@ es-abstract@^1.17.2, es-abstract@^1.19.0, es-abstract@^1.19.1: string.prototype.trimstart "^1.0.4" unbox-primitive "^1.0.1" -es-module-lexer@^0.9.0: - version "0.9.3" - resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19" - integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== +es-module-lexer@^1.2.1: + version "1.5.4" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.4.tgz#a8efec3a3da991e60efa6b633a7cad6ab8d26b78" + integrity sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw== es-to-primitive@^1.2.1: version "1.2.1" @@ -5104,6 +5153,11 @@ escalade@^3.1.1: resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== +escalade@^3.1.2: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" @@ -5970,7 +6024,7 @@ good-listener@^1.2.2: dependencies: delegate "^3.1.2" -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6: version "4.2.9" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== @@ -5980,6 +6034,11 @@ graceful-fs@^4.1.9: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== +graceful-fs@^4.2.11: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + graphql@^15.5.1: version "15.8.0" resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.8.0.tgz#33410e96b012fa3bdb1091cc99a94769db212b38" @@ -7228,6 +7287,15 @@ jest-worker@^27.0.2, jest-worker@^27.3.1, jest-worker@^27.4.1, jest-worker@^27.4 merge-stream "^2.0.0" supports-color "^8.0.0" +jest-worker@^27.4.5: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + jest@^27.4.3: version "27.4.7" resolved "https://registry.yarnpkg.com/jest/-/jest-27.4.7.tgz#87f74b9026a1592f2da05b4d258e57505f28eca4" @@ -7983,6 +8051,11 @@ node-releases@^2.0.1: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.1.tgz#3d1d395f204f1f2f29a54358b9fb678765ad2fc5" integrity sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA== +node-releases@^2.0.18: + version "2.0.18" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" + integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== + normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" @@ -8406,6 +8479,11 @@ picocolors@^1.0.0: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== +picocolors@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.0.tgz#5358b76a78cde483ba5cef6a9dc9671440b27d59" + integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw== + picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3, picomatch@^2.3.0: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" @@ -9945,7 +10023,7 @@ schema-utils@^2.6.5: ajv "^6.12.4" ajv-keywords "^3.5.2" -schema-utils@^3.0.0, schema-utils@^3.1.0, schema-utils@^3.1.1: +schema-utils@^3.0.0, schema-utils@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281" integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== @@ -9954,6 +10032,15 @@ schema-utils@^3.0.0, schema-utils@^3.1.0, schema-utils@^3.1.1: ajv "^6.12.5" ajv-keywords "^3.5.2" +schema-utils@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" + integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + schema-utils@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.0.0.tgz#60331e9e3ae78ec5d16353c467c34b3a0a1d3df7" @@ -10031,6 +10118,13 @@ serialize-javascript@^6.0.0: dependencies: randombytes "^2.1.0" +serialize-javascript@^6.0.1: + version "6.0.2" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" + integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== + dependencies: + randombytes "^2.1.0" + serialize-query-params@^1.3.5: version "1.3.6" resolved "https://registry.yarnpkg.com/serialize-query-params/-/serialize-query-params-1.3.6.tgz#5dd5225db85ce747fe6fbc4897628504faafec6d" @@ -10626,7 +10720,7 @@ terminal-link@^2.0.0: ansi-escapes "^4.2.1" supports-hyperlinks "^2.0.0" -terser-webpack-plugin@^5.1.3, terser-webpack-plugin@^5.2.5: +terser-webpack-plugin@^5.2.5: version "5.3.0" resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.0.tgz#21641326486ecf91d8054161c816e464435bae9f" integrity sha512-LPIisi3Ol4chwAaPP8toUJ3L4qCM1G0wao7L3qNv57Drezxj6+VEyySpPw4B1HSO2Eg/hDY/MNF5XihCAoqnsQ== @@ -10637,6 +10731,17 @@ terser-webpack-plugin@^5.1.3, terser-webpack-plugin@^5.2.5: source-map "^0.6.1" terser "^5.7.2" +terser-webpack-plugin@^5.3.10: + version "5.3.10" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz#904f4c9193c6fd2a03f693a2150c62a92f40d199" + integrity sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w== + dependencies: + "@jridgewell/trace-mapping" "^0.3.20" + jest-worker "^27.4.5" + schema-utils "^3.1.1" + serialize-javascript "^6.0.1" + terser "^5.26.0" + terser@^5.0.0, terser@^5.10.0, terser@^5.7.2: version "5.14.2" resolved "https://registry.yarnpkg.com/terser/-/terser-5.14.2.tgz#9ac9f22b06994d736174f4091aa368db896f1c10" @@ -10647,6 +10752,16 @@ terser@^5.0.0, terser@^5.10.0, terser@^5.7.2: commander "^2.20.0" source-map-support "~0.5.20" +terser@^5.26.0: + version "5.31.6" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.31.6.tgz#c63858a0f0703988d0266a82fcbf2d7ba76422b1" + integrity sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.8.2" + commander "^2.20.0" + source-map-support "~0.5.20" + test-exclude@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" @@ -11062,6 +11177,14 @@ upath@^1.2.0: resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== +update-browserslist-db@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz#7ca61c0d8650766090728046e416a8cde682859e" + integrity sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ== + dependencies: + escalade "^3.1.2" + picocolors "^1.0.1" + uri-js@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" @@ -11205,10 +11328,10 @@ walker@^1.0.7: dependencies: makeerror "1.0.12" -watchpack@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" - integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== +watchpack@^2.4.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.2.tgz#2feeaed67412e7c33184e5a79ca738fbd38564da" + integrity sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw== dependencies: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" @@ -11328,33 +11451,32 @@ webpack-sources@^3.2.3: integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== webpack@^5.64.4: - version "5.76.1" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.76.1.tgz#7773de017e988bccb0f13c7d75ec245f377d295c" - integrity sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ== - dependencies: - "@types/eslint-scope" "^3.7.3" - "@types/estree" "^0.0.51" - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/wasm-edit" "1.11.1" - "@webassemblyjs/wasm-parser" "1.11.1" + version "5.94.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.94.0.tgz#77a6089c716e7ab90c1c67574a28da518a20970f" + integrity sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg== + dependencies: + "@types/estree" "^1.0.5" + "@webassemblyjs/ast" "^1.12.1" + "@webassemblyjs/wasm-edit" "^1.12.1" + "@webassemblyjs/wasm-parser" "^1.12.1" acorn "^8.7.1" - acorn-import-assertions "^1.7.6" - browserslist "^4.14.5" + acorn-import-attributes "^1.9.5" + browserslist "^4.21.10" chrome-trace-event "^1.0.2" - enhanced-resolve "^5.10.0" - es-module-lexer "^0.9.0" + enhanced-resolve "^5.17.1" + es-module-lexer "^1.2.1" eslint-scope "5.1.1" events "^3.2.0" glob-to-regexp "^0.4.1" - graceful-fs "^4.2.9" + graceful-fs "^4.2.11" json-parse-even-better-errors "^2.3.1" loader-runner "^4.2.0" mime-types "^2.1.27" neo-async "^2.6.2" - schema-utils "^3.1.0" + schema-utils "^3.2.0" tapable "^2.1.1" - terser-webpack-plugin "^5.1.3" - watchpack "^2.4.0" + terser-webpack-plugin "^5.3.10" + watchpack "^2.4.1" webpack-sources "^3.2.3" websocket-driver@>=0.5.1, websocket-driver@^0.7.4: From cb2aa2c310203f395da027c7ddf8cda0e59f8359 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 22 Sep 2024 22:23:53 +0400 Subject: [PATCH 096/185] chore: Bump micromatch from 4.0.5 to 4.0.8 in /sdk/python/feast/ui (#4448) Bumps [micromatch](https://github.com/micromatch/micromatch) from 4.0.5 to 4.0.8. - [Release notes](https://github.com/micromatch/micromatch/releases) - [Changelog](https://github.com/micromatch/micromatch/blob/4.0.8/CHANGELOG.md) - [Commits](https://github.com/micromatch/micromatch/compare/4.0.5...4.0.8) --- updated-dependencies: - dependency-name: micromatch dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdk/python/feast/ui/yarn.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sdk/python/feast/ui/yarn.lock b/sdk/python/feast/ui/yarn.lock index 452b6f9f31..1132d6ed0f 100644 --- a/sdk/python/feast/ui/yarn.lock +++ b/sdk/python/feast/ui/yarn.lock @@ -3593,7 +3593,7 @@ brace-expansion@^2.0.1: dependencies: balanced-match "^1.0.0" -braces@^3.0.2, braces@~3.0.2: +braces@^3.0.3, braces@~3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== @@ -7436,11 +7436,11 @@ methods@~1.1.2: integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= micromatch@^4.0.2, micromatch@^4.0.4: - version "4.0.5" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" - integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== dependencies: - braces "^3.0.2" + braces "^3.0.3" picomatch "^2.3.1" microseconds@0.2.0: From 792fb7325661a1dcbd1100b681ccdd58f396a1c4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Sep 2024 09:51:38 +0400 Subject: [PATCH 097/185] chore: Bump express from 4.19.2 to 4.21.0 in /sdk/python/feast/ui (#4522) Bumps [express](https://github.com/expressjs/express) from 4.19.2 to 4.21.0. - [Release notes](https://github.com/expressjs/express/releases) - [Changelog](https://github.com/expressjs/express/blob/4.21.0/History.md) - [Commits](https://github.com/expressjs/express/compare/4.19.2...4.21.0) --- updated-dependencies: - dependency-name: express dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdk/python/feast/ui/yarn.lock | 194 ++++++++++++++++++++++++++-------- 1 file changed, 150 insertions(+), 44 deletions(-) diff --git a/sdk/python/feast/ui/yarn.lock b/sdk/python/feast/ui/yarn.lock index 1132d6ed0f..d9e9ce03c8 100644 --- a/sdk/python/feast/ui/yarn.lock +++ b/sdk/python/feast/ui/yarn.lock @@ -3545,10 +3545,10 @@ bluebird@^3.5.5: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== -body-parser@1.20.2: - version "1.20.2" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" - integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== +body-parser@1.20.3: + version "1.20.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" + integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== dependencies: bytes "3.1.2" content-type "~1.0.5" @@ -3558,7 +3558,7 @@ body-parser@1.20.2: http-errors "2.0.0" iconv-lite "0.4.24" on-finished "2.4.1" - qs "6.11.0" + qs "6.13.0" raw-body "2.5.2" type-is "~1.6.18" unpipe "1.0.0" @@ -3675,6 +3675,17 @@ call-bind@^1.0.0, call-bind@^1.0.2: function-bind "^1.1.1" get-intrinsic "^1.0.2" +call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -4632,6 +4643,15 @@ default-gateway@^6.0.3: dependencies: execa "^5.0.0" +define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + define-lazy-prop@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" @@ -4912,6 +4932,11 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= +encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + enhanced-resolve@^5.17.1: version "5.17.1" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" @@ -4968,6 +4993,18 @@ es-abstract@^1.17.2, es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19 string.prototype.trimstart "^1.0.5" unbox-primitive "^1.0.2" +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + es-module-lexer@^1.2.1: version "1.5.4" resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.4.tgz#a8efec3a3da991e60efa6b633a7cad6ab8d26b78" @@ -5330,36 +5367,36 @@ expect@^27.5.1: jest-message-util "^27.5.1" express@^4.17.3: - version "4.19.2" - resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" - integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== + version "4.21.0" + resolved "https://registry.yarnpkg.com/express/-/express-4.21.0.tgz#d57cb706d49623d4ac27833f1cbc466b668eb915" + integrity sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng== dependencies: accepts "~1.3.8" array-flatten "1.1.1" - body-parser "1.20.2" + body-parser "1.20.3" content-disposition "0.5.4" content-type "~1.0.4" cookie "0.6.0" cookie-signature "1.0.6" debug "2.6.9" depd "2.0.0" - encodeurl "~1.0.2" + encodeurl "~2.0.0" escape-html "~1.0.3" etag "~1.8.1" - finalhandler "1.2.0" + finalhandler "1.3.1" fresh "0.5.2" http-errors "2.0.0" - merge-descriptors "1.0.1" + merge-descriptors "1.0.3" methods "~1.1.2" on-finished "2.4.1" parseurl "~1.3.3" - path-to-regexp "0.1.7" + path-to-regexp "0.1.10" proxy-addr "~2.0.7" - qs "6.11.0" + qs "6.13.0" range-parser "~1.2.1" safe-buffer "5.2.1" - send "0.18.0" - serve-static "1.15.0" + send "0.19.0" + serve-static "1.16.2" setprototypeof "1.2.0" statuses "2.0.1" type-is "~1.6.18" @@ -5471,13 +5508,13 @@ filter-obj@^1.1.0: resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-1.1.0.tgz#9b311112bc6c6127a16e016c6c5d7f19e0805c5b" integrity sha1-mzERErxsYSehbgFsbF1/GeCAXFs= -finalhandler@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" - integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== +finalhandler@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019" + integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== dependencies: debug "2.6.9" - encodeurl "~1.0.2" + encodeurl "~2.0.0" escape-html "~1.0.3" on-finished "2.4.1" parseurl "~1.3.3" @@ -5645,6 +5682,11 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + function.prototype.name@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621" @@ -5684,6 +5726,17 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: has "^1.0.3" has-symbols "^1.0.1" +get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + get-nonce@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3" @@ -5790,6 +5843,13 @@ good-listener@^1.2.2: dependencies: delegate "^3.1.2" +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: version "4.2.10" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" @@ -5839,6 +5899,18 @@ has-property-descriptors@^1.0.0: dependencies: get-intrinsic "^1.1.1" +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + has-symbols@^1.0.1, has-symbols@^1.0.2, has-symbols@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" @@ -5858,6 +5930,13 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +hasown@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + hast-to-hyperscript@^9.0.0: version "9.0.1" resolved "https://registry.yarnpkg.com/hast-to-hyperscript/-/hast-to-hyperscript-9.0.1.tgz#9b67fd188e4c81e8ad66f803855334173920218d" @@ -7415,10 +7494,10 @@ memfs@^3.4.3: resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== -merge-descriptors@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" - integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= +merge-descriptors@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" + integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== merge-stream@^2.0.0: version "2.0.0" @@ -7671,6 +7750,11 @@ object-inspect@^1.12.0, object-inspect@^1.9.0: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0" integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g== +object-inspect@^1.13.1: + version "1.13.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" + integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== + object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" @@ -7958,10 +8042,10 @@ path-parse@^1.0.6, path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-to-regexp@0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" - integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= +path-to-regexp@0.1.10: + version "0.1.10" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b" + integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w== path-type@^4.0.0: version "4.0.0" @@ -8682,12 +8766,12 @@ q@^1.1.2: resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= -qs@6.11.0: - version "6.11.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" - integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== +qs@6.13.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== dependencies: - side-channel "^1.0.4" + side-channel "^1.0.6" query-string@^7.1.1: version "7.1.1" @@ -9533,10 +9617,10 @@ semver@^7.3.2, semver@^7.3.5: dependencies: lru-cache "^6.0.0" -send@0.18.0: - version "0.18.0" - resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" - integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== +send@0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" + integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== dependencies: debug "2.6.9" depd "2.0.0" @@ -9591,15 +9675,27 @@ serve-index@^1.9.1: mime-types "~2.1.17" parseurl "~1.3.2" -serve-static@1.15.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" - integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== +serve-static@1.16.2: + version "1.16.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296" + integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== dependencies: - encodeurl "~1.0.2" + encodeurl "~2.0.0" escape-html "~1.0.3" parseurl "~1.3.3" - send "0.18.0" + send "0.19.0" + +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" setprototypeof@1.1.0: version "1.1.0" @@ -9642,6 +9738,16 @@ side-channel@^1.0.4: get-intrinsic "^1.0.2" object-inspect "^1.9.0" +side-channel@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + signal-exit@^3.0.2, signal-exit@^3.0.3: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" From d5ef57e0b52ba3439ddf69dbc818d57f15280e14 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Sep 2024 06:16:04 -0400 Subject: [PATCH 098/185] chore: Bump cryptography from 42.0.8 to 43.0.1 in /sdk/python/requirements (#4557) --- sdk/python/requirements/py3.10-ci-requirements.txt | 2 +- sdk/python/requirements/py3.11-ci-requirements.txt | 2 +- sdk/python/requirements/py3.9-ci-requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdk/python/requirements/py3.10-ci-requirements.txt b/sdk/python/requirements/py3.10-ci-requirements.txt index 0a46400007..6268de6ae1 100644 --- a/sdk/python/requirements/py3.10-ci-requirements.txt +++ b/sdk/python/requirements/py3.10-ci-requirements.txt @@ -114,7 +114,7 @@ comm==0.2.2 # ipywidgets coverage[toml]==7.6.1 # via pytest-cov -cryptography==42.0.8 +cryptography==43.0.1 # via # azure-identity # azure-storage-blob diff --git a/sdk/python/requirements/py3.11-ci-requirements.txt b/sdk/python/requirements/py3.11-ci-requirements.txt index 26ced829c6..946d4e0519 100644 --- a/sdk/python/requirements/py3.11-ci-requirements.txt +++ b/sdk/python/requirements/py3.11-ci-requirements.txt @@ -112,7 +112,7 @@ comm==0.2.2 # ipywidgets coverage[toml]==7.6.1 # via pytest-cov -cryptography==42.0.8 +cryptography==43.0.1 # via # azure-identity # azure-storage-blob diff --git a/sdk/python/requirements/py3.9-ci-requirements.txt b/sdk/python/requirements/py3.9-ci-requirements.txt index c8c92969fc..5ea2c58819 100644 --- a/sdk/python/requirements/py3.9-ci-requirements.txt +++ b/sdk/python/requirements/py3.9-ci-requirements.txt @@ -116,7 +116,7 @@ comm==0.2.2 # ipywidgets coverage[toml]==7.6.1 # via pytest-cov -cryptography==42.0.8 +cryptography==43.0.1 # via # azure-identity # azure-storage-blob From 07954960c5501e2ecc1f1285ddf4aa68f9ac880b Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Mon, 23 Sep 2024 14:21:43 -0400 Subject: [PATCH 099/185] feat: Updating FeatureViewProjection and OnDemandFeatureView to add batch_source and entities (#4530) * feat: Updating protos for Projections to include more info Signed-off-by: Francisco Javier Arceo * adding unit test Signed-off-by: Francisco Javier Arceo * adding type checking where batch source is already serialized into protobuf Signed-off-by: Francisco Javier Arceo * almost got everything working and type validation behaving Signed-off-by: Francisco Javier Arceo * cleaned up and have tests behaving Signed-off-by: Francisco Javier Arceo * removed comment Signed-off-by: Francisco Javier Arceo * updated FeatureViewProjection batch_source serialization Signed-off-by: Francisco Javier Arceo * trying to debug a test Signed-off-by: Francisco Javier Arceo * handling snowflake issue, cant confirm why it is happening so just going to put a workaround Signed-off-by: Francisco Javier Arceo * linter Signed-off-by: Francisco Javier Arceo * trying to handle it correctly Signed-off-by: Francisco Javier Arceo * handling the else case for from_feature_view_definition Signed-off-by: Francisco Javier Arceo * adding print Signed-off-by: Francisco Javier Arceo * adding test of issue Signed-off-by: Francisco Javier Arceo * think i got everything working now Signed-off-by: Francisco Javier Arceo * removing print Signed-off-by: Francisco Javier Arceo --------- Signed-off-by: Francisco Javier Arceo --- protos/feast/core/FeatureViewProjection.proto | 10 ++ protos/feast/core/OnDemandFeatureView.proto | 6 + sdk/python/feast/base_feature_view.py | 12 +- sdk/python/feast/feature_view.py | 5 +- sdk/python/feast/feature_view_projection.py | 86 +++++++++++- sdk/python/feast/inference.py | 110 +++++++++++++-- .../infra/materialization/snowflake_engine.py | 8 +- sdk/python/feast/on_demand_feature_view.py | 129 ++++++++++++++++-- sdk/python/feast/utils.py | 4 +- .../materialization/test_snowflake.py | 2 + .../test_local_feature_store.py | 3 +- sdk/python/tests/unit/test_feature_views.py | 22 ++- 12 files changed, 363 insertions(+), 34 deletions(-) diff --git a/protos/feast/core/FeatureViewProjection.proto b/protos/feast/core/FeatureViewProjection.proto index 36d17632e7..b0e697b656 100644 --- a/protos/feast/core/FeatureViewProjection.proto +++ b/protos/feast/core/FeatureViewProjection.proto @@ -6,6 +6,7 @@ option java_outer_classname = "FeatureReferenceProto"; option java_package = "feast.proto.core"; import "feast/core/Feature.proto"; +import "feast/core/DataSource.proto"; // A projection to be applied on top of a FeatureView. @@ -22,4 +23,13 @@ message FeatureViewProjection { // Map for entity join_key overrides of feature data entity join_key to entity data join_key map join_key_map = 4; + + string timestamp_field = 5; + string date_partition_column = 6; + string created_timestamp_column = 7; + // Batch/Offline DataSource where this view can retrieve offline feature data. + DataSource batch_source = 8; + // Streaming DataSource from where this view can consume "online" feature data. + DataSource stream_source = 9; + } diff --git a/protos/feast/core/OnDemandFeatureView.proto b/protos/feast/core/OnDemandFeatureView.proto index 7a5fec1650..c915e32e16 100644 --- a/protos/feast/core/OnDemandFeatureView.proto +++ b/protos/feast/core/OnDemandFeatureView.proto @@ -63,6 +63,12 @@ message OnDemandFeatureViewSpec { // Owner of the on demand feature view. string owner = 8; string mode = 11; + bool write_to_online_store = 12; + + // List of names of entities associated with this feature view. + repeated string entities = 13; + // List of specifications for each entity defined as part of this feature view. + repeated FeatureSpecV2 entity_columns = 14; } message OnDemandFeatureViewMeta { diff --git a/sdk/python/feast/base_feature_view.py b/sdk/python/feast/base_feature_view.py index 31140e2899..d7dc2237bd 100644 --- a/sdk/python/feast/base_feature_view.py +++ b/sdk/python/feast/base_feature_view.py @@ -18,6 +18,7 @@ from google.protobuf.json_format import MessageToJson from google.protobuf.message import Message +from feast.data_source import DataSource from feast.feature_view_projection import FeatureViewProjection from feast.field import Field from feast.protos.feast.core.FeatureView_pb2 import FeatureView as FeatureViewProto @@ -65,6 +66,7 @@ def __init__( description: str = "", tags: Optional[Dict[str, str]] = None, owner: str = "", + source: Optional[DataSource] = None, ): """ Creates a BaseFeatureView object. @@ -76,7 +78,8 @@ def __init__( tags (optional): A dictionary of key-value pairs to store arbitrary metadata. owner (optional): The owner of the base feature view, typically the email of the primary maintainer. - + source (optional): The source of data for this group of features. May be a stream source, or a batch source. + If a stream source, the source should contain a batch_source for backfills & batch materialization. Raises: ValueError: A field mapping conflicts with an Entity or a Feature. """ @@ -90,6 +93,9 @@ def __init__( self.created_timestamp = None self.last_updated_timestamp = None + if source: + self.source = source + @property @abstractmethod def proto_class(self) -> Type[Message]: @@ -156,6 +162,10 @@ def __eq__(self, other): or self.tags != other.tags or self.owner != other.owner ): + # This is meant to ignore the File Source change to Push Source + if isinstance(type(self.source), type(other.source)): + if self.source != other.source: + return False return False return True diff --git a/sdk/python/feast/feature_view.py b/sdk/python/feast/feature_view.py index dd01078e20..33ea761158 100644 --- a/sdk/python/feast/feature_view.py +++ b/sdk/python/feast/feature_view.py @@ -206,6 +206,7 @@ def __init__( description=description, tags=tags, owner=owner, + source=source, ) self.online = online self.materialization_intervals = [] @@ -429,7 +430,9 @@ def from_proto(cls, feature_view_proto: FeatureViewProto): # FeatureViewProjections are not saved in the FeatureView proto. # Create the default projection. - feature_view.projection = FeatureViewProjection.from_definition(feature_view) + feature_view.projection = FeatureViewProjection.from_feature_view_definition( + feature_view + ) if feature_view_proto.meta.HasField("created_timestamp"): feature_view.created_timestamp = ( diff --git a/sdk/python/feast/feature_view_projection.py b/sdk/python/feast/feature_view_projection.py index ff5b1b6e06..70415e9ed3 100644 --- a/sdk/python/feast/feature_view_projection.py +++ b/sdk/python/feast/feature_view_projection.py @@ -2,6 +2,7 @@ from attr import dataclass +from feast.data_source import DataSource from feast.field import Field from feast.protos.feast.core.FeatureViewProjection_pb2 import ( FeatureViewProjection as FeatureViewProjectionProto, @@ -9,6 +10,7 @@ if TYPE_CHECKING: from feast.base_feature_view import BaseFeatureView + from feast.feature_view import FeatureView @dataclass @@ -27,6 +29,13 @@ class FeatureViewProjection: is not ready to be projected, i.e. still needs to go through feature inference. join_key_map: A map to modify join key columns during retrieval of this feature view projection. + timestamp_field: The timestamp field of the feature view projection. + date_partition_column: The date partition column of the feature view projection. + created_timestamp_column: The created timestamp column of the feature view projection. + batch_source: The batch source of data where this group of features + is stored. This is optional ONLY if a push source is specified as the + stream_source, since push sources contain their own batch sources. + """ name: str @@ -34,15 +43,29 @@ class FeatureViewProjection: desired_features: List[str] features: List[Field] join_key_map: Dict[str, str] = {} + timestamp_field: Optional[str] = None + date_partition_column: Optional[str] = None + created_timestamp_column: Optional[str] = None + batch_source: Optional[DataSource] = None def name_to_use(self): return self.name_alias or self.name def to_proto(self) -> FeatureViewProjectionProto: + batch_source = None + if getattr(self, "batch_source", None): + if isinstance(self.batch_source, DataSource): + batch_source = self.batch_source.to_proto() + else: + batch_source = self.batch_source feature_reference_proto = FeatureViewProjectionProto( feature_view_name=self.name, feature_view_name_alias=self.name_alias or "", join_key_map=self.join_key_map, + timestamp_field=self.timestamp_field or "", + date_partition_column=self.date_partition_column or "", + created_timestamp_column=self.created_timestamp_column or "", + batch_source=batch_source, ) for feature in self.features: feature_reference_proto.feature_columns.append(feature.to_proto()) @@ -50,27 +73,76 @@ def to_proto(self) -> FeatureViewProjectionProto: return feature_reference_proto @staticmethod - def from_proto(proto: FeatureViewProjectionProto): + def from_proto(proto: FeatureViewProjectionProto) -> "FeatureViewProjection": + batch_source = ( + DataSource.from_proto(proto.batch_source) + if str(getattr(proto, "batch_source")) + else None + ) feature_view_projection = FeatureViewProjection( name=proto.feature_view_name, name_alias=proto.feature_view_name_alias or None, features=[], join_key_map=dict(proto.join_key_map), desired_features=[], + timestamp_field=proto.timestamp_field or None, + date_partition_column=proto.date_partition_column or None, + created_timestamp_column=proto.created_timestamp_column or None, + batch_source=batch_source, ) for feature_column in proto.feature_columns: feature_view_projection.features.append(Field.from_proto(feature_column)) return feature_view_projection + @staticmethod + def from_feature_view_definition(feature_view: "FeatureView"): + # TODO need to implement this for StreamFeatureViews + if getattr(feature_view, "batch_source", None): + return FeatureViewProjection( + name=feature_view.name, + name_alias=None, + features=feature_view.features, + desired_features=[], + timestamp_field=feature_view.batch_source.created_timestamp_column + or None, + created_timestamp_column=feature_view.batch_source.created_timestamp_column + or None, + date_partition_column=feature_view.batch_source.date_partition_column + or None, + batch_source=feature_view.batch_source or None, + ) + else: + return FeatureViewProjection( + name=feature_view.name, + name_alias=None, + features=feature_view.features, + desired_features=[], + ) + @staticmethod def from_definition(base_feature_view: "BaseFeatureView"): - return FeatureViewProjection( - name=base_feature_view.name, - name_alias=None, - features=base_feature_view.features, - desired_features=[], - ) + if getattr(base_feature_view, "batch_source", None): + return FeatureViewProjection( + name=base_feature_view.name, + name_alias=None, + features=base_feature_view.features, + desired_features=[], + timestamp_field=base_feature_view.batch_source.created_timestamp_column # type:ignore[attr-defined] + or None, + created_timestamp_column=base_feature_view.batch_source.created_timestamp_column # type:ignore[attr-defined] + or None, + date_partition_column=base_feature_view.batch_source.date_partition_column # type:ignore[attr-defined] + or None, + batch_source=base_feature_view.batch_source or None, # type:ignore[attr-defined] + ) + else: + return FeatureViewProjection( + name=base_feature_view.name, + name_alias=None, + features=base_feature_view.features, + desired_features=[], + ) def get_feature(self, feature_name: str) -> Field: try: diff --git a/sdk/python/feast/inference.py b/sdk/python/feast/inference.py index 28a170172c..b9fb9b694d 100644 --- a/sdk/python/feast/inference.py +++ b/sdk/python/feast/inference.py @@ -13,6 +13,7 @@ from feast.infra.offline_stores.file_source import FileSource from feast.infra.offline_stores.redshift_source import RedshiftSource from feast.infra.offline_stores.snowflake_source import SnowflakeSource +from feast.on_demand_feature_view import OnDemandFeatureView from feast.repo_config import RepoConfig from feast.stream_feature_view import StreamFeatureView from feast.types import String @@ -94,7 +95,7 @@ def update_data_sources_with_inferred_event_timestamp_col( def update_feature_views_with_inferred_features_and_entities( - fvs: Union[List[FeatureView], List[StreamFeatureView]], + fvs: Union[List[FeatureView], List[StreamFeatureView], List[OnDemandFeatureView]], entities: List[Entity], config: RepoConfig, ) -> None: @@ -121,35 +122,37 @@ def update_feature_views_with_inferred_features_and_entities( join_keys = set( [ entity_name_to_join_key_map.get(entity_name) - for entity_name in fv.entities + for entity_name in getattr(fv, "entities", []) ] ) # Fields whose names match a join key are considered to be entity columns; all # other fields are considered to be feature columns. + entity_columns = fv.entity_columns if fv.entity_columns else [] for field in fv.schema: if field.name in join_keys: # Do not override a preexisting field with the same name. if field.name not in [ - entity_column.name for entity_column in fv.entity_columns + entity_column.name for entity_column in entity_columns ]: - fv.entity_columns.append(field) + entity_columns.append(field) else: if field.name not in [feature.name for feature in fv.features]: fv.features.append(field) # Respect the `value_type` attribute of the entity, if it is specified. - for entity_name in fv.entities: + fv_entities = getattr(fv, "entities", []) + for entity_name in fv_entities: entity = entity_name_to_entity_map.get(entity_name) # pass when entity does not exist. Entityless feature view case if entity is None: continue if ( entity.join_key - not in [entity_column.name for entity_column in fv.entity_columns] + not in [entity_column.name for entity_column in entity_columns] and entity.value_type != ValueType.UNKNOWN ): - fv.entity_columns.append( + entity_columns.append( Field( name=entity.join_key, dtype=from_value_type(entity.value_type), @@ -158,12 +161,13 @@ def update_feature_views_with_inferred_features_and_entities( # Infer a dummy entity column for entityless feature views. if ( - len(fv.entities) == 1 - and fv.entities[0] == DUMMY_ENTITY_NAME - and not fv.entity_columns + len(fv_entities) == 1 + and fv_entities[0] == DUMMY_ENTITY_NAME + and not entity_columns ): - fv.entity_columns.append(Field(name=DUMMY_ENTITY_ID, dtype=String)) + entity_columns.append(Field(name=DUMMY_ENTITY_ID, dtype=String)) + fv.entity_columns = entity_columns # Run inference for entity columns if there are fewer entity fields than expected. run_inference_for_entities = len(fv.entity_columns) < len(join_keys) @@ -186,7 +190,7 @@ def update_feature_views_with_inferred_features_and_entities( def _infer_features_and_entities( - fv: FeatureView, + fv: Union[FeatureView, OnDemandFeatureView], join_keys: Set[Optional[str]], run_inference_for_features, config, @@ -200,6 +204,11 @@ def _infer_features_and_entities( run_inference_for_features: Whether to run inference for features. config: The config for the current feature store. """ + if isinstance(fv, OnDemandFeatureView): + return _infer_on_demand_features_and_entities( + fv, join_keys, run_inference_for_features, config + ) + columns_to_exclude = { fv.batch_source.timestamp_field, fv.batch_source.created_timestamp_column, @@ -246,3 +255,80 @@ def _infer_features_and_entities( ) if field.name not in [feature.name for feature in fv.features]: fv.features.append(field) + + +def _infer_on_demand_features_and_entities( + fv: OnDemandFeatureView, + join_keys: Set[Optional[str]], + run_inference_for_features, + config, +) -> None: + """ + Updates the specific feature in place with inferred features and entities. + Args: + fv: The feature view on which to run inference. + join_keys: The set of join keys for the feature view's entities. + run_inference_for_features: Whether to run inference for features. + config: The config for the current feature store. + """ + entity_columns: list[Field] = [] + columns_to_exclude = set() + for ( + source_feature_view_name, + source_feature_view, + ) in fv.source_feature_view_projections.items(): + columns_to_exclude.add(source_feature_view.timestamp_field) + columns_to_exclude.add(source_feature_view.created_timestamp_column) + + batch_source = getattr(source_feature_view, "batch_source") + batch_field_mapping = getattr(batch_source or None, "field_mapping") + if batch_field_mapping: + for ( + original_col, + mapped_col, + ) in batch_field_mapping.items(): + if mapped_col in columns_to_exclude: + columns_to_exclude.remove(mapped_col) + columns_to_exclude.add(original_col) + + table_column_names_and_types = ( + batch_source.get_table_column_names_and_types(config) + ) + for col_name, col_datatype in table_column_names_and_types: + if col_name in columns_to_exclude: + continue + elif col_name in join_keys: + field = Field( + name=col_name, + dtype=from_value_type( + batch_source.source_datatype_to_feast_value_type()(col_datatype) + ), + ) + if field.name not in [ + entity_column.name + for entity_column in entity_columns + if hasattr(entity_column, "name") + ]: + entity_columns.append(field) + elif not re.match( + "^__|__$", col_name + ): # double underscores often signal an internal-use column + if run_inference_for_features: + feature_name = ( + batch_field_mapping[col_name] + if col_name in batch_field_mapping + else col_name + ) + field = Field( + name=feature_name, + dtype=from_value_type( + batch_source.source_datatype_to_feast_value_type()( + col_datatype + ) + ), + ) + if field.name not in [ + feature.name for feature in source_feature_view.features + ]: + source_feature_view.features.append(field) + fv.entity_columns = entity_columns diff --git a/sdk/python/feast/infra/materialization/snowflake_engine.py b/sdk/python/feast/infra/materialization/snowflake_engine.py index e8b0857e5d..600e1b20d8 100644 --- a/sdk/python/feast/infra/materialization/snowflake_engine.py +++ b/sdk/python/feast/infra/materialization/snowflake_engine.py @@ -285,8 +285,14 @@ def _materialize_one( fv_latest_values_sql = offline_job.to_sql() + if feature_view.entity_columns: + first_feature_view_entity_name = getattr( + feature_view.entity_columns[0], "name", None + ) + else: + first_feature_view_entity_name = None if ( - feature_view.entity_columns[0].name == DUMMY_ENTITY_ID + first_feature_view_entity_name == DUMMY_ENTITY_ID ): # entityless Feature View's placeholder entity entities_to_write = 1 else: diff --git a/sdk/python/feast/on_demand_feature_view.py b/sdk/python/feast/on_demand_feature_view.py index 47fcf29926..1b75d23ed4 100644 --- a/sdk/python/feast/on_demand_feature_view.py +++ b/sdk/python/feast/on_demand_feature_view.py @@ -3,7 +3,7 @@ import inspect import warnings from types import FunctionType -from typing import Any, Optional, Union, get_type_hints +from typing import Any, List, Optional, Union, get_type_hints import dill import pandas as pd @@ -12,8 +12,9 @@ from feast.base_feature_view import BaseFeatureView from feast.data_source import RequestSource +from feast.entity import Entity from feast.errors import RegistryInferenceFailure, SpecifiedFeaturesNotPresentError -from feast.feature_view import FeatureView +from feast.feature_view import DUMMY_ENTITY_NAME, FeatureView from feast.feature_view_projection import FeatureViewProjection from feast.field import Field, from_value_type from feast.protos.feast.core.OnDemandFeatureView_pb2 import ( @@ -61,7 +62,8 @@ class OnDemandFeatureView(BaseFeatureView): """ name: str - features: list[Field] + entities: Optional[List[str]] + features: List[Field] source_feature_view_projections: dict[str, FeatureViewProjection] source_request_sources: dict[str, RequestSource] feature_transformation: Union[ @@ -71,13 +73,15 @@ class OnDemandFeatureView(BaseFeatureView): description: str tags: dict[str, str] owner: str + write_to_online_store: bool def __init__( # noqa: C901 self, *, name: str, - schema: list[Field], - sources: list[ + entities: Optional[List[Entity]] = None, + schema: Optional[List[Field]] = None, + sources: List[ Union[ FeatureView, RequestSource, @@ -93,12 +97,14 @@ def __init__( # noqa: C901 description: str = "", tags: Optional[dict[str, str]] = None, owner: str = "", + write_to_online_store: bool = False, ): """ Creates an OnDemandFeatureView object. Args: name: The unique name of the on demand feature view. + entities (optional): The list of names of entities that this feature view is associated with. schema: The list of features in the output of the on demand feature view, after the transformation has been applied. sources: A map from input source names to the actual input sources, which may be @@ -113,6 +119,8 @@ def __init__( # noqa: C901 tags (optional): A dictionary of key-value pairs to store arbitrary metadata. owner (optional): The owner of the on demand feature view, typically the email of the primary maintainer. + write_to_online_store (optional): A boolean that indicates whether to write the on demand feature view to + the online store for faster retrieval. """ super().__init__( name=name, @@ -122,6 +130,8 @@ def __init__( # noqa: C901 owner=owner, ) + schema = schema or [] + self.entities = [e.name for e in entities] if entities else [DUMMY_ENTITY_NAME] self.mode = mode.lower() if self.mode not in {"python", "pandas", "substrait"}: @@ -152,12 +162,48 @@ def __init__( # noqa: C901 self.source_request_sources[odfv_source.name] = odfv_source elif isinstance(odfv_source, FeatureViewProjection): self.source_feature_view_projections[odfv_source.name] = odfv_source + else: self.source_feature_view_projections[odfv_source.name] = ( odfv_source.projection ) + features: List[Field] = [] + self.entity_columns = [] + + join_keys: List[str] = [] + if entities: + for entity in entities: + join_keys.append(entity.join_key) + # Ensure that entities have unique join keys. + if len(set(join_keys)) < len(join_keys): + raise ValueError( + "A feature view should not have entities that share a join key." + ) + + for field in schema: + if field.name in join_keys: + self.entity_columns.append(field) + + # Confirm that the inferred type matches the specified entity type, if it exists. + matching_entities = ( + [e for e in entities if e.join_key == field.name] + if entities + else [] + ) + assert len(matching_entities) == 1 + entity = matching_entities[0] + if entity.value_type != ValueType.UNKNOWN: + if from_value_type(entity.value_type) != field.dtype: + raise ValueError( + f"Entity {entity.name} has type {entity.value_type}, which does not match the inferred type {field.dtype}." + ) + else: + features.append(field) + + self.features = features self.feature_transformation = feature_transformation + self.write_to_online_store = write_to_online_store @property def proto_class(self) -> type[OnDemandFeatureViewProto]: @@ -174,8 +220,13 @@ def __copy__(self): description=self.description, tags=self.tags, owner=self.owner, + write_to_online_store=self.write_to_online_store, ) + fv.entities = self.entities + fv.features = self.features fv.projection = copy.copy(self.projection) + fv.entity_columns = copy.copy(self.entity_columns) + return fv def __eq__(self, other): @@ -184,20 +235,46 @@ def __eq__(self, other): "Comparisons should only involve OnDemandFeatureView class objects." ) - if not super().__eq__(other): - return False - + # Note, no longer evaluating the base feature view layer as ODFVs can have + # multiple datasources and a base_feature_view only has one source + # though maybe that shouldn't be true if ( self.source_feature_view_projections != other.source_feature_view_projections + or self.description != other.description or self.source_request_sources != other.source_request_sources or self.mode != other.mode or self.feature_transformation != other.feature_transformation + or self.write_to_online_store != other.write_to_online_store + or sorted(self.entity_columns) != sorted(other.entity_columns) ): return False return True + @property + def join_keys(self) -> List[str]: + """Returns a list of all the join keys.""" + return [entity.name for entity in self.entity_columns] + + @property + def schema(self) -> List[Field]: + return list(set(self.entity_columns + self.features)) + + def ensure_valid(self): + """ + Validates the state of this feature view locally. + + Raises: + ValueError: The On Demand feature view does not have an entity when trying to use write_to_online_store. + """ + super().ensure_valid() + + if self.write_to_online_store and not self.entities: + raise ValueError( + "On Demand Feature views require an entity if write_to_online_store=True" + ) + def __hash__(self): return super().__hash__() @@ -216,7 +293,7 @@ def to_proto(self) -> OnDemandFeatureViewProto: sources = {} for source_name, fv_projection in self.source_feature_view_projections.items(): sources[source_name] = OnDemandSource( - feature_view_projection=fv_projection.to_proto() + feature_view_projection=fv_projection.to_proto(), ) for ( source_name, @@ -239,6 +316,10 @@ def to_proto(self) -> OnDemandFeatureViewProto: ) spec = OnDemandFeatureViewSpec( name=self.name, + entities=self.entities if self.entities else None, + entity_columns=[ + field.to_proto() for field in self.entity_columns if self.entity_columns + ], features=[feature.to_proto() for feature in self.features], sources=sources, feature_transformation=feature_transformation, @@ -246,6 +327,7 @@ def to_proto(self) -> OnDemandFeatureViewProto: description=self.description, tags=self.tags, owner=self.owner, + write_to_online_store=self.write_to_online_store, ) return OnDemandFeatureViewProto(spec=spec, meta=meta) @@ -335,6 +417,24 @@ def from_proto( else: raise ValueError("At least one transformation type needs to be provided") + if hasattr(on_demand_feature_view_proto.spec, "write_to_online_store"): + write_to_online_store = ( + on_demand_feature_view_proto.spec.write_to_online_store + ) + else: + write_to_online_store = False + if hasattr(on_demand_feature_view_proto.spec, "entities"): + entities = list(on_demand_feature_view_proto.spec.entities) + else: + entities = [] + if hasattr(on_demand_feature_view_proto.spec, "entity_columns"): + entity_columns = [ + Field.from_proto(field_proto) + for field_proto in on_demand_feature_view_proto.spec.entity_columns + ] + else: + entity_columns = [] + on_demand_feature_view_obj = cls( name=on_demand_feature_view_proto.spec.name, schema=[ @@ -350,8 +450,12 @@ def from_proto( description=on_demand_feature_view_proto.spec.description, tags=dict(on_demand_feature_view_proto.spec.tags), owner=on_demand_feature_view_proto.spec.owner, + write_to_online_store=write_to_online_store, ) + on_demand_feature_view_obj.entities = entities + on_demand_feature_view_obj.entity_columns = entity_columns + # FeatureViewProjections are not saved in the OnDemandFeatureView proto. # Create the default projection. on_demand_feature_view_obj.projection = FeatureViewProjection.from_definition( @@ -595,6 +699,7 @@ def get_requested_odfvs( def on_demand_feature_view( *, + entities: Optional[List[Entity]] = None, schema: list[Field], sources: list[ Union[ @@ -607,11 +712,13 @@ def on_demand_feature_view( description: str = "", tags: Optional[dict[str, str]] = None, owner: str = "", + write_to_online_store: bool = False, ): """ Creates an OnDemandFeatureView object with the given user function as udf. Args: + entities (Optional): The list of names of entities that this feature view is associated with. schema: The list of features in the output of the on demand feature view, after the transformation has been applied. sources: A map from input source names to the actual input sources, which may be @@ -622,6 +729,8 @@ def on_demand_feature_view( tags (optional): A dictionary of key-value pairs to store arbitrary metadata. owner (optional): The owner of the on demand feature view, typically the email of the primary maintainer. + write_to_online_store (optional): A boolean that indicates whether to write the on demand feature view to + the online store for faster retrieval. """ def mainify(obj) -> None: @@ -664,6 +773,8 @@ def decorator(user_function): description=description, tags=tags, owner=owner, + write_to_online_store=write_to_online_store, + entities=entities, ) functools.update_wrapper( wrapper=on_demand_feature_view_obj, wrapped=user_function diff --git a/sdk/python/feast/utils.py b/sdk/python/feast/utils.py index 2ab73ae089..8a9f1fadae 100644 --- a/sdk/python/feast/utils.py +++ b/sdk/python/feast/utils.py @@ -344,7 +344,9 @@ def _group_feature_refs( # on demand view to on demand view proto on_demand_view_index = { - view.projection.name_to_use(): view for view in all_on_demand_feature_views + view.projection.name_to_use(): view + for view in all_on_demand_feature_views + if view.projection } # view name to feature names diff --git a/sdk/python/tests/integration/materialization/test_snowflake.py b/sdk/python/tests/integration/materialization/test_snowflake.py index f53c3ca753..dc9d684ab5 100644 --- a/sdk/python/tests/integration/materialization/test_snowflake.py +++ b/sdk/python/tests/integration/materialization/test_snowflake.py @@ -221,9 +221,11 @@ def test_snowflake_materialization_entityless_fv(): ttl=timedelta(weeks=52), source=ds, ) + assert overall_stats_fv.entity_columns == [] try: fs.apply([overall_stats_fv, driver]) + assert overall_stats_fv.entity_columns != [] # materialization is run in two steps and # we use timestamp from generated dataframe as a split point diff --git a/sdk/python/tests/unit/local_feast_tests/test_local_feature_store.py b/sdk/python/tests/unit/local_feast_tests/test_local_feature_store.py index 5ed16a8430..cc48295b20 100644 --- a/sdk/python/tests/unit/local_feast_tests/test_local_feature_store.py +++ b/sdk/python/tests/unit/local_feast_tests/test_local_feature_store.py @@ -209,8 +209,9 @@ def test_apply_feature_view_with_inline_batch_source( test_feature_store.apply([entity, driver_fv]) fvs = test_feature_store.list_batch_feature_views() + dfv = fvs[0] assert len(fvs) == 1 - assert fvs[0] == driver_fv + assert dfv == driver_fv ds = test_feature_store.list_data_sources() assert len(ds) == 1 diff --git a/sdk/python/tests/unit/test_feature_views.py b/sdk/python/tests/unit/test_feature_views.py index 981968df0d..ce789c706c 100644 --- a/sdk/python/tests/unit/test_feature_views.py +++ b/sdk/python/tests/unit/test_feature_views.py @@ -111,7 +111,27 @@ def test_hash(): assert len(s4) == 3 -# TODO(felixwang9817): Add tests for proto conversion. +def test_proto_conversion(): + file_source = FileSource(name="my-file-source", path="test.parquet") + feature_view_1 = FeatureView( + name="my-feature-view", + entities=[], + schema=[ + Field(name="feature1", dtype=Float32), + Field(name="feature2", dtype=Float32), + ], + source=file_source, + ) + + feature_view_proto = feature_view_1.to_proto() + assert ( + feature_view_proto.spec.name == "my-feature-view" + and feature_view_proto.spec.batch_source.file_options.uri == "test.parquet" + and feature_view_proto.spec.batch_source.name == "my-file-source" + and feature_view_proto.spec.batch_source.type == 1 + ) + + # TODO(felixwang9817): Add tests for field mapping logic. From e675cbdaf638c6208cb09a41fe8ed34216c9b87f Mon Sep 17 00:00:00 2001 From: Daniele Martinoli <86618610+dmartinol@users.noreply.github.com> Date: Mon, 23 Sep 2024 22:38:43 +0200 Subject: [PATCH 100/185] docs: Example to install feast on local computer using Kind (#4528) * Simple deployment on Kind Signed-off-by: Daniele Martinoli * updated notebooks output Signed-off-by: Daniele Martinoli * added missing README Signed-off-by: Daniele Martinoli * typo Signed-off-by: Daniele Martinoli --------- Signed-off-by: Daniele Martinoli --- examples/kind-quickstart/01-Install.ipynb | 932 ++++++++++++++++++ examples/kind-quickstart/02-Client.ipynb | 606 ++++++++++++ examples/kind-quickstart/03-Uninstall.ipynb | 120 +++ examples/kind-quickstart/README.md | 7 + examples/kind-quickstart/client/__init__.py | 0 .../kind-quickstart/client/feature_store.yaml | 14 + examples/kind-quickstart/init-job.yaml | 31 + .../kind-quickstart/postgres/postgres.yaml | 83 ++ examples/kind-quickstart/src/__init__.py | 0 examples/kind-quickstart/src/utils.py | 12 + 10 files changed, 1805 insertions(+) create mode 100644 examples/kind-quickstart/01-Install.ipynb create mode 100644 examples/kind-quickstart/02-Client.ipynb create mode 100644 examples/kind-quickstart/03-Uninstall.ipynb create mode 100644 examples/kind-quickstart/README.md create mode 100644 examples/kind-quickstart/client/__init__.py create mode 100644 examples/kind-quickstart/client/feature_store.yaml create mode 100644 examples/kind-quickstart/init-job.yaml create mode 100644 examples/kind-quickstart/postgres/postgres.yaml create mode 100644 examples/kind-quickstart/src/__init__.py create mode 100644 examples/kind-quickstart/src/utils.py diff --git a/examples/kind-quickstart/01-Install.ipynb b/examples/kind-quickstart/01-Install.ipynb new file mode 100644 index 0000000000..e5ece97fc2 --- /dev/null +++ b/examples/kind-quickstart/01-Install.ipynb @@ -0,0 +1,932 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Requirement already satisfied: feast==0.40.1 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (0.40.1)\n", + "Requirement already satisfied: click<9.0.0,>=7.0.0 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from feast==0.40.1) (8.1.7)\n", + "Requirement already satisfied: colorama<1,>=0.3.9 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from feast==0.40.1) (0.4.6)\n", + "Requirement already satisfied: dill~=0.3.0 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from feast==0.40.1) (0.3.8)\n", + "Requirement already satisfied: mypy-protobuf>=3.1 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from feast==0.40.1) (3.6.0)\n", + "Requirement already satisfied: Jinja2<4,>=2 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from feast==0.40.1) (3.1.4)\n", + "Requirement already satisfied: jsonschema in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from feast==0.40.1) (4.22.0)\n", + "Requirement already satisfied: mmh3 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from feast==0.40.1) (4.1.0)\n", + "Requirement already satisfied: numpy<2,>=1.22 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from feast==0.40.1) (1.26.4)\n", + "Requirement already satisfied: pandas<3,>=1.4.3 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from feast==0.40.1) (2.2.2)\n", + "Requirement already satisfied: protobuf<5.0.0,>=4.24.0 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from feast==0.40.1) (4.25.4)\n", + "Requirement already satisfied: pyarrow>=4 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from feast==0.40.1) (15.0.2)\n", + "Requirement already satisfied: pydantic>=2.0.0 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from feast==0.40.1) (2.7.4)\n", + "Requirement already satisfied: pygments<3,>=2.12.0 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from feast==0.40.1) (2.18.0)\n", + "Requirement already satisfied: PyYAML<7,>=5.4.0 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from feast==0.40.1) (6.0.1)\n", + "Requirement already satisfied: requests in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from feast==0.40.1) (2.32.3)\n", + "Requirement already satisfied: SQLAlchemy>1 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from SQLAlchemy[mypy]>1->feast==0.40.1) (2.0.34)\n", + "Requirement already satisfied: tabulate<1,>=0.8.0 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from feast==0.40.1) (0.9.0)\n", + "Requirement already satisfied: tenacity<9,>=7 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from feast==0.40.1) (8.5.0)\n", + "Requirement already satisfied: toml<1,>=0.10.0 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from feast==0.40.1) (0.10.2)\n", + "Requirement already satisfied: tqdm<5,>=4 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from feast==0.40.1) (4.66.4)\n", + "Requirement already satisfied: typeguard>=4.0.0 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from feast==0.40.1) (4.3.0)\n", + "Requirement already satisfied: fastapi>=0.68.0 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from feast==0.40.1) (0.114.2)\n", + "Requirement already satisfied: uvicorn<1,>=0.14.0 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from uvicorn[standard]<1,>=0.14.0->feast==0.40.1) (0.30.6)\n", + "Requirement already satisfied: dask>=2024.2.1 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from dask[dataframe]>=2024.2.1->feast==0.40.1) (2024.6.2)\n", + "Requirement already satisfied: gunicorn in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from feast==0.40.1) (23.0.0)\n", + "Requirement already satisfied: cloudpickle>=1.5.0 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from dask>=2024.2.1->dask[dataframe]>=2024.2.1->feast==0.40.1) (3.0.0)\n", + "Requirement already satisfied: fsspec>=2021.09.0 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from dask>=2024.2.1->dask[dataframe]>=2024.2.1->feast==0.40.1) (2023.12.2)\n", + "Requirement already satisfied: packaging>=20.0 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from dask>=2024.2.1->dask[dataframe]>=2024.2.1->feast==0.40.1) (24.1)\n", + "Requirement already satisfied: partd>=1.2.0 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from dask>=2024.2.1->dask[dataframe]>=2024.2.1->feast==0.40.1) (1.4.2)\n", + "Requirement already satisfied: toolz>=0.10.0 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from dask>=2024.2.1->dask[dataframe]>=2024.2.1->feast==0.40.1) (0.12.1)\n", + "Requirement already satisfied: importlib-metadata>=4.13.0 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from dask>=2024.2.1->dask[dataframe]>=2024.2.1->feast==0.40.1) (8.0.0)\n", + "Requirement already satisfied: dask-expr<1.2,>=1.1 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from dask[dataframe]>=2024.2.1->feast==0.40.1) (1.1.6)\n", + "Requirement already satisfied: starlette<0.39.0,>=0.37.2 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from fastapi>=0.68.0->feast==0.40.1) (0.38.5)\n", + "Requirement already satisfied: typing-extensions>=4.8.0 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from fastapi>=0.68.0->feast==0.40.1) (4.12.2)\n", + "Requirement already satisfied: MarkupSafe>=2.0 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from Jinja2<4,>=2->feast==0.40.1) (2.1.5)\n", + "Requirement already satisfied: types-protobuf>=4.24 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from mypy-protobuf>=3.1->feast==0.40.1) (5.27.0.20240626)\n", + "Requirement already satisfied: python-dateutil>=2.8.2 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from pandas<3,>=1.4.3->feast==0.40.1) (2.9.0.post0)\n", + "Requirement already satisfied: pytz>=2020.1 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from pandas<3,>=1.4.3->feast==0.40.1) (2024.1)\n", + "Requirement already satisfied: tzdata>=2022.7 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from pandas<3,>=1.4.3->feast==0.40.1) (2024.1)\n", + "Requirement already satisfied: annotated-types>=0.4.0 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from pydantic>=2.0.0->feast==0.40.1) (0.7.0)\n", + "Requirement already satisfied: pydantic-core==2.18.4 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from pydantic>=2.0.0->feast==0.40.1) (2.18.4)\n", + "Requirement already satisfied: mypy>=0.910 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from SQLAlchemy[mypy]>1->feast==0.40.1) (1.10.1)\n", + "Requirement already satisfied: h11>=0.8 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from uvicorn<1,>=0.14.0->uvicorn[standard]<1,>=0.14.0->feast==0.40.1) (0.14.0)\n", + "Requirement already satisfied: httptools>=0.5.0 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from uvicorn[standard]<1,>=0.14.0->feast==0.40.1) (0.6.1)\n", + "Requirement already satisfied: python-dotenv>=0.13 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from uvicorn[standard]<1,>=0.14.0->feast==0.40.1) (1.0.1)\n", + "Requirement already satisfied: uvloop!=0.15.0,!=0.15.1,>=0.14.0 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from uvicorn[standard]<1,>=0.14.0->feast==0.40.1) (0.19.0)\n", + "Requirement already satisfied: watchfiles>=0.13 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from uvicorn[standard]<1,>=0.14.0->feast==0.40.1) (0.22.0)\n", + "Requirement already satisfied: websockets>=10.4 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from uvicorn[standard]<1,>=0.14.0->feast==0.40.1) (12.0)\n", + "Requirement already satisfied: attrs>=22.2.0 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from jsonschema->feast==0.40.1) (23.2.0)\n", + "Requirement already satisfied: jsonschema-specifications>=2023.03.6 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from jsonschema->feast==0.40.1) (2023.12.1)\n", + "Requirement already satisfied: referencing>=0.28.4 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from jsonschema->feast==0.40.1) (0.35.1)\n", + "Requirement already satisfied: rpds-py>=0.7.1 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from jsonschema->feast==0.40.1) (0.18.1)\n", + "Requirement already satisfied: charset-normalizer<4,>=2 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from requests->feast==0.40.1) (3.3.2)\n", + "Requirement already satisfied: idna<4,>=2.5 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from requests->feast==0.40.1) (3.7)\n", + "Requirement already satisfied: urllib3<3,>=1.21.1 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from requests->feast==0.40.1) (1.26.19)\n", + "Requirement already satisfied: certifi>=2017.4.17 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from requests->feast==0.40.1) (2024.7.4)\n", + "Requirement already satisfied: zipp>=0.5 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from importlib-metadata>=4.13.0->dask>=2024.2.1->dask[dataframe]>=2024.2.1->feast==0.40.1) (3.19.1)\n", + "Requirement already satisfied: mypy-extensions>=1.0.0 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from mypy>=0.910->SQLAlchemy[mypy]>1->feast==0.40.1) (1.0.0)\n", + "Requirement already satisfied: locket in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from partd>=1.2.0->dask>=2024.2.1->dask[dataframe]>=2024.2.1->feast==0.40.1) (1.0.0)\n", + "Requirement already satisfied: six>=1.5 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from python-dateutil>=2.8.2->pandas<3,>=1.4.3->feast==0.40.1) (1.16.0)\n", + "Requirement already satisfied: anyio<5,>=3.4.0 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from starlette<0.39.0,>=0.37.2->fastapi>=0.68.0->feast==0.40.1) (4.4.0)\n", + "Requirement already satisfied: sniffio>=1.1 in /Users/dmartino/.pyenv/versions/3.11.9/lib/python3.11/site-packages (from anyio<5,>=3.4.0->starlette<0.39.0,>=0.37.2->fastapi>=0.68.0->feast==0.40.1) (1.3.1)\n", + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# WE MUST ENSURE PYTHON CONSISTENCY BETWEEN NOTEBOOK AND FEAST SERVERS\n", + "# LAUNCH THIS NOTEBOOK FROM A CLEAN PYTHON ENVIRONMENT >3.9\n", + "%pip install feast==0.40.1" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Install Feast on Kind\n", + "## Objective\n", + "\n", + "Provide a reference implementation of a runbook to deploy a Feast development environment on a Kubernets cluster using [Kind](https://kind.sigs.k8s.io/docs/user/quick-start).\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "* [Kind](https://kind.sigs.k8s.io/) cluster and a Docker runtime container\n", + "* [kubectl](https://kubernetes.io/docs/tasks/tools/#kubectl) Kubernetes CLI tool.\n", + "* [Helm](https://helm.sh/) Kubernetes package manager.\n", + "* [yq](https://github.com/mikefarah/yq) YAML processor." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Install Prerequisites\n", + "The following commands install and configure all the prerequisites on MacOS environment. You can find the\n", + "equivalent instructions on the offical documentation pages:\n", + "* Install Kind and Docker runtime (e.g. [Colima](https://github.com/abiosoft/colima)).\n", + "* Create Kind cluster named `feast`.\n", + "* Install and setup the `kubectl` context.\n", + "* `Helm`.\n", + "* `yq`.\n", + "```bash\n", + "brew install colima\n", + "colima start\n", + "brew install kind\n", + "kind create cluster --name feast\n", + "kind start\n", + "brew install helm\n", + "brew install kubectl\n", + "kubectl config use-context kind-feast\n", + "brew install yq\n", + "```\n", + "\n", + "Additionally, we create a `feast` namespace and use it as the default for the `kubectl` CLI:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "namespace/feast created\n", + "Context \"kind-feast\" modified.\n" + ] + } + ], + "source": [ + "!kubectl create ns feast\n", + "!kubectl config set-context --current --namespace feast" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Validate the cluster setup:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "NAME STATUS AGE\n", + "default Active 26h\n", + "feast Active 3s\n", + "kube-node-lease Active 26h\n", + "kube-public Active 26h\n", + "kube-system Active 26h\n", + "local-path-storage Active 26h\n" + ] + } + ], + "source": [ + "!kubectl get ns" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Deployment Architecture\n", + "The primary objective of this runbook is to guide the deployment of Feast services on a Kubernetes Kind cluster, using the default `postgres` template to set up a basic feature store.\n", + "\n", + "> 🚀 We will also add instructions to repeat the example with a custom project, for a personalized experience.\n", + "\n", + "In this notebook, we will deploy a distributed topology of Feast services, which includes:\n", + "\n", + "* `Registry Server`: Exposes endpoints at the [default port 6570](https://github.com/feast-dev/feast/blob/89bc5512572130510dd18690309b5a392aaf73b1/sdk/python/feast/constants.py#L39) and handles metadata storage for feature definitions.\n", + "* `Online Store Server`: Exposes endpoints at the [default port 6566](https://github.com/feast-dev/feast/blob/4a6b663f80bc91d6de35ed2ec428d34811d17a18/sdk/python/feast/cli.py#L871-L872). This service uses the `Registry Server` to query metadata and is responsible for low-latency serving of features.\n", + "* `Offline Store Server`: Exposes endpoints at the [default port 8815](https://github.com/feast-dev/feast/blob/89bc5512572130510dd18690309b5a392aaf73b1/sdk/python/feast/constants.py#L42). It uses the `Registry Server` to query metadata and provides access to batch data for historical feature retrieval.\n", + "\n", + "Each service is backed by a `PostgreSQL` database, which is also deployed within the same Kind cluster.\n", + "\n", + "Finally, port forwarding will be configured to expose these Feast services locally. This will allow a local client, implemented in the accompanying client notebook, to interact with the deployed services." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Install PostgreSQL\n", + "Install the [reference deployment](./postgres/postgres.yaml) to install and configure a simple PostgreSQL database." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "secret/postgres-secret created\n", + "persistentvolume/postgres-volume created\n", + "persistentvolumeclaim/postgres-volume-claim created\n", + "deployment.apps/postgres created\n", + "service/postgres created\n", + "deployment.apps/postgres condition met\n" + ] + } + ], + "source": [ + "!kubectl apply -f postgres/postgres.yaml\n", + "!kubectl wait --for=condition=available deployment/postgres --timeout=2m" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "NAME READY STATUS RESTARTS AGE\n", + "postgres-76c8d94d6-pngvm 1/1 Running 0 8s\n", + "NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE\n", + "postgres NodePort 10.96.231.4 5432:30565/TCP 8s\n" + ] + } + ], + "source": [ + "!kubectl get pods\n", + "!kubectl get svc" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create the feature store project\n", + "Use the `feast init` command to create the default project.\n", + "\n", + "We also start port forwarding for the `postgres` service to populate the tables with default data." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> 🚀 If you want to use a custom configuration, replace it under the sample/feature_repo folder and skip this section" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Port-forwarding postgres with process ID: 9611\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Forwarding from 127.0.0.1:5432 -> 5432\n", + "Forwarding from [::1]:5432 -> 5432\n" + ] + } + ], + "source": [ + "from src.utils import port_forward\n", + "psql_process = port_forward(\"postgres\", 5432, 5432)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We are going to emulate the `feast init -t postgres sample` command using Python code. This is needed to mock the request of additional\n", + "parameters to configure the DB connection and also request the upload of example data to Postgres tables." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Handling connection for 5432\n", + "Handling connection for 5432\n", + "\n", + "Creating a new Feast repository in \u001b[1m\u001b[32m/Users/dmartino/projects/AI/feast/feast/examples/kind-quickstart/sample\u001b[0m.\n", + "\n" + ] + } + ], + "source": [ + "from feast.repo_operations import init_repo\n", + "from unittest import mock\n", + "from feast.templates.postgres.bootstrap import bootstrap\n", + "\n", + "project_directory = \"sample\"\n", + "template = \"postgres\"\n", + "\n", + "with mock.patch(\"click.prompt\", side_effect=[\"localhost\", \"5432\", \"feast\", \"public\", \"feast\", \"feast\"]):\n", + " with mock.patch(\"click.confirm\", side_effect=[True]):\n", + " init_repo(project_directory, template)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Verify that the DB includes the expected tables with pre-populated data." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " List of relations\n", + " Schema | Name | Type | Owner \n", + "--------+---------------------------+-------+-------\n", + " public | feast_driver_hourly_stats | table | feast\n", + "(1 row)\n", + "\n", + " count \n", + "-------\n", + " 1807\n", + "(1 row)\n", + "\n" + ] + } + ], + "source": [ + "!PSQL_POD=$(kubectl get pods -l app=postgres -oname) && kubectl exec $PSQL_POD -- psql -h localhost -U feast feast -c '\\dt'\n", + "!PSQL_POD=$(kubectl get pods -l app=postgres -oname) && kubectl exec $PSQL_POD -- psql -h localhost -U feast feast -c 'select count(*) from feast_driver_hourly_stats'" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, let's stop port forwarding." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 501 10392 6947 0 1:12PM ttys051 0:00.12 /bin/zsh -c ps -ef | grep port-forward\n", + " 501 10394 10392 0 1:12PM ttys051 0:00.00 grep port-forward\n" + ] + } + ], + "source": [ + "psql_process.terminate()\n", + "!ps -ef | grep port-forward" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Generate server configurations\n", + "Each server has its own configuration that we generate from the one initialized before.\n", + "\n", + "We use `yq` to manipulate the original configuration and generate the server specifics.\n", + "\n", + "Note: from now on, we assume that the Feast service names will be as follows:\n", + "* For `Registry Server`: `registry-server`\n", + "* For `Online Store`: `online-server`\n", + "* For `Offline Store`: `offline-server`\n", + "\n", + "> 🚀 If you used different service names, replace the `host` parameter in the following `yq` commands." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "env: FEATURE_REPO_DIR=sample/feature_repo\n", + "project: sample\n", + "provider: local\n", + "registry:\n", + " registry_type: sql\n", + " path: postgresql://feast:feast@postgres:5432/feast\n", + " cache_ttl_seconds: 60\n", + " sqlalchemy_config_kwargs:\n", + " echo: false\n", + " pool_pre_ping: true\n", + "online_store:\n", + " type: postgres\n", + " host: postgres\n", + " port: 5432\n", + " database: feast\n", + " db_schema: public\n", + " user: feast\n", + " password: feast\n", + "offline_store:\n", + " type: postgres\n", + " host: postgres\n", + " port: 5432\n", + " database: feast\n", + " db_schema: public\n", + " user: feast\n", + " password: feast\n", + "entity_key_serialization_version: 2\n" + ] + } + ], + "source": [ + "%env FEATURE_REPO_DIR=sample/feature_repo\n", + "# Adjust the database host to match the postgres service\n", + "!yq -i '.registry.path=\"postgresql://feast:feast@postgres:5432/feast\"' $FEATURE_REPO_DIR/feature_store.yaml\n", + "!yq -i '.online_store.host=\"postgres\"' $FEATURE_REPO_DIR/feature_store.yaml\n", + "!yq -i '.offline_store.host=\"postgres\"' $FEATURE_REPO_DIR/feature_store.yaml\n", + "!cat $FEATURE_REPO_DIR/feature_store.yaml" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "project: sample\n", + "registry:\n", + " registry_type: sql\n", + " path: postgresql://feast:feast@postgres:5432/feast\n", + " cache_ttl_seconds: 60\n", + " sqlalchemy_config_kwargs:\n", + " echo: false\n", + " pool_pre_ping: true\n", + "provider: local\n", + "entity_key_serialization_version: 2\n" + ] + } + ], + "source": [ + "# Registry server has only `registry` section\n", + "!cat $FEATURE_REPO_DIR/feature_store.yaml | yq '.project | {key: .}, .registry | {key: .}, .provider | {key: .}, .entity_key_serialization_version | {key: .}' > registry_feature_store.yaml\n", + "! cat registry_feature_store.yaml" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "project: sample\n", + "provider: local\n", + "online_store:\n", + " type: postgres\n", + " host: postgres\n", + " port: 5432\n", + " database: feast\n", + " db_schema: public\n", + " user: feast\n", + " password: feast\n", + "entity_key_serialization_version: 2\n", + "registry:\n", + " path: registry-server:80\n", + " registry_type: remote\n", + "offline_store:\n", + " type: remote\n", + " host: offline-server\n", + " port: 80\n" + ] + } + ], + "source": [ + "# Online server has `online_store` section, a remote `registry` and a remote `offline_store`\n", + "!cat $FEATURE_REPO_DIR/feature_store.yaml | yq '.project | {key: .}, .provider | {key: .}, .online_store | {key: .}, .entity_key_serialization_version | {key: .}' > online_feature_store.yaml\n", + "!yq -i '.registry.path=\"registry-server:80\"' online_feature_store.yaml\n", + "!yq -i '.registry.registry_type=\"remote\"' online_feature_store.yaml\n", + "!yq -i '.offline_store.type=\"remote\"' online_feature_store.yaml\n", + "!yq -i '.offline_store.host=\"offline-server\"' online_feature_store.yaml\n", + "!yq -i '.offline_store.port=80' online_feature_store.yaml\n", + "\n", + "!cat online_feature_store.yaml" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "project: sample\n", + "provider: local\n", + "offline_store:\n", + " type: postgres\n", + " host: postgres\n", + " port: 5432\n", + " database: feast\n", + " db_schema: public\n", + " user: feast\n", + " password: feast\n", + "entity_key_serialization_version: 2\n", + "registry:\n", + " path: registry-server:80\n", + " registry_type: remote\n" + ] + } + ], + "source": [ + "# Offline server has `offline_store` section and a remote `registry`\n", + "!cat $FEATURE_REPO_DIR/feature_store.yaml | yq '.project | {key: .}, .provider | {key: .}, .offline_store | {key: .}, .entity_key_serialization_version | {key: .}' > offline_feature_store.yaml\n", + "!yq -i '.registry.path=\"registry-server:80\"' offline_feature_store.yaml\n", + "!yq -i '.registry.registry_type=\"remote\"' offline_feature_store.yaml\n", + "!cat offline_feature_store.yaml" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Encode configuration files\n", + "Next step is to encode in base64 the configuration files for each server. We'll store the output in environment variables." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "def base64_file(file):\n", + " import base64\n", + "\n", + " with open(file, 'rb') as file:\n", + " yaml_content = file.read()\n", + " return base64.b64encode(yaml_content).decode('utf-8')\n", + "\n", + "os.environ['REGISTRY_CONFIG_BASE64'] = base64_file('registry_feature_store.yaml')\n", + "os.environ['ONLINE_CONFIG_BASE64'] = base64_file('online_feature_store.yaml')\n", + "os.environ['OFFLINE_CONFIG_BASE64'] = base64_file('offline_feature_store.yaml')" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "REGISTRY_CONFIG_BASE64=cHJvamVjdDogc2FtcGxlCnJlZ2lzdHJ5OgogIHJlZ2lzdHJ5X3R5cGU6IHNxbAogIHBhdGg6IHBvc3RncmVzcWw6Ly9mZWFzdDpmZWFzdEBwb3N0Z3Jlczo1NDMyL2ZlYXN0CiAgY2FjaGVfdHRsX3NlY29uZHM6IDYwCiAgc3FsYWxjaGVteV9jb25maWdfa3dhcmdzOgogICAgZWNobzogZmFsc2UKICAgIHBvb2xfcHJlX3Bpbmc6IHRydWUKcHJvdmlkZXI6IGxvY2FsCmVudGl0eV9rZXlfc2VyaWFsaXphdGlvbl92ZXJzaW9uOiAyCg==\n", + "ONLINE_CONFIG_BASE64=cHJvamVjdDogc2FtcGxlCnByb3ZpZGVyOiBsb2NhbApvbmxpbmVfc3RvcmU6CiAgdHlwZTogcG9zdGdyZXMKICBob3N0OiBwb3N0Z3JlcwogIHBvcnQ6IDU0MzIKICBkYXRhYmFzZTogZmVhc3QKICBkYl9zY2hlbWE6IHB1YmxpYwogIHVzZXI6IGZlYXN0CiAgcGFzc3dvcmQ6IGZlYXN0CmVudGl0eV9rZXlfc2VyaWFsaXphdGlvbl92ZXJzaW9uOiAyCnJlZ2lzdHJ5OgogIHBhdGg6IHJlZ2lzdHJ5LXNlcnZlcjo4MAogIHJlZ2lzdHJ5X3R5cGU6IHJlbW90ZQpvZmZsaW5lX3N0b3JlOgogIHR5cGU6IHJlbW90ZQogIGhvc3Q6IG9mZmxpbmUtc2VydmVyCiAgcG9ydDogODAK\n", + "OFFLINE_CONFIG_BASE64=cHJvamVjdDogc2FtcGxlCnByb3ZpZGVyOiBsb2NhbApvZmZsaW5lX3N0b3JlOgogIHR5cGU6IHBvc3RncmVzCiAgaG9zdDogcG9zdGdyZXMKICBwb3J0OiA1NDMyCiAgZGF0YWJhc2U6IGZlYXN0CiAgZGJfc2NoZW1hOiBwdWJsaWMKICB1c2VyOiBmZWFzdAogIHBhc3N3b3JkOiBmZWFzdAplbnRpdHlfa2V5X3NlcmlhbGl6YXRpb25fdmVyc2lvbjogMgpyZWdpc3RyeToKICBwYXRoOiByZWdpc3RyeS1zZXJ2ZXI6ODAKICByZWdpc3RyeV90eXBlOiByZW1vdGUK\n" + ] + } + ], + "source": [ + "!env | grep BASE64" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Install servers\n", + "We'll use the charts defined in this local repository to install the servers.\n", + "\n", + "The installation order reflects the dependency between the deployments:\n", + "* `Registry Server` starts first because it has no dependencies\n", + "* Then `Offline Server` as it depends only on the `Registry Server`\n", + "* Last the `Online Server` that depends on both the other servers" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "env: FEAST_IMAGE_REPO=feastdev/feature-server\n", + "env: FEAST_IMAGE_VERSION=0.40.1\n" + ] + } + ], + "source": [ + "%env FEAST_IMAGE_REPO=feastdev/feature-server\n", + "%env FEAST_IMAGE_VERSION=0.40.1" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Release \"feast-registry\" does not exist. Installing it now.\n", + "NAME: feast-registry\n", + "LAST DEPLOYED: Tue Sep 17 13:14:05 2024\n", + "NAMESPACE: feast\n", + "STATUS: deployed\n", + "REVISION: 1\n", + "TEST SUITE: None\n", + "deployment.apps/registry-server condition met\n" + ] + } + ], + "source": [ + "# Registry\n", + "!helm upgrade --install feast-registry ../../infra/charts/feast-feature-server \\\n", + "--set fullnameOverride=registry-server --set feast_mode=registry \\\n", + "--set image.repository=${FEAST_IMAGE_REPO} --set image.tag=${FEAST_IMAGE_VERSION} \\\n", + "--set feature_store_yaml_base64=$REGISTRY_CONFIG_BASE64\n", + "\n", + "!kubectl wait --for=condition=available deployment/registry-server --timeout=2m" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Release \"feast-offline\" does not exist. Installing it now.\n", + "NAME: feast-offline\n", + "LAST DEPLOYED: Tue Sep 17 13:14:33 2024\n", + "NAMESPACE: feast\n", + "STATUS: deployed\n", + "REVISION: 1\n", + "TEST SUITE: None\n", + "deployment.apps/offline-server condition met\n" + ] + } + ], + "source": [ + "# Offline\n", + "!helm upgrade --install feast-offline ../../infra/charts/feast-feature-server \\\n", + "--set fullnameOverride=offline-server --set feast_mode=offline \\\n", + "--set image.repository=${FEAST_IMAGE_REPO} --set image.tag=${FEAST_IMAGE_VERSION} \\\n", + "--set feature_store_yaml_base64=$OFFLINE_CONFIG_BASE64\n", + "\n", + "!kubectl wait --for=condition=available deployment/offline-server --timeout=2m" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Release \"feast-online\" does not exist. Installing it now.\n", + "NAME: feast-online\n", + "LAST DEPLOYED: Tue Sep 17 13:14:55 2024\n", + "NAMESPACE: feast\n", + "STATUS: deployed\n", + "REVISION: 1\n", + "TEST SUITE: None\n", + "deployment.apps/online-server condition met\n" + ] + } + ], + "source": [ + "# Online\n", + "!helm upgrade --install feast-online ../../infra/charts/feast-feature-server \\\n", + "--set fullnameOverride=online-server --set feast_mode=online \\\n", + "--set image.repository=${FEAST_IMAGE_REPO} --set image.tag=${FEAST_IMAGE_VERSION} \\\n", + "--set feature_store_yaml_base64=$ONLINE_CONFIG_BASE64\n", + "\n", + "!kubectl wait --for=condition=available deployment/online-server --timeout=2m" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Validate deployment\n", + "Fist validate application and service status:" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE\n", + "offline-server ClusterIP 10.96.24.216 80/TCP 44s\n", + "online-server ClusterIP 10.96.36.113 80/TCP 22s\n", + "postgres NodePort 10.96.231.4 5432:30565/TCP 4m14s\n", + "registry-server ClusterIP 10.96.128.48 80/TCP 71s\n", + "NAME READY UP-TO-DATE AVAILABLE AGE\n", + "offline-server 1/1 1 1 44s\n", + "online-server 1/1 1 1 22s\n", + "postgres 1/1 1 1 4m14s\n", + "registry-server 1/1 1 1 71s\n", + "NAME READY STATUS RESTARTS AGE\n", + "offline-server-6c59467c75-9jvq7 1/1 Running 0 45s\n", + "online-server-76968bbc48-qlvvj 1/1 Running 0 23s\n", + "postgres-76c8d94d6-pngvm 1/1 Running 0 4m15s\n", + "registry-server-597c5cd445-nrm75 1/1 Running 0 72s\n" + ] + } + ], + "source": [ + "!kubectl get svc\n", + "!kubectl get deployments\n", + "!kubectl get pods" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then verify the content of the local configuration file (it's stored in `/tmp/` folder with random subfolder)." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "project: sample\n", + "registry:\n", + " registry_type: sql\n", + " path: postgresql://feast:feast@postgres:5432/feast\n", + " cache_ttl_seconds: 60\n", + " sqlalchemy_config_kwargs:\n", + " echo: false\n", + " pool_pre_ping: true\n", + "provider: local\n", + "entity_key_serialization_version: 2\n" + ] + } + ], + "source": [ + "!kubectl exec deployment/registry-server -- find /tmp -name feature_store.yaml -exec cat {} \\;" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "project: sample\n", + "provider: local\n", + "offline_store:\n", + " type: postgres\n", + " host: postgres\n", + " port: 5432\n", + " database: feast\n", + " db_schema: public\n", + " user: feast\n", + " password: feast\n", + "entity_key_serialization_version: 2\n", + "registry:\n", + " path: registry-server:80\n", + " registry_type: remote\n" + ] + } + ], + "source": [ + "!kubectl exec deployment/offline-server -- find /tmp -name feature_store.yaml -exec cat {} \\;" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "project: sample\n", + "provider: local\n", + "online_store:\n", + " type: postgres\n", + " host: postgres\n", + " port: 5432\n", + " database: feast\n", + " db_schema: public\n", + " user: feast\n", + " password: feast\n", + "entity_key_serialization_version: 2\n", + "registry:\n", + " path: registry-server:80\n", + " registry_type: remote\n", + "offline_store:\n", + " type: remote\n", + " host: offline-server\n", + " port: 80\n" + ] + } + ], + "source": [ + "!kubectl exec deployment/online-server -- find /tmp -name feature_store.yaml -exec cat {} \\;" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, let's verify the `feast` version in each server" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + ": MADV_DONTNEED does not work (memset will be used instead)\n", + ": (This is the expected behaviour if you are running under QEMU)\n", + "Feast SDK Version: \"0.40.1\"\n", + ": MADV_DONTNEED does not work (memset will be used instead)\n", + ": (This is the expected behaviour if you are running under QEMU)\n", + "Feast SDK Version: \"0.40.1\"\n", + ": MADV_DONTNEED does not work (memset will be used instead)\n", + ": (This is the expected behaviour if you are running under QEMU)\n", + "Feast SDK Version: \"0.40.1\"\n" + ] + } + ], + "source": [ + "!kubectl exec deployment/registry-server -- feast version\n", + "!kubectl exec deployment/offline-server -- feast version\n", + "!kubectl exec deployment/online-server -- feast version" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "feast3.11", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/kind-quickstart/02-Client.ipynb b/examples/kind-quickstart/02-Client.ipynb new file mode 100644 index 0000000000..322a95a61b --- /dev/null +++ b/examples/kind-quickstart/02-Client.ipynb @@ -0,0 +1,606 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Requirement already satisfied: feast==0.40.1 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (0.40.1)\n", + "Requirement already satisfied: click<9.0.0,>=7.0.0 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from feast==0.40.1) (8.1.7)\n", + "Requirement already satisfied: colorama<1,>=0.3.9 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from feast==0.40.1) (0.4.6)\n", + "Requirement already satisfied: dill~=0.3.0 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from feast==0.40.1) (0.3.8)\n", + "Requirement already satisfied: mypy-protobuf>=3.1 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from feast==0.40.1) (3.3.0)\n", + "Requirement already satisfied: Jinja2<4,>=2 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from feast==0.40.1) (3.1.4)\n", + "Requirement already satisfied: jsonschema in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from feast==0.40.1) (4.22.0)\n", + "Requirement already satisfied: mmh3 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from feast==0.40.1) (4.1.0)\n", + "Requirement already satisfied: numpy<2,>=1.22 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from feast==0.40.1) (1.26.4)\n", + "Requirement already satisfied: pandas<3,>=1.4.3 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from feast==0.40.1) (2.2.2)\n", + "Requirement already satisfied: protobuf<5.0.0,>=4.24.0 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from feast==0.40.1) (4.25.3)\n", + "Requirement already satisfied: pyarrow>=4 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from feast==0.40.1) (15.0.2)\n", + "Requirement already satisfied: pydantic>=2.0.0 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from feast==0.40.1) (2.7.4)\n", + "Requirement already satisfied: pygments<3,>=2.12.0 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from feast==0.40.1) (2.18.0)\n", + "Requirement already satisfied: PyYAML<7,>=5.4.0 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from feast==0.40.1) (6.0.1)\n", + "Requirement already satisfied: requests in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from feast==0.40.1) (2.32.3)\n", + "Requirement already satisfied: SQLAlchemy>1 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from SQLAlchemy[mypy]>1->feast==0.40.1) (2.0.31)\n", + "Requirement already satisfied: tabulate<1,>=0.8.0 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from feast==0.40.1) (0.9.0)\n", + "Requirement already satisfied: tenacity<9,>=7 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from feast==0.40.1) (8.4.2)\n", + "Requirement already satisfied: toml<1,>=0.10.0 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from feast==0.40.1) (0.10.2)\n", + "Requirement already satisfied: tqdm<5,>=4 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from feast==0.40.1) (4.66.4)\n", + "Requirement already satisfied: typeguard>=4.0.0 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from feast==0.40.1) (4.3.0)\n", + "Requirement already satisfied: fastapi>=0.68.0 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from feast==0.40.1) (0.111.0)\n", + "Requirement already satisfied: uvicorn<1,>=0.14.0 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from uvicorn[standard]<1,>=0.14.0->feast==0.40.1) (0.30.1)\n", + "Requirement already satisfied: dask>=2024.2.1 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from dask[dataframe]>=2024.2.1->feast==0.40.1) (2024.6.2)\n", + "Requirement already satisfied: gunicorn in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from feast==0.40.1) (22.0.0)\n", + "Requirement already satisfied: cloudpickle>=1.5.0 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from dask>=2024.2.1->dask[dataframe]>=2024.2.1->feast==0.40.1) (3.0.0)\n", + "Requirement already satisfied: fsspec>=2021.09.0 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from dask>=2024.2.1->dask[dataframe]>=2024.2.1->feast==0.40.1) (2023.12.2)\n", + "Requirement already satisfied: packaging>=20.0 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from dask>=2024.2.1->dask[dataframe]>=2024.2.1->feast==0.40.1) (24.1)\n", + "Requirement already satisfied: partd>=1.2.0 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from dask>=2024.2.1->dask[dataframe]>=2024.2.1->feast==0.40.1) (1.4.2)\n", + "Requirement already satisfied: toolz>=0.10.0 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from dask>=2024.2.1->dask[dataframe]>=2024.2.1->feast==0.40.1) (0.12.1)\n", + "Requirement already satisfied: importlib-metadata>=4.13.0 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from dask>=2024.2.1->dask[dataframe]>=2024.2.1->feast==0.40.1) (8.0.0)\n", + "Requirement already satisfied: dask-expr<1.2,>=1.1 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from dask[dataframe]>=2024.2.1->feast==0.40.1) (1.1.6)\n", + "Requirement already satisfied: starlette<0.38.0,>=0.37.2 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from fastapi>=0.68.0->feast==0.40.1) (0.37.2)\n", + "Requirement already satisfied: typing-extensions>=4.8.0 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from fastapi>=0.68.0->feast==0.40.1) (4.12.2)\n", + "Requirement already satisfied: fastapi-cli>=0.0.2 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from fastapi>=0.68.0->feast==0.40.1) (0.0.4)\n", + "Requirement already satisfied: httpx>=0.23.0 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from fastapi>=0.68.0->feast==0.40.1) (0.27.0)\n", + "Requirement already satisfied: python-multipart>=0.0.7 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from fastapi>=0.68.0->feast==0.40.1) (0.0.9)\n", + "Requirement already satisfied: ujson!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,>=4.0.1 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from fastapi>=0.68.0->feast==0.40.1) (5.10.0)\n", + "Requirement already satisfied: orjson>=3.2.1 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from fastapi>=0.68.0->feast==0.40.1) (3.10.5)\n", + "Requirement already satisfied: email_validator>=2.0.0 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from fastapi>=0.68.0->feast==0.40.1) (2.2.0)\n", + "Requirement already satisfied: MarkupSafe>=2.0 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from Jinja2<4,>=2->feast==0.40.1) (2.1.5)\n", + "Requirement already satisfied: types-protobuf>=3.19.12 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from mypy-protobuf>=3.1->feast==0.40.1) (3.19.22)\n", + "Requirement already satisfied: python-dateutil>=2.8.2 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from pandas<3,>=1.4.3->feast==0.40.1) (2.9.0.post0)\n", + "Requirement already satisfied: pytz>=2020.1 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from pandas<3,>=1.4.3->feast==0.40.1) (2024.1)\n", + "Requirement already satisfied: tzdata>=2022.7 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from pandas<3,>=1.4.3->feast==0.40.1) (2024.1)\n", + "Requirement already satisfied: annotated-types>=0.4.0 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from pydantic>=2.0.0->feast==0.40.1) (0.7.0)\n", + "Requirement already satisfied: pydantic-core==2.18.4 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from pydantic>=2.0.0->feast==0.40.1) (2.18.4)\n", + "Requirement already satisfied: mypy>=0.910 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from SQLAlchemy[mypy]>1->feast==0.40.1) (1.10.1)\n", + "Requirement already satisfied: h11>=0.8 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from uvicorn<1,>=0.14.0->uvicorn[standard]<1,>=0.14.0->feast==0.40.1) (0.14.0)\n", + "Requirement already satisfied: httptools>=0.5.0 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from uvicorn[standard]<1,>=0.14.0->feast==0.40.1) (0.6.1)\n", + "Requirement already satisfied: python-dotenv>=0.13 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from uvicorn[standard]<1,>=0.14.0->feast==0.40.1) (1.0.1)\n", + "Requirement already satisfied: uvloop!=0.15.0,!=0.15.1,>=0.14.0 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from uvicorn[standard]<1,>=0.14.0->feast==0.40.1) (0.19.0)\n", + "Requirement already satisfied: watchfiles>=0.13 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from uvicorn[standard]<1,>=0.14.0->feast==0.40.1) (0.22.0)\n", + "Requirement already satisfied: websockets>=10.4 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from uvicorn[standard]<1,>=0.14.0->feast==0.40.1) (12.0)\n", + "Requirement already satisfied: attrs>=22.2.0 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from jsonschema->feast==0.40.1) (23.2.0)\n", + "Requirement already satisfied: jsonschema-specifications>=2023.03.6 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from jsonschema->feast==0.40.1) (2023.12.1)\n", + "Requirement already satisfied: referencing>=0.28.4 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from jsonschema->feast==0.40.1) (0.35.1)\n", + "Requirement already satisfied: rpds-py>=0.7.1 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from jsonschema->feast==0.40.1) (0.18.1)\n", + "Requirement already satisfied: charset-normalizer<4,>=2 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from requests->feast==0.40.1) (3.3.2)\n", + "Requirement already satisfied: idna<4,>=2.5 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from requests->feast==0.40.1) (3.7)\n", + "Requirement already satisfied: urllib3<3,>=1.21.1 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from requests->feast==0.40.1) (1.26.19)\n", + "Requirement already satisfied: certifi>=2017.4.17 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from requests->feast==0.40.1) (2024.7.4)\n", + "Requirement already satisfied: dnspython>=2.0.0 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from email_validator>=2.0.0->fastapi>=0.68.0->feast==0.40.1) (2.6.1)\n", + "Requirement already satisfied: typer>=0.12.3 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from fastapi-cli>=0.0.2->fastapi>=0.68.0->feast==0.40.1) (0.12.3)\n", + "Requirement already satisfied: anyio in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from httpx>=0.23.0->fastapi>=0.68.0->feast==0.40.1) (4.4.0)\n", + "Requirement already satisfied: httpcore==1.* in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from httpx>=0.23.0->fastapi>=0.68.0->feast==0.40.1) (1.0.5)\n", + "Requirement already satisfied: sniffio in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from httpx>=0.23.0->fastapi>=0.68.0->feast==0.40.1) (1.3.1)\n", + "Requirement already satisfied: zipp>=0.5 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from importlib-metadata>=4.13.0->dask>=2024.2.1->dask[dataframe]>=2024.2.1->feast==0.40.1) (3.19.1)\n", + "Requirement already satisfied: mypy-extensions>=1.0.0 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from mypy>=0.910->SQLAlchemy[mypy]>1->feast==0.40.1) (1.0.0)\n", + "Requirement already satisfied: locket in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from partd>=1.2.0->dask>=2024.2.1->dask[dataframe]>=2024.2.1->feast==0.40.1) (1.0.0)\n", + "Requirement already satisfied: six>=1.5 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from python-dateutil>=2.8.2->pandas<3,>=1.4.3->feast==0.40.1) (1.16.0)\n", + "Requirement already satisfied: shellingham>=1.3.0 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from typer>=0.12.3->fastapi-cli>=0.0.2->fastapi>=0.68.0->feast==0.40.1) (1.5.4)\n", + "Requirement already satisfied: rich>=10.11.0 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from typer>=0.12.3->fastapi-cli>=0.0.2->fastapi>=0.68.0->feast==0.40.1) (13.7.1)\n", + "Requirement already satisfied: markdown-it-py>=2.2.0 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from rich>=10.11.0->typer>=0.12.3->fastapi-cli>=0.0.2->fastapi>=0.68.0->feast==0.40.1) (3.0.0)\n", + "Requirement already satisfied: mdurl~=0.1 in /Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages (from markdown-it-py>=2.2.0->rich>=10.11.0->typer>=0.12.3->fastapi-cli>=0.0.2->fastapi>=0.68.0->feast==0.40.1) (0.1.2)\n", + "\n", + "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m24.1.1\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m24.2\u001b[0m\n", + "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpip install --upgrade pip\u001b[0m\n", + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "# WE MUST ENSURE PYTHON CONSISTENCY BETWEEN NOTEBOOK AND FEAST SERVERS\n", + "# LAUNCH THIS NOTEBOOK FROM A CLEAN PYTHON ENVIRONMENT >3.9\n", + "%pip install feast==0.40.1" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Run a test client\n", + "\n", + "> 🚀 This test is developer to work only with the default feature store generated by `feast init`. \n", + "> \n", + "> To test a custom feature store you need to run a custom test application, but still using the same client configuration that we've prepared." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Apply the feature store definitions" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The feature store cannot be initialized using remote services. \n", + "\n", + "We'll use the original `feature_store.yaml` from within a Kubernetes `Job` to run `feast apply`.\n", + "\n", + "For the same reason, we also run an initial materialization from the `Job`, otherwise it would fail because of uninmplemented APIs in the remote servers, like [online_write_batch](https://github.com/feast-dev/feast/blob/4a6b663f80bc91d6de35ed2ec428d34811d17a18/sdk/python/feast/infra/online_stores/remote.py#L50).\n", + "\n", + "First we create a `ConfigMap` holding the required code and configuration." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "env: FEATURE_REPO_DIR=sample/feature_repo\n", + "Error from server (NotFound): configmaps \"sample-repo\" not found\n", + "configmap/sample-repo created\n", + "\n", + "Inspect keys of sample-repo ConfigMap\n", + "example_repo.py\n", + "feature_store.yaml\n" + ] + } + ], + "source": [ + "%env FEATURE_REPO_DIR=sample/feature_repo\n", + "!kubectl delete configmap sample-repo\n", + "!kubectl create configmap sample-repo --from-file=${FEATURE_REPO_DIR}/example_repo.py,${FEATURE_REPO_DIR}/feature_store.yaml\n", + "!echo\n", + "!echo \"Inspect keys of sample-repo ConfigMap\"\n", + "!kubectl get configmaps sample-repo -oyaml | yq '.data[] | key'" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then we create the `Job` to apply the definitions, according to the [init-job.yaml](./init-job.yaml) manifest" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Error from server (NotFound): error when deleting \"init-job.yaml\": jobs.batch \"feast-apply-job\" not found\n", + "job.batch/feast-apply-job created\n" + ] + } + ], + "source": [ + "!kubectl delete -f init-job.yaml\n", + "!kubectl apply -f init-job.yaml" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Monitoring the log of the `Job`." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "pod/feast-apply-job-tzscd condition met\n", + "Starting feast initialization job...\n", + ": MADV_DONTNEED does not work (memset will be used instead)\n", + ": (This is the expected behaviour if you are running under QEMU)\n", + "09/17/2024 11:18:10 AM feast.repo_config WARNING: The `path` of the `RegistryConfig` starts with a plain `postgresql` string. We are updating this to `postgresql+psycopg` to ensure that the `psycopg3` driver is used by `sqlalchemy`. If you want to use `psycopg2` pass `postgresql+psycopg2` explicitely to `path`. To silence this warning, pass `postgresql+psycopg` explicitely to `path`.\n", + "/usr/local/lib/python3.11/site-packages/feast/feature_store.py:590: RuntimeWarning: On demand feature view is an experimental feature. This API is stable, but the functionality does not scale well for offline retrieval\n", + " warnings.warn(\n", + "Deploying infrastructure for driver_hourly_stats_fresh\n", + "Deploying infrastructure for driver_hourly_stats\n", + ": MADV_DONTNEED does not work (memset will be used instead)\n", + ": (This is the expected behaviour if you are running under QEMU)\n", + "09/17/2024 11:18:21 AM feast.repo_config WARNING: The `path` of the `RegistryConfig` starts with a plain `postgresql` string. We are updating this to `postgresql+psycopg` to ensure that the `psycopg3` driver is used by `sqlalchemy`. If you want to use `psycopg2` pass `postgresql+psycopg2` explicitely to `path`. To silence this warning, pass `postgresql+psycopg` explicitely to `path`.\n", + "09/17/2024 11:18:21 AM root WARNING: _list_feature_views will make breaking changes. Please use _list_batch_feature_views instead. _list_feature_views will behave like _list_all_feature_views in the future.\n", + "Materializing \u001b[1m\u001b[32m2\u001b[0m feature views to \u001b[1m\u001b[32m2024-09-17 11:18:11+00:00\u001b[0m into the \u001b[1m\u001b[32mpostgres\u001b[0m online store.\n", + "\n", + "\u001b[1m\u001b[32mdriver_hourly_stats_fresh\u001b[0m from \u001b[1m\u001b[32m2024-09-16 11:18:21+00:00\u001b[0m to \u001b[1m\u001b[32m2024-09-17 11:18:11+00:00\u001b[0m:\n", + "100%|█████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 72.23it/s]\n", + "\u001b[1m\u001b[32mdriver_hourly_stats\u001b[0m from \u001b[1m\u001b[32m2024-09-16 11:18:22+00:00\u001b[0m to \u001b[1m\u001b[32m2024-09-17 11:18:11+00:00\u001b[0m:\n", + "100%|████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 654.75it/s]\n", + "Feast initialization completed successfully.\n" + ] + } + ], + "source": [ + "!INIT_JOB_POD=$(kubectl get pods -l job-name=feast-apply-job -oname) && kubectl wait --for=condition=podscheduled $INIT_JOB_POD --timeout=2m\n", + "!INIT_JOB_POD=$(kubectl get pods -l job-name=feast-apply-job -oname) && kubectl logs -f $INIT_JOB_POD\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Forwarding the feast service ports" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To run the test client from the notebook, we need to forward the service ports to ports on the current host." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE\n", + "offline-server ClusterIP 10.96.24.216 80/TCP 3m58s\n", + "online-server ClusterIP 10.96.36.113 80/TCP 3m36s\n", + "postgres NodePort 10.96.231.4 5432:30565/TCP 7m28s\n", + "registry-server ClusterIP 10.96.128.48 80/TCP 4m25s\n" + ] + } + ], + "source": [ + "!kubectl get svc" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Port-forwarding registry-server with process ID: 15094\n", + "Port-forwarding offline-server with process ID: 15095\n", + "Port-forwarding online-server with process ID: 15096\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Forwarding from 127.0.0.1:8002 -> 8815\n", + "Forwarding from 127.0.0.1:8003 -> 6566\n", + "Forwarding from 127.0.0.1:8001 -> 6570\n", + "Forwarding from [::1]:8002 -> 8815\n", + "Forwarding from [::1]:8003 -> 6566\n", + "Forwarding from [::1]:8001 -> 6570\n" + ] + } + ], + "source": [ + "from src.utils import port_forward\n", + "registry_process = port_forward(\"registry-server\", 8001)\n", + "offline_process = port_forward(\"offline-server\", 8002)\n", + "online_process = port_forward(\"online-server\", 8003)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 501 15094 13456 0 1:18PM ?? 0:00.06 kubectl port-forward service/registry-server 8001:80\n", + " 501 15095 13456 0 1:18PM ?? 0:00.05 kubectl port-forward service/offline-server 8002:80\n", + " 501 15096 13456 0 1:18PM ?? 0:00.06 kubectl port-forward service/online-server 8003:80\n", + " 501 15170 13456 0 1:18PM ttys051 0:00.14 /bin/zsh -c ps -ef | grep port-forward\n", + " 501 15173 15170 0 1:18PM ttys051 0:00.00 grep port-forward\n" + ] + } + ], + "source": [ + "!ps -ef | grep port-forward" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Client configuration\n", + "The client configuration is using only remote clients connected to the forwarded ports, from 8001 to 8003." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "project: sample\n", + "registry:\n", + " path: localhost:8001\n", + " registry_type: remote\n", + "offline_store:\n", + " host: localhost\n", + " port: 8002\n", + " type: remote\n", + "online_store:\n", + " path: http://localhost:8003\n", + " type: remote\n", + "entity_key_serialization_version: 2\n", + "auth:\n", + " type: no_auth\n" + ] + } + ], + "source": [ + "!cat client/feature_store.yaml" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Install test code\n", + "First we copy the test code from `sample/feature_repo` to `client` folder." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "client/__init__.py client/test_workflow.py\n" + ] + } + ], + "source": [ + "!cp sample/feature_repo/test_workflow.py client\n", + "!ls client/*.py" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We update the original test to comment the `apply`, `teardown` and `materialize-incremental` commands." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "12,13c12,13\n", + "< # print(\"\\n--- Run feast apply to setup feature store on Postgres ---\")\n", + "< # subprocess.run([\"feast\", \"apply\"])\n", + "---\n", + "> print(\"\\n--- Run feast apply to setup feature store on Postgres ---\")\n", + "> subprocess.run([\"feast\", \"apply\"])\n", + "21,22c21,22\n", + "< # print(\"\\n--- Load features into online store ---\")\n", + "< # store.materialize_incremental(end_date=datetime.now())\n", + "---\n", + "> print(\"\\n--- Load features into online store ---\")\n", + "> store.materialize_incremental(end_date=datetime.now())\n", + "56,57c56,57\n", + "< # print(\"\\n--- Run feast teardown ---\")\n", + "< # subprocess.run([\"feast\", \"teardown\"])\n", + "---\n", + "> print(\"\\n--- Run feast teardown ---\")\n", + "> subprocess.run([\"feast\", \"teardown\"])\n" + ] + } + ], + "source": [ + "!sed -i.bk 's/subprocess.run/# subprocess.run/' client/test_workflow.py\n", + "!sed -i.bk 's/print(\"\\\\n--- Run feast/# print(\"\\\\n--- Run feast/' client/test_workflow.py\n", + "!sed -i.bk 's/store.materialize_incremental/# store.materialize_incremental/' client/test_workflow.py\n", + "!sed -i.bk 's/print(\"\\\\n--- Load features/# print(\"\\\\n--- Load features/' client/test_workflow.py\n", + "!diff client/test_workflow.py sample/feature_repo/test_workflow.py" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, we run the full test suite from the client folder." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Handling connection for 8001\n", + "\n", + "--- Historical features for training ---\n", + "WARNING:root:_list_feature_views will make breaking changes. Please use _list_batch_feature_views instead. _list_feature_views will behave like _list_all_feature_views in the future.\n", + "Handling connection for 8002\n", + " driver_id event_timestamp ... conv_rate_plus_val1 conv_rate_plus_val2\n", + "0 1001 2021-04-12 10:59:42 ... 1.302426 10.302426\n", + "1 1002 2021-04-12 08:12:10 ... 2.436384 20.436384\n", + "2 1003 2021-04-12 16:40:26 ... 3.954102 30.954102\n", + "\n", + "[3 rows x 10 columns]\n", + "\n", + "--- Historical features for batch scoring ---\n", + "WARNING:root:_list_feature_views will make breaking changes. Please use _list_batch_feature_views instead. _list_feature_views will behave like _list_all_feature_views in the future.\n", + "Handling connection for 8002\n", + " driver_id ... conv_rate_plus_val2\n", + "0 1001 ... 10.798974\n", + "1 1002 ... 20.316096\n", + "2 1003 ... 30.202964\n", + "\n", + "[3 rows x 10 columns]\n", + "\n", + "--- Online features ---\n", + "WARNING:root:_list_feature_views will make breaking changes. Please use _list_batch_feature_views instead. _list_feature_views will behave like _list_all_feature_views in the future.\n", + "Handling connection for 8003\n", + "acc_rate : [0.22748562693595886, 0.9316393733024597]\n", + "conv_rate_plus_val1 : [1000.7989742159843, 1001.3160955905914]Handling connection for 8003\n", + "\n", + "conv_rate_plus_val2 : [2000.7989742159843, 2002.3160955905914]\n", + "driver_id : [1001, 1002]\n", + "\n", + "--- Online features retrieved (instead) through a feature service---\n", + "WARNING:root:_list_feature_views will make breaking changes. Please use _list_batch_feature_views instead. _list_feature_views will behave like _list_all_feature_views in the future.\n", + "Handling connection for 8003\n", + "conv_rate : [0.7989742159843445, 0.31609559059143066]\n", + "conv_rate_plus_val1 : [1000.7989742159843, 1001.3160955905914]\n", + "conv_rate_plus_val2 : [2000.7989742159843, 2002.3160955905914]\n", + "driver_id : [1001, 1002]\n", + "\n", + "--- Online features retrieved (using feature service v3, which uses a feature view with a push source---\n", + "WARNING:root:_list_feature_views will make breaking changes. Please use _list_batch_feature_views instead. _list_feature_views will behave like _list_all_feature_views in the future.\n", + "acc_rate : [0.22748562693595886, 0.9316393733024597]\n", + "avg_daily_trips : [451, 417]\n", + "conv_rate : [0.7989742159843445, 0.31609559059143066]\n", + "conv_rate_plus_val1 : [1000.7989742159843, 1001.3160955905914]\n", + "conv_rate_plus_val2 : [2000.7989742159843, 2002.3160955905914]\n", + "driver_id : [1001, 1002]\n", + "\n", + "--- Simulate a stream event ingestion of the hourly stats df ---\n", + " driver_id event_timestamp ... acc_rate avg_daily_trips\n", + "0 1001 2024-09-17 13:19:54.105733 ... 1.0 1000\n", + "\n", + "[1 rows x 6 columns]\n", + "WARNING:root:list_feature_views will make breaking changes. Please use list_batch_feature_views instead. list_feature_views will behave like list_all_feature_views in the future.\n", + "WARNING:root:_list_feature_views will make breaking changes. Please use _list_batch_feature_views instead. _list_feature_views will behave like _list_all_feature_views in the future.\n", + "Traceback (most recent call last):\n", + " File \"/Users/dmartino/projects/AI/feast/feast/examples/kind-quickstart/client/test_workflow.py\", line 130, in \n", + " run_demo()\n", + " File \"/Users/dmartino/projects/AI/feast/feast/examples/kind-quickstart/client/test_workflow.py\", line 51, in run_demo\n", + " store.push(\"driver_stats_push_source\", event_df, to=PushMode.ONLINE_AND_OFFLINE)\n", + " File \"/Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages/feast/feature_store.py\", line 1423, in push\n", + " self.write_to_online_store(\n", + " File \"/Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages/feast/feature_store.py\", line 1449, in write_to_online_store\n", + " feature_view: FeatureView = self.get_stream_feature_view(\n", + " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + " File \"/Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages/feast/feature_store.py\", line 504, in get_stream_feature_view\n", + " return self._get_stream_feature_view(\n", + " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + " File \"/Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages/feast/feature_store.py\", line 514, in _get_stream_feature_view\n", + " stream_feature_view = self._registry.get_stream_feature_view(\n", + " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + " File \"/Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages/feast/infra/registry/remote.py\", line 209, in get_stream_feature_view\n", + " response = self.stub.GetStreamFeatureView(request)\n", + " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + " File \"/Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages/grpc/_channel.py\", line 1181, in __call__\n", + " return _end_unary_response_blocking(state, call, False, None)\n", + " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + " File \"/Users/dmartino/miniconda3/envs/feast3.11/lib/python3.11/site-packages/grpc/_channel.py\", line 1006, in _end_unary_response_blocking\n", + " raise _InactiveRpcError(state) # pytype: disable=not-instantiable\n", + " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + "grpc._channel._InactiveRpcError: <_InactiveRpcError of RPC that terminated with:\n", + "\tstatus = StatusCode.UNKNOWN\n", + "\tdetails = \"Exception calling application: Feature view driver_hourly_stats_fresh does not exist in project sample\"\n", + "\tdebug_error_string = \"UNKNOWN:Error received from peer {grpc_message:\"Exception calling application: Feature view driver_hourly_stats_fresh does not exist in project sample\", grpc_status:2, created_time:\"2024-09-17T13:19:54.127834+02:00\"}\"\n", + ">\n" + ] + } + ], + "source": [ + "!cd client && python test_workflow.py" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note If you see the following error, it is likely due to the [issue #4392](https://github.com/feast-dev/feast/issues/4392):\n", + "Remote registry client does not map application errors:\n", + "\n", + "```\n", + "Feature view driver_hourly_stats_fresh does not exist in project sample\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Terminate port forwarding" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 501 16434 13456 0 1:20PM ttys051 0:00.12 /bin/zsh -c ps -ef | grep port-forward\n", + " 501 16436 16434 0 1:20PM ttys051 0:00.00 grep port-forward\n" + ] + } + ], + "source": [ + "registry_process.terminate()\n", + "offline_process.terminate()\n", + "online_process.terminate()\n", + "!ps -ef | grep port-forward" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "feast3.11", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/kind-quickstart/03-Uninstall.ipynb b/examples/kind-quickstart/03-Uninstall.ipynb new file mode 100644 index 0000000000..20874fc1b7 --- /dev/null +++ b/examples/kind-quickstart/03-Uninstall.ipynb @@ -0,0 +1,120 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Uninstall deployment\n", + "Use Helm to uninstall all the previous deployments" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "release \"feast-online\" uninstalled\n", + "release \"feast-offline\" uninstalled\n", + "release \"feast-registry\" uninstalled\n", + "NAME\tNAMESPACE\tREVISION\tUPDATED\tSTATUS\tCHART\tAPP VERSION\n" + ] + } + ], + "source": [ + "!helm uninstall feast-online\n", + "!helm uninstall feast-offline\n", + "!helm uninstall feast-registry\n", + "!helm list" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Delete the PostgreSQL deployment." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "secret \"postgres-secret\" deleted\n", + "persistentvolume \"postgres-volume\" deleted\n", + "persistentvolumeclaim \"postgres-volume-claim\" deleted\n", + "deployment.apps \"postgres\" deleted\n", + "service \"postgres\" deleted\n" + ] + } + ], + "source": [ + "!kubectl delete -f postgres/postgres.yaml" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "No resources found in feast namespace.\n", + "No resources found in feast namespace.\n", + "NAME READY STATUS RESTARTS AGE\n", + "feast-apply-job-tzscd 0/1 Completed 0 2m40s\n" + ] + } + ], + "source": [ + "!kubectl get svc\n", + "!kubectl get deployments\n", + "!kubectl get pods" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "feast3.11", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/kind-quickstart/README.md b/examples/kind-quickstart/README.md new file mode 100644 index 0000000000..25ecfc8ecf --- /dev/null +++ b/examples/kind-quickstart/README.md @@ -0,0 +1,7 @@ +# Install and run Feast with Kind + +The following notebooks will guide you through an end-to-end journey to install and validate a simple Feast feature store in a +Kind Kubernetes cluster: +* [01-Install.ipynb](./01-Install.ipynb): Install and configure the cluster, then the Feast components. +* [02-Client.ipynb](./02-Client.ipynb): Validate the feature store with a remote test application runnning on the notebook. +* [03-Uninstall.ipynb](./03-Uninstall.ipynb): Clear the installed deployments. diff --git a/examples/kind-quickstart/client/__init__.py b/examples/kind-quickstart/client/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/kind-quickstart/client/feature_store.yaml b/examples/kind-quickstart/client/feature_store.yaml new file mode 100644 index 0000000000..62acd3ead6 --- /dev/null +++ b/examples/kind-quickstart/client/feature_store.yaml @@ -0,0 +1,14 @@ +project: sample +registry: + path: localhost:8001 + registry_type: remote +offline_store: + host: localhost + port: 8002 + type: remote +online_store: + path: http://localhost:8003 + type: remote +entity_key_serialization_version: 2 +auth: + type: no_auth diff --git a/examples/kind-quickstart/init-job.yaml b/examples/kind-quickstart/init-job.yaml new file mode 100644 index 0000000000..68df35af73 --- /dev/null +++ b/examples/kind-quickstart/init-job.yaml @@ -0,0 +1,31 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: feast-apply-job +spec: + template: + spec: + containers: + - name: feast-apply + image: feastdev/feature-server:0.40.1 + command: ["/bin/sh", "-c"] + args: + - | + echo "Starting feast initialization job..."; + mkdir /tmp/sample; + cd /tmp/sample; + cp /sample/* .; + sed -i 's/localhost/postgres/' feature_store.yaml; + feast apply; + CURRENT_TIME=$(date -u +"%Y-%m-%dT%H:%M:%S"); + feast materialize-incremental $CURRENT_TIME; + echo "Feast initialization completed successfully."; + volumeMounts: + - name: sample-repo-files + mountPath: /sample + restartPolicy: Never + volumes: + - name: sample-repo-files + configMap: + name: sample-repo + backoffLimit: 1 diff --git a/examples/kind-quickstart/postgres/postgres.yaml b/examples/kind-quickstart/postgres/postgres.yaml new file mode 100644 index 0000000000..c89a01f0f4 --- /dev/null +++ b/examples/kind-quickstart/postgres/postgres.yaml @@ -0,0 +1,83 @@ +#https://www.digitalocean.com/community/tutorials/how-to-deploy-postgres-to-kubernetes-cluster +apiVersion: v1 +kind: Secret +metadata: + name: postgres-secret + labels: + app: postgres +stringData: + POSTGRES_DB: feast + POSTGRES_USER: feast + POSTGRES_PASSWORD: feast +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: postgres-volume + labels: + type: local + app: postgres +spec: + capacity: + storage: 1Gi + accessModes: + - ReadWriteOnce + hostPath: + path: /data/postgresql +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: postgres-volume-claim + labels: + app: postgres +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: postgres +spec: + replicas: 1 + selector: + matchLabels: + app: postgres + template: + metadata: + labels: + app: postgres + spec: + containers: + - name: postgres + image: 'postgres:15-alpine' + imagePullPolicy: IfNotPresent + ports: + - containerPort: 5432 + envFrom: + - secretRef: + name: postgres-secret + volumeMounts: + - mountPath: /var/lib/postgresql/data + name: postgresdata + volumes: + - name: postgresdata + persistentVolumeClaim: + claimName: postgres-volume-claim +--- +apiVersion: v1 +kind: Service +metadata: + name: postgres + labels: + app: postgres +spec: + type: NodePort + ports: + - port: 5432 + selector: + app: postgres \ No newline at end of file diff --git a/examples/kind-quickstart/src/__init__.py b/examples/kind-quickstart/src/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/kind-quickstart/src/utils.py b/examples/kind-quickstart/src/utils.py new file mode 100644 index 0000000000..ea549d7ed8 --- /dev/null +++ b/examples/kind-quickstart/src/utils.py @@ -0,0 +1,12 @@ +import subprocess + +def port_forward(service, external_port, local_port=80) : + """ + Run a background process to forward port 80 of the given `service` service to the given `external_port` port. + + Returns: the process instance + """ + command = ["kubectl", "port-forward", f"service/{service}", f"{external_port}:{local_port}"] + process = subprocess.Popen(command) + print(f"Port-forwarding {service} with process ID: {process.pid}") + return process From 0192b2eb245c8e0ea9a913195ddf28382dc23982 Mon Sep 17 00:00:00 2001 From: lokeshrangineni <19699092+lokeshrangineni@users.noreply.github.com> Date: Tue, 24 Sep 2024 01:29:07 -0400 Subject: [PATCH 101/185] fix: Fixing the master branch build failure. (#4563) Fixing the master branch build failure. Signed-off-by: Lokesh Rangineni <19699092+lokeshrangineni@users.noreply.github.com> --- .../feast/core/FeatureViewProjection_pb2.py | 11 +++++---- .../feast/core/FeatureViewProjection_pb2.pyi | 23 +++++++++++++++++- .../feast/core/OnDemandFeatureView_pb2.py | 24 +++++++++---------- .../feast/core/OnDemandFeatureView_pb2.pyi | 15 +++++++++++- 4 files changed, 54 insertions(+), 19 deletions(-) diff --git a/sdk/python/feast/protos/feast/core/FeatureViewProjection_pb2.py b/sdk/python/feast/protos/feast/core/FeatureViewProjection_pb2.py index 286f511658..b47d4fe392 100644 --- a/sdk/python/feast/protos/feast/core/FeatureViewProjection_pb2.py +++ b/sdk/python/feast/protos/feast/core/FeatureViewProjection_pb2.py @@ -13,9 +13,10 @@ from feast.protos.feast.core import Feature_pb2 as feast_dot_core_dot_Feature__pb2 +from feast.protos.feast.core import DataSource_pb2 as feast_dot_core_dot_DataSource__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n&feast/core/FeatureViewProjection.proto\x12\nfeast.core\x1a\x18\x66\x65\x61st/core/Feature.proto\"\x83\x02\n\x15\x46\x65\x61tureViewProjection\x12\x19\n\x11\x66\x65\x61ture_view_name\x18\x01 \x01(\t\x12\x1f\n\x17\x66\x65\x61ture_view_name_alias\x18\x03 \x01(\t\x12\x32\n\x0f\x66\x65\x61ture_columns\x18\x02 \x03(\x0b\x32\x19.feast.core.FeatureSpecV2\x12G\n\x0cjoin_key_map\x18\x04 \x03(\x0b\x32\x31.feast.core.FeatureViewProjection.JoinKeyMapEntry\x1a\x31\n\x0fJoinKeyMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42Z\n\x10\x66\x65\x61st.proto.coreB\x15\x46\x65\x61tureReferenceProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n&feast/core/FeatureViewProjection.proto\x12\nfeast.core\x1a\x18\x66\x65\x61st/core/Feature.proto\x1a\x1b\x66\x65\x61st/core/DataSource.proto\"\xba\x03\n\x15\x46\x65\x61tureViewProjection\x12\x19\n\x11\x66\x65\x61ture_view_name\x18\x01 \x01(\t\x12\x1f\n\x17\x66\x65\x61ture_view_name_alias\x18\x03 \x01(\t\x12\x32\n\x0f\x66\x65\x61ture_columns\x18\x02 \x03(\x0b\x32\x19.feast.core.FeatureSpecV2\x12G\n\x0cjoin_key_map\x18\x04 \x03(\x0b\x32\x31.feast.core.FeatureViewProjection.JoinKeyMapEntry\x12\x17\n\x0ftimestamp_field\x18\x05 \x01(\t\x12\x1d\n\x15\x64\x61te_partition_column\x18\x06 \x01(\t\x12 \n\x18\x63reated_timestamp_column\x18\x07 \x01(\t\x12,\n\x0c\x62\x61tch_source\x18\x08 \x01(\x0b\x32\x16.feast.core.DataSource\x12-\n\rstream_source\x18\t \x01(\x0b\x32\x16.feast.core.DataSource\x1a\x31\n\x0fJoinKeyMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42Z\n\x10\x66\x65\x61st.proto.coreB\x15\x46\x65\x61tureReferenceProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -25,8 +26,8 @@ _globals['DESCRIPTOR']._serialized_options = b'\n\020feast.proto.coreB\025FeatureReferenceProtoZ/github.com/feast-dev/feast/go/protos/feast/core' _globals['_FEATUREVIEWPROJECTION_JOINKEYMAPENTRY']._options = None _globals['_FEATUREVIEWPROJECTION_JOINKEYMAPENTRY']._serialized_options = b'8\001' - _globals['_FEATUREVIEWPROJECTION']._serialized_start=81 - _globals['_FEATUREVIEWPROJECTION']._serialized_end=340 - _globals['_FEATUREVIEWPROJECTION_JOINKEYMAPENTRY']._serialized_start=291 - _globals['_FEATUREVIEWPROJECTION_JOINKEYMAPENTRY']._serialized_end=340 + _globals['_FEATUREVIEWPROJECTION']._serialized_start=110 + _globals['_FEATUREVIEWPROJECTION']._serialized_end=552 + _globals['_FEATUREVIEWPROJECTION_JOINKEYMAPENTRY']._serialized_start=503 + _globals['_FEATUREVIEWPROJECTION_JOINKEYMAPENTRY']._serialized_end=552 # @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/FeatureViewProjection_pb2.pyi b/sdk/python/feast/protos/feast/core/FeatureViewProjection_pb2.pyi index 2c0a298e14..6b44ad4a93 100644 --- a/sdk/python/feast/protos/feast/core/FeatureViewProjection_pb2.pyi +++ b/sdk/python/feast/protos/feast/core/FeatureViewProjection_pb2.pyi @@ -4,6 +4,7 @@ isort:skip_file """ import builtins import collections.abc +import feast.core.DataSource_pb2 import feast.core.Feature_pb2 import google.protobuf.descriptor import google.protobuf.internal.containers @@ -43,6 +44,11 @@ class FeatureViewProjection(google.protobuf.message.Message): FEATURE_VIEW_NAME_ALIAS_FIELD_NUMBER: builtins.int FEATURE_COLUMNS_FIELD_NUMBER: builtins.int JOIN_KEY_MAP_FIELD_NUMBER: builtins.int + TIMESTAMP_FIELD_FIELD_NUMBER: builtins.int + DATE_PARTITION_COLUMN_FIELD_NUMBER: builtins.int + CREATED_TIMESTAMP_COLUMN_FIELD_NUMBER: builtins.int + BATCH_SOURCE_FIELD_NUMBER: builtins.int + STREAM_SOURCE_FIELD_NUMBER: builtins.int feature_view_name: builtins.str """The feature view name""" feature_view_name_alias: builtins.str @@ -53,6 +59,15 @@ class FeatureViewProjection(google.protobuf.message.Message): @property def join_key_map(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: """Map for entity join_key overrides of feature data entity join_key to entity data join_key""" + timestamp_field: builtins.str + date_partition_column: builtins.str + created_timestamp_column: builtins.str + @property + def batch_source(self) -> feast.core.DataSource_pb2.DataSource: + """Batch/Offline DataSource where this view can retrieve offline feature data.""" + @property + def stream_source(self) -> feast.core.DataSource_pb2.DataSource: + """Streaming DataSource from where this view can consume "online" feature data.""" def __init__( self, *, @@ -60,7 +75,13 @@ class FeatureViewProjection(google.protobuf.message.Message): feature_view_name_alias: builtins.str = ..., feature_columns: collections.abc.Iterable[feast.core.Feature_pb2.FeatureSpecV2] | None = ..., join_key_map: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + timestamp_field: builtins.str = ..., + date_partition_column: builtins.str = ..., + created_timestamp_column: builtins.str = ..., + batch_source: feast.core.DataSource_pb2.DataSource | None = ..., + stream_source: feast.core.DataSource_pb2.DataSource | None = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["feature_columns", b"feature_columns", "feature_view_name", b"feature_view_name", "feature_view_name_alias", b"feature_view_name_alias", "join_key_map", b"join_key_map"]) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["batch_source", b"batch_source", "stream_source", b"stream_source"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["batch_source", b"batch_source", "created_timestamp_column", b"created_timestamp_column", "date_partition_column", b"date_partition_column", "feature_columns", b"feature_columns", "feature_view_name", b"feature_view_name", "feature_view_name_alias", b"feature_view_name_alias", "join_key_map", b"join_key_map", "stream_source", b"stream_source", "timestamp_field", b"timestamp_field"]) -> None: ... global___FeatureViewProjection = FeatureViewProjection diff --git a/sdk/python/feast/protos/feast/core/OnDemandFeatureView_pb2.py b/sdk/python/feast/protos/feast/core/OnDemandFeatureView_pb2.py index 4be551724c..a27c4fba3b 100644 --- a/sdk/python/feast/protos/feast/core/OnDemandFeatureView_pb2.py +++ b/sdk/python/feast/protos/feast/core/OnDemandFeatureView_pb2.py @@ -20,7 +20,7 @@ from feast.protos.feast.core import Transformation_pb2 as feast_dot_core_dot_Transformation__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$feast/core/OnDemandFeatureView.proto\x12\nfeast.core\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1c\x66\x65\x61st/core/FeatureView.proto\x1a&feast/core/FeatureViewProjection.proto\x1a\x18\x66\x65\x61st/core/Feature.proto\x1a\x1b\x66\x65\x61st/core/DataSource.proto\x1a\x1f\x66\x65\x61st/core/Transformation.proto\"{\n\x13OnDemandFeatureView\x12\x31\n\x04spec\x18\x01 \x01(\x0b\x32#.feast.core.OnDemandFeatureViewSpec\x12\x31\n\x04meta\x18\x02 \x01(\x0b\x32#.feast.core.OnDemandFeatureViewMeta\"\x99\x04\n\x17OnDemandFeatureViewSpec\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12+\n\x08\x66\x65\x61tures\x18\x03 \x03(\x0b\x32\x19.feast.core.FeatureSpecV2\x12\x41\n\x07sources\x18\x04 \x03(\x0b\x32\x30.feast.core.OnDemandFeatureViewSpec.SourcesEntry\x12\x42\n\x15user_defined_function\x18\x05 \x01(\x0b\x32\x1f.feast.core.UserDefinedFunctionB\x02\x18\x01\x12\x43\n\x16\x66\x65\x61ture_transformation\x18\n \x01(\x0b\x32#.feast.core.FeatureTransformationV2\x12\x13\n\x0b\x64\x65scription\x18\x06 \x01(\t\x12;\n\x04tags\x18\x07 \x03(\x0b\x32-.feast.core.OnDemandFeatureViewSpec.TagsEntry\x12\r\n\x05owner\x18\x08 \x01(\t\x12\x0c\n\x04mode\x18\x0b \x01(\t\x1aJ\n\x0cSourcesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.feast.core.OnDemandSource:\x02\x38\x01\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x8c\x01\n\x17OnDemandFeatureViewMeta\x12\x35\n\x11\x63reated_timestamp\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12:\n\x16last_updated_timestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xc8\x01\n\x0eOnDemandSource\x12/\n\x0c\x66\x65\x61ture_view\x18\x01 \x01(\x0b\x32\x17.feast.core.FeatureViewH\x00\x12\x44\n\x17\x66\x65\x61ture_view_projection\x18\x03 \x01(\x0b\x32!.feast.core.FeatureViewProjectionH\x00\x12\x35\n\x13request_data_source\x18\x02 \x01(\x0b\x32\x16.feast.core.DataSourceH\x00\x42\x08\n\x06source\"H\n\x13UserDefinedFunction\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04\x62ody\x18\x02 \x01(\x0c\x12\x11\n\tbody_text\x18\x03 \x01(\t:\x02\x18\x01\x42]\n\x10\x66\x65\x61st.proto.coreB\x18OnDemandFeatureViewProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$feast/core/OnDemandFeatureView.proto\x12\nfeast.core\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1c\x66\x65\x61st/core/FeatureView.proto\x1a&feast/core/FeatureViewProjection.proto\x1a\x18\x66\x65\x61st/core/Feature.proto\x1a\x1b\x66\x65\x61st/core/DataSource.proto\x1a\x1f\x66\x65\x61st/core/Transformation.proto\"{\n\x13OnDemandFeatureView\x12\x31\n\x04spec\x18\x01 \x01(\x0b\x32#.feast.core.OnDemandFeatureViewSpec\x12\x31\n\x04meta\x18\x02 \x01(\x0b\x32#.feast.core.OnDemandFeatureViewMeta\"\xfd\x04\n\x17OnDemandFeatureViewSpec\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12+\n\x08\x66\x65\x61tures\x18\x03 \x03(\x0b\x32\x19.feast.core.FeatureSpecV2\x12\x41\n\x07sources\x18\x04 \x03(\x0b\x32\x30.feast.core.OnDemandFeatureViewSpec.SourcesEntry\x12\x42\n\x15user_defined_function\x18\x05 \x01(\x0b\x32\x1f.feast.core.UserDefinedFunctionB\x02\x18\x01\x12\x43\n\x16\x66\x65\x61ture_transformation\x18\n \x01(\x0b\x32#.feast.core.FeatureTransformationV2\x12\x13\n\x0b\x64\x65scription\x18\x06 \x01(\t\x12;\n\x04tags\x18\x07 \x03(\x0b\x32-.feast.core.OnDemandFeatureViewSpec.TagsEntry\x12\r\n\x05owner\x18\x08 \x01(\t\x12\x0c\n\x04mode\x18\x0b \x01(\t\x12\x1d\n\x15write_to_online_store\x18\x0c \x01(\x08\x12\x10\n\x08\x65ntities\x18\r \x03(\t\x12\x31\n\x0e\x65ntity_columns\x18\x0e \x03(\x0b\x32\x19.feast.core.FeatureSpecV2\x1aJ\n\x0cSourcesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.feast.core.OnDemandSource:\x02\x38\x01\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x8c\x01\n\x17OnDemandFeatureViewMeta\x12\x35\n\x11\x63reated_timestamp\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12:\n\x16last_updated_timestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xc8\x01\n\x0eOnDemandSource\x12/\n\x0c\x66\x65\x61ture_view\x18\x01 \x01(\x0b\x32\x17.feast.core.FeatureViewH\x00\x12\x44\n\x17\x66\x65\x61ture_view_projection\x18\x03 \x01(\x0b\x32!.feast.core.FeatureViewProjectionH\x00\x12\x35\n\x13request_data_source\x18\x02 \x01(\x0b\x32\x16.feast.core.DataSourceH\x00\x42\x08\n\x06source\"H\n\x13UserDefinedFunction\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04\x62ody\x18\x02 \x01(\x0c\x12\x11\n\tbody_text\x18\x03 \x01(\t:\x02\x18\x01\x42]\n\x10\x66\x65\x61st.proto.coreB\x18OnDemandFeatureViewProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -39,15 +39,15 @@ _globals['_ONDEMANDFEATUREVIEW']._serialized_start=243 _globals['_ONDEMANDFEATUREVIEW']._serialized_end=366 _globals['_ONDEMANDFEATUREVIEWSPEC']._serialized_start=369 - _globals['_ONDEMANDFEATUREVIEWSPEC']._serialized_end=906 - _globals['_ONDEMANDFEATUREVIEWSPEC_SOURCESENTRY']._serialized_start=787 - _globals['_ONDEMANDFEATUREVIEWSPEC_SOURCESENTRY']._serialized_end=861 - _globals['_ONDEMANDFEATUREVIEWSPEC_TAGSENTRY']._serialized_start=863 - _globals['_ONDEMANDFEATUREVIEWSPEC_TAGSENTRY']._serialized_end=906 - _globals['_ONDEMANDFEATUREVIEWMETA']._serialized_start=909 - _globals['_ONDEMANDFEATUREVIEWMETA']._serialized_end=1049 - _globals['_ONDEMANDSOURCE']._serialized_start=1052 - _globals['_ONDEMANDSOURCE']._serialized_end=1252 - _globals['_USERDEFINEDFUNCTION']._serialized_start=1254 - _globals['_USERDEFINEDFUNCTION']._serialized_end=1326 + _globals['_ONDEMANDFEATUREVIEWSPEC']._serialized_end=1006 + _globals['_ONDEMANDFEATUREVIEWSPEC_SOURCESENTRY']._serialized_start=887 + _globals['_ONDEMANDFEATUREVIEWSPEC_SOURCESENTRY']._serialized_end=961 + _globals['_ONDEMANDFEATUREVIEWSPEC_TAGSENTRY']._serialized_start=963 + _globals['_ONDEMANDFEATUREVIEWSPEC_TAGSENTRY']._serialized_end=1006 + _globals['_ONDEMANDFEATUREVIEWMETA']._serialized_start=1009 + _globals['_ONDEMANDFEATUREVIEWMETA']._serialized_end=1149 + _globals['_ONDEMANDSOURCE']._serialized_start=1152 + _globals['_ONDEMANDSOURCE']._serialized_end=1352 + _globals['_USERDEFINEDFUNCTION']._serialized_start=1354 + _globals['_USERDEFINEDFUNCTION']._serialized_end=1426 # @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/OnDemandFeatureView_pb2.pyi b/sdk/python/feast/protos/feast/core/OnDemandFeatureView_pb2.pyi index d72a8f9862..b2ec15b186 100644 --- a/sdk/python/feast/protos/feast/core/OnDemandFeatureView_pb2.pyi +++ b/sdk/python/feast/protos/feast/core/OnDemandFeatureView_pb2.pyi @@ -104,6 +104,9 @@ class OnDemandFeatureViewSpec(google.protobuf.message.Message): TAGS_FIELD_NUMBER: builtins.int OWNER_FIELD_NUMBER: builtins.int MODE_FIELD_NUMBER: builtins.int + WRITE_TO_ONLINE_STORE_FIELD_NUMBER: builtins.int + ENTITIES_FIELD_NUMBER: builtins.int + ENTITY_COLUMNS_FIELD_NUMBER: builtins.int name: builtins.str """Name of the feature view. Must be unique. Not updated.""" project: builtins.str @@ -127,6 +130,13 @@ class OnDemandFeatureViewSpec(google.protobuf.message.Message): owner: builtins.str """Owner of the on demand feature view.""" mode: builtins.str + write_to_online_store: builtins.bool + @property + def entities(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: + """List of names of entities associated with this feature view.""" + @property + def entity_columns(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.Feature_pb2.FeatureSpecV2]: + """List of specifications for each entity defined as part of this feature view.""" def __init__( self, *, @@ -140,9 +150,12 @@ class OnDemandFeatureViewSpec(google.protobuf.message.Message): tags: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., owner: builtins.str = ..., mode: builtins.str = ..., + write_to_online_store: builtins.bool = ..., + entities: collections.abc.Iterable[builtins.str] | None = ..., + entity_columns: collections.abc.Iterable[feast.core.Feature_pb2.FeatureSpecV2] | None = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["feature_transformation", b"feature_transformation", "user_defined_function", b"user_defined_function"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["description", b"description", "feature_transformation", b"feature_transformation", "features", b"features", "mode", b"mode", "name", b"name", "owner", b"owner", "project", b"project", "sources", b"sources", "tags", b"tags", "user_defined_function", b"user_defined_function"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["description", b"description", "entities", b"entities", "entity_columns", b"entity_columns", "feature_transformation", b"feature_transformation", "features", b"features", "mode", b"mode", "name", b"name", "owner", b"owner", "project", b"project", "sources", b"sources", "tags", b"tags", "user_defined_function", b"user_defined_function", "write_to_online_store", b"write_to_online_store"]) -> None: ... global___OnDemandFeatureViewSpec = OnDemandFeatureViewSpec From 5850adc1290aa6e93f36fc6df4214cd9fdee39d4 Mon Sep 17 00:00:00 2001 From: Francisco Javier Arceo Date: Tue, 24 Sep 2024 11:32:40 -0400 Subject: [PATCH 102/185] chore: Adding docs, community, and examples to ignore during PR tests Signed-off-by: Francisco Javier Arceo --- .github/workflows/pr_integration_tests.yml | 6 +++++- .github/workflows/pr_local_integration_tests.yml | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr_integration_tests.yml b/.github/workflows/pr_integration_tests.yml index f4a9132d29..98dc06bcc1 100644 --- a/.github/workflows/pr_integration_tests.yml +++ b/.github/workflows/pr_integration_tests.yml @@ -2,6 +2,10 @@ name: pr-integration-tests on: pull_request_target: + paths-ignore: + - 'community/**' + - 'docs/**' + - 'examples/**' types: - opened - synchronize @@ -96,4 +100,4 @@ jobs: SNOWFLAKE_CI_PASSWORD: ${{ secrets.SNOWFLAKE_CI_PASSWORD }} SNOWFLAKE_CI_ROLE: ${{ secrets.SNOWFLAKE_CI_ROLE }} SNOWFLAKE_CI_WAREHOUSE: ${{ secrets.SNOWFLAKE_CI_WAREHOUSE }} - run: make test-python-integration \ No newline at end of file + run: make test-python-integration diff --git a/.github/workflows/pr_local_integration_tests.yml b/.github/workflows/pr_local_integration_tests.yml index 3de7262193..0a85361e3c 100644 --- a/.github/workflows/pr_local_integration_tests.yml +++ b/.github/workflows/pr_local_integration_tests.yml @@ -3,11 +3,14 @@ name: pr-local-integration-tests on: pull_request_target: + paths-ignore: + - 'community/**' + - 'docs/**' + - 'examples/**' types: - opened - synchronize - labeled - jobs: integration-test-python-local: # when using pull_request_target, all jobs MUST have this if check for 'ok-to-test' or 'approved' for security purposes. From 626c94f755fa8f04a5672be40abdd7bc0e1502b2 Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Tue, 24 Sep 2024 23:29:12 -0400 Subject: [PATCH 103/185] chore: Test FeatureView apply to expose entity_columns behavior pre and post apply() (#4571) * chore: Adding docs, community, and examples to ignore during PR tests Signed-off-by: Francisco Javier Arceo * chore: Updating test to confirm feature view apply behavior for entity columns Signed-off-by: Francisco Javier Arceo * removing changes from other PR Signed-off-by: Francisco Javier Arceo --------- Signed-off-by: Francisco Javier Arceo --- .github/workflows/pr_integration_tests.yml | 6 +----- .github/workflows/pr_local_integration_tests.yml | 5 +---- .../tests/unit/test_on_demand_python_transformation.py | 7 +++++++ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/pr_integration_tests.yml b/.github/workflows/pr_integration_tests.yml index 98dc06bcc1..f4a9132d29 100644 --- a/.github/workflows/pr_integration_tests.yml +++ b/.github/workflows/pr_integration_tests.yml @@ -2,10 +2,6 @@ name: pr-integration-tests on: pull_request_target: - paths-ignore: - - 'community/**' - - 'docs/**' - - 'examples/**' types: - opened - synchronize @@ -100,4 +96,4 @@ jobs: SNOWFLAKE_CI_PASSWORD: ${{ secrets.SNOWFLAKE_CI_PASSWORD }} SNOWFLAKE_CI_ROLE: ${{ secrets.SNOWFLAKE_CI_ROLE }} SNOWFLAKE_CI_WAREHOUSE: ${{ secrets.SNOWFLAKE_CI_WAREHOUSE }} - run: make test-python-integration + run: make test-python-integration \ No newline at end of file diff --git a/.github/workflows/pr_local_integration_tests.yml b/.github/workflows/pr_local_integration_tests.yml index 0a85361e3c..3de7262193 100644 --- a/.github/workflows/pr_local_integration_tests.yml +++ b/.github/workflows/pr_local_integration_tests.yml @@ -3,14 +3,11 @@ name: pr-local-integration-tests on: pull_request_target: - paths-ignore: - - 'community/**' - - 'docs/**' - - 'examples/**' types: - opened - synchronize - labeled + jobs: integration-test-python-local: # when using pull_request_target, all jobs MUST have this if check for 'ok-to-test' or 'approved' for security purposes. diff --git a/sdk/python/tests/unit/test_on_demand_python_transformation.py b/sdk/python/tests/unit/test_on_demand_python_transformation.py index c5bd68d6a8..ff7ad494ca 100644 --- a/sdk/python/tests/unit/test_on_demand_python_transformation.py +++ b/sdk/python/tests/unit/test_on_demand_python_transformation.py @@ -307,6 +307,8 @@ def setUp(self): online=True, source=driver_stats_source, ) + assert driver_stats_fv.entities == [driver.name] + assert driver_stats_fv.entity_columns == [] request_source = RequestSource( name="request_source", @@ -373,6 +375,11 @@ def python_view(inputs: dict[str, Any]) -> dict[str, Any]: feature_view_name="driver_hourly_stats", df=driver_df ) + fv_applied = self.store.get_feature_view("driver_hourly_stats") + assert fv_applied.entities == [driver.name] + # Note here that after apply() is called, the entity_columns are populated with the join_key + assert fv_applied.entity_columns[0].name == driver.join_key + def test_python_transformation_returning_all_data_types(self): entity_rows = [ { From 354c059e5475f9c3927d9180a421118507a22cf0 Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Tue, 24 Sep 2024 23:31:14 -0400 Subject: [PATCH 104/185] feat: Adding registry cache support for get_on_demand_feature_view (#4572) Signed-off-by: Francisco Javier Arceo --- sdk/python/feast/feature_store.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/sdk/python/feast/feature_store.py b/sdk/python/feast/feature_store.py index ab2bc6cec2..52556eda15 100644 --- a/sdk/python/feast/feature_store.py +++ b/sdk/python/feast/feature_store.py @@ -492,20 +492,24 @@ def _get_stream_feature_view( stream_feature_view.entities = [] return stream_feature_view - def get_on_demand_feature_view(self, name: str) -> OnDemandFeatureView: + def get_on_demand_feature_view( + self, name: str, allow_registry_cache: bool = False + ) -> OnDemandFeatureView: """ Retrieves a feature view. Args: name: Name of feature view. - + allow_registry_cache: (Optional) Whether to allow returning this entity from a cached registry Returns: The specified feature view. Raises: FeatureViewNotFoundException: The feature view could not be found. """ - return self._registry.get_on_demand_feature_view(name, self.project) + return self._registry.get_on_demand_feature_view( + name, self.project, allow_cache=allow_registry_cache + ) def get_data_source(self, name: str) -> DataSource: """ From 3198371fc0e07f6b51b62c7e3abbc48729078bb9 Mon Sep 17 00:00:00 2001 From: Theodor Mihalache <84387487+tmihalac@users.noreply.github.com> Date: Wed, 25 Sep 2024 13:01:49 -0400 Subject: [PATCH 105/185] fix: Removed usage of pull_request_target as much as possible to prevent security concerns (#4549) * Test workflow changes: on from pull_request_target to pull_request Signed-off-by: Theodor Mihalache * fix: Removed usage of pull_request_target as much as possible to prevent security concerns Signed-off-by: Theodor Mihalache --------- Signed-off-by: Theodor Mihalache --- .github/workflows/java_pr.yml | 7 +++++++ .github/workflows/lint_pr.yml | 8 ++------ .github/workflows/pr_integration_tests.yml | 4 ++++ .github/workflows/pr_local_integration_tests.yml | 12 +++++------- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/.github/workflows/java_pr.yml b/.github/workflows/java_pr.yml index fa373fea23..caf31ab47f 100644 --- a/.github/workflows/java_pr.yml +++ b/.github/workflows/java_pr.yml @@ -7,6 +7,9 @@ on: - synchronize - labeled +permissions: + pull-requests: read + jobs: lint-java: # when using pull_request_target, all jobs MUST have this if check for 'ok-to-test' or 'approved' for security purposes. @@ -23,6 +26,7 @@ jobs: # code from the PR. ref: refs/pull/${{ github.event.pull_request.number }}/merge submodules: recursive + persist-credentials: false - name: Lint java run: make lint-java @@ -42,6 +46,7 @@ jobs: # code from the PR. ref: refs/pull/${{ github.event.pull_request.number }}/merge submodules: recursive + persist-credentials: false - name: Set up JDK 11 uses: actions/setup-java@v1 with: @@ -84,6 +89,7 @@ jobs: - uses: actions/checkout@v4 with: submodules: 'true' + persist-credentials: false - name: Setup Python uses: actions/setup-python@v5 id: setup-python @@ -120,6 +126,7 @@ jobs: # code from the PR. ref: refs/pull/${{ github.event.pull_request.number }}/merge submodules: recursive + persist-credentials: false - name: Set up JDK 11 uses: actions/setup-java@v1 with: diff --git a/.github/workflows/lint_pr.yml b/.github/workflows/lint_pr.yml index d1aa7d16a3..8173225845 100644 --- a/.github/workflows/lint_pr.yml +++ b/.github/workflows/lint_pr.yml @@ -1,20 +1,16 @@ name: lint-pr on: - pull_request_target: + pull_request: types: - opened - edited - synchronize -permissions: - # read-only perms specified due to use of pull_request_target in lieu of security label check - pull-requests: read - jobs: validate-title: if: - github.repository == 'feast-dev/feast' + github.event.pull_request.base.repo.full_name == 'feast-dev/feast' name: Validate PR title runs-on: ubuntu-latest steps: diff --git a/.github/workflows/pr_integration_tests.yml b/.github/workflows/pr_integration_tests.yml index f4a9132d29..59de3ce958 100644 --- a/.github/workflows/pr_integration_tests.yml +++ b/.github/workflows/pr_integration_tests.yml @@ -11,6 +11,9 @@ on: #concurrency: # group: pr-integration-tests-${{ github.event.pull_request.number }} # cancel-in-progress: true +permissions: + actions: write + pull-requests: read jobs: integration-test-python: @@ -46,6 +49,7 @@ jobs: # code from the PR. ref: refs/pull/${{ github.event.pull_request.number }}/merge submodules: recursive + persist-credentials: false - name: Setup Python uses: actions/setup-python@v5 id: setup-python diff --git a/.github/workflows/pr_local_integration_tests.yml b/.github/workflows/pr_local_integration_tests.yml index 3de7262193..6515d411f0 100644 --- a/.github/workflows/pr_local_integration_tests.yml +++ b/.github/workflows/pr_local_integration_tests.yml @@ -2,7 +2,7 @@ name: pr-local-integration-tests # This runs local tests with containerized stubs of online stores. This is the main dev workflow on: - pull_request_target: + pull_request: types: - opened - synchronize @@ -10,11 +10,10 @@ on: jobs: integration-test-python-local: - # when using pull_request_target, all jobs MUST have this if check for 'ok-to-test' or 'approved' for security purposes. if: ((github.event.action == 'labeled' && (github.event.label.name == 'approved' || github.event.label.name == 'lgtm' || github.event.label.name == 'ok-to-test')) || (github.event.action != 'labeled' && (contains(github.event.pull_request.labels.*.name, 'ok-to-test') || contains(github.event.pull_request.labels.*.name, 'approved') || contains(github.event.pull_request.labels.*.name, 'lgtm')))) && - github.repository == 'feast-dev/feast' + github.event.pull_request.base.repo.full_name == 'feast-dev/feast' runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -27,10 +26,9 @@ jobs: steps: - uses: actions/checkout@v4 with: - # pull_request_target runs the workflow in the context of the base repo - # as such actions/checkout needs to be explicit configured to retrieve - # code from the PR. - ref: refs/pull/${{ github.event.pull_request.number }}/merge + repository: ${{ github.event.repository.full_name }} # Uses the full repository name + ref: ${{ github.ref }} # Uses the ref from the event + token: ${{ secrets.GITHUB_TOKEN }} # Automatically provided token submodules: recursive - name: Setup Python uses: actions/setup-python@v5 From 9ac7f4ea77357219fc8420f05e493767bc5357c2 Mon Sep 17 00:00:00 2001 From: Harri Lehtola <1781172+peruukki@users.noreply.github.com> Date: Thu, 26 Sep 2024 14:03:01 +0300 Subject: [PATCH 106/185] fix: Remove unnecessary peer dependencies from Feast UI (#4577) --- docs/reference/alpha-web-ui.md | 7 +++++-- ui/README.md | 7 +++++-- ui/package.json | 21 +++++++++------------ 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/docs/reference/alpha-web-ui.md b/docs/reference/alpha-web-ui.md index 2caeed9e2a..02dd107f1b 100644 --- a/docs/reference/alpha-web-ui.md +++ b/docs/reference/alpha-web-ui.md @@ -45,11 +45,14 @@ Start with bootstrapping a React app with `create-react-app` npx create-react-app your-feast-ui ``` -Then, in your app folder, install Feast UI and its peer dependencies. Assuming you use yarn +Then, in your app folder, install Feast UI and optionally its peer dependencies. Assuming you use yarn ``` yarn add @feast-dev/feast-ui -yarn add @elastic/eui @elastic/datemath @emotion/react moment prop-types inter-ui react-query react-router-dom use-query-params zod typescript query-string d3 @types/d3 +# For custom UI using the Elastic UI Framework (optional): +yarn add @elastic/eui +# For general custom styling (optional): +yarn add @emotion/react ``` Edit `index.js` in the React app to use Feast UI. diff --git a/ui/README.md b/ui/README.md index 852bddc296..a2326e1a9e 100644 --- a/ui/README.md +++ b/ui/README.md @@ -21,11 +21,14 @@ Start with bootstrapping a React app with `create-react-app` npx create-react-app your-feast-ui ``` -Then, in your app folder, install Feast UI and its peer dependencies. Assuming you use yarn +Then, in your app folder, install Feast UI and optionally its peer dependencies. Assuming you use yarn ``` yarn add @feast-dev/feast-ui -yarn add @elastic/eui @elastic/datemath @emotion/react moment prop-types inter-ui react-query react-router-dom use-query-params zod typescript query-string d3 @types/d3 +# For custom UI using the Elastic UI Framework (optional): +yarn add @elastic/eui +# For general custom styling (optional): +yarn add @emotion/react ``` Edit `index.js` in the React app to use Feast UI. diff --git a/ui/package.json b/ui/package.json index bc2a71378a..9524944db5 100644 --- a/ui/package.json +++ b/ui/package.json @@ -9,21 +9,18 @@ "types": "./dist/FeastUI.d.ts", "module": "./dist/feast-ui.module.js", "peerDependencies": { - "@elastic/datemath": "^5.0.3", "@elastic/eui": "^55.0.1", "@emotion/react": "^11.7.1", - "d3": "^7.3.0", - "inter-ui": "^3.19.3", - "moment": "^2.29.1", - "prop-types": "^15.8.1", - "query-string": "^7.1.1", "react": "^17.0.2", - "react-dom": "^17.0.2", - "react-query": "^3.34.12", - "react-router-dom": "<6.4.0", - "react-scripts": "^5.0.0", - "use-query-params": "^1.2.3", - "zod": "^3.11.6" + "react-dom": "^17.0.2" + }, + "peerDependenciesMeta": { + "@elastic/eui": { + "optional": true + }, + "@emotion/react": { + "optional": true + } }, "dependencies": { "@elastic/datemath": "^5.0.3", From 0390d8a86f50360a4df89165db62972328d22ca4 Mon Sep 17 00:00:00 2001 From: Shuchu Han Date: Thu, 26 Sep 2024 23:30:02 -0400 Subject: [PATCH 107/185] fix: Update the base image for feature-server. (#4576) Signed-off-by: Shuchu Han --- .../feast/infra/feature_servers/multicloud/Dockerfile | 7 ++++--- .../feast/infra/feature_servers/multicloud/Dockerfile.dev | 6 ++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/sdk/python/feast/infra/feature_servers/multicloud/Dockerfile b/sdk/python/feast/infra/feature_servers/multicloud/Dockerfile index 3114f03f52..9084eb4bce 100644 --- a/sdk/python/feast/infra/feature_servers/multicloud/Dockerfile +++ b/sdk/python/feast/infra/feature_servers/multicloud/Dockerfile @@ -1,8 +1,9 @@ -FROM python:3.11 - +FROM debian:11-slim RUN apt update && \ apt install -y \ jq \ + python3 \ + python3-pip \ python3-dev \ build-essential @@ -18,4 +19,4 @@ RUN apt update RUN apt -y install libarrow-dev # modify permissions to support running with a random uid RUN mkdir -m 775 /.cache -RUN chmod g+w $(python -c "import feast.ui as _; print(_.__path__)" | tr -d "[']")/build/projects-list.json +RUN chmod g+w $(python3 -c "import feast.ui as _; print(_.__path__)" | tr -d "[']")/build/projects-list.json diff --git a/sdk/python/feast/infra/feature_servers/multicloud/Dockerfile.dev b/sdk/python/feast/infra/feature_servers/multicloud/Dockerfile.dev index 49e70839a9..3be42056f0 100644 --- a/sdk/python/feast/infra/feature_servers/multicloud/Dockerfile.dev +++ b/sdk/python/feast/infra/feature_servers/multicloud/Dockerfile.dev @@ -1,8 +1,10 @@ -FROM python:3.11 +FROM debian:11-slim RUN apt update && \ apt install -y \ jq \ + python3 \ + python3-pip \ python3-dev \ build-essential @@ -19,4 +21,4 @@ RUN apt update RUN apt -y install libarrow-dev # modify permissions to support running with a random uid RUN mkdir -m 775 /.cache -RUN chmod g+w $(python -c "import feast.ui as _; print(_.__path__)" | tr -d "[']")/build/projects-list.json +RUN chmod g+w $(python3 -c "import feast.ui as _; print(_.__path__)" | tr -d "[']")/build/projects-list.json From 6adc157fc366eb2b49a5213ab4c4ec1415f85594 Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Fri, 27 Sep 2024 15:50:02 -0400 Subject: [PATCH 108/185] chore: Remove sqlite_vec assertion in test_online_retrieval.py (#4579) --- sdk/python/tests/unit/online_store/test_online_retrieval.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sdk/python/tests/unit/online_store/test_online_retrieval.py b/sdk/python/tests/unit/online_store/test_online_retrieval.py index 48adb6da7d..0a4880164f 100644 --- a/sdk/python/tests/unit/online_store/test_online_retrieval.py +++ b/sdk/python/tests/unit/online_store/test_online_retrieval.py @@ -548,7 +548,6 @@ def test_sqlite_vec_import() -> None: sqlite_version, vec_version = db.execute( "select sqlite_version(), vec_version()" ).fetchone() - assert vec_version == "v0.1.1" print(f"sqlite_version={sqlite_version}, vec_version={vec_version}") result = db.execute(""" From 8cd0dcff0cc3b1387c7ca65018ebb09e03538242 Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Fri, 27 Sep 2024 16:46:37 -0400 Subject: [PATCH 109/185] feat: Updated Feast model Inference Architecture (#4570) * Feast model Inference Architecture * Update model-inference.md * Update model-inference.md --- .../feast_model_inference_architecture.png | Bin 0 -> 878157 bytes .../architecture/model-inference.md | 8 ++++++++ 2 files changed, 8 insertions(+) create mode 100644 docs/assets/feast_model_inference_architecture.png diff --git a/docs/assets/feast_model_inference_architecture.png b/docs/assets/feast_model_inference_architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..3ea4fba4d05462643cb1efa4c6b7fadf8aab6e3e GIT binary patch literal 878157 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

QP;#Lb0dDVT!*f_>UhxPMplm=^66})vycY|2%K? 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 literal 0 HcmV?d00001 diff --git a/infra/charts/feast-feature-server/templates/deployment.yaml b/infra/charts/feast-feature-server/templates/deployment.yaml index 85b323610d..a550433db5 100644 --- a/infra/charts/feast-feature-server/templates/deployment.yaml +++ b/infra/charts/feast-feature-server/templates/deployment.yaml @@ -14,6 +14,9 @@ spec: {{- with .Values.podAnnotations }} annotations: {{- toYaml . | nindent 8 }} + {{- if .Values.metrics.enabled }} + instrumentation.opentelemetry.io/inject-python: "true" + {{- end }} {{- end }} labels: {{- include "feast-feature-server.selectorLabels" . | nindent 8 }} @@ -48,10 +51,18 @@ spec: - "feast" - "serve_registry" {{- else }} + {{- if .Values.metrics.enlabled }} - "feast" - "serve" + - "--metrics" - "-h" - "0.0.0.0" + {{- else }} + - "feast" + - "serve" + - "-h" + - "0.0.0.0" + {{- end }} {{- end }} ports: - name: {{ .Values.feast_mode }} @@ -88,4 +99,4 @@ spec: {{- with .Values.tolerations }} tolerations: {{- toYaml . | nindent 8 }} - {{- end }} + {{- end }} \ No newline at end of file diff --git a/infra/charts/feast-feature-server/templates/service.yaml b/infra/charts/feast-feature-server/templates/service.yaml index 68f096264e..11f7cc4cf2 100644 --- a/infra/charts/feast-feature-server/templates/service.yaml +++ b/infra/charts/feast-feature-server/templates/service.yaml @@ -11,5 +11,11 @@ spec: targetPort: {{ .Values.feast_mode }} protocol: TCP name: http + {{- if .Values.metrics.enabled }} + - name: metrics + port: 8000 + protocol: TCP + targetPort: 8000 # metrics port + {{- end }} selector: {{- include "feast-feature-server.selectorLabels" . | nindent 4 }} diff --git a/infra/charts/feast-feature-server/values.yaml b/infra/charts/feast-feature-server/values.yaml index 0c46bfff85..64d805a66c 100644 --- a/infra/charts/feast-feature-server/values.yaml +++ b/infra/charts/feast-feature-server/values.yaml @@ -15,6 +15,12 @@ imagePullSecrets: [] nameOverride: "" fullnameOverride: "" +metrics: + enabled: false + otelCollector: + endpoint: "" # sample endpoint: "otel-collector.default.svc.cluster.local:4317" + port: 4317 + # feature_store_yaml_base64 -- [required] a base64 encoded version of feature_store.yaml feature_store_yaml_base64: "" diff --git a/sdk/python/feast/cli.py b/sdk/python/feast/cli.py index f0655c40f2..f4e3e97d27 100644 --- a/sdk/python/feast/cli.py +++ b/sdk/python/feast/cli.py @@ -684,6 +684,13 @@ def init_command(project_directory, minimal: bool, template: str): default=5, show_default=True, ) +@click.option( + "--metrics", + "-m", + is_flag=True, + show_default=True, + help="Enable the Metrics Server", +) @click.pass_context def serve_command( ctx: click.Context, @@ -692,6 +699,7 @@ def serve_command( type_: str, no_access_log: bool, workers: int, + metrics: bool, keep_alive_timeout: int, registry_ttl_sec: int = 5, ): @@ -704,6 +712,7 @@ def serve_command( type_=type_, no_access_log=no_access_log, workers=workers, + metrics=metrics, keep_alive_timeout=keep_alive_timeout, registry_ttl_sec=registry_ttl_sec, ) diff --git a/sdk/python/feast/feature_server.py b/sdk/python/feast/feature_server.py index bf20e51df9..908c9741c2 100644 --- a/sdk/python/feast/feature_server.py +++ b/sdk/python/feast/feature_server.py @@ -1,16 +1,19 @@ import json import sys import threading +import time import traceback from contextlib import asynccontextmanager from typing import List, Optional import pandas as pd +import psutil from dateutil import parser from fastapi import FastAPI, HTTPException, Request, Response, status from fastapi.logger import logger from fastapi.params import Depends from google.protobuf.json_format import MessageToDict +from prometheus_client import Gauge, start_http_server from pydantic import BaseModel import feast @@ -19,6 +22,14 @@ from feast.data_source import PushMode from feast.errors import PushSourceNotFoundException +# Define prometheus metrics +cpu_usage_gauge = Gauge( + "feast_feature_server_cpu_usage", "CPU usage of the Feast feature server" +) +memory_usage_gauge = Gauge( + "feast_feature_server_memory_usage", "Memory usage of the Feast feature server" +) + # TODO: deprecate this in favor of push features class WriteToFeatureStoreRequest(BaseModel): @@ -218,6 +229,22 @@ def load(self): return self._app +def monitor_resources(self, interval: int = 5): + """Function to monitor and update CPU and memory usage metrics.""" + print(f"Start monitor_resources({interval})") + p = psutil.Process() + print(f"PID is {p.pid}") + while True: + with p.oneshot(): + cpu_usage = p.cpu_percent() + memory_usage = p.memory_percent() + print(f"cpu_usage is {cpu_usage}") + print(f"memory_usage is {memory_usage}") + cpu_usage_gauge.set(cpu_usage) + memory_usage_gauge.set(memory_usage) + time.sleep(interval) + + def start_server( store: "feast.FeatureStore", host: str, @@ -226,7 +253,18 @@ def start_server( workers: int, keep_alive_timeout: int, registry_ttl_sec: int, + metrics: bool, ): + if metrics: + print("Start Prometheus Server") + start_http_server(8000) + + print("Start a background thread to monitor CPU and memory usage") + monitoring_thread = threading.Thread( + target=monitor_resources, args=(5,), daemon=True + ) + monitoring_thread.start() + if sys.platform != "win32": FeastServeApplication( store=store, diff --git a/sdk/python/feast/feature_store.py b/sdk/python/feast/feature_store.py index 9600732e17..77638f5a62 100644 --- a/sdk/python/feast/feature_store.py +++ b/sdk/python/feast/feature_store.py @@ -1738,6 +1738,7 @@ def serve( type_: str = "http", no_access_log: bool = True, workers: int = 1, + metrics: bool = False, keep_alive_timeout: int = 30, registry_ttl_sec: int = 2, ) -> None: @@ -1754,6 +1755,7 @@ def serve( port=port, no_access_log=no_access_log, workers=workers, + metrics=metrics, keep_alive_timeout=keep_alive_timeout, registry_ttl_sec=registry_ttl_sec, ) diff --git a/sdk/python/feast/infra/feature_servers/multicloud/Dockerfile b/sdk/python/feast/infra/feature_servers/multicloud/Dockerfile index c272f4ed66..3114f03f52 100644 --- a/sdk/python/feast/infra/feature_servers/multicloud/Dockerfile +++ b/sdk/python/feast/infra/feature_servers/multicloud/Dockerfile @@ -7,7 +7,7 @@ RUN apt update && \ build-essential RUN pip install pip --upgrade -RUN pip install "feast[aws,gcp,snowflake,redis,go,mysql,postgres]" +RUN pip install "feast[aws,gcp,snowflake,redis,go,mysql,postgres,opentelemetry]" RUN apt update diff --git a/sdk/python/feast/infra/feature_servers/multicloud/Dockerfile.dev b/sdk/python/feast/infra/feature_servers/multicloud/Dockerfile.dev index 858a5ae7d1..49e70839a9 100644 --- a/sdk/python/feast/infra/feature_servers/multicloud/Dockerfile.dev +++ b/sdk/python/feast/infra/feature_servers/multicloud/Dockerfile.dev @@ -9,7 +9,7 @@ RUN apt update && \ RUN pip install pip --upgrade COPY . . -RUN pip install "feast[aws,gcp,snowflake,redis,go,mysql,postgres]" +RUN pip install "feast[aws,gcp,snowflake,redis,go,mysql,postgres,opentelemetry]" RUN apt update RUN apt install -y -V ca-certificates lsb-release wget diff --git a/sdk/python/requirements/py3.10-ci-requirements.txt b/sdk/python/requirements/py3.10-ci-requirements.txt index a9f1d625a8..ddf72c59f4 100644 --- a/sdk/python/requirements/py3.10-ci-requirements.txt +++ b/sdk/python/requirements/py3.10-ci-requirements.txt @@ -1,6 +1,7 @@ # This file was autogenerated by uv via the following command: # uv pip compile --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.10-ci-requirements.txt aiobotocore==2.13.1 + # via feast (setup.py) aiohttp==3.9.5 # via aiobotocore aioitertools==0.11.0 @@ -19,6 +20,8 @@ anyio==4.4.0 # jupyter-server # starlette # watchfiles +appnope==0.1.4 + # via ipykernel argon2-cffi==23.1.0 # via jupyter-server argon2-cffi-bindings==21.2.0 @@ -28,6 +31,7 @@ arrow==1.3.0 asn1crypto==1.5.1 # via snowflake-connector-python assertpy==1.1 + # via feast (setup.py) asttokens==2.4.1 # via stack-data async-lru==2.0.4 @@ -48,7 +52,9 @@ azure-core==1.30.2 # azure-identity # azure-storage-blob azure-identity==1.17.1 + # via feast (setup.py) azure-storage-blob==12.20.0 + # via feast (setup.py) babel==2.15.0 # via # jupyterlab-server @@ -60,7 +66,9 @@ bidict==0.23.1 bleach==6.1.0 # via nbconvert boto3==1.34.131 - # via moto + # via + # feast (setup.py) + # moto botocore==1.34.131 # via # aiobotocore @@ -69,11 +77,13 @@ botocore==1.34.131 # s3transfer build==1.2.1 # via + # feast (setup.py) # pip-tools # singlestoredb cachetools==5.3.3 # via google-auth cassandra-driver==3.29.1 + # via feast (setup.py) certifi==2024.7.4 # via # elastic-transport @@ -96,6 +106,7 @@ charset-normalizer==3.3.2 # snowflake-connector-python click==8.1.7 # via + # feast (setup.py) # dask # geomet # great-expectations @@ -105,7 +116,9 @@ click==8.1.7 cloudpickle==3.0.0 # via dask colorama==0.4.6 - # via great-expectations + # via + # feast (setup.py) + # great-expectations comm==0.2.2 # via # ipykernel @@ -114,6 +127,7 @@ coverage[toml]==7.5.4 # via pytest-cov cryptography==42.0.8 # via + # feast (setup.py) # azure-identity # azure-storage-blob # great-expectations @@ -125,7 +139,9 @@ cryptography==42.0.8 # types-pyopenssl # types-redis dask[dataframe]==2024.6.2 - # via dask-expr + # via + # feast (setup.py) + # dask-expr dask-expr==1.1.6 # via dask db-dtypes==1.2.0 @@ -137,7 +153,9 @@ decorator==5.1.1 defusedxml==0.7.1 # via nbconvert deltalake==0.18.1 + # via feast (setup.py) dill==0.3.8 + # via feast (setup.py) distlib==0.3.8 # via virtualenv dnspython==2.6.1 @@ -151,6 +169,7 @@ duckdb==0.10.3 elastic-transport==8.13.1 # via elasticsearch elasticsearch==8.14.0 + # via feast (setup.py) email-validator==2.2.0 # via fastapi entrypoints==0.4 @@ -165,6 +184,7 @@ execnet==2.1.1 executing==2.0.1 # via stack-data fastapi==0.111.0 + # via feast (setup.py) fastapi-cli==0.0.4 # via fastapi fastjsonschema==2.20.0 @@ -180,13 +200,16 @@ frozenlist==1.4.1 # aiohttp # aiosignal fsspec==2023.12.2 - # via dask + # via + # feast (setup.py) + # dask geojson==2.5.0 # via rockset geomet==0.2.1.post1 # via cassandra-driver google-api-core[grpc]==2.19.1 # via + # feast (setup.py) # google-cloud-bigquery # google-cloud-bigquery-storage # google-cloud-bigtable @@ -201,8 +224,11 @@ google-auth==2.30.0 # google-cloud-storage # kubernetes google-cloud-bigquery[pandas]==3.13.0 + # via feast (setup.py) google-cloud-bigquery-storage==2.25.0 + # via feast (setup.py) google-cloud-bigtable==2.24.0 + # via feast (setup.py) google-cloud-core==2.4.1 # via # google-cloud-bigquery @@ -210,7 +236,9 @@ google-cloud-core==2.4.1 # google-cloud-datastore # google-cloud-storage google-cloud-datastore==2.19.0 + # via feast (setup.py) google-cloud-storage==2.17.0 + # via feast (setup.py) google-crc32c==1.5.0 # via # google-cloud-storage @@ -221,16 +249,19 @@ google-resumable-media==2.7.1 # google-cloud-storage googleapis-common-protos[grpc]==1.63.2 # via + # feast (setup.py) # google-api-core # grpc-google-iam-v1 # grpcio-status great-expectations==0.18.16 + # via feast (setup.py) greenlet==3.0.3 # via sqlalchemy grpc-google-iam-v1==0.13.1 # via google-cloud-bigtable grpcio==1.64.1 # via + # feast (setup.py) # google-api-core # google-cloud-bigquery # googleapis-common-protos @@ -241,30 +272,42 @@ grpcio==1.64.1 # grpcio-testing # grpcio-tools grpcio-health-checking==1.62.2 + # via feast (setup.py) grpcio-reflection==1.62.2 + # via feast (setup.py) grpcio-status==1.62.2 # via google-api-core grpcio-testing==1.62.2 + # via feast (setup.py) grpcio-tools==1.62.2 + # via feast (setup.py) gunicorn==22.0.0 + # via feast (setup.py) h11==0.14.0 # via # httpcore # uvicorn happybase==1.2.0 + # via feast (setup.py) hazelcast-python-client==5.4.0 + # via feast (setup.py) hiredis==2.3.2 + # via feast (setup.py) httpcore==1.0.5 # via httpx httptools==0.6.1 # via uvicorn httpx==0.27.0 # via + # feast (setup.py) # fastapi # jupyterlab ibis-framework[duckdb]==9.1.0 - # via ibis-substrait + # via + # feast (setup.py) + # ibis-substrait ibis-substrait==4.0.0 + # via feast (setup.py) identify==2.5.36 # via pre-commit idna==3.7 @@ -299,6 +342,7 @@ jedi==0.19.1 # via ipython jinja2==3.1.4 # via + # feast (setup.py) # altair # fastapi # great-expectations @@ -322,6 +366,7 @@ jsonpointer==3.0.0 # jsonschema jsonschema[format-nongpl]==4.22.0 # via + # feast (setup.py) # altair # great-expectations # jupyter-events @@ -367,6 +412,7 @@ jupyterlab-server==2.27.2 jupyterlab-widgets==3.0.11 # via ipywidgets kubernetes==20.13.0 + # via feast (setup.py) locket==1.0.0 # via partd makefun==1.15.2 @@ -387,13 +433,17 @@ matplotlib-inline==0.1.7 mdurl==0.1.2 # via markdown-it-py minio==7.1.0 + # via feast (setup.py) mistune==3.0.2 # via # great-expectations # nbconvert mmh3==4.1.0 + # via feast (setup.py) mock==2.0.0 + # via feast (setup.py) moto==4.2.14 + # via feast (setup.py) msal==1.29.0 # via # azure-identity @@ -405,10 +455,13 @@ multidict==6.0.5 # aiohttp # yarl mypy==1.10.1 - # via sqlalchemy + # via + # feast (setup.py) + # sqlalchemy mypy-extensions==1.0.0 # via mypy mypy-protobuf==3.3.0 + # via feast (setup.py) nbclient==0.10.0 # via nbconvert nbconvert==7.16.4 @@ -431,6 +484,7 @@ notebook-shim==0.2.4 # notebook numpy==1.26.4 # via + # feast (setup.py) # altair # dask # db-dtypes @@ -465,6 +519,7 @@ packaging==24.1 # sphinx pandas==2.2.2 # via + # feast (setup.py) # altair # dask # dask-expr @@ -490,6 +545,7 @@ pexpect==4.9.0 pip==24.1.1 # via pip-tools pip-tools==7.4.1 + # via feast (setup.py) platformdirs==3.11.0 # via # jupyter-core @@ -502,8 +558,11 @@ ply==3.11 portalocker==2.10.0 # via msal-extensions pre-commit==3.3.1 + # via feast (setup.py) prometheus-client==0.20.0 - # via jupyter-server + # via + # feast (setup.py) + # jupyter-server prompt-toolkit==3.0.47 # via ipython proto-plus==1.24.0 @@ -515,6 +574,7 @@ proto-plus==1.24.0 # google-cloud-datastore protobuf==4.25.3 # via + # feast (setup.py) # google-api-core # google-cloud-bigquery # google-cloud-bigquery-storage @@ -531,8 +591,11 @@ protobuf==4.25.3 # proto-plus # substrait psutil==5.9.0 - # via ipykernel + # via + # feast (setup.py) + # ipykernel psycopg[binary, pool]==3.1.19 + # via feast (setup.py) psycopg-binary==3.1.19 # via psycopg psycopg-pool==3.2.2 @@ -544,12 +607,14 @@ ptyprocess==0.7.0 pure-eval==0.2.2 # via stack-data py==1.11.0 + # via feast (setup.py) py-cpuinfo==9.0.0 # via pytest-benchmark py4j==0.10.9.7 # via pyspark pyarrow==15.0.2 # via + # feast (setup.py) # dask-expr # db-dtypes # deltalake @@ -567,16 +632,19 @@ pyasn1==0.6.0 pyasn1-modules==0.4.0 # via google-auth pybindgen==0.22.1 + # via feast (setup.py) pycparser==2.22 # via cffi pydantic==2.7.4 # via + # feast (setup.py) # fastapi # great-expectations pydantic-core==2.18.4 # via pydantic pygments==2.18.0 # via + # feast (setup.py) # ipython # nbconvert # rich @@ -587,8 +655,11 @@ pyjwt[crypto]==2.8.0 # singlestoredb # snowflake-connector-python pymssql==2.3.0 + # via feast (setup.py) pymysql==1.1.1 + # via feast (setup.py) pyodbc==5.1.0 + # via feast (setup.py) pyopenssl==24.1.0 # via snowflake-connector-python pyparsing==3.1.2 @@ -598,8 +669,10 @@ pyproject-hooks==1.1.0 # build # pip-tools pyspark==3.5.1 + # via feast (setup.py) pytest==7.4.4 # via + # feast (setup.py) # pytest-benchmark # pytest-cov # pytest-env @@ -609,13 +682,21 @@ pytest==7.4.4 # pytest-timeout # pytest-xdist pytest-benchmark==3.4.1 + # via feast (setup.py) pytest-cov==5.0.0 + # via feast (setup.py) pytest-env==1.1.3 + # via feast (setup.py) pytest-lazy-fixture==0.6.3 + # via feast (setup.py) pytest-mock==1.10.4 + # via feast (setup.py) pytest-ordering==0.6 + # via feast (setup.py) pytest-timeout==1.4.2 + # via feast (setup.py) pytest-xdist==3.6.1 + # via feast (setup.py) python-dateutil==2.9.0.post0 # via # arrow @@ -644,6 +725,7 @@ pytz==2024.1 # trino pyyaml==6.0.1 # via + # feast (setup.py) # dask # ibis-substrait # jupyter-events @@ -657,15 +739,19 @@ pyzmq==26.0.3 # jupyter-client # jupyter-server redis==4.6.0 + # via feast (setup.py) referencing==0.35.1 # via # jsonschema # jsonschema-specifications # jupyter-events regex==2024.5.15 - # via parsimonious + # via + # feast (setup.py) + # parsimonious requests==2.32.3 # via + # feast (setup.py) # azure-core # docker # google-api-core @@ -699,6 +785,7 @@ rich==13.7.1 # ibis-framework # typer rockset==2.1.2 + # via feast (setup.py) rpds-py==0.18.1 # via # jsonschema @@ -708,6 +795,7 @@ rsa==4.9 ruamel-yaml==0.17.17 # via great-expectations ruff==0.4.10 + # via feast (setup.py) s3transfer==0.10.2 # via boto3 scipy==1.14.0 @@ -724,6 +812,7 @@ setuptools==70.1.1 shellingham==1.5.4 # via typer singlestoredb==1.4.0 + # via feast (setup.py) six==1.16.0 # via # asttokens @@ -744,11 +833,13 @@ sniffio==1.3.1 snowballstemmer==2.2.0 # via sphinx snowflake-connector-python[pandas]==3.11.0 + # via feast (setup.py) sortedcontainers==2.4.0 # via snowflake-connector-python soupsieve==2.5 # via beautifulsoup4 sphinx==6.2.1 + # via feast (setup.py) sphinxcontrib-applehelp==1.0.8 # via sphinx sphinxcontrib-devhelp==1.0.6 @@ -762,9 +853,11 @@ sphinxcontrib-qthelp==1.0.7 sphinxcontrib-serializinghtml==1.1.10 # via sphinx sqlalchemy[mypy]==2.0.31 + # via feast (setup.py) sqlglot==25.1.0 # via ibis-framework sqlite-vec==0.0.1a10 + # via feast (setup.py) sqlparams==6.0.1 # via singlestoredb stack-data==0.6.3 @@ -774,17 +867,21 @@ starlette==0.37.2 substrait==0.19.0 # via ibis-substrait tabulate==0.9.0 + # via feast (setup.py) tenacity==8.4.2 + # via feast (setup.py) terminado==0.18.1 # via # jupyter-server # jupyter-server-terminals testcontainers==4.4.0 + # via feast (setup.py) thriftpy2==0.5.1 # via happybase tinycss2==1.3.0 # via nbconvert toml==0.10.2 + # via feast (setup.py) tomli==2.0.1 # via # build @@ -812,7 +909,9 @@ tornado==6.4.1 # notebook # terminado tqdm==4.66.4 - # via great-expectations + # via + # feast (setup.py) + # great-expectations traitlets==5.14.3 # via # comm @@ -829,25 +928,39 @@ traitlets==5.14.3 # nbconvert # nbformat trino==0.328.0 + # via feast (setup.py) typeguard==4.3.0 + # via feast (setup.py) typer==0.12.3 # via fastapi-cli types-cffi==1.16.0.20240331 # via types-pyopenssl types-protobuf==3.19.22 - # via mypy-protobuf + # via + # feast (setup.py) + # mypy-protobuf types-pymysql==1.1.0.20240524 + # via feast (setup.py) types-pyopenssl==24.1.0.20240425 # via types-redis types-python-dateutil==2.9.0.20240316 - # via arrow + # via + # feast (setup.py) + # arrow types-pytz==2024.1.0.20240417 + # via feast (setup.py) types-pyyaml==6.0.12.20240311 + # via feast (setup.py) types-redis==4.6.0.20240425 + # via feast (setup.py) types-requests==2.30.0.0 + # via feast (setup.py) types-setuptools==70.1.0.20240627 - # via types-cffi + # via + # feast (setup.py) + # types-cffi types-tabulate==0.9.0.20240106 + # via feast (setup.py) types-urllib3==1.26.25.14 # via types-requests typing-extensions==4.12.2 @@ -884,6 +997,7 @@ uri-template==1.3.0 # via jsonschema urllib3==1.26.19 # via + # feast (setup.py) # botocore # docker # elastic-transport @@ -895,11 +1009,15 @@ urllib3==1.26.19 # rockset # testcontainers uvicorn[standard]==0.30.1 - # via fastapi + # via + # feast (setup.py) + # fastapi uvloop==0.19.0 # via uvicorn virtualenv==20.23.0 - # via pre-commit + # via + # feast (setup.py) + # pre-commit watchfiles==0.22.0 # via uvicorn wcwidth==0.2.13 diff --git a/sdk/python/requirements/py3.10-requirements.txt b/sdk/python/requirements/py3.10-requirements.txt index 0cca106863..93322ae434 100644 --- a/sdk/python/requirements/py3.10-requirements.txt +++ b/sdk/python/requirements/py3.10-requirements.txt @@ -20,17 +20,22 @@ charset-normalizer==3.3.2 # via requests click==8.1.7 # via + # feast (setup.py) # dask # typer # uvicorn cloudpickle==3.0.0 # via dask colorama==0.4.6 + # via feast (setup.py) dask[dataframe]==2024.5.0 - # via dask-expr + # via + # feast (setup.py) + # dask-expr dask-expr==1.1.0 # via dask dill==0.3.8 + # via feast (setup.py) dnspython==2.6.1 # via email-validator email-validator==2.1.1 @@ -38,7 +43,9 @@ email-validator==2.1.1 exceptiongroup==1.2.1 # via anyio fastapi==0.111.0 - # via fastapi-cli + # via + # feast (setup.py) + # fastapi-cli fastapi-cli==0.0.2 # via fastapi fsspec==2024.3.1 @@ -46,6 +53,7 @@ fsspec==2024.3.1 greenlet==3.0.3 # via sqlalchemy gunicorn==22.0.0 + # via feast (setup.py) h11==0.14.0 # via # httpcore @@ -65,8 +73,11 @@ idna==3.7 importlib-metadata==7.1.0 # via dask jinja2==3.1.4 - # via fastapi + # via + # feast (setup.py) + # fastapi jsonschema==4.22.0 + # via feast (setup.py) jsonschema-specifications==2023.12.1 # via jsonschema locket==1.0.0 @@ -78,13 +89,16 @@ markupsafe==2.1.5 mdurl==0.1.2 # via markdown-it-py mmh3==4.1.0 + # via feast (setup.py) mypy==1.10.0 # via sqlalchemy mypy-extensions==1.0.0 # via mypy mypy-protobuf==3.6.0 + # via feast (setup.py) numpy==1.26.4 # via + # feast (setup.py) # dask # pandas # pyarrow @@ -96,20 +110,33 @@ packaging==24.0 # gunicorn pandas==2.2.2 # via + # feast (setup.py) # dask # dask-expr partd==1.4.2 # via dask +prometheus-client==0.20.0 + # via feast (setup.py) protobuf==4.25.3 - # via mypy-protobuf + # via + # feast (setup.py) + # mypy-protobuf +psutil==6.0.0 + # via feast (setup.py) pyarrow==16.0.0 - # via dask-expr + # via + # feast (setup.py) + # dask-expr pydantic==2.7.1 - # via fastapi + # via + # feast (setup.py) + # fastapi pydantic-core==2.18.2 # via pydantic pygments==2.18.0 - # via rich + # via + # feast (setup.py) + # rich python-dateutil==2.9.0.post0 # via pandas python-dotenv==1.0.1 @@ -120,6 +147,7 @@ pytz==2024.1 # via pandas pyyaml==6.0.1 # via + # feast (setup.py) # dask # uvicorn referencing==0.35.1 @@ -127,6 +155,7 @@ referencing==0.35.1 # jsonschema # jsonschema-specifications requests==2.31.0 + # via feast (setup.py) rich==13.7.1 # via typer rpds-py==0.18.1 @@ -142,11 +171,15 @@ sniffio==1.3.1 # anyio # httpx sqlalchemy[mypy]==2.0.30 + # via feast (setup.py) starlette==0.37.2 # via fastapi tabulate==0.9.0 + # via feast (setup.py) tenacity==8.3.0 + # via feast (setup.py) toml==0.10.2 + # via feast (setup.py) tomli==2.0.1 # via mypy toolz==0.12.1 @@ -154,7 +187,9 @@ toolz==0.12.1 # dask # partd tqdm==4.66.4 + # via feast (setup.py) typeguard==4.2.1 + # via feast (setup.py) typer==0.12.3 # via fastapi-cli types-protobuf==5.26.0.20240422 @@ -178,6 +213,7 @@ urllib3==2.2.1 # via requests uvicorn[standard]==0.29.0 # via + # feast (setup.py) # fastapi # fastapi-cli uvloop==0.19.0 diff --git a/sdk/python/requirements/py3.11-ci-requirements.txt b/sdk/python/requirements/py3.11-ci-requirements.txt index ce2e50b8a7..a451ad58af 100644 --- a/sdk/python/requirements/py3.11-ci-requirements.txt +++ b/sdk/python/requirements/py3.11-ci-requirements.txt @@ -1,6 +1,7 @@ # This file was autogenerated by uv via the following command: # uv pip compile --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.11-ci-requirements.txt aiobotocore==2.13.1 + # via feast (setup.py) aiohttp==3.9.5 # via aiobotocore aioitertools==0.11.0 @@ -19,6 +20,8 @@ anyio==4.4.0 # jupyter-server # starlette # watchfiles +appnope==0.1.4 + # via ipykernel argon2-cffi==23.1.0 # via jupyter-server argon2-cffi-bindings==21.2.0 @@ -28,6 +31,7 @@ arrow==1.3.0 asn1crypto==1.5.1 # via snowflake-connector-python assertpy==1.1 + # via feast (setup.py) asttokens==2.4.1 # via stack-data async-lru==2.0.4 @@ -44,7 +48,9 @@ azure-core==1.30.2 # azure-identity # azure-storage-blob azure-identity==1.17.1 + # via feast (setup.py) azure-storage-blob==12.20.0 + # via feast (setup.py) babel==2.15.0 # via # jupyterlab-server @@ -56,7 +62,9 @@ bidict==0.23.1 bleach==6.1.0 # via nbconvert boto3==1.34.131 - # via moto + # via + # feast (setup.py) + # moto botocore==1.34.131 # via # aiobotocore @@ -65,11 +73,13 @@ botocore==1.34.131 # s3transfer build==1.2.1 # via + # feast (setup.py) # pip-tools # singlestoredb cachetools==5.3.3 # via google-auth cassandra-driver==3.29.1 + # via feast (setup.py) certifi==2024.7.4 # via # elastic-transport @@ -92,6 +102,7 @@ charset-normalizer==3.3.2 # snowflake-connector-python click==8.1.7 # via + # feast (setup.py) # dask # geomet # great-expectations @@ -101,7 +112,9 @@ click==8.1.7 cloudpickle==3.0.0 # via dask colorama==0.4.6 - # via great-expectations + # via + # feast (setup.py) + # great-expectations comm==0.2.2 # via # ipykernel @@ -110,6 +123,7 @@ coverage[toml]==7.5.4 # via pytest-cov cryptography==42.0.8 # via + # feast (setup.py) # azure-identity # azure-storage-blob # great-expectations @@ -121,7 +135,9 @@ cryptography==42.0.8 # types-pyopenssl # types-redis dask[dataframe]==2024.6.2 - # via dask-expr + # via + # feast (setup.py) + # dask-expr dask-expr==1.1.6 # via dask db-dtypes==1.2.0 @@ -133,7 +149,9 @@ decorator==5.1.1 defusedxml==0.7.1 # via nbconvert deltalake==0.18.1 + # via feast (setup.py) dill==0.3.8 + # via feast (setup.py) distlib==0.3.8 # via virtualenv dnspython==2.6.1 @@ -147,6 +165,7 @@ duckdb==0.10.3 elastic-transport==8.13.1 # via elasticsearch elasticsearch==8.14.0 + # via feast (setup.py) email-validator==2.2.0 # via fastapi entrypoints==0.4 @@ -156,6 +175,7 @@ execnet==2.1.1 executing==2.0.1 # via stack-data fastapi==0.111.0 + # via feast (setup.py) fastapi-cli==0.0.4 # via fastapi fastjsonschema==2.20.0 @@ -171,13 +191,16 @@ frozenlist==1.4.1 # aiohttp # aiosignal fsspec==2023.12.2 - # via dask + # via + # feast (setup.py) + # dask geojson==2.5.0 # via rockset geomet==0.2.1.post1 # via cassandra-driver google-api-core[grpc]==2.19.1 # via + # feast (setup.py) # google-cloud-bigquery # google-cloud-bigquery-storage # google-cloud-bigtable @@ -192,8 +215,11 @@ google-auth==2.30.0 # google-cloud-storage # kubernetes google-cloud-bigquery[pandas]==3.13.0 + # via feast (setup.py) google-cloud-bigquery-storage==2.25.0 + # via feast (setup.py) google-cloud-bigtable==2.24.0 + # via feast (setup.py) google-cloud-core==2.4.1 # via # google-cloud-bigquery @@ -201,7 +227,9 @@ google-cloud-core==2.4.1 # google-cloud-datastore # google-cloud-storage google-cloud-datastore==2.19.0 + # via feast (setup.py) google-cloud-storage==2.17.0 + # via feast (setup.py) google-crc32c==1.5.0 # via # google-cloud-storage @@ -212,16 +240,19 @@ google-resumable-media==2.7.1 # google-cloud-storage googleapis-common-protos[grpc]==1.63.2 # via + # feast (setup.py) # google-api-core # grpc-google-iam-v1 # grpcio-status great-expectations==0.18.16 + # via feast (setup.py) greenlet==3.0.3 # via sqlalchemy grpc-google-iam-v1==0.13.1 # via google-cloud-bigtable grpcio==1.64.1 # via + # feast (setup.py) # google-api-core # google-cloud-bigquery # googleapis-common-protos @@ -232,30 +263,42 @@ grpcio==1.64.1 # grpcio-testing # grpcio-tools grpcio-health-checking==1.62.2 + # via feast (setup.py) grpcio-reflection==1.62.2 + # via feast (setup.py) grpcio-status==1.62.2 # via google-api-core grpcio-testing==1.62.2 + # via feast (setup.py) grpcio-tools==1.62.2 + # via feast (setup.py) gunicorn==22.0.0 + # via feast (setup.py) h11==0.14.0 # via # httpcore # uvicorn happybase==1.2.0 + # via feast (setup.py) hazelcast-python-client==5.4.0 + # via feast (setup.py) hiredis==2.3.2 + # via feast (setup.py) httpcore==1.0.5 # via httpx httptools==0.6.1 # via uvicorn httpx==0.27.0 # via + # feast (setup.py) # fastapi # jupyterlab ibis-framework[duckdb]==9.1.0 - # via ibis-substrait + # via + # feast (setup.py) + # ibis-substrait ibis-substrait==4.0.0 + # via feast (setup.py) identify==2.5.36 # via pre-commit idna==3.7 @@ -290,6 +333,7 @@ jedi==0.19.1 # via ipython jinja2==3.1.4 # via + # feast (setup.py) # altair # fastapi # great-expectations @@ -313,6 +357,7 @@ jsonpointer==3.0.0 # jsonschema jsonschema[format-nongpl]==4.22.0 # via + # feast (setup.py) # altair # great-expectations # jupyter-events @@ -358,6 +403,7 @@ jupyterlab-server==2.27.2 jupyterlab-widgets==3.0.11 # via ipywidgets kubernetes==20.13.0 + # via feast (setup.py) locket==1.0.0 # via partd makefun==1.15.2 @@ -378,13 +424,17 @@ matplotlib-inline==0.1.7 mdurl==0.1.2 # via markdown-it-py minio==7.1.0 + # via feast (setup.py) mistune==3.0.2 # via # great-expectations # nbconvert mmh3==4.1.0 + # via feast (setup.py) mock==2.0.0 + # via feast (setup.py) moto==4.2.14 + # via feast (setup.py) msal==1.29.0 # via # azure-identity @@ -396,10 +446,13 @@ multidict==6.0.5 # aiohttp # yarl mypy==1.10.1 - # via sqlalchemy + # via + # feast (setup.py) + # sqlalchemy mypy-extensions==1.0.0 # via mypy mypy-protobuf==3.3.0 + # via feast (setup.py) nbclient==0.10.0 # via nbconvert nbconvert==7.16.4 @@ -422,6 +475,7 @@ notebook-shim==0.2.4 # notebook numpy==1.26.4 # via + # feast (setup.py) # altair # dask # db-dtypes @@ -456,6 +510,7 @@ packaging==24.1 # sphinx pandas==2.2.2 # via + # feast (setup.py) # altair # dask # dask-expr @@ -481,6 +536,7 @@ pexpect==4.9.0 pip==24.1.1 # via pip-tools pip-tools==7.4.1 + # via feast (setup.py) platformdirs==3.11.0 # via # jupyter-core @@ -493,8 +549,11 @@ ply==3.11 portalocker==2.10.0 # via msal-extensions pre-commit==3.3.1 + # via feast (setup.py) prometheus-client==0.20.0 - # via jupyter-server + # via + # feast (setup.py) + # jupyter-server prompt-toolkit==3.0.47 # via ipython proto-plus==1.24.0 @@ -506,6 +565,7 @@ proto-plus==1.24.0 # google-cloud-datastore protobuf==4.25.3 # via + # feast (setup.py) # google-api-core # google-cloud-bigquery # google-cloud-bigquery-storage @@ -522,8 +582,11 @@ protobuf==4.25.3 # proto-plus # substrait psutil==5.9.0 - # via ipykernel + # via + # feast (setup.py) + # ipykernel psycopg[binary, pool]==3.1.19 + # via feast (setup.py) psycopg-binary==3.1.19 # via psycopg psycopg-pool==3.2.2 @@ -535,12 +598,14 @@ ptyprocess==0.7.0 pure-eval==0.2.2 # via stack-data py==1.11.0 + # via feast (setup.py) py-cpuinfo==9.0.0 # via pytest-benchmark py4j==0.10.9.7 # via pyspark pyarrow==15.0.2 # via + # feast (setup.py) # dask-expr # db-dtypes # deltalake @@ -558,16 +623,19 @@ pyasn1==0.6.0 pyasn1-modules==0.4.0 # via google-auth pybindgen==0.22.1 + # via feast (setup.py) pycparser==2.22 # via cffi pydantic==2.7.4 # via + # feast (setup.py) # fastapi # great-expectations pydantic-core==2.18.4 # via pydantic pygments==2.18.0 # via + # feast (setup.py) # ipython # nbconvert # rich @@ -578,8 +646,11 @@ pyjwt[crypto]==2.8.0 # singlestoredb # snowflake-connector-python pymssql==2.3.0 + # via feast (setup.py) pymysql==1.1.1 + # via feast (setup.py) pyodbc==5.1.0 + # via feast (setup.py) pyopenssl==24.1.0 # via snowflake-connector-python pyparsing==3.1.2 @@ -589,8 +660,10 @@ pyproject-hooks==1.1.0 # build # pip-tools pyspark==3.5.1 + # via feast (setup.py) pytest==7.4.4 # via + # feast (setup.py) # pytest-benchmark # pytest-cov # pytest-env @@ -600,13 +673,21 @@ pytest==7.4.4 # pytest-timeout # pytest-xdist pytest-benchmark==3.4.1 + # via feast (setup.py) pytest-cov==5.0.0 + # via feast (setup.py) pytest-env==1.1.3 + # via feast (setup.py) pytest-lazy-fixture==0.6.3 + # via feast (setup.py) pytest-mock==1.10.4 + # via feast (setup.py) pytest-ordering==0.6 + # via feast (setup.py) pytest-timeout==1.4.2 + # via feast (setup.py) pytest-xdist==3.6.1 + # via feast (setup.py) python-dateutil==2.9.0.post0 # via # arrow @@ -635,6 +716,7 @@ pytz==2024.1 # trino pyyaml==6.0.1 # via + # feast (setup.py) # dask # ibis-substrait # jupyter-events @@ -648,15 +730,19 @@ pyzmq==26.0.3 # jupyter-client # jupyter-server redis==4.6.0 + # via feast (setup.py) referencing==0.35.1 # via # jsonschema # jsonschema-specifications # jupyter-events regex==2024.5.15 - # via parsimonious + # via + # feast (setup.py) + # parsimonious requests==2.32.3 # via + # feast (setup.py) # azure-core # docker # google-api-core @@ -690,6 +776,7 @@ rich==13.7.1 # ibis-framework # typer rockset==2.1.2 + # via feast (setup.py) rpds-py==0.18.1 # via # jsonschema @@ -699,6 +786,7 @@ rsa==4.9 ruamel-yaml==0.17.17 # via great-expectations ruff==0.4.10 + # via feast (setup.py) s3transfer==0.10.2 # via boto3 scipy==1.14.0 @@ -715,6 +803,7 @@ setuptools==70.1.1 shellingham==1.5.4 # via typer singlestoredb==1.4.0 + # via feast (setup.py) six==1.16.0 # via # asttokens @@ -735,11 +824,13 @@ sniffio==1.3.1 snowballstemmer==2.2.0 # via sphinx snowflake-connector-python[pandas]==3.11.0 + # via feast (setup.py) sortedcontainers==2.4.0 # via snowflake-connector-python soupsieve==2.5 # via beautifulsoup4 sphinx==6.2.1 + # via feast (setup.py) sphinxcontrib-applehelp==1.0.8 # via sphinx sphinxcontrib-devhelp==1.0.6 @@ -753,9 +844,11 @@ sphinxcontrib-qthelp==1.0.7 sphinxcontrib-serializinghtml==1.1.10 # via sphinx sqlalchemy[mypy]==2.0.31 + # via feast (setup.py) sqlglot==25.1.0 # via ibis-framework sqlite-vec==0.0.1a10 + # via feast (setup.py) sqlparams==6.0.1 # via singlestoredb stack-data==0.6.3 @@ -765,17 +858,21 @@ starlette==0.37.2 substrait==0.19.0 # via ibis-substrait tabulate==0.9.0 + # via feast (setup.py) tenacity==8.4.2 + # via feast (setup.py) terminado==0.18.1 # via # jupyter-server # jupyter-server-terminals testcontainers==4.4.0 + # via feast (setup.py) thriftpy2==0.5.1 # via happybase tinycss2==1.3.0 # via nbconvert toml==0.10.2 + # via feast (setup.py) tomlkit==0.12.5 # via snowflake-connector-python toolz==0.12.1 @@ -793,7 +890,9 @@ tornado==6.4.1 # notebook # terminado tqdm==4.66.4 - # via great-expectations + # via + # feast (setup.py) + # great-expectations traitlets==5.14.3 # via # comm @@ -810,25 +909,39 @@ traitlets==5.14.3 # nbconvert # nbformat trino==0.328.0 + # via feast (setup.py) typeguard==4.3.0 + # via feast (setup.py) typer==0.12.3 # via fastapi-cli types-cffi==1.16.0.20240331 # via types-pyopenssl types-protobuf==3.19.22 - # via mypy-protobuf + # via + # feast (setup.py) + # mypy-protobuf types-pymysql==1.1.0.20240524 + # via feast (setup.py) types-pyopenssl==24.1.0.20240425 # via types-redis types-python-dateutil==2.9.0.20240316 - # via arrow + # via + # feast (setup.py) + # arrow types-pytz==2024.1.0.20240417 + # via feast (setup.py) types-pyyaml==6.0.12.20240311 + # via feast (setup.py) types-redis==4.6.0.20240425 + # via feast (setup.py) types-requests==2.30.0.0 + # via feast (setup.py) types-setuptools==70.1.0.20240627 - # via types-cffi + # via + # feast (setup.py) + # types-cffi types-tabulate==0.9.0.20240106 + # via feast (setup.py) types-urllib3==1.26.25.14 # via types-requests typing-extensions==4.12.2 @@ -862,6 +975,7 @@ uri-template==1.3.0 # via jsonschema urllib3==1.26.19 # via + # feast (setup.py) # botocore # docker # elastic-transport @@ -873,11 +987,15 @@ urllib3==1.26.19 # rockset # testcontainers uvicorn[standard]==0.30.1 - # via fastapi + # via + # feast (setup.py) + # fastapi uvloop==0.19.0 # via uvicorn virtualenv==20.23.0 - # via pre-commit + # via + # feast (setup.py) + # pre-commit watchfiles==0.22.0 # via uvicorn wcwidth==0.2.13 diff --git a/sdk/python/requirements/py3.11-requirements.txt b/sdk/python/requirements/py3.11-requirements.txt index 687e4bfe52..48104ebc2c 100644 --- a/sdk/python/requirements/py3.11-requirements.txt +++ b/sdk/python/requirements/py3.11-requirements.txt @@ -20,23 +20,30 @@ charset-normalizer==3.3.2 # via requests click==8.1.7 # via + # feast (setup.py) # dask # typer # uvicorn cloudpickle==3.0.0 # via dask colorama==0.4.6 + # via feast (setup.py) dask[dataframe]==2024.5.0 - # via dask-expr + # via + # feast (setup.py) + # dask-expr dask-expr==1.1.0 # via dask dill==0.3.8 + # via feast (setup.py) dnspython==2.6.1 # via email-validator email-validator==2.1.1 # via fastapi fastapi==0.111.0 - # via fastapi-cli + # via + # feast (setup.py) + # fastapi-cli fastapi-cli==0.0.2 # via fastapi fsspec==2024.3.1 @@ -44,6 +51,7 @@ fsspec==2024.3.1 greenlet==3.0.3 # via sqlalchemy gunicorn==22.0.0 + # via feast (setup.py) h11==0.14.0 # via # httpcore @@ -63,8 +71,11 @@ idna==3.7 importlib-metadata==7.1.0 # via dask jinja2==3.1.4 - # via fastapi + # via + # feast (setup.py) + # fastapi jsonschema==4.22.0 + # via feast (setup.py) jsonschema-specifications==2023.12.1 # via jsonschema locket==1.0.0 @@ -76,13 +87,16 @@ markupsafe==2.1.5 mdurl==0.1.2 # via markdown-it-py mmh3==4.1.0 + # via feast (setup.py) mypy==1.10.0 # via sqlalchemy mypy-extensions==1.0.0 # via mypy mypy-protobuf==3.6.0 + # via feast (setup.py) numpy==1.26.4 # via + # feast (setup.py) # dask # pandas # pyarrow @@ -94,20 +108,33 @@ packaging==24.0 # gunicorn pandas==2.2.2 # via + # feast (setup.py) # dask # dask-expr partd==1.4.2 # via dask +prometheus-client==0.20.0 + # via feast (setup.py) protobuf==4.25.3 - # via mypy-protobuf + # via + # feast (setup.py) + # mypy-protobuf +psutil==6.0.0 + # via feast (setup.py) pyarrow==16.0.0 - # via dask-expr + # via + # feast (setup.py) + # dask-expr pydantic==2.7.1 - # via fastapi + # via + # feast (setup.py) + # fastapi pydantic-core==2.18.2 # via pydantic pygments==2.18.0 - # via rich + # via + # feast (setup.py) + # rich python-dateutil==2.9.0.post0 # via pandas python-dotenv==1.0.1 @@ -118,6 +145,7 @@ pytz==2024.1 # via pandas pyyaml==6.0.1 # via + # feast (setup.py) # dask # uvicorn referencing==0.35.1 @@ -125,6 +153,7 @@ referencing==0.35.1 # jsonschema # jsonschema-specifications requests==2.31.0 + # via feast (setup.py) rich==13.7.1 # via typer rpds-py==0.18.1 @@ -140,17 +169,23 @@ sniffio==1.3.1 # anyio # httpx sqlalchemy[mypy]==2.0.30 + # via feast (setup.py) starlette==0.37.2 # via fastapi tabulate==0.9.0 + # via feast (setup.py) tenacity==8.3.0 + # via feast (setup.py) toml==0.10.2 + # via feast (setup.py) toolz==0.12.1 # via # dask # partd tqdm==4.66.4 + # via feast (setup.py) typeguard==4.2.1 + # via feast (setup.py) typer==0.12.3 # via fastapi-cli types-protobuf==5.26.0.20240422 @@ -172,6 +207,7 @@ urllib3==2.2.1 # via requests uvicorn[standard]==0.29.0 # via + # feast (setup.py) # fastapi # fastapi-cli uvloop==0.19.0 diff --git a/sdk/python/requirements/py3.9-ci-requirements.txt b/sdk/python/requirements/py3.9-ci-requirements.txt index 017c1c8920..5055b3c79f 100644 --- a/sdk/python/requirements/py3.9-ci-requirements.txt +++ b/sdk/python/requirements/py3.9-ci-requirements.txt @@ -1,6 +1,7 @@ # This file was autogenerated by uv via the following command: # uv pip compile --system --no-strip-extras setup.py --extra ci --output-file sdk/python/requirements/py3.9-ci-requirements.txt aiobotocore==2.13.1 + # via feast (setup.py) aiohttp==3.9.5 # via aiobotocore aioitertools==0.11.0 @@ -19,6 +20,8 @@ anyio==4.4.0 # jupyter-server # starlette # watchfiles +appnope==0.1.4 + # via ipykernel argon2-cffi==23.1.0 # via jupyter-server argon2-cffi-bindings==21.2.0 @@ -28,6 +31,7 @@ arrow==1.3.0 asn1crypto==1.5.1 # via snowflake-connector-python assertpy==1.1 + # via feast (setup.py) asttokens==2.4.1 # via stack-data async-lru==2.0.4 @@ -48,7 +52,9 @@ azure-core==1.30.2 # azure-identity # azure-storage-blob azure-identity==1.17.1 + # via feast (setup.py) azure-storage-blob==12.20.0 + # via feast (setup.py) babel==2.15.0 # via # jupyterlab-server @@ -60,7 +66,9 @@ bidict==0.23.1 bleach==6.1.0 # via nbconvert boto3==1.34.131 - # via moto + # via + # feast (setup.py) + # moto botocore==1.34.131 # via # aiobotocore @@ -69,11 +77,13 @@ botocore==1.34.131 # s3transfer build==1.2.1 # via + # feast (setup.py) # pip-tools # singlestoredb cachetools==5.3.3 # via google-auth cassandra-driver==3.29.1 + # via feast (setup.py) certifi==2024.7.4 # via # elastic-transport @@ -96,6 +106,7 @@ charset-normalizer==3.3.2 # snowflake-connector-python click==8.1.7 # via + # feast (setup.py) # dask # geomet # great-expectations @@ -105,7 +116,9 @@ click==8.1.7 cloudpickle==3.0.0 # via dask colorama==0.4.6 - # via great-expectations + # via + # feast (setup.py) + # great-expectations comm==0.2.2 # via # ipykernel @@ -114,6 +127,7 @@ coverage[toml]==7.5.4 # via pytest-cov cryptography==42.0.8 # via + # feast (setup.py) # azure-identity # azure-storage-blob # great-expectations @@ -125,7 +139,9 @@ cryptography==42.0.8 # types-pyopenssl # types-redis dask[dataframe]==2024.6.2 - # via dask-expr + # via + # feast (setup.py) + # dask-expr dask-expr==1.1.6 # via dask db-dtypes==1.2.0 @@ -137,7 +153,9 @@ decorator==5.1.1 defusedxml==0.7.1 # via nbconvert deltalake==0.18.1 + # via feast (setup.py) dill==0.3.8 + # via feast (setup.py) distlib==0.3.8 # via virtualenv dnspython==2.6.1 @@ -151,6 +169,7 @@ duckdb==0.10.3 elastic-transport==8.13.1 # via elasticsearch elasticsearch==8.14.0 + # via feast (setup.py) email-validator==2.2.0 # via fastapi entrypoints==0.4 @@ -165,6 +184,7 @@ execnet==2.1.1 executing==2.0.1 # via stack-data fastapi==0.111.0 + # via feast (setup.py) fastapi-cli==0.0.4 # via fastapi fastjsonschema==2.20.0 @@ -180,13 +200,16 @@ frozenlist==1.4.1 # aiohttp # aiosignal fsspec==2023.12.2 - # via dask + # via + # feast (setup.py) + # dask geojson==2.5.0 # via rockset geomet==0.2.1.post1 # via cassandra-driver google-api-core[grpc]==2.19.1 # via + # feast (setup.py) # google-cloud-bigquery # google-cloud-bigquery-storage # google-cloud-bigtable @@ -201,8 +224,11 @@ google-auth==2.30.0 # google-cloud-storage # kubernetes google-cloud-bigquery[pandas]==3.13.0 + # via feast (setup.py) google-cloud-bigquery-storage==2.25.0 + # via feast (setup.py) google-cloud-bigtable==2.24.0 + # via feast (setup.py) google-cloud-core==2.4.1 # via # google-cloud-bigquery @@ -210,7 +236,9 @@ google-cloud-core==2.4.1 # google-cloud-datastore # google-cloud-storage google-cloud-datastore==2.19.0 + # via feast (setup.py) google-cloud-storage==2.17.0 + # via feast (setup.py) google-crc32c==1.5.0 # via # google-cloud-storage @@ -221,16 +249,19 @@ google-resumable-media==2.7.1 # google-cloud-storage googleapis-common-protos[grpc]==1.63.2 # via + # feast (setup.py) # google-api-core # grpc-google-iam-v1 # grpcio-status great-expectations==0.18.16 + # via feast (setup.py) greenlet==3.0.3 # via sqlalchemy grpc-google-iam-v1==0.13.1 # via google-cloud-bigtable grpcio==1.64.1 # via + # feast (setup.py) # google-api-core # google-cloud-bigquery # googleapis-common-protos @@ -241,30 +272,42 @@ grpcio==1.64.1 # grpcio-testing # grpcio-tools grpcio-health-checking==1.62.2 + # via feast (setup.py) grpcio-reflection==1.62.2 + # via feast (setup.py) grpcio-status==1.62.2 # via google-api-core grpcio-testing==1.62.2 + # via feast (setup.py) grpcio-tools==1.62.2 + # via feast (setup.py) gunicorn==22.0.0 + # via feast (setup.py) h11==0.14.0 # via # httpcore # uvicorn happybase==1.2.0 + # via feast (setup.py) hazelcast-python-client==5.4.0 + # via feast (setup.py) hiredis==2.3.2 + # via feast (setup.py) httpcore==1.0.5 # via httpx httptools==0.6.1 # via uvicorn httpx==0.27.0 # via + # feast (setup.py) # fastapi # jupyterlab ibis-framework[duckdb]==9.0.0 - # via ibis-substrait + # via + # feast (setup.py) + # ibis-substrait ibis-substrait==4.0.0 + # via feast (setup.py) identify==2.5.36 # via pre-commit idna==3.7 @@ -308,6 +351,7 @@ jedi==0.19.1 # via ipython jinja2==3.1.4 # via + # feast (setup.py) # altair # fastapi # great-expectations @@ -331,6 +375,7 @@ jsonpointer==3.0.0 # jsonschema jsonschema[format-nongpl]==4.22.0 # via + # feast (setup.py) # altair # great-expectations # jupyter-events @@ -376,6 +421,7 @@ jupyterlab-server==2.27.2 jupyterlab-widgets==3.0.11 # via ipywidgets kubernetes==20.13.0 + # via feast (setup.py) locket==1.0.0 # via partd makefun==1.15.2 @@ -396,13 +442,17 @@ matplotlib-inline==0.1.7 mdurl==0.1.2 # via markdown-it-py minio==7.1.0 + # via feast (setup.py) mistune==3.0.2 # via # great-expectations # nbconvert mmh3==4.1.0 + # via feast (setup.py) mock==2.0.0 + # via feast (setup.py) moto==4.2.14 + # via feast (setup.py) msal==1.29.0 # via # azure-identity @@ -414,10 +464,13 @@ multidict==6.0.5 # aiohttp # yarl mypy==1.10.1 - # via sqlalchemy + # via + # feast (setup.py) + # sqlalchemy mypy-extensions==1.0.0 # via mypy mypy-protobuf==3.3.0 + # via feast (setup.py) nbclient==0.10.0 # via nbconvert nbconvert==7.16.4 @@ -440,6 +493,7 @@ notebook-shim==0.2.4 # notebook numpy==1.26.4 # via + # feast (setup.py) # altair # dask # db-dtypes @@ -474,6 +528,7 @@ packaging==24.1 # sphinx pandas==2.2.2 # via + # feast (setup.py) # altair # dask # dask-expr @@ -499,6 +554,7 @@ pexpect==4.9.0 pip==24.1.1 # via pip-tools pip-tools==7.4.1 + # via feast (setup.py) platformdirs==3.11.0 # via # jupyter-core @@ -511,8 +567,11 @@ ply==3.11 portalocker==2.10.0 # via msal-extensions pre-commit==3.3.1 + # via feast (setup.py) prometheus-client==0.20.0 - # via jupyter-server + # via + # feast (setup.py) + # jupyter-server prompt-toolkit==3.0.47 # via ipython proto-plus==1.24.0 @@ -524,6 +583,7 @@ proto-plus==1.24.0 # google-cloud-datastore protobuf==4.25.3 # via + # feast (setup.py) # google-api-core # google-cloud-bigquery # google-cloud-bigquery-storage @@ -540,8 +600,11 @@ protobuf==4.25.3 # proto-plus # substrait psutil==5.9.0 - # via ipykernel + # via + # feast (setup.py) + # ipykernel psycopg[binary, pool]==3.1.18 + # via feast (setup.py) psycopg-binary==3.1.18 # via psycopg psycopg-pool==3.2.2 @@ -553,12 +616,14 @@ ptyprocess==0.7.0 pure-eval==0.2.2 # via stack-data py==1.11.0 + # via feast (setup.py) py-cpuinfo==9.0.0 # via pytest-benchmark py4j==0.10.9.7 # via pyspark pyarrow==15.0.2 # via + # feast (setup.py) # dask-expr # db-dtypes # deltalake @@ -576,16 +641,19 @@ pyasn1==0.6.0 pyasn1-modules==0.4.0 # via google-auth pybindgen==0.22.1 + # via feast (setup.py) pycparser==2.22 # via cffi pydantic==2.7.4 # via + # feast (setup.py) # fastapi # great-expectations pydantic-core==2.18.4 # via pydantic pygments==2.18.0 # via + # feast (setup.py) # ipython # nbconvert # rich @@ -596,8 +664,11 @@ pyjwt[crypto]==2.8.0 # singlestoredb # snowflake-connector-python pymssql==2.3.0 + # via feast (setup.py) pymysql==1.1.1 + # via feast (setup.py) pyodbc==5.1.0 + # via feast (setup.py) pyopenssl==24.1.0 # via snowflake-connector-python pyparsing==3.1.2 @@ -607,8 +678,10 @@ pyproject-hooks==1.1.0 # build # pip-tools pyspark==3.5.1 + # via feast (setup.py) pytest==7.4.4 # via + # feast (setup.py) # pytest-benchmark # pytest-cov # pytest-env @@ -618,13 +691,21 @@ pytest==7.4.4 # pytest-timeout # pytest-xdist pytest-benchmark==3.4.1 + # via feast (setup.py) pytest-cov==5.0.0 + # via feast (setup.py) pytest-env==1.1.3 + # via feast (setup.py) pytest-lazy-fixture==0.6.3 + # via feast (setup.py) pytest-mock==1.10.4 + # via feast (setup.py) pytest-ordering==0.6 + # via feast (setup.py) pytest-timeout==1.4.2 + # via feast (setup.py) pytest-xdist==3.6.1 + # via feast (setup.py) python-dateutil==2.9.0.post0 # via # arrow @@ -653,6 +734,7 @@ pytz==2024.1 # trino pyyaml==6.0.1 # via + # feast (setup.py) # dask # ibis-substrait # jupyter-events @@ -666,15 +748,19 @@ pyzmq==26.0.3 # jupyter-client # jupyter-server redis==4.6.0 + # via feast (setup.py) referencing==0.35.1 # via # jsonschema # jsonschema-specifications # jupyter-events regex==2024.5.15 - # via parsimonious + # via + # feast (setup.py) + # parsimonious requests==2.32.3 # via + # feast (setup.py) # azure-core # docker # google-api-core @@ -708,6 +794,7 @@ rich==13.7.1 # ibis-framework # typer rockset==2.1.2 + # via feast (setup.py) rpds-py==0.18.1 # via # jsonschema @@ -719,6 +806,7 @@ ruamel-yaml==0.17.17 ruamel-yaml-clib==0.2.8 # via ruamel-yaml ruff==0.4.10 + # via feast (setup.py) s3transfer==0.10.2 # via boto3 scipy==1.13.1 @@ -735,6 +823,7 @@ setuptools==70.1.1 shellingham==1.5.4 # via typer singlestoredb==1.4.0 + # via feast (setup.py) six==1.16.0 # via # asttokens @@ -755,11 +844,13 @@ sniffio==1.3.1 snowballstemmer==2.2.0 # via sphinx snowflake-connector-python[pandas]==3.11.0 + # via feast (setup.py) sortedcontainers==2.4.0 # via snowflake-connector-python soupsieve==2.5 # via beautifulsoup4 sphinx==6.2.1 + # via feast (setup.py) sphinxcontrib-applehelp==1.0.8 # via sphinx sphinxcontrib-devhelp==1.0.6 @@ -773,9 +864,11 @@ sphinxcontrib-qthelp==1.0.7 sphinxcontrib-serializinghtml==1.1.10 # via sphinx sqlalchemy[mypy]==2.0.31 + # via feast (setup.py) sqlglot==23.12.2 # via ibis-framework sqlite-vec==0.0.1a10 + # via feast (setup.py) sqlparams==6.0.1 # via singlestoredb stack-data==0.6.3 @@ -785,17 +878,21 @@ starlette==0.37.2 substrait==0.19.0 # via ibis-substrait tabulate==0.9.0 + # via feast (setup.py) tenacity==8.4.2 + # via feast (setup.py) terminado==0.18.1 # via # jupyter-server # jupyter-server-terminals testcontainers==4.4.0 + # via feast (setup.py) thriftpy2==0.5.1 # via happybase tinycss2==1.3.0 # via nbconvert toml==0.10.2 + # via feast (setup.py) tomli==2.0.1 # via # build @@ -823,7 +920,9 @@ tornado==6.4.1 # notebook # terminado tqdm==4.66.4 - # via great-expectations + # via + # feast (setup.py) + # great-expectations traitlets==5.14.3 # via # comm @@ -840,25 +939,39 @@ traitlets==5.14.3 # nbconvert # nbformat trino==0.328.0 + # via feast (setup.py) typeguard==4.3.0 + # via feast (setup.py) typer==0.12.3 # via fastapi-cli types-cffi==1.16.0.20240331 # via types-pyopenssl types-protobuf==3.19.22 - # via mypy-protobuf + # via + # feast (setup.py) + # mypy-protobuf types-pymysql==1.1.0.20240524 + # via feast (setup.py) types-pyopenssl==24.1.0.20240425 # via types-redis types-python-dateutil==2.9.0.20240316 - # via arrow + # via + # feast (setup.py) + # arrow types-pytz==2024.1.0.20240417 + # via feast (setup.py) types-pyyaml==6.0.12.20240311 + # via feast (setup.py) types-redis==4.6.0.20240425 + # via feast (setup.py) types-requests==2.30.0.0 + # via feast (setup.py) types-setuptools==70.1.0.20240627 - # via types-cffi + # via + # feast (setup.py) + # types-cffi types-tabulate==0.9.0.20240106 + # via feast (setup.py) types-urllib3==1.26.25.14 # via types-requests typing-extensions==4.12.2 @@ -897,6 +1010,7 @@ uri-template==1.3.0 # via jsonschema urllib3==1.26.19 # via + # feast (setup.py) # botocore # docker # elastic-transport @@ -909,11 +1023,15 @@ urllib3==1.26.19 # snowflake-connector-python # testcontainers uvicorn[standard]==0.30.1 - # via fastapi + # via + # feast (setup.py) + # fastapi uvloop==0.19.0 # via uvicorn virtualenv==20.23.0 - # via pre-commit + # via + # feast (setup.py) + # pre-commit watchfiles==0.22.0 # via uvicorn wcwidth==0.2.13 diff --git a/sdk/python/requirements/py3.9-requirements.txt b/sdk/python/requirements/py3.9-requirements.txt index 096f54ab1f..7d549f908d 100644 --- a/sdk/python/requirements/py3.9-requirements.txt +++ b/sdk/python/requirements/py3.9-requirements.txt @@ -20,25 +20,32 @@ charset-normalizer==3.3.2 # via requests click==8.1.7 # via + # feast (setup.py) # dask # typer # uvicorn cloudpickle==3.0.0 # via dask colorama==0.4.6 + # via feast (setup.py) dask[dataframe]==2024.5.0 - # via dask-expr + # via + # feast (setup.py) + # dask-expr dask-expr==1.1.0 # via dask dill==0.3.8 + # via feast (setup.py) dnspython==2.6.1 # via email-validator email-validator==2.1.1 # via fastapi -exceptiongroup==1.2.1 +exceptiongroup==1.2.2 # via anyio fastapi==0.111.0 - # via fastapi-cli + # via + # feast (setup.py) + # fastapi-cli fastapi-cli==0.0.2 # via fastapi fsspec==2024.3.1 @@ -46,6 +53,7 @@ fsspec==2024.3.1 greenlet==3.0.3 # via sqlalchemy gunicorn==22.0.0 + # via feast (setup.py) h11==0.14.0 # via # httpcore @@ -62,13 +70,16 @@ idna==3.7 # email-validator # httpx # requests -importlib-metadata==7.1.0 +importlib-metadata==8.2.0 # via # dask # typeguard jinja2==3.1.4 - # via fastapi + # via + # feast (setup.py) + # fastapi jsonschema==4.22.0 + # via feast (setup.py) jsonschema-specifications==2023.12.1 # via jsonschema locket==1.0.0 @@ -80,13 +91,16 @@ markupsafe==2.1.5 mdurl==0.1.2 # via markdown-it-py mmh3==4.1.0 + # via feast (setup.py) mypy==1.10.0 # via sqlalchemy mypy-extensions==1.0.0 # via mypy mypy-protobuf==3.6.0 + # via feast (setup.py) numpy==1.26.4 # via + # feast (setup.py) # dask # pandas # pyarrow @@ -98,20 +112,33 @@ packaging==24.0 # gunicorn pandas==2.2.2 # via + # feast (setup.py) # dask # dask-expr partd==1.4.2 # via dask +prometheus-client==0.20.0 + # via feast (setup.py) protobuf==4.25.3 - # via mypy-protobuf + # via + # feast (setup.py) + # mypy-protobuf +psutil==6.0.0 + # via feast (setup.py) pyarrow==16.0.0 - # via dask-expr + # via + # feast (setup.py) + # dask-expr pydantic==2.7.1 - # via fastapi + # via + # feast (setup.py) + # fastapi pydantic-core==2.18.2 # via pydantic pygments==2.18.0 - # via rich + # via + # feast (setup.py) + # rich python-dateutil==2.9.0.post0 # via pandas python-dotenv==1.0.1 @@ -122,6 +149,7 @@ pytz==2024.1 # via pandas pyyaml==6.0.1 # via + # feast (setup.py) # dask # uvicorn referencing==0.35.1 @@ -129,6 +157,7 @@ referencing==0.35.1 # jsonschema # jsonschema-specifications requests==2.31.0 + # via feast (setup.py) rich==13.7.1 # via typer rpds-py==0.18.1 @@ -144,11 +173,15 @@ sniffio==1.3.1 # anyio # httpx sqlalchemy[mypy]==2.0.30 + # via feast (setup.py) starlette==0.37.2 # via fastapi tabulate==0.9.0 + # via feast (setup.py) tenacity==8.3.0 + # via feast (setup.py) toml==0.10.2 + # via feast (setup.py) tomli==2.0.1 # via mypy toolz==0.12.1 @@ -156,7 +189,9 @@ toolz==0.12.1 # dask # partd tqdm==4.66.4 + # via feast (setup.py) typeguard==4.2.1 + # via feast (setup.py) typer==0.12.3 # via fastapi-cli types-protobuf==5.26.0.20240422 @@ -181,6 +216,7 @@ urllib3==2.2.1 # via requests uvicorn[standard]==0.29.0 # via + # feast (setup.py) # fastapi # fastapi-cli uvloop==0.19.0 @@ -189,5 +225,5 @@ watchfiles==0.21.0 # via uvicorn websockets==12.0 # via uvicorn -zipp==3.19.1 +zipp==3.19.2 # via importlib-metadata diff --git a/sdk/python/tests/integration/online_store/test_remote_online_store.py b/sdk/python/tests/integration/online_store/test_remote_online_store.py index 1d5dd0fca0..21ac00583b 100644 --- a/sdk/python/tests/integration/online_store/test_remote_online_store.py +++ b/sdk/python/tests/integration/online_store/test_remote_online_store.py @@ -195,7 +195,7 @@ def _overwrite_remote_client_feature_store_yaml( ) -def _start_feature_server(repo_path: str, server_port: int): +def _start_feature_server(repo_path: str, server_port: int, metrics: bool = False): host = "0.0.0.0" cmd = [ "feast", @@ -217,6 +217,21 @@ def _start_feature_server(repo_path: str, server_port: int): timeout_msg=f"Unable to start the feast server in {_time_out_sec} seconds for remote online store type, port={server_port}", ) + if metrics: + cmd.append("--metrics") + + # Check if metrics are enabled and Prometheus server is running + if metrics: + wait_retry_backoff( + lambda: (None, check_port_open("localhost", 8000)), + timeout_secs=_time_out_sec, + timeout_msg="Unable to start the Prometheus server in 60 seconds.", + ) + else: + assert not check_port_open( + "localhost", 8000 + ), "Prometheus server is running when it should be disabled." + yield f"http://localhost:{server_port}" if feast_server_process is not None: diff --git a/setup.py b/setup.py index b983617712..f5a4dd8c62 100644 --- a/setup.py +++ b/setup.py @@ -59,6 +59,8 @@ "uvicorn[standard]>=0.14.0,<1", "gunicorn; platform_system != 'Windows'", "dask[dataframe]>=2024.2.1", + "prometheus_client", + "psutil", ] GCP_REQUIRED = [ @@ -98,6 +100,8 @@ "psycopg[binary,pool]>=3.0.0,<4", ] +OPENTELEMETRY = ["prometheus_client","psutil"] + MYSQL_REQUIRED = ["pymysql", "types-PyMySQL"] HBASE_REQUIRED = [ @@ -215,6 +219,7 @@ + ELASTICSEARCH_REQUIRED + SQLITE_VEC_REQUIRED + SINGLESTORE_REQUIRED + + OPENTELEMETRY ) DOCS_REQUIRED = CI_REQUIRED @@ -385,6 +390,7 @@ def run(self): "elasticsearch": ELASTICSEARCH_REQUIRED, "sqlite_vec": SQLITE_VEC_REQUIRED, "singlestore": SINGLESTORE_REQUIRED, + "opentelemetry": OPENTELEMETRY, }, include_package_data=True, license="Apache", From b6886ed225b392f018063dd13a426169ecd18a3a Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Tue, 6 Aug 2024 11:48:17 -0400 Subject: [PATCH 006/185] chore: Update pull_request_template.md (#4388) --- .github/pull_request_template.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 4bec4d79e1..b7d630e8bc 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -21,4 +21,7 @@ Usage: `Fixes #`, or `Fixes (paste link of issue)`. --> -# Fixes +# Misc + From b734cb147a4afd28407ec57d95f70ff604f82954 Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Tue, 6 Aug 2024 14:07:33 -0400 Subject: [PATCH 007/185] feat: Update sqlite-vec package (#4389) * docs: Adding some content about the push vs pull model Signed-off-by: Francisco Javier Arceo * feat: Updating SQLite-Vec to stable release version: Signed-off-by: Francisco Javier Arceo * removing push model changes from other branch Signed-off-by: Francisco Javier Arceo --------- Signed-off-by: Francisco Javier Arceo --- sdk/python/requirements/py3.10-ci-requirements.txt | 4 +--- sdk/python/requirements/py3.10-requirements.txt | 2 -- sdk/python/requirements/py3.11-ci-requirements.txt | 4 +--- sdk/python/requirements/py3.11-requirements.txt | 2 -- sdk/python/requirements/py3.9-ci-requirements.txt | 4 +--- sdk/python/requirements/py3.9-requirements.txt | 2 -- sdk/python/tests/unit/online_store/test_online_retrieval.py | 2 +- setup.py | 2 +- 8 files changed, 5 insertions(+), 17 deletions(-) diff --git a/sdk/python/requirements/py3.10-ci-requirements.txt b/sdk/python/requirements/py3.10-ci-requirements.txt index ddf72c59f4..785342550a 100644 --- a/sdk/python/requirements/py3.10-ci-requirements.txt +++ b/sdk/python/requirements/py3.10-ci-requirements.txt @@ -255,8 +255,6 @@ googleapis-common-protos[grpc]==1.63.2 # grpcio-status great-expectations==0.18.16 # via feast (setup.py) -greenlet==3.0.3 - # via sqlalchemy grpc-google-iam-v1==0.13.1 # via google-cloud-bigtable grpcio==1.64.1 @@ -856,7 +854,7 @@ sqlalchemy[mypy]==2.0.31 # via feast (setup.py) sqlglot==25.1.0 # via ibis-framework -sqlite-vec==0.0.1a10 +sqlite-vec==0.1.1 # via feast (setup.py) sqlparams==6.0.1 # via singlestoredb diff --git a/sdk/python/requirements/py3.10-requirements.txt b/sdk/python/requirements/py3.10-requirements.txt index 93322ae434..250e617b85 100644 --- a/sdk/python/requirements/py3.10-requirements.txt +++ b/sdk/python/requirements/py3.10-requirements.txt @@ -50,8 +50,6 @@ fastapi-cli==0.0.2 # via fastapi fsspec==2024.3.1 # via dask -greenlet==3.0.3 - # via sqlalchemy gunicorn==22.0.0 # via feast (setup.py) h11==0.14.0 diff --git a/sdk/python/requirements/py3.11-ci-requirements.txt b/sdk/python/requirements/py3.11-ci-requirements.txt index a451ad58af..f16b486aa5 100644 --- a/sdk/python/requirements/py3.11-ci-requirements.txt +++ b/sdk/python/requirements/py3.11-ci-requirements.txt @@ -246,8 +246,6 @@ googleapis-common-protos[grpc]==1.63.2 # grpcio-status great-expectations==0.18.16 # via feast (setup.py) -greenlet==3.0.3 - # via sqlalchemy grpc-google-iam-v1==0.13.1 # via google-cloud-bigtable grpcio==1.64.1 @@ -847,7 +845,7 @@ sqlalchemy[mypy]==2.0.31 # via feast (setup.py) sqlglot==25.1.0 # via ibis-framework -sqlite-vec==0.0.1a10 +sqlite-vec==0.1.1 # via feast (setup.py) sqlparams==6.0.1 # via singlestoredb diff --git a/sdk/python/requirements/py3.11-requirements.txt b/sdk/python/requirements/py3.11-requirements.txt index 48104ebc2c..4f1655de09 100644 --- a/sdk/python/requirements/py3.11-requirements.txt +++ b/sdk/python/requirements/py3.11-requirements.txt @@ -48,8 +48,6 @@ fastapi-cli==0.0.2 # via fastapi fsspec==2024.3.1 # via dask -greenlet==3.0.3 - # via sqlalchemy gunicorn==22.0.0 # via feast (setup.py) h11==0.14.0 diff --git a/sdk/python/requirements/py3.9-ci-requirements.txt b/sdk/python/requirements/py3.9-ci-requirements.txt index 5055b3c79f..94bfa82058 100644 --- a/sdk/python/requirements/py3.9-ci-requirements.txt +++ b/sdk/python/requirements/py3.9-ci-requirements.txt @@ -255,8 +255,6 @@ googleapis-common-protos[grpc]==1.63.2 # grpcio-status great-expectations==0.18.16 # via feast (setup.py) -greenlet==3.0.3 - # via sqlalchemy grpc-google-iam-v1==0.13.1 # via google-cloud-bigtable grpcio==1.64.1 @@ -867,7 +865,7 @@ sqlalchemy[mypy]==2.0.31 # via feast (setup.py) sqlglot==23.12.2 # via ibis-framework -sqlite-vec==0.0.1a10 +sqlite-vec==0.1.1 # via feast (setup.py) sqlparams==6.0.1 # via singlestoredb diff --git a/sdk/python/requirements/py3.9-requirements.txt b/sdk/python/requirements/py3.9-requirements.txt index 7d549f908d..f9fa856a0e 100644 --- a/sdk/python/requirements/py3.9-requirements.txt +++ b/sdk/python/requirements/py3.9-requirements.txt @@ -50,8 +50,6 @@ fastapi-cli==0.0.2 # via fastapi fsspec==2024.3.1 # via dask -greenlet==3.0.3 - # via sqlalchemy gunicorn==22.0.0 # via feast (setup.py) h11==0.14.0 diff --git a/sdk/python/tests/unit/online_store/test_online_retrieval.py b/sdk/python/tests/unit/online_store/test_online_retrieval.py index 0b552c0453..48adb6da7d 100644 --- a/sdk/python/tests/unit/online_store/test_online_retrieval.py +++ b/sdk/python/tests/unit/online_store/test_online_retrieval.py @@ -548,7 +548,7 @@ def test_sqlite_vec_import() -> None: sqlite_version, vec_version = db.execute( "select sqlite_version(), vec_version()" ).fetchone() - assert vec_version == "v0.0.1-alpha.10" + assert vec_version == "v0.1.1" print(f"sqlite_version={sqlite_version}, vec_version={vec_version}") result = db.execute(""" diff --git a/setup.py b/setup.py index f5a4dd8c62..6fb5bfee61 100644 --- a/setup.py +++ b/setup.py @@ -92,7 +92,7 @@ ] SQLITE_VEC_REQUIRED = [ - "sqlite-vec==v0.0.1-alpha.10", + "sqlite-vec==v0.1.1", ] TRINO_REQUIRED = ["trino>=0.305.0,<0.400.0", "regex"] From 125fa49fc700a8e6f1994c710a5076210fb97bf9 Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Tue, 6 Aug 2024 15:48:24 -0400 Subject: [PATCH 008/185] docs: Updating feast arch diagram (#4387) --- docs/assets/feast_marchitecture.png | Bin 224458 -> 786459 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/assets/feast_marchitecture.png b/docs/assets/feast_marchitecture.png index 2b162ec06e99be8bda7748226c958f217e0ad4e3..9f7df7c01969608f5a8b1d48b21f20ddeaed5590 100644 GIT binary patch literal 786459 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%0D0)%S_yxZ#SC~0g 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