From 83fad152ffe01a3b2691095a45b90eb30044c859 Mon Sep 17 00:00:00 2001 From: Hao Xu Date: Tue, 18 Jun 2024 11:48:40 -0700 Subject: [PATCH 01/59] feat: Entity key deserialization (#4284) * Add new version of serialization and desrialization Signed-off-by: cmuhao * Add new version of serialization and desrialization Signed-off-by: cmuhao * fix test Signed-off-by: cmuhao * fix test Signed-off-by: cmuhao * add test Signed-off-by: cmuhao * add test Signed-off-by: cmuhao * update doc Signed-off-by: cmuhao --------- Signed-off-by: cmuhao --- sdk/python/feast/infra/key_encoding_utils.py | 80 ++++++++++++++++++- sdk/python/feast/repo_config.py | 8 +- .../unit/infra/test_key_encoding_utils.py | 71 +++++++++++++++- 3 files changed, 154 insertions(+), 5 deletions(-) diff --git a/sdk/python/feast/infra/key_encoding_utils.py b/sdk/python/feast/infra/key_encoding_utils.py index ca834f1917..1f9ffeef14 100644 --- a/sdk/python/feast/infra/key_encoding_utils.py +++ b/sdk/python/feast/infra/key_encoding_utils.py @@ -20,7 +20,23 @@ def _serialize_val( return struct.pack(" ValueProto: + if value_type == ValueType.INT64: + value = struct.unpack(" bytes: @@ -50,6 +66,15 @@ def serialize_entity_key( serialize to the same byte string[1]. [1] https://developers.google.com/protocol-buffers/docs/encoding + + Args: + entity_key_serialization_version: version of the entity key serialization + version 1: int64 values are serialized as 4 bytes + version 2: int64 values are serialized as 8 bytes + version 3: entity_key size is added to the serialization for deserialization purposes + entity_key: EntityKeyProto + + Returns: bytes of the serialized entity key """ sorted_keys, sorted_values = zip( *sorted(zip(entity_key.join_keys, entity_key.entity_values)) @@ -58,6 +83,8 @@ def serialize_entity_key( output: List[bytes] = [] for k in sorted_keys: output.append(struct.pack(" 2: + output.append(struct.pack(" EntityKeyProto: + """ + Deserialize entity key from a bytestring. This function can only be used with entity_key_serialization_version > 2. + Args: + entity_key_serialization_version: version of the entity key serialization + serialized_entity_key: serialized entity key bytes + + Returns: EntityKeyProto + + """ + if entity_key_serialization_version <= 2: + raise ValueError( + "Deserialization of entity key with version <= 2 is not supported. Please use version > 2 by setting entity_key_serialization_version=3" + ) + offset = 0 + keys = [] + values = [] + while offset < len(serialized_entity_key): + key_type = struct.unpack_from(" Date: Tue, 18 Jun 2024 14:51:33 -0400 Subject: [PATCH 02/59] feat: Ignore paths feast apply (#4276) --- sdk/python/feast/repo_operations.py | 8 ++++++++ .../tests/unit/infra/scaffolding/test_repo_operations.py | 2 ++ 2 files changed, 10 insertions(+) diff --git a/sdk/python/feast/repo_operations.py b/sdk/python/feast/repo_operations.py index 274a0af02b..05a7d05e23 100644 --- a/sdk/python/feast/repo_operations.py +++ b/sdk/python/feast/repo_operations.py @@ -83,6 +83,14 @@ def get_repo_files(repo_root: Path) -> List[Path]: # Read ignore paths from .feastignore and create a set of all files that match any of these paths ignore_paths = read_feastignore(repo_root) ignore_files = get_ignore_files(repo_root, ignore_paths) + ignore_paths += [ + ".git", + ".feastignore", + ".venv", + ".pytest_cache", + "__pycache__", + ".ipynb_checkpoints", + ] # List all Python files in the root directory (recursively) repo_files = { 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 70c8b05c2e..aa4ff1c40f 100644 --- a/sdk/python/tests/unit/infra/scaffolding/test_repo_operations.py +++ b/sdk/python/tests/unit/infra/scaffolding/test_repo_operations.py @@ -15,12 +15,14 @@ def feature_repo(feastignore_contents: Optional[str]): repo_root = Path(tmp_dir) (repo_root / "foo").mkdir() (repo_root / "foo1").mkdir() + (repo_root / ".ipynb_checkpoints/").mkdir() (repo_root / "foo1/bar").mkdir() (repo_root / "bar").mkdir() (repo_root / "bar/subdir1").mkdir() (repo_root / "bar/subdir1/subdir2").mkdir() (repo_root / "a.py").touch() + (repo_root / ".ipynb_checkpoints/test-checkpoint.ipynb").touch() (repo_root / "foo/b.py").touch() (repo_root / "foo1/c.py").touch() (repo_root / "foo1/bar/d.py").touch() From 89bc5512572130510dd18690309b5a392aaf73b1 Mon Sep 17 00:00:00 2001 From: Tornike Gurgenidze Date: Wed, 19 Jun 2024 05:18:19 +0400 Subject: [PATCH 03/59] chore: Remove serverless feature server deployments (#4272) --- .github/workflows/master_only.yml | 2 +- Makefile | 8 - docs/SUMMARY.md | 2 - docs/getting-started/faq.md | 2 +- docs/reference/feature-servers/README.md | 5 +- .../alpha-aws-lambda-feature-server.md | 197 ---------- .../feature-servers/python-feature-server.md | 2 - sdk/python/feast/constants.py | 6 - sdk/python/feast/errors.py | 26 -- sdk/python/feast/infra/aws.py | 349 +----------------- .../feature_servers/aws_lambda/Dockerfile | 26 -- .../feature_servers/aws_lambda/__init__.py | 0 .../infra/feature_servers/aws_lambda/app.py | 27 -- .../feature_servers/aws_lambda/config.py | 21 -- .../aws_lambda/requirements.txt | 2 - .../feature_servers/gcp_cloudrun/Dockerfile | 32 -- .../feature_servers/gcp_cloudrun/__init__.py | 0 .../infra/feature_servers/gcp_cloudrun/app.py | 24 -- .../feature_servers/gcp_cloudrun/config.py | 18 - .../gcp_cloudrun/requirements.txt | 1 - sdk/python/feast/repo_config.py | 4 - sdk/python/tests/conftest.py | 18 +- .../feature_repos/repo_configuration.py | 23 +- setup.py | 2 +- 24 files changed, 13 insertions(+), 784 deletions(-) delete mode 100644 docs/reference/feature-servers/alpha-aws-lambda-feature-server.md delete mode 100644 sdk/python/feast/infra/feature_servers/aws_lambda/Dockerfile delete mode 100644 sdk/python/feast/infra/feature_servers/aws_lambda/__init__.py delete mode 100644 sdk/python/feast/infra/feature_servers/aws_lambda/app.py delete mode 100644 sdk/python/feast/infra/feature_servers/aws_lambda/config.py delete mode 100644 sdk/python/feast/infra/feature_servers/aws_lambda/requirements.txt delete mode 100644 sdk/python/feast/infra/feature_servers/gcp_cloudrun/Dockerfile delete mode 100644 sdk/python/feast/infra/feature_servers/gcp_cloudrun/__init__.py delete mode 100644 sdk/python/feast/infra/feature_servers/gcp_cloudrun/app.py delete mode 100644 sdk/python/feast/infra/feature_servers/gcp_cloudrun/config.py delete mode 100644 sdk/python/feast/infra/feature_servers/gcp_cloudrun/requirements.txt diff --git a/.github/workflows/master_only.yml b/.github/workflows/master_only.yml index 1b401997a7..7166246da5 100644 --- a/.github/workflows/master_only.yml +++ b/.github/workflows/master_only.yml @@ -94,7 +94,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - component: [ feature-server-python-aws, feature-server-java, feature-transformation-server ] + component: [ feature-server-java, feature-transformation-server ] env: MAVEN_CACHE: gs://feast-templocation-kf-feast/.m2.2020-08-19.tar REGISTRY: gcr.io/kf-feast diff --git a/Makefile b/Makefile index b44aaf0ee5..2ad693c7a1 100644 --- a/Makefile +++ b/Makefile @@ -397,14 +397,6 @@ build-feature-server-docker: -t $(REGISTRY)/feature-server:$$VERSION \ -f sdk/python/feast/infra/feature_servers/multicloud/Dockerfile --load . -push-feature-server-python-aws-docker: - docker push $(REGISTRY)/feature-server-python-aws:$$VERSION - -build-feature-server-python-aws-docker: - docker buildx build --build-arg VERSION=$$VERSION \ - -t $(REGISTRY)/feature-server-python-aws:$$VERSION \ - -f sdk/python/feast/infra/feature_servers/aws_lambda/Dockerfile --load . - push-feature-transformation-server-docker: docker push $(REGISTRY)/feature-transformation-server:$(VERSION) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 06c5edcc8b..3f0506cf1e 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -118,9 +118,7 @@ * [Feature servers](reference/feature-servers/README.md) * [Python feature server](reference/feature-servers/python-feature-server.md) * [\[Alpha\] Go feature server](reference/feature-servers/go-feature-server.md) - * [\[Alpha\] AWS Lambda feature server](reference/feature-servers/alpha-aws-lambda-feature-server.md) * [Offline Feature Server](reference/feature-servers/offline-feature-server) - * [\[Beta\] Web UI](reference/alpha-web-ui.md) * [\[Alpha\] On demand feature view](reference/alpha-on-demand-feature-view.md) * [\[Alpha\] Data quality monitoring](reference/dqm.md) diff --git a/docs/getting-started/faq.md b/docs/getting-started/faq.md index 8948eed588..02f9db7d0c 100644 --- a/docs/getting-started/faq.md +++ b/docs/getting-started/faq.md @@ -95,7 +95,7 @@ The list of supported offline and online stores can be found [here](../reference ### Does Feast support using different clouds for offline vs online stores? -Yes. Using a GCP or AWS provider in `feature_store.yaml` primarily sets default offline / online stores and configures where the remote registry file can live (Using the AWS provider also allows for deployment to AWS Lambda). You can override the offline and online stores to be in different clouds if you wish. +Yes. Using a GCP or AWS provider in `feature_store.yaml` primarily sets default offline / online stores and configures where the remote registry file can live. You can override the offline and online stores to be in different clouds if you wish. ### What is the difference between a data source and an offline store? diff --git a/docs/reference/feature-servers/README.md b/docs/reference/feature-servers/README.md index d5a4312f73..124834f8a7 100644 --- a/docs/reference/feature-servers/README.md +++ b/docs/reference/feature-servers/README.md @@ -8,10 +8,7 @@ Feast users can choose to retrieve features from a feature server, as opposed to {% content-ref url="go-feature-server.md" %} [go-feature-server.md](go-feature-server.md) -{% endcontent-ref %} - -{% content-ref url="alpha-aws-lambda-feature-server.md" %} -[alpha-aws-lambda-feature-server.md](alpha-aws-lambda-feature-server.md) +======= {% endcontent-ref %} {% content-ref url="offline-feature-server.md" %} diff --git a/docs/reference/feature-servers/alpha-aws-lambda-feature-server.md b/docs/reference/feature-servers/alpha-aws-lambda-feature-server.md deleted file mode 100644 index caf5542bdc..0000000000 --- a/docs/reference/feature-servers/alpha-aws-lambda-feature-server.md +++ /dev/null @@ -1,197 +0,0 @@ -# \[Alpha] AWS Lambda feature server - -**Warning**: This is an _experimental_ feature. It's intended for early testing and feedback, and could change without warnings in future releases. - -## Overview - -The AWS Lambda feature server is an HTTP endpoint that serves features with JSON I/O, deployed as a Docker image through AWS Lambda and AWS API Gateway. This enables users to get features from Feast using any programming language that can make HTTP requests. A [local feature server](python-feature-server.md) is also available. A remote feature server on GCP Cloud Run is currently being developed. - -## Deployment - -The AWS Lambda feature server is only available to projects using the `AwsProvider` with registries on S3. It is disabled by default. To enable it, `feature_store.yaml` must be modified; specifically, the `enable` flag must be on and an `execution_role_name` must be specified. For example, after running `feast init -t aws`, changing the registry to be on S3, and enabling the feature server, the contents of `feature_store.yaml` should look similar to the following: - -``` -project: dev -registry: s3://feast/registries/dev -provider: aws -online_store: - region: us-west-2 -offline_store: - cluster_id: feast - region: us-west-2 - user: admin - database: feast - s3_staging_location: s3://feast/redshift/tests/staging_location - iam_role: arn:aws:iam::{aws_account}:role/redshift_s3_access_role -feature_server: - enabled: True - execution_role_name: arn:aws:iam::{aws_account}:role/lambda_execution_role -``` - -If enabled, the feature server will be deployed during `feast apply`. After it is deployed, the `feast endpoint` CLI command will indicate the server's endpoint. - -## Permissions - -Feast requires the following permissions in order to deploy and teardown AWS Lambda feature server: - -| Permissions | Resources | -| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -|

lambda:CreateFunction

lambda:GetFunction

lambda:DeleteFunction

lambda:AddPermission

lambda:UpdateFunctionConfiguration

| arn:aws:lambda:\:\:function:feast-\* | -|

ecr:CreateRepository

ecr:DescribeRepositories

ecr:DeleteRepository

ecr:PutImage

ecr:DescribeImages

ecr:BatchDeleteImage

ecr:CompleteLayerUpload

ecr:UploadLayerPart

ecr:InitiateLayerUpload

ecr:BatchCheckLayerAvailability

ecr:GetDownloadUrlForLayer

ecr:GetRepositoryPolicy

ecr:SetRepositoryPolicy

ecr:GetAuthorizationToken

| \* | -|

iam:PassRole

| arn:aws:iam::\:role/ | -|

apigateway:*

|

arn:aws:apigateway:*::/apis/*/routes/*/routeresponses

arn:aws:apigateway:*::/apis/*/routes/*/routeresponses/*

arn:aws:apigateway:*::/apis/*/routes/*

arn:aws:apigateway:*::/apis/*/routes

arn:aws:apigateway:*::/apis/*/integrations

arn:aws:apigateway:*::/apis/*/stages/*/routesettings/*

arn:aws:apigateway:*::/apis/*

arn:aws:apigateway:*::/apis

| - -The following inline policy can be used to grant Feast the necessary permissions: - -```javascript -{ - "Statement": [ - { - Action = [ - "lambda:CreateFunction", - "lambda:GetFunction", - "lambda:DeleteFunction", - "lambda:AddPermission", - "lambda:UpdateFunctionConfiguration", - ] - Effect = "Allow" - Resource = "arn:aws:lambda:::function:feast-*" - }, - { - Action = [ - "ecr:CreateRepository", - "ecr:DescribeRepositories", - "ecr:DeleteRepository", - "ecr:PutImage", - "ecr:DescribeImages", - "ecr:BatchDeleteImage", - "ecr:CompleteLayerUpload", - "ecr:UploadLayerPart", - "ecr:InitiateLayerUpload", - "ecr:BatchCheckLayerAvailability", - "ecr:GetDownloadUrlForLayer", - "ecr:GetRepositoryPolicy", - "ecr:SetRepositoryPolicy", - "ecr:GetAuthorizationToken" - ] - Effect = "Allow" - Resource = "*" - }, - { - Action = "iam:PassRole" - Effect = "Allow" - Resource = "arn:aws:iam:::role/" - }, - { - Effect = "Allow" - Action = "apigateway:*" - Resource = [ - "arn:aws:apigateway:*::/apis/*/routes/*/routeresponses", - "arn:aws:apigateway:*::/apis/*/routes/*/routeresponses/*", - "arn:aws:apigateway:*::/apis/*/routes/*", - "arn:aws:apigateway:*::/apis/*/routes", - "arn:aws:apigateway:*::/apis/*/integrations", - "arn:aws:apigateway:*::/apis/*/stages/*/routesettings/*", - "arn:aws:apigateway:*::/apis/*", - "arn:aws:apigateway:*::/apis", - ] - }, - ], - "Version": "2012-10-17" -} -``` - -## Example - -After `feature_store.yaml` has been modified as described in the previous section, it can be deployed as follows: - -```bash -$ feast apply -10/07/2021 03:57:26 PM INFO:Pulling remote image feastdev/feature-server-python-aws:aws: -10/07/2021 03:57:28 PM INFO:Creating remote ECR repository feast-python-server-key_shark-0_13_1_dev23_gb3c08320: -10/07/2021 03:57:29 PM INFO:Pushing local image to remote 402087665549.dkr.ecr.us-west-2.amazonaws.com/feast-python-server-key_shark-0_13_1_dev23_gb3c08320:0_13_1_dev23_gb3c08320: -10/07/2021 03:58:44 PM INFO:Deploying feature server... -10/07/2021 03:58:45 PM INFO: Creating AWS Lambda... -10/07/2021 03:58:46 PM INFO: Creating AWS API Gateway... -Registered entity driver_id -Registered feature view driver_hourly_stats -Deploying infrastructure for driver_hourly_stats - -$ feast endpoint -10/07/2021 03:59:01 PM INFO:Feature server endpoint: https://hkosgmz4m2.execute-api.us-west-2.amazonaws.com - -$ feast materialize-incremental $(date +%Y-%m-%d) -Materializing 1 feature views to 2021-10-06 17:00:00-07:00 into the dynamodb online store. - -driver_hourly_stats from 2020-10-08 23:01:34-07:00 to 2021-10-06 17:00:00-07:00: -100%|█████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 16.89it/s] -``` - -After the feature server starts, we can execute cURL commands against it: - -```bash -$ curl -X POST \ - "https://hkosgmz4m2.execute-api.us-west-2.amazonaws.com/get-online-features" \ - -H "Content-type: application/json" \ - -H "Accept: application/json" \ - -d '{ - "features": [ - "driver_hourly_stats:conv_rate", - "driver_hourly_stats:acc_rate", - "driver_hourly_stats:avg_daily_trips" - ], - "entities": { - "driver_id": [1001, 1002, 1003] - }, - "full_feature_names": true - }' | jq - % Total % Received % Xferd Average Speed Time Time Time Current - Dload Upload Total Spent Left Speed -100 1346 100 1055 100 291 3436 947 --:--:-- --:--:-- --:--:-- 4370 -{ - "field_values": [ - { - "fields": { - "driver_id": 1001, - "driver_hourly_stats__conv_rate": 0.025330161675810814, - "driver_hourly_stats__avg_daily_trips": 785, - "driver_hourly_stats__acc_rate": 0.835975170135498 - }, - "statuses": { - "driver_hourly_stats__avg_daily_trips": "PRESENT", - "driver_id": "PRESENT", - "driver_hourly_stats__conv_rate": "PRESENT", - "driver_hourly_stats__acc_rate": "PRESENT" - } - }, - { - "fields": { - "driver_hourly_stats__conv_rate": 0.7595187425613403, - "driver_hourly_stats__acc_rate": 0.1740121990442276, - "driver_id": 1002, - "driver_hourly_stats__avg_daily_trips": 875 - }, - "statuses": { - "driver_hourly_stats__acc_rate": "PRESENT", - "driver_id": "PRESENT", - "driver_hourly_stats__avg_daily_trips": "PRESENT", - "driver_hourly_stats__conv_rate": "PRESENT" - } - }, - { - "fields": { - "driver_hourly_stats__acc_rate": 0.7785481214523315, - "driver_hourly_stats__conv_rate": 0.33832859992980957, - "driver_hourly_stats__avg_daily_trips": 846, - "driver_id": 1003 - }, - "statuses": { - "driver_id": "PRESENT", - "driver_hourly_stats__conv_rate": "PRESENT", - "driver_hourly_stats__acc_rate": "PRESENT", - "driver_hourly_stats__avg_daily_trips": "PRESENT" - } - } - ] -} -``` diff --git a/docs/reference/feature-servers/python-feature-server.md b/docs/reference/feature-servers/python-feature-server.md index c189f97ae0..0d8a0aef75 100644 --- a/docs/reference/feature-servers/python-feature-server.md +++ b/docs/reference/feature-servers/python-feature-server.md @@ -12,8 +12,6 @@ There is a CLI command that starts the server: `feast serve`. By default, Feast One can deploy a feature server by building a docker image that bundles in the project's `feature_store.yaml`. See this [helm chart](https://github.com/feast-dev/feast/blob/master/infra/charts/feast-feature-server) for an example on how to run Feast on Kubernetes. -A [remote feature server](alpha-aws-lambda-feature-server.md) on AWS Lambda is also available. - ## Example ### Initializing a feature server diff --git a/sdk/python/feast/constants.py b/sdk/python/feast/constants.py index fa8674d91d..7dd3945821 100644 --- a/sdk/python/feast/constants.py +++ b/sdk/python/feast/constants.py @@ -17,9 +17,6 @@ # Maximum interval(secs) to wait between retries for retry function MAX_WAIT_INTERVAL: str = "60" -AWS_LAMBDA_FEATURE_SERVER_IMAGE = "feastdev/feature-server-python-aws" -AWS_LAMBDA_FEATURE_SERVER_REPOSITORY = "feast-python-server" - # feature_store.yaml environment variable name for remote feature server FEATURE_STORE_YAML_ENV_NAME: str = "FEATURE_STORE_YAML_BASE64" @@ -44,8 +41,5 @@ # Default offline server port DEFAULT_OFFLINE_SERVER_PORT = 8815 -# Environment variable for feature server docker image tag -DOCKER_IMAGE_TAG_ENV_NAME: str = "FEAST_SERVER_DOCKER_IMAGE_TAG" - # Default feature server registry ttl (seconds) DEFAULT_FEATURE_SERVER_REGISTRY_TTL = 5 diff --git a/sdk/python/feast/errors.py b/sdk/python/feast/errors.py index 52fefce9d9..22de402f20 100644 --- a/sdk/python/feast/errors.py +++ b/sdk/python/feast/errors.py @@ -350,32 +350,6 @@ def __init__(self, feature_view_name: str): ) -class RepoConfigPathDoesNotExist(Exception): - def __init__(self): - super().__init__("The repo_path attribute does not exist for the repo_config.") - - -class AwsLambdaDoesNotExist(Exception): - def __init__(self, resource_name: str): - super().__init__( - f"The AWS Lambda function {resource_name} should have been created properly, but does not exist." - ) - - -class AwsAPIGatewayDoesNotExist(Exception): - def __init__(self, resource_name: str): - super().__init__( - f"The AWS API Gateway {resource_name} should have been created properly, but does not exist." - ) - - -class IncompatibleRegistryStoreClass(Exception): - def __init__(self, actual_class: str, expected_class: str): - super().__init__( - f"The registry store class was expected to be {expected_class}, but was instead {actual_class}." - ) - - class FeastInvalidInfraObjectType(Exception): def __init__(self): super().__init__("Could not identify the type of the InfraObject.") diff --git a/sdk/python/feast/infra/aws.py b/sdk/python/feast/infra/aws.py index bb896fa961..47fec9b05b 100644 --- a/sdk/python/feast/infra/aws.py +++ b/sdk/python/feast/infra/aws.py @@ -1,350 +1,9 @@ -import base64 -import hashlib -import logging -import os -import uuid -import warnings -from typing import Optional, Sequence - -from colorama import Fore, Style - -from feast import utils -from feast.constants import ( - AWS_LAMBDA_FEATURE_SERVER_IMAGE, - AWS_LAMBDA_FEATURE_SERVER_REPOSITORY, - DOCKER_IMAGE_TAG_ENV_NAME, - FEATURE_STORE_YAML_ENV_NAME, -) -from feast.entity import Entity -from feast.errors import ( - AwsAPIGatewayDoesNotExist, - AwsLambdaDoesNotExist, - IncompatibleRegistryStoreClass, - RepoConfigPathDoesNotExist, -) -from feast.feature_view import FeatureView -from feast.infra.feature_servers.aws_lambda.config import AwsLambdaFeatureServerConfig from feast.infra.passthrough_provider import PassthroughProvider -from feast.infra.registry.registry import get_registry_store_class_from_scheme -from feast.infra.registry.s3 import S3RegistryStore -from feast.infra.utils import aws_utils -from feast.version import get_version - -try: - import boto3 -except ImportError as e: - from feast.errors import FeastExtrasDependencyImportError - - raise FeastExtrasDependencyImportError("aws", str(e)) - -_logger = logging.getLogger(__name__) class AwsProvider(PassthroughProvider): - def update_infra( - self, - project: str, - tables_to_delete: Sequence[FeatureView], - tables_to_keep: Sequence[FeatureView], - entities_to_delete: Sequence[Entity], - entities_to_keep: Sequence[Entity], - partial: bool, - ): - # Call update only if there is an online store - if self.online_store: - self.online_store.update( - config=self.repo_config, - tables_to_delete=tables_to_delete, - tables_to_keep=tables_to_keep, - entities_to_keep=entities_to_keep, - entities_to_delete=entities_to_delete, - partial=partial, - ) - - if self.repo_config.feature_server and self.repo_config.feature_server.enabled: - warnings.warn( - "AWS Lambda based feature serving is an experimental feature. " - "We do not guarantee that future changes will maintain backward compatibility.", - RuntimeWarning, - ) - - # Since the AWS Lambda feature server will attempt to load the registry, we - # only allow the registry to be in S3. - registry_path = ( - self.repo_config.registry - if isinstance(self.repo_config.registry, str) - else self.repo_config.registry.path - ) - registry_store_class = get_registry_store_class_from_scheme(registry_path) - if registry_store_class != S3RegistryStore: - raise IncompatibleRegistryStoreClass( - registry_store_class.__name__, S3RegistryStore.__name__ - ) - - ecr_client = boto3.client("ecr") - docker_image_version = _get_docker_image_version() - repository_uri = self._create_or_get_repository_uri(ecr_client) - # Only download & upload the docker image if it doesn't already exist in ECR - if not ecr_client.batch_get_image( - repositoryName=AWS_LAMBDA_FEATURE_SERVER_REPOSITORY, - imageIds=[{"imageTag": docker_image_version}], - ).get("images"): - image_uri = self._upload_docker_image( - ecr_client, repository_uri, docker_image_version - ) - else: - image_uri = f"{repository_uri}:{docker_image_version}" - - self._deploy_feature_server(project, image_uri) - - if self.batch_engine: - self.batch_engine.update( - project, - tables_to_delete, - tables_to_keep, - entities_to_delete, - entities_to_keep, - ) - - def _deploy_feature_server(self, project: str, image_uri: str): - _logger.info("Deploying feature server...") - - if not self.repo_config.repo_path: - raise RepoConfigPathDoesNotExist() - - with open( - utils.get_default_yaml_file_path(self.repo_config.repo_path), "rb" - ) as f: - config_bytes = f.read() - config_base64 = base64.b64encode(config_bytes).decode() - - resource_name = _get_lambda_name(project) - lambda_client = boto3.client("lambda") - api_gateway_client = boto3.client("apigatewayv2") - function = aws_utils.get_lambda_function(lambda_client, resource_name) - _logger.debug("Using function name: %s", resource_name) - _logger.debug("Found function: %s", function) - - if function is None: - # If the Lambda function does not exist, create it. - _logger.info(" Creating AWS Lambda...") - assert isinstance( - self.repo_config.feature_server, AwsLambdaFeatureServerConfig - ) - lambda_client.create_function( - FunctionName=resource_name, - Role=self.repo_config.feature_server.execution_role_name, - Code={"ImageUri": image_uri}, - PackageType="Image", - MemorySize=1769, - Environment={"Variables": {FEATURE_STORE_YAML_ENV_NAME: config_base64}}, - Tags={ - "feast-owned": "True", - "project": project, - "feast-sdk-version": get_version(), - }, - ) - function = aws_utils.get_lambda_function(lambda_client, resource_name) - if not function: - raise AwsLambdaDoesNotExist(resource_name) - else: - # If the feature_store.yaml has changed, need to update the environment variable. - env = function.get("Environment", {}).get("Variables", {}) - if env.get(FEATURE_STORE_YAML_ENV_NAME) != config_base64: - # Note, that this does not update Lambda gracefully (e.g. no rolling deployment). - # It's expected that feature_store.yaml is not regularly updated while the lambda - # is serving production traffic. However, the update in registry (e.g. modifying - # feature views, feature services, and other definitions does not update lambda). - _logger.info(" Updating AWS Lambda...") - - aws_utils.update_lambda_function_environment( - lambda_client, - resource_name, - {"Variables": {FEATURE_STORE_YAML_ENV_NAME: config_base64}}, - ) - - api = aws_utils.get_first_api_gateway(api_gateway_client, resource_name) - if not api: - # If the API Gateway doesn't exist, create it - _logger.info(" Creating AWS API Gateway...") - api = api_gateway_client.create_api( - Name=resource_name, - ProtocolType="HTTP", - Target=function["FunctionArn"], - RouteKey="POST /get-online-features", - Tags={ - "feast-owned": "True", - "project": project, - "feast-sdk-version": get_version(), - }, - ) - if not api: - raise AwsAPIGatewayDoesNotExist(resource_name) - # Make sure to give AWS Lambda a permission to be invoked by the newly created API Gateway - api_id = api["ApiId"] - region = lambda_client.meta.region_name - account_id = aws_utils.get_account_id() - lambda_client.add_permission( - FunctionName=function["FunctionArn"], - StatementId=str(uuid.uuid4()), - Action="lambda:InvokeFunction", - Principal="apigateway.amazonaws.com", - SourceArn=f"arn:aws:execute-api:{region}:{account_id}:{api_id}/*/*/get-online-features", - ) - - def teardown_infra( - self, - project: str, - tables: Sequence[FeatureView], - entities: Sequence[Entity], - ) -> None: - super(AwsProvider, self).teardown_infra(project, tables, entities) - - if ( - self.repo_config.feature_server is not None - and self.repo_config.feature_server.enabled - ): - _logger.info("Tearing down feature server...") - resource_name = _get_lambda_name(project) - lambda_client = boto3.client("lambda") - api_gateway_client = boto3.client("apigatewayv2") - - function = aws_utils.get_lambda_function(lambda_client, resource_name) - - if function is not None: - _logger.info(" Tearing down AWS Lambda...") - aws_utils.delete_lambda_function(lambda_client, resource_name) - - api = aws_utils.get_first_api_gateway(api_gateway_client, resource_name) - if api is not None: - _logger.info(" Tearing down AWS API Gateway...") - aws_utils.delete_api_gateway(api_gateway_client, api["ApiId"]) - - def get_feature_server_endpoint(self) -> Optional[str]: - project = self.repo_config.project - resource_name = _get_lambda_name(project) - api_gateway_client = boto3.client("apigatewayv2") - api = aws_utils.get_first_api_gateway(api_gateway_client, resource_name) - - if not api: - return None - - api_id = api["ApiId"] - lambda_client = boto3.client("lambda") - region = lambda_client.meta.region_name - return f"https://{api_id}.execute-api.{region}.amazonaws.com" - - def _upload_docker_image( - self, ecr_client, repository_uri: str, docker_image_version: str - ) -> str: - """ - Pulls the AWS Lambda docker image from Dockerhub and uploads it to AWS ECR. - - Returns: - The URI of the uploaded docker image. - """ - try: - import docker - from docker.errors import APIError - except ImportError as e: - from feast.errors import FeastExtrasDependencyImportError - - raise FeastExtrasDependencyImportError("docker", str(e)) - - try: - docker_client = docker.from_env() - except APIError: - from feast.errors import DockerDaemonNotRunning - - raise DockerDaemonNotRunning() - - dockerhub_image = f"{AWS_LAMBDA_FEATURE_SERVER_IMAGE}:{docker_image_version}" - _logger.info( - f"Pulling remote image {Style.BRIGHT + Fore.GREEN}{dockerhub_image}{Style.RESET_ALL}" - ) - for line in docker_client.api.pull(dockerhub_image, stream=True, decode=True): - _logger.debug(f" {line}") - - auth_token = ecr_client.get_authorization_token()["authorizationData"][0][ - "authorizationToken" - ] - username, password = base64.b64decode(auth_token).decode("utf-8").split(":") - - ecr_address = repository_uri.split("/")[0] - _logger.info( - f"Logging in Docker client to {Style.BRIGHT + Fore.GREEN}{ecr_address}{Style.RESET_ALL}" - ) - login_status = docker_client.login( - username=username, password=password, registry=ecr_address - ) - _logger.debug(f" {login_status}") - - image = docker_client.images.get(dockerhub_image) - image_remote_name = f"{repository_uri}:{docker_image_version}" - _logger.info( - f"Pushing local image to remote {Style.BRIGHT + Fore.GREEN}{image_remote_name}{Style.RESET_ALL}" - ) - image.tag(image_remote_name) - for line in docker_client.api.push( - repository_uri, tag=docker_image_version, stream=True, decode=True - ): - _logger.debug(f" {line}") - - return image_remote_name - - def _create_or_get_repository_uri(self, ecr_client): - try: - return ecr_client.describe_repositories( - repositoryNames=[AWS_LAMBDA_FEATURE_SERVER_REPOSITORY] - )["repositories"][0]["repositoryUri"] - except ecr_client.exceptions.RepositoryNotFoundException: - _logger.info( - f"Creating remote ECR repository {Style.BRIGHT + Fore.GREEN}{AWS_LAMBDA_FEATURE_SERVER_REPOSITORY}{Style.RESET_ALL}" - ) - response = ecr_client.create_repository( - repositoryName=AWS_LAMBDA_FEATURE_SERVER_REPOSITORY - ) - return response["repository"]["repositoryUri"] - - -def _get_lambda_name(project: str): - lambda_prefix = AWS_LAMBDA_FEATURE_SERVER_REPOSITORY - lambda_suffix = f"{project}-{_get_docker_image_version().replace('.', '_')}" - # AWS Lambda name can't have the length greater than 64 bytes. - # This usually occurs during integration tests where feast version is long - if len(lambda_prefix) + len(lambda_suffix) >= 63: - lambda_suffix = hashlib.md5(lambda_suffix.encode()).hexdigest() - return f"{lambda_prefix}-{lambda_suffix}" - - -def _get_docker_image_version() -> str: - """Returns a version for the feature server Docker image. - - If the feast.constants.DOCKER_IMAGE_TAG_ENV_NAME environment variable is set, - we return that (mostly used for integration tests, but can be used for local testing too). - - For public Feast releases this equals to the Feast SDK version modified by replacing "." with "_". - For example, Feast SDK version "0.14.1" would correspond to Docker image version "0_14_1". - - During development (when Feast is installed in editable mode) this equals to the Feast SDK version - modified by removing the "dev..." suffix and replacing "." with "_". For example, Feast SDK version - "0.14.1.dev41+g1cbfa225.d20211103" would correspond to Docker image version "0_14_1". This way, - Feast SDK will use an already existing Docker image built during the previous public release. - """ - tag = os.environ.get(DOCKER_IMAGE_TAG_ENV_NAME) - if tag is not None: - return tag - else: - version = get_version() - if "dev" in version: - version = version[: version.find("dev") - 1] - _logger.warning( - "You are trying to use AWS Lambda feature server while Feast is in a development mode. " - f"Feast will use a docker image version {version} derived from Feast SDK " - f"version {get_version()}. If you want to update the Feast SDK version, make " - "sure to first fetch all new release tags from Github and then reinstall the library:\n" - "> git fetch --all --tags\n" - "> pip install -e '.'" - ) - return version + This class only exists for backwards compatibility. + """ + + pass diff --git a/sdk/python/feast/infra/feature_servers/aws_lambda/Dockerfile b/sdk/python/feast/infra/feature_servers/aws_lambda/Dockerfile deleted file mode 100644 index 929227a810..0000000000 --- a/sdk/python/feast/infra/feature_servers/aws_lambda/Dockerfile +++ /dev/null @@ -1,26 +0,0 @@ -FROM public.ecr.aws/lambda/python:3.9 - -RUN yum install -y git - - -# Copy app handler code -COPY sdk/python/feast/infra/feature_servers/aws_lambda/app.py ${LAMBDA_TASK_ROOT} - -# Copy necessary parts of the Feast codebase -COPY sdk/python sdk/python -COPY protos protos -COPY go go -COPY setup.py setup.py -COPY pyproject.toml pyproject.toml -COPY README.md README.md - -# Install Feast for AWS with Lambda dependencies -# We need this mount thingy because setuptools_scm needs access to the -# git dir to infer the version of feast we're installing. -# https://github.com/pypa/setuptools_scm#usage-from-docker -# I think it also assumes that this dockerfile is being built from the root of the directory. -RUN --mount=source=.git,target=.git,type=bind pip3 install --no-cache-dir -e '.[aws,redis]' -RUN pip3 install -r sdk/python/feast/infra/feature_servers/aws_lambda/requirements.txt --target "${LAMBDA_TASK_ROOT}" - -# Set the CMD to your handler (could also be done as a parameter override outside of the Dockerfile) -CMD [ "app.handler" ] diff --git a/sdk/python/feast/infra/feature_servers/aws_lambda/__init__.py b/sdk/python/feast/infra/feature_servers/aws_lambda/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/sdk/python/feast/infra/feature_servers/aws_lambda/app.py b/sdk/python/feast/infra/feature_servers/aws_lambda/app.py deleted file mode 100644 index e90364ed68..0000000000 --- a/sdk/python/feast/infra/feature_servers/aws_lambda/app.py +++ /dev/null @@ -1,27 +0,0 @@ -import base64 -import os -import tempfile -from pathlib import Path - -from mangum import Mangum - -from feast import FeatureStore -from feast.constants import FEATURE_STORE_YAML_ENV_NAME -from feast.feature_server import get_app - -# Load RepoConfig -config_base64 = os.environ[FEATURE_STORE_YAML_ENV_NAME] -config_bytes = base64.b64decode(config_base64) - -# Create a new unique directory for writing feature_store.yaml -repo_path = Path(tempfile.mkdtemp()) - -with open(repo_path / "feature_store.yaml", "wb") as f: - f.write(config_bytes) - -# Initialize the feature store -store = FeatureStore(repo_path=str(repo_path.resolve())) - -# Create the FastAPI app and AWS Lambda handler -app = get_app(store) -handler = Mangum(app) diff --git a/sdk/python/feast/infra/feature_servers/aws_lambda/config.py b/sdk/python/feast/infra/feature_servers/aws_lambda/config.py deleted file mode 100644 index 946831a18f..0000000000 --- a/sdk/python/feast/infra/feature_servers/aws_lambda/config.py +++ /dev/null @@ -1,21 +0,0 @@ -from typing import Literal - -from pydantic import StrictBool, StrictStr - -from feast.infra.feature_servers.base_config import BaseFeatureServerConfig - - -class AwsLambdaFeatureServerConfig(BaseFeatureServerConfig): - """Feature server config for AWS Lambda.""" - - type: Literal["aws_lambda"] = "aws_lambda" - """Feature server type selector.""" - - public: StrictBool = True - """Whether the endpoint should be publicly accessible.""" - - auth: Literal["none", "api-key"] = "none" - """Authentication method for the endpoint.""" - - execution_role_name: StrictStr - """The execution role for the AWS Lambda function.""" diff --git a/sdk/python/feast/infra/feature_servers/aws_lambda/requirements.txt b/sdk/python/feast/infra/feature_servers/aws_lambda/requirements.txt deleted file mode 100644 index 845aa14802..0000000000 --- a/sdk/python/feast/infra/feature_servers/aws_lambda/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -fastapi -mangum diff --git a/sdk/python/feast/infra/feature_servers/gcp_cloudrun/Dockerfile b/sdk/python/feast/infra/feature_servers/gcp_cloudrun/Dockerfile deleted file mode 100644 index 6b89d4f73c..0000000000 --- a/sdk/python/feast/infra/feature_servers/gcp_cloudrun/Dockerfile +++ /dev/null @@ -1,32 +0,0 @@ -FROM python:3.11-slim - -RUN apt-get update && apt-get install -y git - -# Allow statements and log messages to immediately appear in the Knative logs -ENV PYTHONUNBUFFERED True - -# Copy local code to the container image. -ENV APP_HOME /app -WORKDIR $APP_HOME - -# Copy app handler code -COPY sdk/python/feast/infra/feature_servers/gcp_cloudrun/app.py ./app.py - -# Copy necessary parts of the Feast codebase -COPY sdk/python ./sdk/python -COPY protos ./protos -COPY setup.py setup.py -COPY pyproject.toml pyproject.toml -COPY README.md ./README.md - -# Install production dependencies. -RUN --mount=source=.git,target=.git,type=bind pip install --no-cache-dir \ - -e '.[gcp,redis]' \ - -r ./sdk/python/feast/infra/feature_servers/gcp_cloudrun/requirements.txt - -# Run the web service on container startup. Here we use the gunicorn -# webserver, with one worker process and 8 threads. -# For environments with multiple CPU cores, increase the number of workers -# to be equal to the cores available. -# Timeout is set to 0 to disable the timeouts of the workers to allow Cloud Run to handle instance scaling. -CMD exec gunicorn -k uvicorn.workers.UvicornWorker --bind :$PORT --workers 1 --threads 8 --timeout 0 app:app diff --git a/sdk/python/feast/infra/feature_servers/gcp_cloudrun/__init__.py b/sdk/python/feast/infra/feature_servers/gcp_cloudrun/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/sdk/python/feast/infra/feature_servers/gcp_cloudrun/app.py b/sdk/python/feast/infra/feature_servers/gcp_cloudrun/app.py deleted file mode 100644 index 06749b0cd3..0000000000 --- a/sdk/python/feast/infra/feature_servers/gcp_cloudrun/app.py +++ /dev/null @@ -1,24 +0,0 @@ -import base64 -import os -import tempfile -from pathlib import Path - -from feast import FeatureStore -from feast.constants import FEATURE_STORE_YAML_ENV_NAME -from feast.feature_server import get_app - -# Load RepoConfig -config_base64 = os.environ[FEATURE_STORE_YAML_ENV_NAME] -config_bytes = base64.b64decode(config_base64) - -# Create a new unique directory for writing feature_store.yaml -repo_path = Path(tempfile.mkdtemp()) - -with open(repo_path / "feature_store.yaml", "wb") as f: - f.write(config_bytes) - -# Initialize the feature store -store = FeatureStore(repo_path=str(repo_path.resolve())) - -# Create the FastAPI app -app = get_app(store) diff --git a/sdk/python/feast/infra/feature_servers/gcp_cloudrun/config.py b/sdk/python/feast/infra/feature_servers/gcp_cloudrun/config.py deleted file mode 100644 index ddcbde7924..0000000000 --- a/sdk/python/feast/infra/feature_servers/gcp_cloudrun/config.py +++ /dev/null @@ -1,18 +0,0 @@ -from typing import Literal - -from pydantic import StrictBool - -from feast.infra.feature_servers.base_config import BaseFeatureServerConfig - - -class GcpCloudRunFeatureServerConfig(BaseFeatureServerConfig): - """Feature server config for GCP CloudRun.""" - - type: Literal["gcp_cloudrun"] = "gcp_cloudrun" - """Feature server type selector.""" - - public: StrictBool = True - """Whether the endpoint should be publicly accessible.""" - - auth: Literal["none", "api-key"] = "none" - """Authentication method for the endpoint.""" diff --git a/sdk/python/feast/infra/feature_servers/gcp_cloudrun/requirements.txt b/sdk/python/feast/infra/feature_servers/gcp_cloudrun/requirements.txt deleted file mode 100644 index 8f22dccf99..0000000000 --- a/sdk/python/feast/infra/feature_servers/gcp_cloudrun/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -gunicorn diff --git a/sdk/python/feast/repo_config.py b/sdk/python/feast/repo_config.py index aacb95f420..1c8041b4dd 100644 --- a/sdk/python/feast/repo_config.py +++ b/sdk/python/feast/repo_config.py @@ -82,14 +82,10 @@ } FEATURE_SERVER_CONFIG_CLASS_FOR_TYPE = { - "aws_lambda": "feast.infra.feature_servers.aws_lambda.config.AwsLambdaFeatureServerConfig", - "gcp_cloudrun": "feast.infra.feature_servers.gcp_cloudrun.config.GcpCloudRunFeatureServerConfig", "local": "feast.infra.feature_servers.local_process.config.LocalFeatureServerConfig", } FEATURE_SERVER_TYPE_FOR_PROVIDER = { - "aws": "aws_lambda", - "gcp": "gcp_cloudrun", "local": "local", } diff --git a/sdk/python/tests/conftest.py b/sdk/python/tests/conftest.py index 48f482f542..fb6b7e5608 100644 --- a/sdk/python/tests/conftest.py +++ b/sdk/python/tests/conftest.py @@ -257,12 +257,7 @@ def pytest_generate_tests(metafunc: pytest.Metafunc): extra_dimensions: List[Dict[str, Any]] = [{}] if "python_server" in metafunc.fixturenames: - extra_dimensions.extend( - [ - {"python_feature_server": True}, - {"python_feature_server": True, "provider": "aws"}, - ] - ) + extra_dimensions.extend([{"python_feature_server": True}]) configs = [] if offline_stores: @@ -277,17 +272,6 @@ def pytest_generate_tests(metafunc: pytest.Metafunc): **dim, } - # aws lambda works only with dynamo - if ( - config.get("python_feature_server") - and config.get("provider") == "aws" - and ( - not isinstance(online_store, dict) - or online_store["type"] != "dynamodb" - ) - ): - continue - c = IntegrationTestRepoConfig(**config) if c not in _config_cache: diff --git a/sdk/python/tests/integration/feature_repos/repo_configuration.py b/sdk/python/tests/integration/feature_repos/repo_configuration.py index 7123bd0fc1..9e3c02b9c0 100644 --- a/sdk/python/tests/integration/feature_repos/repo_configuration.py +++ b/sdk/python/tests/integration/feature_repos/repo_configuration.py @@ -474,27 +474,12 @@ def construct_test_environment( else: online_creator = None - if test_repo_config.python_feature_server and test_repo_config.provider == "aws": - from feast.infra.feature_servers.aws_lambda.config import ( - AwsLambdaFeatureServerConfig, - ) - - feature_server: Any = AwsLambdaFeatureServerConfig( - enabled=True, - execution_role_name=os.getenv( - "AWS_LAMBDA_ROLE", - "arn:aws:iam::402087665549:role/lambda_execution_role", - ), - ) - else: - feature_server = LocalFeatureServerConfig( - feature_logging=FeatureLoggingConfig(enabled=True) - ) + feature_server = LocalFeatureServerConfig( + feature_logging=FeatureLoggingConfig(enabled=True) + ) repo_dir_name = tempfile.mkdtemp() - if ( - test_repo_config.python_feature_server and test_repo_config.provider == "aws" - ) or test_repo_config.registry_location == RegistryLocation.S3: + if test_repo_config.registry_location == RegistryLocation.S3: aws_registry_path = os.getenv( "AWS_REGISTRY_PATH", "s3://feast-int-bucket/registries" ) diff --git a/setup.py b/setup.py index 9b3d0e55e6..f954f19898 100644 --- a/setup.py +++ b/setup.py @@ -84,7 +84,7 @@ "hiredis>=2.0.0,<3", ] -AWS_REQUIRED = ["boto3>=1.17.0,<2", "docker>=5.0.2", "fsspec<=2024.1.0", "aiobotocore>2,<3"] +AWS_REQUIRED = ["boto3>=1.17.0,<2", "fsspec<=2024.1.0", "aiobotocore>2,<3"] KUBERNETES_REQUIRED = ["kubernetes<=20.13.0"] From 8028ae0f39e706637bc2781850a3b7d8925a87f7 Mon Sep 17 00:00:00 2001 From: Meenakshi Sistla <85261163+msistla96@users.noreply.github.com> Date: Tue, 18 Jun 2024 20:24:23 -0500 Subject: [PATCH 04/59] fix: Update Feast object metadata in the registry (#4257) --- sdk/python/feast/feature_view.py | 10 + .../feast/infra/registry/base_registry.py | 28 ++ sdk/python/feast/infra/registry/registry.py | 34 +- sdk/python/feast/infra/registry/remote.py | 1 + sdk/python/feast/infra/registry/sql.py | 18 + sdk/python/feast/registry_server.py | 5 +- .../registration/test_universal_registry.py | 324 +++++++++++++++++- .../test_local_feature_store.py | 14 + sdk/python/tests/unit/test_feature_views.py | 53 ++- .../tests/unit/test_stream_feature_view.py | 82 ++++- sdk/python/tests/utils/e2e_test_validation.py | 10 + 11 files changed, 570 insertions(+), 9 deletions(-) diff --git a/sdk/python/feast/feature_view.py b/sdk/python/feast/feature_view.py index ff41400eac..1a85a4b90c 100644 --- a/sdk/python/feast/feature_view.py +++ b/sdk/python/feast/feature_view.py @@ -311,6 +311,16 @@ def with_join_key_map(self, join_key_map: Dict[str, str]): return cp + def update_materialization_intervals( + self, existing_materialization_intervals: List[Tuple[datetime, datetime]] + ): + if ( + len(existing_materialization_intervals) > 0 + and len(self.materialization_intervals) == 0 + ): + for interval in existing_materialization_intervals: + self.materialization_intervals.append((interval[0], interval[1])) + def to_proto(self) -> FeatureViewProto: """ Converts a feature view object to its protobuf representation. diff --git a/sdk/python/feast/infra/registry/base_registry.py b/sdk/python/feast/infra/registry/base_registry.py index bc08796e39..03bec64830 100644 --- a/sdk/python/feast/infra/registry/base_registry.py +++ b/sdk/python/feast/infra/registry/base_registry.py @@ -29,7 +29,19 @@ from feast.infra.infra_object import Infra from feast.on_demand_feature_view import OnDemandFeatureView 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 ( + FeatureService as FeatureServiceProto, +) +from feast.protos.feast.core.FeatureView_pb2 import FeatureView as FeatureViewProto +from feast.protos.feast.core.OnDemandFeatureView_pb2 import ( + OnDemandFeatureView as OnDemandFeatureViewProto, +) 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 ( + StreamFeatureView as StreamFeatureViewProto, +) from feast.saved_dataset import SavedDataset, ValidationReference from feast.stream_feature_view import StreamFeatureView from feast.transformation.pandas_transformation import PandasTransformation @@ -705,3 +717,19 @@ def to_dict(self, project: str) -> Dict[str, List[Any]]: self._message_to_sorted_dict(infra_object.to_proto()) ) return registry_dict + + @staticmethod + def deserialize_registry_values(serialized_proto, feast_obj_type) -> Any: + if feast_obj_type == Entity: + return EntityProto.FromString(serialized_proto) + if feast_obj_type == SavedDataset: + return SavedDatasetProto.FromString(serialized_proto) + if feast_obj_type == FeatureView: + return FeatureViewProto.FromString(serialized_proto) + if feast_obj_type == StreamFeatureView: + return StreamFeatureViewProto.FromString(serialized_proto) + if feast_obj_type == OnDemandFeatureView: + return OnDemandFeatureViewProto.FromString(serialized_proto) + if feast_obj_type == FeatureService: + return FeatureServiceProto.FromString(serialized_proto) + return None diff --git a/sdk/python/feast/infra/registry/registry.py b/sdk/python/feast/infra/registry/registry.py index 39cdedb490..4d6bff4cc7 100644 --- a/sdk/python/feast/infra/registry/registry.py +++ b/sdk/python/feast/infra/registry/registry.py @@ -265,9 +265,13 @@ def apply_entity(self, entity: Entity, project: str, commit: bool = True): existing_entity_proto.spec.name == entity_proto.spec.name and existing_entity_proto.spec.project == project ): + entity.created_timestamp = ( + existing_entity_proto.meta.created_timestamp.ToDatetime() + ) + entity_proto = entity.to_proto() + entity_proto.spec.project = project del self.cached_registry_proto.entities[idx] break - self.cached_registry_proto.entities.append(entity_proto) if commit: self.commit() @@ -346,6 +350,11 @@ def apply_feature_service( == feature_service_proto.spec.name and existing_feature_service_proto.spec.project == project ): + feature_service.created_timestamp = ( + existing_feature_service_proto.meta.created_timestamp.ToDatetime() + ) + 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) if commit: @@ -421,6 +430,18 @@ def apply_feature_view( ): return else: + existing_feature_view = type(feature_view).from_proto( + existing_feature_view_proto + ) + feature_view.created_timestamp = ( + existing_feature_view.created_timestamp + ) + if isinstance(feature_view, (FeatureView, StreamFeatureView)): + feature_view.update_materialization_intervals( + existing_feature_view.materialization_intervals + ) + feature_view_proto = feature_view.to_proto() + feature_view_proto.spec.project = project del existing_feature_views_of_same_type[idx] break @@ -660,6 +681,17 @@ def apply_saved_dataset( existing_saved_dataset_proto.spec.name == saved_dataset_proto.spec.name and existing_saved_dataset_proto.spec.project == project ): + saved_dataset.created_timestamp = ( + existing_saved_dataset_proto.meta.created_timestamp.ToDatetime() + ) + saved_dataset.min_event_timestamp = ( + existing_saved_dataset_proto.meta.min_event_timestamp.ToDatetime() + ) + saved_dataset.max_event_timestamp = ( + existing_saved_dataset_proto.meta.max_event_timestamp.ToDatetime() + ) + saved_dataset_proto = saved_dataset.to_proto() + saved_dataset_proto.spec.project = project del self.cached_registry_proto.saved_datasets[idx] break diff --git a/sdk/python/feast/infra/registry/remote.py b/sdk/python/feast/infra/registry/remote.py index 0eddf03cf6..9fa6d8ebee 100644 --- a/sdk/python/feast/infra/registry/remote.py +++ b/sdk/python/feast/infra/registry/remote.py @@ -296,6 +296,7 @@ def apply_materialization( start_date_timestamp.FromDatetime(start_date) end_date_timestamp.FromDatetime(end_date) + # TODO: for this to work for stream feature views, ApplyMaterializationRequest needs to be updated request = RegistryServer_pb2.ApplyMaterializationRequest( feature_view=feature_view.to_proto(), project=project, diff --git a/sdk/python/feast/infra/registry/sql.py b/sdk/python/feast/infra/registry/sql.py index d0af6872c1..42bd19eb5f 100644 --- a/sdk/python/feast/infra/registry/sql.py +++ b/sdk/python/feast/infra/registry/sql.py @@ -713,6 +713,24 @@ def _apply_object( obj.last_updated_timestamp = update_datetime if row: + if proto_field_name in [ + "entity_proto", + "saved_dataset_proto", + "feature_view_proto", + "feature_service_proto", + ]: + deserialized_proto = self.deserialize_registry_values( + row._mapping[proto_field_name], type(obj) + ) + obj.created_timestamp = ( + deserialized_proto.meta.created_timestamp.ToDatetime() + ) + if isinstance(obj, (FeatureView, StreamFeatureView)): + obj.update_materialization_intervals( + type(obj) + .from_proto(deserialized_proto) + .materialization_intervals + ) values = { proto_field_name: obj.to_proto().SerializeToString(), "last_updated_timestamp": update_time, diff --git a/sdk/python/feast/registry_server.py b/sdk/python/feast/registry_server.py index 1b6798b022..4a96ba76a8 100644 --- a/sdk/python/feast/registry_server.py +++ b/sdk/python/feast/registry_server.py @@ -3,6 +3,7 @@ import grpc from google.protobuf.empty_pb2 import Empty +from pytz import utc from feast import FeatureStore from feast.data_source import DataSource @@ -313,10 +314,10 @@ 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 + request.start_date.seconds + request.start_date.nanos / 1e9, tz=utc ), end_date=datetime.fromtimestamp( - request.end_date.seconds + request.end_date.nanos / 1e9 + request.end_date.seconds + request.end_date.nanos / 1e9, tz=utc ), commit=request.commit, ) diff --git a/sdk/python/tests/integration/registration/test_universal_registry.py b/sdk/python/tests/integration/registration/test_universal_registry.py index cd741853cc..24ba9fe42a 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 datetime, timedelta from tempfile import mkstemp from unittest import mock @@ -22,12 +22,13 @@ 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 from testcontainers.mysql import MySqlContainer -from feast import FileSource, RequestSource +from feast import FeatureService, FileSource, RequestSource from feast.data_format import AvroFormat, ParquetFormat from feast.data_source import KafkaSource from feast.entity import Entity @@ -308,6 +309,22 @@ def test_apply_entity_success(test_registry): # 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_uuid(project, project_uuid, test_registry) entities = test_registry.list_entities(project) @@ -601,11 +618,54 @@ def odfv1(feature_df: pd.DataFrame) -> pd.DataFrame: data["odfv1_my_feature_2"] = feature_df["my_input_1"].astype("int32") return data + def simple_udf(x: int): + return x + 3 + + entity_sfv = Entity(name="sfv_my_entity_1", join_keys=["test_key"]) + + stream_source = KafkaSource( + name="kafka", + timestamp_field="event_timestamp", + kafka_bootstrap_servers="", + message_format=AvroFormat(""), + topic="topic", + batch_source=FileSource(path="some path"), + watermark_delay_threshold=timedelta(days=1), + ) + + sfv = StreamFeatureView( + name="test kafka stream feature view", + entities=[entity_sfv], + ttl=timedelta(days=30), + owner="test@example.com", + online=True, + schema=[Field(name="dummy_field", dtype=Float32)], + description="desc", + aggregations=[ + Aggregation( + column="dummy_field", + function="max", + time_window=timedelta(days=1), + ), + Aggregation( + column="dummy_field2", + function="count", + time_window=timedelta(days=24), + ), + ], + timestamp_field="event_timestamp", + mode="spark", + source=stream_source, + udf=simple_udf, + tags={}, + ) + 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) # Modify odfv by changing a single feature dtype @on_demand_feature_view( @@ -621,6 +681,8 @@ def odfv1(feature_df: pd.DataFrame) -> pd.DataFrame: data["odfv1_my_feature_2"] = feature_df["my_input_1"].astype("int32") return data + existing_odfv = test_registry.get_on_demand_feature_view("odfv1", project) + # Apply the modified odfv test_registry.apply_feature_view(odfv1, project) @@ -655,6 +717,11 @@ def odfv1(feature_df: pd.DataFrame) -> pd.DataFrame: and list(request_schema.values())[0] == ValueType.INT32 ) + assert ( + feature_view.created_timestamp is not None + and feature_view.created_timestamp == existing_odfv.created_timestamp + ) + # Make sure fv1 is untouched feature_views = test_registry.list_feature_views(project, tags=fv1.tags) @@ -675,7 +742,162 @@ def odfv1(feature_df: pd.DataFrame) -> pd.DataFrame: and feature_view.entities[0] == "fs1_my_entity_1" ) - test_registry.teardown() + # Simulate materialization + current_date = datetime.utcnow() + end_date = current_date.replace(tzinfo=utc) + start_date = (current_date - timedelta(days=1)).replace(tzinfo=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 + ) + + # Check if created_timestamp, along with materialized_intervals are updated + assert ( + materialized_feature_view.created_timestamp is not None + and materialized_feature_view.created_timestamp + == feature_view.created_timestamp + and len(materialized_feature_view.materialization_intervals) > 0 + and materialized_feature_view.materialization_intervals[0][0] == start_date + and materialized_feature_view.materialization_intervals[0][1] == end_date + ) + + # Modify fv1 by changing a single dtype + updated_fv1 = FeatureView( + name="my_feature_view_1", + schema=[ + Field(name="test", dtype=Int64), + Field(name="fs1_my_feature_1", dtype=String), + ], + entities=[entity], + tags={"team": "matchmaking"}, + source=batch_source, + ttl=timedelta(minutes=5), + ) + + # Check that these fields are empty before apply + assert updated_fv1.created_timestamp is None + assert len(updated_fv1.materialization_intervals) == 0 + + # Apply the modified fv1 + test_registry.apply_feature_view(updated_fv1, project) + + # Verify feature view after modification + updated_feature_views = test_registry.list_feature_views(project) + + # List Feature Views + assert ( + len(updated_feature_views) == 1 + and updated_feature_views[0].name == "my_feature_view_1" + and updated_feature_views[0].features[0].name == "fs1_my_feature_1" + and updated_feature_views[0].features[0].dtype == String + and updated_feature_views[0].entities[0] == "fs1_my_entity_1" + ) + + updated_feature_view = test_registry.get_feature_view("my_feature_view_1", project) + assert ( + updated_feature_view.name == "my_feature_view_1" + and updated_feature_view.features[0].name == "fs1_my_feature_1" + and updated_feature_view.features[0].dtype == String + and updated_feature_view.entities[0] == "fs1_my_entity_1" + ) + + # Check if materialization_intervals and created_timestamp values propagates on each apply + # materialization_intervals will populate only when it's empty + assert ( + updated_feature_view.created_timestamp is not None + and updated_feature_view.created_timestamp == feature_view.created_timestamp + and len(updated_feature_view.materialization_intervals) == 1 + and updated_feature_view.materialization_intervals[0][0] == start_date + and updated_feature_view.materialization_intervals[0][1] == end_date + ) + + # Simulate materialization a second time + current_date = datetime.utcnow() + end_date_1 = current_date.replace(tzinfo=utc) + start_date_1 = (current_date - timedelta(days=1)).replace(tzinfo=utc) + test_registry.apply_materialization( + updated_feature_view, project, start_date_1, end_date_1 + ) + materialized_feature_view_1 = test_registry.get_feature_view( + "my_feature_view_1", project + ) + + assert ( + materialized_feature_view_1.created_timestamp is not None + and materialized_feature_view_1.created_timestamp + == feature_view.created_timestamp + and len(materialized_feature_view_1.materialization_intervals) == 2 + and materialized_feature_view_1.materialization_intervals[0][0] == start_date + and materialized_feature_view_1.materialization_intervals[0][1] == end_date + and materialized_feature_view_1.materialization_intervals[1][0] == start_date_1 + and materialized_feature_view_1.materialization_intervals[1][1] == end_date_1 + ) + + # Modify sfv by changing the dtype + + sfv = StreamFeatureView( + name="test kafka stream feature view", + entities=[entity_sfv], + ttl=timedelta(days=30), + owner="test@example.com", + online=True, + schema=[Field(name="dummy_field", dtype=String)], + description="desc", + aggregations=[ + Aggregation( + column="dummy_field", + function="max", + time_window=timedelta(days=1), + ), + Aggregation( + column="dummy_field2", + function="count", + time_window=timedelta(days=24), + ), + ], + timestamp_field="event_timestamp", + mode="spark", + source=stream_source, + udf=simple_udf, + tags={}, + ) + + existing_sfv = test_registry.get_stream_feature_view( + "test kafka stream feature view", project + ) + # Apply the modified sfv + test_registry.apply_feature_view(sfv, project) + + # Verify feature view after modification + updated_stream_feature_views = test_registry.list_stream_feature_views(project) + + # List Feature Views + assert ( + len(updated_stream_feature_views) == 1 + and updated_stream_feature_views[0].name == "test kafka stream feature view" + and updated_stream_feature_views[0].features[0].name == "dummy_field" + and updated_stream_feature_views[0].features[0].dtype == String + and updated_stream_feature_views[0].entities[0] == "sfv_my_entity_1" + ) + + updated_sfv = test_registry.get_stream_feature_view( + "test kafka stream feature view", project + ) + assert ( + updated_sfv.name == "test kafka stream feature view" + and updated_sfv.features[0].name == "dummy_field" + and updated_sfv.features[0].dtype == String + and updated_sfv.entities[0] == "sfv_my_entity_1" + ) + + # The created_timestamp for the stream feature view should be set to the created_timestamp value stored from the + # previous apply + # Materialization_intervals is not set + assert ( + updated_sfv.created_timestamp is not None + and updated_sfv.created_timestamp == existing_sfv.created_timestamp + and len(updated_sfv.materialization_intervals) == 0 + ) @pytest.mark.integration @@ -825,7 +1047,7 @@ def simple_udf(x: int): project = "project" - # Register Feature View + # Register Stream Feature View test_registry.apply_feature_view(sfv, project) stream_feature_views = test_registry.list_stream_feature_views( @@ -843,6 +1065,100 @@ def simple_udf(x: int): test_registry.teardown() +@pytest.mark.integration +@pytest.mark.parametrize( + "test_registry", + all_fixtures, +) +def test_apply_feature_service_success(test_registry): + # Create Feature Service + 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, + ) + fs = FeatureService( + name="my_feature_service_1", features=[feature_view[["feature1", "feature2"]]] + ) + project = "project" + + # Register Feature Service + test_registry.apply_feature_service(fs, project) + + feature_services = test_registry.list_feature_services(project) + + # List Feature Services + assert len(feature_services) == 1 + assert feature_services[0] == fs + + # Delete Feature Service + test_registry.delete_feature_service("my_feature_service_1", project) + feature_services = test_registry.list_feature_services(project) + assert len(feature_services) == 0 + + test_registry.teardown() + + +@pytest.mark.integration +@pytest.mark.parametrize( + "test_registry", + all_fixtures, +) +def test_modify_feature_service_success(test_registry): + # Create Feature Service + 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, + ) + fs = FeatureService( + name="my_feature_service_1", features=[feature_view[["feature1", "feature2"]]] + ) + project = "project" + + # Register Feature service + test_registry.apply_feature_service(fs, project) + + feature_services = test_registry.list_feature_services(project) + + # List Feature Services + assert len(feature_services) == 1 + assert feature_services[0] == fs + + # Modify Feature Service by removing a feature + fs = FeatureService( + name="my_feature_service_1", features=[feature_view[["feature1"]]] + ) + + # Apply modified Feature Service + test_registry.apply_feature_service(fs, project) + + updated_feature_services = test_registry.list_feature_services(project) + + # Verify Feature Services + assert len(updated_feature_services) == 1 + assert updated_feature_services[0] == fs + # The created_timestamp for the feature service should be set to the created_timestamp value stored from the + # previous apply + assert ( + updated_feature_services[0].created_timestamp is not None + and updated_feature_services[0].created_timestamp + == feature_services[0].created_timestamp + ) + + test_registry.teardown() + + @pytest.mark.integration def test_commit(): fd, registry_path = mkstemp() 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 6b7856f347..9b75d1a2c9 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 @@ -389,6 +389,20 @@ def test_reapply_feature_view(test_feature_store, dataframe_source): # Check Feature View fv_stored = test_feature_store.get_feature_view(fv1.name) + assert len(fv_stored.materialization_intervals) == 1 + + # Change and apply Feature View, this time, only the name + fv2 = FeatureView( + name="my_feature_view_2", + schema=[Field(name="int64_col", dtype=Int64)], + entities=[e], + source=file_source, + ttl=timedelta(minutes=5), + ) + test_feature_store.apply([fv2]) + + # Check Feature View + fv_stored = test_feature_store.get_feature_view(fv2.name) assert len(fv_stored.materialization_intervals) == 0 test_feature_store.teardown() diff --git a/sdk/python/tests/unit/test_feature_views.py b/sdk/python/tests/unit/test_feature_views.py index 0220d1a8a9..b387f55d8b 100644 --- a/sdk/python/tests/unit/test_feature_views.py +++ b/sdk/python/tests/unit/test_feature_views.py @@ -1,8 +1,9 @@ -from datetime import timedelta +from datetime import datetime, timedelta import pytest from typeguard import TypeCheckError +from feast import utils from feast.batch_feature_view import BatchFeatureView from feast.data_format import AvroFormat from feast.data_source import KafkaSource @@ -117,3 +118,53 @@ def test_hash(): def test_field_types(): with pytest.raises(TypeCheckError): Field(name="name", dtype=ValueType.INT32) + + +def test_update_materialization_intervals(): + batch_source = FileSource(path="some path") + entity = Entity(name="entity_1", description="Some entity") + # Create a feature view that is already present in the SQL registry + stored_feature_view = FeatureView( + name="my-feature-view", + entities=[entity], + ttl=timedelta(days=1), + source=batch_source, + ) + + # Update the Feature View without modifying anything + updated_feature_view = FeatureView( + name="my-feature-view", + entities=[entity], + ttl=timedelta(days=1), + source=batch_source, + ) + updated_feature_view.update_materialization_intervals( + stored_feature_view.materialization_intervals + ) + assert len(updated_feature_view.materialization_intervals) == 0 + + current_time = datetime.utcnow() + start_date = utils.make_tzaware(current_time - timedelta(days=1)) + end_date = utils.make_tzaware(current_time) + updated_feature_view.materialization_intervals.append((start_date, end_date)) + + # Update the Feature View, i.e. simply update the name + second_updated_feature_view = FeatureView( + name="my-feature-view-1", + entities=[entity], + ttl=timedelta(days=1), + source=batch_source, + ) + + second_updated_feature_view.update_materialization_intervals( + updated_feature_view.materialization_intervals + ) + assert len(second_updated_feature_view.materialization_intervals) == 1 + assert ( + second_updated_feature_view.materialization_intervals[0][0] + == updated_feature_view.materialization_intervals[0][0] + ) + assert ( + second_updated_feature_view.materialization_intervals[0][1] + == updated_feature_view.materialization_intervals[0][1] + ) diff --git a/sdk/python/tests/unit/test_stream_feature_view.py b/sdk/python/tests/unit/test_stream_feature_view.py index b53f9a593a..77431666c3 100644 --- a/sdk/python/tests/unit/test_stream_feature_view.py +++ b/sdk/python/tests/unit/test_stream_feature_view.py @@ -1,8 +1,9 @@ import copy -from datetime import timedelta +from datetime import datetime, timedelta import pytest +from feast import utils from feast.aggregation import Aggregation from feast.batch_feature_view import BatchFeatureView from feast.data_format import AvroFormat @@ -250,3 +251,82 @@ def test_stream_feature_view_copy(): aggregations=[], ) assert sfv == copy.copy(sfv) + + +def test_update_materialization_intervals(): + entity = Entity(name="driver_entity", join_keys=["test_key"]) + stream_source = KafkaSource( + name="kafka", + timestamp_field="event_timestamp", + kafka_bootstrap_servers="", + message_format=AvroFormat(""), + topic="topic", + batch_source=FileSource(path="some path"), + ) + + # Create a stream feature view that is already present in the SQL registry + stored_stream_feature_view = StreamFeatureView( + name="test kafka stream feature view", + entities=[entity], + ttl=timedelta(days=30), + owner="test@example.com", + online=True, + schema=[Field(name="dummy_field", dtype=Float32)], + description="desc", + aggregations=[ + Aggregation( + column="dummy_field", + function="max", + time_window=timedelta(days=1), + ) + ], + timestamp_field="event_timestamp", + mode="spark", + source=stream_source, + udf=simple_udf, + tags={}, + ) + current_time = datetime.utcnow() + start_date = utils.make_tzaware(current_time - timedelta(days=1)) + end_date = utils.make_tzaware(current_time) + stored_stream_feature_view.materialization_intervals.append((start_date, end_date)) + + # Update the stream feature view i.e. here it's simply the name + updated_stream_feature_view = StreamFeatureView( + name="test kafka stream feature view updated", + entities=[entity], + ttl=timedelta(days=30), + owner="test@example.com", + online=True, + schema=[Field(name="dummy_field", dtype=Float32)], + description="desc", + aggregations=[ + Aggregation( + column="dummy_field", + function="max", + time_window=timedelta(days=1), + ) + ], + timestamp_field="event_timestamp", + mode="spark", + source=stream_source, + udf=simple_udf, + tags={}, + ) + + updated_stream_feature_view.update_materialization_intervals( + stored_stream_feature_view.materialization_intervals + ) + + assert ( + updated_stream_feature_view.materialization_intervals is not None + and len(stored_stream_feature_view.materialization_intervals) == 1 + ) + assert ( + updated_stream_feature_view.materialization_intervals[0][0] + == stored_stream_feature_view.materialization_intervals[0][0] + ) + assert ( + updated_stream_feature_view.materialization_intervals[0][1] + == stored_stream_feature_view.materialization_intervals[0][1] + ) diff --git a/sdk/python/tests/utils/e2e_test_validation.py b/sdk/python/tests/utils/e2e_test_validation.py index 885798db10..d9104bae42 100644 --- a/sdk/python/tests/utils/e2e_test_validation.py +++ b/sdk/python/tests/utils/e2e_test_validation.py @@ -78,6 +78,16 @@ def validate_offline_online_store_consistency( # run materialize_incremental() fs.materialize_incremental(feature_views=[fv.name], end_date=now) + updated_fv = fs.registry.get_feature_view(fv.name, fs.project) + + # Check if materialization_intervals was updated by the registry + assert ( + len(updated_fv.materialization_intervals) == 2 + 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) + ) # check result of materialize_incremental() _check_offline_and_online_features( From 6c75e84b036f84910dcbd7f1733ebd0d8839ab6c Mon Sep 17 00:00:00 2001 From: Shuchu Han Date: Thu, 20 Jun 2024 00:49:51 -0400 Subject: [PATCH 05/59] fix: Minor typo in the unit test. (#4296) Signed-off-by: Shuchu Han --- .../tests/unit/local_feast_tests/test_local_feature_store.py | 2 +- 1 file changed, 1 insertion(+), 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 9b75d1a2c9..0e834e314b 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 @@ -134,7 +134,7 @@ def test_apply_feature_view(test_feature_store): tags_filter = utils.tags_str_to_dict("('team:matchmaking',)") assert tags_filter == tags_dict tags_filter = utils.tags_list_to_dict(("team:matchmaking", "test")) - assert tags_dict == tags_dict + assert tags_filter == tags_dict # List Feature Views feature_views = test_feature_store.list_batch_feature_views(tags=tags_filter) From de5b0eb8e4922f16b7a8f36ed6373490f8b2da8d Mon Sep 17 00:00:00 2001 From: Theodor Mihalache <84387487+tmihalac@users.noreply.github.com> Date: Thu, 20 Jun 2024 10:27:09 -0400 Subject: [PATCH 06/59] refactor: Add parameters validation to OfflineServer (#4289) Add parameters validation to OfflineServer Signed-off-by: Theodor Mihalache --- sdk/python/feast/offline_server.py | 96 +++++++++++++++++++++++++++--- 1 file changed, 88 insertions(+), 8 deletions(-) diff --git a/sdk/python/feast/offline_server.py b/sdk/python/feast/offline_server.py index 718da1b109..be92620d68 100644 --- a/sdk/python/feast/offline_server.py +++ b/sdk/python/feast/offline_server.py @@ -74,14 +74,15 @@ def do_put( logger.debug(f"do_put: command is{command}, data is {data}") self.flights[key] = data - self._call_api(command, key) + self._call_api(command["api"], command, key) else: logger.warning(f"No 'api' field in command: {command}") - def _call_api(self, command: dict, key: str): + def _call_api(self, api: str, command: dict, key: str): + assert api is not None, "api can not be empty" + remove_data = False try: - api = command["api"] if api == OfflineServer.offline_write_batch.__name__: self.offline_write_batch(command, key) remove_data = True @@ -89,7 +90,7 @@ def _call_api(self, command: dict, key: str): self.write_logged_features(command, key) remove_data = True elif api == OfflineServer.persist.__name__: - self.persist(command["retrieve_func"], command, key) + self.persist(command, key) remove_data = True except Exception as e: remove_data = True @@ -150,6 +151,9 @@ def list_feature_views_by_name( for index, fv_name in enumerate(feature_view_names) ] + def _validate_do_get_parameters(self, command: dict): + assert "api" in command, "api parameter is mandatory" + # Extracts the API parameters from the flights dictionary, delegates the execution to the FeatureStore instance # and returns the stream of data def do_get(self, context: fl.ServerCallContext, ticket: fl.Ticket): @@ -159,6 +163,9 @@ def do_get(self, context: fl.ServerCallContext, ticket: fl.Ticket): return None command = json.loads(key[1]) + + self._validate_do_get_parameters(command) + api = command["api"] logger.debug(f"get command is {command}") logger.debug(f"requested api is {api}") @@ -180,13 +187,26 @@ def do_get(self, context: fl.ServerCallContext, ticket: fl.Ticket): del self.flights[key] return fl.RecordBatchStream(table) - def offline_write_batch(self, command: dict, key: str): + def _validate_offline_write_batch_parameters(self, command: dict): + assert ( + "feature_view_names" in command + ), "feature_view_names is a mandatory parameter" + assert "name_aliases" in command, "name_aliases is a mandatory parameter" + feature_view_names = command["feature_view_names"] assert ( len(feature_view_names) == 1 ), "feature_view_names list should only have one item" + name_aliases = command["name_aliases"] assert len(name_aliases) == 1, "name_aliases list should only have one item" + + def offline_write_batch(self, command: dict, key: str): + self._validate_offline_write_batch_parameters(command) + + feature_view_names = command["feature_view_names"] + name_aliases = command["name_aliases"] + project = self.store.config.project feature_views = self.list_feature_views_by_name( feature_view_names=feature_view_names, @@ -194,19 +214,25 @@ def offline_write_batch(self, command: dict, key: str): project=project, ) - assert len(feature_views) == 1 + 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"] ) + def _validate_write_logged_features_parameters(self, command: dict): + assert "feature_service_name" in command + def write_logged_features(self, command: dict, key: str): + self._validate_write_logged_features_parameters(command) table = self.flights[key] feature_service = self.store.get_feature_service( command["feature_service_name"] ) - assert feature_service.logging_config is not None + assert ( + feature_service.logging_config is not None + ), "feature service must have logging_config set" self.offline_store.write_logged_features( config=self.store.config, @@ -218,7 +244,23 @@ def write_logged_features(self, command: dict, key: str): registry=self.store.registry, ) + def _validate_pull_all_from_table_or_query_parameters(self, command: dict): + assert ( + "data_source_name" in command + ), "data_source_name is a mandatory parameter" + assert ( + "join_key_columns" in command + ), "join_key_columns is a mandatory parameter" + assert ( + "feature_name_columns" in command + ), "feature_name_columns is a mandatory parameter" + assert "timestamp_field" in command, "timestamp_field is a mandatory parameter" + assert "start_date" in command, "start_date is a mandatory parameter" + assert "end_date" in command, "end_date is a mandatory parameter" + def pull_all_from_table_or_query(self, command: dict): + self._validate_pull_all_from_table_or_query_parameters(command) + return self.offline_store.pull_all_from_table_or_query( self.store.config, self.store.get_data_source(command["data_source_name"]), @@ -229,7 +271,23 @@ def pull_all_from_table_or_query(self, command: dict): utils.make_tzaware(datetime.fromisoformat(command["end_date"])), ) + def _validate_pull_latest_from_table_or_query_parameters(self, command: dict): + assert ( + "data_source_name" in command + ), "data_source_name is a mandatory parameter" + assert ( + "join_key_columns" in command + ), "join_key_columns is a mandatory parameter" + assert ( + "feature_name_columns" in command + ), "feature_name_columns is a mandatory parameter" + assert "timestamp_field" in command, "timestamp_field is a mandatory parameter" + assert "start_date" in command, "start_date is a mandatory parameter" + assert "end_date" in command, "end_date is a mandatory parameter" + def pull_latest_from_table_or_query(self, command: dict): + self._validate_pull_latest_from_table_or_query_parameters(command) + return self.offline_store.pull_latest_from_table_or_query( self.store.config, self.store.get_data_source(command["data_source_name"]), @@ -258,20 +316,33 @@ def list_actions(self, context): ), ] + def _validate_get_historical_features_parameters(self, command: dict, key: str): + assert key in self.flights, f"missing key={key}" + assert "feature_view_names" in command, "feature_view_names is mandatory" + assert "name_aliases" in command, "name_aliases is mandatory" + assert "feature_refs" in command, "feature_refs is mandatory" + assert "project" in command, "project is mandatory" + assert "full_feature_names" in command, "full_feature_names is mandatory" + def get_historical_features(self, command: dict, key: str): + self._validate_get_historical_features_parameters(command, key) + # Extract parameters from the internal flights dictionary entity_df_value = self.flights[key] entity_df = pa.Table.to_pandas(entity_df_value) + feature_view_names = command["feature_view_names"] name_aliases = command["name_aliases"] feature_refs = command["feature_refs"] project = command["project"] full_feature_names = command["full_feature_names"] + feature_views = self.list_feature_views_by_name( feature_view_names=feature_view_names, name_aliases=name_aliases, project=project, ) + retJob = self.offline_store.get_historical_features( config=self.store.config, feature_views=feature_views, @@ -281,10 +352,19 @@ def get_historical_features(self, command: dict, key: str): project=project, full_feature_names=full_feature_names, ) + return retJob - def persist(self, retrieve_func: str, command: dict, key: str): + def _validate_persist_parameters(self, command: dict): + assert "retrieve_func" in command, "retrieve_func is mandatory" + assert "data_source_name" in command, "data_source_name is mandatory" + assert "allow_overwrite" in command, "allow_overwrite is mandatory" + + def persist(self, command: dict, key: str): + self._validate_persist_parameters(command) + try: + retrieve_func = command["retrieve_func"] if retrieve_func == OfflineServer.get_historical_features.__name__: ret_job = self.get_historical_features(command, key) elif ( From 21deec8495a101442e78cabc9a30cb5fbee5382f Mon Sep 17 00:00:00 2001 From: Shuchu Han Date: Mon, 24 Jun 2024 21:08:06 -0400 Subject: [PATCH 07/59] fix: Deprecated the datetime.utcfromtimestamp(). (#4306) --- sdk/python/feast/infra/registry/snowflake.py | 4 ++-- sdk/python/feast/infra/registry/sql.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sdk/python/feast/infra/registry/snowflake.py b/sdk/python/feast/infra/registry/snowflake.py index aaf6c4c48d..d7ab67e7d0 100644 --- a/sdk/python/feast/infra/registry/snowflake.py +++ b/sdk/python/feast/infra/registry/snowflake.py @@ -2,7 +2,7 @@ import os import uuid from binascii import hexlify -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from enum import Enum from threading import Lock from typing import Any, Callable, List, Literal, Optional, Set, Union @@ -994,7 +994,7 @@ def _get_last_updated_metadata(self, project: str): if df.empty: return None - return datetime.utcfromtimestamp(int(df.squeeze())) + return datetime.fromtimestamp(int(df.squeeze()), tz=timezone.utc) def _infer_fv_classes(self, feature_view): if isinstance(feature_view, StreamFeatureView): diff --git a/sdk/python/feast/infra/registry/sql.py b/sdk/python/feast/infra/registry/sql.py index 42bd19eb5f..239898677c 100644 --- a/sdk/python/feast/infra/registry/sql.py +++ b/sdk/python/feast/infra/registry/sql.py @@ -1,6 +1,6 @@ import logging import uuid -from datetime import datetime +from datetime import datetime, timezone from enum import Enum from pathlib import Path from typing import Any, Callable, Dict, List, Optional, Set, Union @@ -903,7 +903,7 @@ def _get_last_updated_metadata(self, project: str): return None update_time = int(row._mapping["last_updated_timestamp"]) - return datetime.utcfromtimestamp(update_time) + return datetime.fromtimestamp(update_time, tz=timezone.utc) def _get_all_projects(self) -> Set[str]: projects = set() From 86af60ad87d537b17e4ce6ec7a5eac0d637fb32d Mon Sep 17 00:00:00 2001 From: Daniele Martinoli <86618610+dmartinol@users.noreply.github.com> Date: Tue, 25 Jun 2024 21:51:39 +0200 Subject: [PATCH 08/59] fix: Added missing type (#4315) Added missing type Signed-off-by: Daniele Martinoli <86618610+dmartinol@users.noreply.github.com> --- sdk/python/feast/diff/registry_diff.py | 2 ++ sdk/python/feast/feast_object.py | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/sdk/python/feast/diff/registry_diff.py b/sdk/python/feast/diff/registry_diff.py index b608757496..9236b087d4 100644 --- a/sdk/python/feast/diff/registry_diff.py +++ b/sdk/python/feast/diff/registry_diff.py @@ -20,6 +20,7 @@ OnDemandFeatureView as OnDemandFeatureViewProto, ) from feast.protos.feast.core.OnDemandFeatureView_pb2 import OnDemandFeatureViewSpec +from feast.protos.feast.core.SavedDataset_pb2 import SavedDataset as SavedDatasetProto from feast.protos.feast.core.StreamFeatureView_pb2 import ( StreamFeatureView as StreamFeatureViewProto, ) @@ -109,6 +110,7 @@ def tag_objects_for_keep_delete_update_add( OnDemandFeatureViewProto, StreamFeatureViewProto, ValidationReferenceProto, + SavedDatasetProto, ) diff --git a/sdk/python/feast/feast_object.py b/sdk/python/feast/feast_object.py index 2d06d8d669..d9505dcb9f 100644 --- a/sdk/python/feast/feast_object.py +++ b/sdk/python/feast/feast_object.py @@ -11,11 +11,12 @@ 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.SavedDataset_pb2 import SavedDatasetSpec from .protos.feast.core.StreamFeatureView_pb2 import StreamFeatureViewSpec from .protos.feast.core.ValidationProfile_pb2 import ( ValidationReference as ValidationReferenceProto, ) -from .saved_dataset import ValidationReference +from .saved_dataset import SavedDataset, ValidationReference from .stream_feature_view import StreamFeatureView # Convenience type representing all Feast objects @@ -28,6 +29,7 @@ FeatureService, DataSource, ValidationReference, + SavedDataset, ] FeastObjectSpecProto = Union[ @@ -38,4 +40,5 @@ FeatureServiceSpec, DataSourceProto, ValidationReferenceProto, + SavedDatasetSpec, ] From 372fd75394c36ab4d864758eedcfa94aafdb6a2e Mon Sep 17 00:00:00 2001 From: Tornike Gurgenidze Date: Thu, 27 Jun 2024 02:27:13 +0400 Subject: [PATCH 09/59] chore: Remove existing providers from the repo (#4298) remove providers Signed-off-by: tokoko --- sdk/python/feast/errors.py | 12 ---- sdk/python/feast/infra/aws.py | 9 --- .../feast/infra/contrib/azure_provider.py | 72 ------------------- sdk/python/feast/infra/gcp.py | 9 --- sdk/python/feast/infra/local.py | 23 ------ .../feast/infra/passthrough_provider.py | 13 ++++ sdk/python/feast/infra/provider.py | 8 +-- sdk/python/feast/repo_config.py | 66 ++--------------- .../offline_stores/test_offline_store.py | 4 +- .../infra/scaffolding/test_repo_config.py | 59 ++++----------- 10 files changed, 40 insertions(+), 235 deletions(-) delete mode 100644 sdk/python/feast/infra/aws.py delete mode 100644 sdk/python/feast/infra/contrib/azure_provider.py delete mode 100644 sdk/python/feast/infra/gcp.py delete mode 100644 sdk/python/feast/infra/local.py diff --git a/sdk/python/feast/errors.py b/sdk/python/feast/errors.py index 22de402f20..6083b3d554 100644 --- a/sdk/python/feast/errors.py +++ b/sdk/python/feast/errors.py @@ -119,23 +119,11 @@ def __init__(self, provider_name): super().__init__(f"Provider '{provider_name}' is not implemented") -class FeastProviderNotSetError(Exception): - def __init__(self): - super().__init__("Provider is not set, but is required") - - class FeastRegistryNotSetError(Exception): def __init__(self): super().__init__("Registry is not set, but is required") -class FeastFeatureServerTypeSetError(Exception): - def __init__(self, feature_server_type: str): - super().__init__( - f"Feature server type was set to {feature_server_type}, but the type should be determined by the provider" - ) - - class FeastFeatureServerTypeInvalidError(Exception): def __init__(self, feature_server_type: str): super().__init__( diff --git a/sdk/python/feast/infra/aws.py b/sdk/python/feast/infra/aws.py deleted file mode 100644 index 47fec9b05b..0000000000 --- a/sdk/python/feast/infra/aws.py +++ /dev/null @@ -1,9 +0,0 @@ -from feast.infra.passthrough_provider import PassthroughProvider - - -class AwsProvider(PassthroughProvider): - """ - This class only exists for backwards compatibility. - """ - - pass diff --git a/sdk/python/feast/infra/contrib/azure_provider.py b/sdk/python/feast/infra/contrib/azure_provider.py deleted file mode 100644 index ac56a2b33e..0000000000 --- a/sdk/python/feast/infra/contrib/azure_provider.py +++ /dev/null @@ -1,72 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. -from datetime import datetime -from typing import Callable - -from tqdm import tqdm - -from feast.feature_view import FeatureView -from feast.infra.passthrough_provider import PassthroughProvider -from feast.infra.registry.base_registry import BaseRegistry -from feast.repo_config import RepoConfig -from feast.utils import ( - _convert_arrow_to_proto, - _get_column_names, - _run_pyarrow_field_mapping, -) - -DEFAULT_BATCH_SIZE = 10_000 - - -class AzureProvider(PassthroughProvider): - def materialize_single_feature_view( - self, - config: RepoConfig, - feature_view: FeatureView, - start_date: datetime, - end_date: datetime, - registry: BaseRegistry, - project: str, - tqdm_builder: Callable[[int], tqdm], - ) -> None: - # TODO(kevjumba): untested - entities = [] - for entity_name in feature_view.entities: - entities.append(registry.get_entity(entity_name, project)) - - ( - join_key_columns, - feature_name_columns, - event_timestamp_column, - created_timestamp_column, - ) = _get_column_names(feature_view, entities) - - offline_job = self.offline_store.pull_latest_from_table_or_query( - config=config, - data_source=feature_view.batch_source, - join_key_columns=join_key_columns, - feature_name_columns=feature_name_columns, - timestamp_field=event_timestamp_column, - created_timestamp_column=created_timestamp_column, - start_date=start_date, - end_date=end_date, - ) - - table = offline_job.to_arrow() - - if feature_view.batch_source.field_mapping is not None: - table = _run_pyarrow_field_mapping( - table, feature_view.batch_source.field_mapping - ) - - join_keys = {entity.join_key: entity.value_type for entity in entities} - - with tqdm_builder(table.num_rows) as pbar: - for batch in table.to_batches(DEFAULT_BATCH_SIZE): - rows_to_write = _convert_arrow_to_proto(batch, feature_view, join_keys) - self.online_write_batch( - self.repo_config, - feature_view, - rows_to_write, - lambda x: pbar.update(x), - ) diff --git a/sdk/python/feast/infra/gcp.py b/sdk/python/feast/infra/gcp.py deleted file mode 100644 index 512378237a..0000000000 --- a/sdk/python/feast/infra/gcp.py +++ /dev/null @@ -1,9 +0,0 @@ -from feast.infra.passthrough_provider import PassthroughProvider - - -class GcpProvider(PassthroughProvider): - """ - This class only exists for backwards compatibility. - """ - - pass diff --git a/sdk/python/feast/infra/local.py b/sdk/python/feast/infra/local.py deleted file mode 100644 index 1226ceaf37..0000000000 --- a/sdk/python/feast/infra/local.py +++ /dev/null @@ -1,23 +0,0 @@ -from typing import List - -from feast.infra.infra_object import Infra, InfraObject -from feast.infra.passthrough_provider import PassthroughProvider -from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto -from feast.repo_config import RepoConfig - - -class LocalProvider(PassthroughProvider): - """ - This class only exists for backwards compatibility. - """ - - def plan_infra( - self, config: RepoConfig, desired_registry_proto: RegistryProto - ) -> Infra: - infra = Infra() - if self.online_store: - infra_objects: List[InfraObject] = self.online_store.plan( - config, desired_registry_proto - ) - infra.infra_objects += infra_objects - return infra diff --git a/sdk/python/feast/infra/passthrough_provider.py b/sdk/python/feast/infra/passthrough_provider.py index e707f9495d..bad6f86cc6 100644 --- a/sdk/python/feast/infra/passthrough_provider.py +++ b/sdk/python/feast/infra/passthrough_provider.py @@ -12,6 +12,7 @@ from feast.feature_logging import FeatureServiceLoggingSource from feast.feature_service import FeatureService from feast.feature_view import FeatureView +from feast.infra.infra_object import Infra, InfraObject from feast.infra.materialization.batch_materialization_engine import ( BatchMaterializationEngine, MaterializationJobStatus, @@ -22,6 +23,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.protos.feast.core.Registry_pb2 import Registry as RegistryProto 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 BATCH_ENGINE_CLASS_FOR_TYPE, RepoConfig @@ -103,6 +105,17 @@ def batch_engine(self) -> BatchMaterializationEngine: self._batch_engine = _batch_engine return _batch_engine + def plan_infra( + self, config: RepoConfig, desired_registry_proto: RegistryProto + ) -> Infra: + infra = Infra() + if self.online_store: + infra_objects: List[InfraObject] = self.online_store.plan( + config, desired_registry_proto + ) + infra.infra_objects += infra_objects + return infra + def update_infra( self, project: str, diff --git a/sdk/python/feast/infra/provider.py b/sdk/python/feast/infra/provider.py index 93077f40b9..75afd6bba8 100644 --- a/sdk/python/feast/infra/provider.py +++ b/sdk/python/feast/infra/provider.py @@ -22,10 +22,10 @@ from feast.saved_dataset import SavedDataset PROVIDERS_CLASS_FOR_TYPE = { - "gcp": "feast.infra.gcp.GcpProvider", - "aws": "feast.infra.aws.AwsProvider", - "local": "feast.infra.local.LocalProvider", - "azure": "feast.infra.contrib.azure_provider.AzureProvider", + "gcp": "feast.infra.passthrough_provider.PassthroughProvider", + "aws": "feast.infra.passthrough_provider.PassthroughProvider", + "local": "feast.infra.passthrough_provider.PassthroughProvider", + "azure": "feast.infra.passthrough_provider.PassthroughProvider", } diff --git a/sdk/python/feast/repo_config.py b/sdk/python/feast/repo_config.py index 1c8041b4dd..99b90a09a5 100644 --- a/sdk/python/feast/repo_config.py +++ b/sdk/python/feast/repo_config.py @@ -18,10 +18,8 @@ from feast.errors import ( FeastFeatureServerTypeInvalidError, - FeastFeatureServerTypeSetError, FeastOfflineStoreInvalidName, FeastOnlineStoreInvalidName, - FeastProviderNotSetError, FeastRegistryNotSetError, FeastRegistryTypeInvalidError, ) @@ -85,10 +83,6 @@ "local": "feast.infra.feature_servers.local_process.config.LocalFeatureServerConfig", } -FEATURE_SERVER_TYPE_FOR_PROVIDER = { - "local": "local", -} - class FeastBaseModel(BaseModel): """Feast Pydantic Configuration Class""" @@ -138,7 +132,7 @@ class RepoConfig(FeastBaseModel): provider account, as long as they have different project ids. """ - provider: StrictStr + provider: StrictStr = "local" """ str: local or gcp or aws """ registry_config: Any = Field(alias="registry", default="data/registry.db") @@ -191,30 +185,10 @@ def __init__(self, **data: Any): self.registry_config = data["registry"] self._offline_store = None - if "offline_store" in data: - self.offline_config = data["offline_store"] - else: - if data["provider"] == "local": - self.offline_config = "file" - elif data["provider"] == "gcp": - self.offline_config = "bigquery" - elif data["provider"] == "aws": - self.offline_config = "redshift" - elif data["provider"] == "azure": - self.offline_config = "mssql" + self.offline_config = data.get("offline_store", "file") self._online_store = None - if "online_store" in data: - self.online_config = data["online_store"] - else: - if data["provider"] == "local": - self.online_config = "sqlite" - elif data["provider"] == "gcp": - self.online_config = "datastore" - elif data["provider"] == "aws": - self.online_config = "dynamodb" - elif data["provider"] == "rockset": - self.online_config = "rockset" + self.online_config = data.get("online_store", "sqlite") self._batch_engine = None if "batch_engine" in data: @@ -325,20 +299,11 @@ def _validate_online_store_config(cls, values: Any) -> Any: values["online_store"] = None return values - # Make sure that the provider configuration is set. We need it to set the defaults - if "provider" not in values: - raise FeastProviderNotSetError() - # Set the default type # This is only direct reference to a provider or online store that we should have # for backwards compatibility. if "type" not in values["online_store"]: - if values["provider"] == "local": - values["online_store"]["type"] = "sqlite" - elif values["provider"] == "gcp": - values["online_store"]["type"] = "datastore" - elif values["provider"] == "aws": - values["online_store"]["type"] = "dynamodb" + values["online_store"]["type"] = "sqlite" online_store_type = values["online_store"]["type"] @@ -361,20 +326,9 @@ def _validate_offline_store_config(cls, values: Any) -> Any: if not isinstance(values["offline_store"], Dict): return values - # Make sure that the provider configuration is set. We need it to set the defaults - if "provider" not in values: - raise FeastProviderNotSetError() - # Set the default type if "type" not in values["offline_store"]: - if values["provider"] == "local": - values["offline_store"]["type"] = "file" - elif values["provider"] == "gcp": - values["offline_store"]["type"] = "bigquery" - elif values["provider"] == "aws": - values["offline_store"]["type"] = "redshift" - if values["provider"] == "azure": - values["offline_store"]["type"] = "mssql" + values["offline_store"]["type"] = "file" offline_store_type = values["offline_store"]["type"] @@ -398,15 +352,7 @@ def _validate_feature_server_config(cls, values: Any) -> Any: if not isinstance(values["feature_server"], Dict): return values - # Make sure that the provider configuration is set. We need it to set the defaults - if "provider" not in values: - raise FeastProviderNotSetError() - - default_type = FEATURE_SERVER_TYPE_FOR_PROVIDER.get(values["provider"]) - defined_type = values["feature_server"].get("type", default_type) - # Make sure that the type is either not set, or set correctly, since it's defined by the provider - if defined_type not in (default_type, "local"): - raise FeastFeatureServerTypeSetError(defined_type) + defined_type = values["feature_server"].get("type", "local") values["feature_server"]["type"] = defined_type # Validate the dict to ensure one of the union types match 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 fd50d37632..3589c8a3fa 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 @@ -124,7 +124,7 @@ def retrieval_job(request, environment): iam_role="arn:aws:iam::585132637328:role/service-role/AmazonRedshift-CommandsAccessRole-20240403T092631", workgroup="", ) - config = environment.config.copy( + config = environment.config.model_copy( update={"offline_config": offline_store_config} ) return RedshiftRetrievalJob( @@ -147,7 +147,7 @@ def retrieval_job(request, environment): storage_integration_name="FEAST_S3", blob_export_location="s3://feast-snowflake-offload/export", ) - config = environment.config.copy( + config = environment.config.model_copy( update={"offline_config": offline_store_config} ) environment.project = "project" 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 e1839fbd8b..98d82ce357 100644 --- a/sdk/python/tests/unit/infra/scaffolding/test_repo_config.py +++ b/sdk/python/tests/unit/infra/scaffolding/test_repo_config.py @@ -33,36 +33,6 @@ def _test_config(config_text, expect_error: Optional[str]): return rc -def test_nullable_online_store_aws(): - _test_config( - dedent( - """ - project: foo - registry: "registry.db" - provider: aws - online_store: null - entity_key_serialization_version: 2 - """ - ), - expect_error="4 validation errors for RepoConfig\nregion\n Field required", - ) - - -def test_nullable_online_store_gcp(): - _test_config( - dedent( - """ - project: foo - registry: "registry.db" - provider: gcp - online_store: null - entity_key_serialization_version: 2 - """ - ), - expect_error=None, - ) - - def test_nullable_online_store_local(): _test_config( dedent( @@ -125,20 +95,6 @@ def test_local_config_with_full_online_class_directly(): assert isinstance(c.online_store, SqliteOnlineStoreConfig) -def test_gcp_config(): - _test_config( - dedent( - """ - project: foo - registry: gs://registry.db - provider: gcp - entity_key_serialization_version: 2 - """ - ), - expect_error=None, - ) - - def test_extra_field(): _test_config( dedent( @@ -224,3 +180,18 @@ def test_invalid_project_name(): ), expect_error="alphanumerical values ", ) + + +def test_no_provider(): + _test_config( + dedent( + """ + project: foo + registry: "registry.db" + online_store: + path: "blah" + entity_key_serialization_version: 2 + """ + ), + expect_error=None, + ) From 2c3894693e9079b8ad7873b139b30440c919e913 Mon Sep 17 00:00:00 2001 From: okramarenko <97118627+okramarenko@users.noreply.github.com> Date: Thu, 27 Jun 2024 01:27:33 +0300 Subject: [PATCH 10/59] feat: Add SingleStore as an OnlineStore (#4285) Add SingleStore as an OnlineStore Signed-off-by: Olha Kramarenko --- Makefile | 11 + docs/SUMMARY.md | 1 + docs/reference/online-stores/README.md | 3 + docs/reference/online-stores/singlestore.md | 51 ++++ .../feast.infra.online_stores.contrib.rst | 8 + .../feast.infra.registry.contrib.postgres.rst | 21 ++ .../source/feast.infra.registry.contrib.rst | 1 + .../singlestore_online_store/singlestore.py | 235 ++++++++++++++++++ .../contrib/singlestore_repo_configuration.py | 10 + sdk/python/feast/repo_config.py | 1 + .../requirements/py3.10-ci-requirements.txt | 17 +- .../requirements/py3.11-ci-requirements.txt | 16 +- .../requirements/py3.9-ci-requirements.txt | 17 +- .../universal/online_store/singlestore.py | 43 ++++ setup.py | 4 + 15 files changed, 433 insertions(+), 6 deletions(-) create mode 100644 docs/reference/online-stores/singlestore.md create mode 100644 sdk/python/docs/source/feast.infra.registry.contrib.postgres.rst create mode 100644 sdk/python/feast/infra/online_stores/contrib/singlestore_online_store/singlestore.py create mode 100644 sdk/python/feast/infra/online_stores/contrib/singlestore_repo_configuration.py create mode 100644 sdk/python/tests/integration/feature_repos/universal/online_store/singlestore.py diff --git a/Makefile b/Makefile index 2ad693c7a1..39406cc17d 100644 --- a/Makefile +++ b/Makefile @@ -331,6 +331,17 @@ test-python-universal-cassandra-no-cloud-providers: not test_snowflake" \ sdk/python/tests +test-python-universal-singlestore-online: + PYTHONPATH='.' \ + FULL_REPO_CONFIGS_MODULE=sdk.python.feast.infra.online_stores.contrib.singlestore_repo_configuration \ + PYTEST_PLUGINS=sdk.python.tests.integration.feature_repos.universal.online_store.singlestore \ + python -m pytest -n 8 --integration \ + -k "not test_universal_cli and \ + not gcs_registry and \ + not s3_registry and \ + not test_snowflake" \ + sdk/python/tests + test-python-universal: python -m pytest -n 8 --integration sdk/python/tests diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 3f0506cf1e..a40c60d97c 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -103,6 +103,7 @@ * [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) * [Providers](reference/providers/README.md) * [Local](reference/providers/local.md) * [Google Cloud Platform](reference/providers/google-cloud-platform.md) diff --git a/docs/reference/online-stores/README.md b/docs/reference/online-stores/README.md index b5f4eb8de8..0acf6701f9 100644 --- a/docs/reference/online-stores/README.md +++ b/docs/reference/online-stores/README.md @@ -64,4 +64,7 @@ Please see [Online Store](../../getting-started/architecture-and-components/onli {% content-ref url="remote.md" %} [remote.md](remote.md) + +{% content-ref url="singlestore.md" %} +[singlestore.md](singlestore.md) {% endcontent-ref %} diff --git a/docs/reference/online-stores/singlestore.md b/docs/reference/online-stores/singlestore.md new file mode 100644 index 0000000000..1777787f22 --- /dev/null +++ b/docs/reference/online-stores/singlestore.md @@ -0,0 +1,51 @@ +# SingleStore online store (contrib) + +## Description + +The SingleStore online store provides support for materializing feature values into a SingleStore database for serving online features. + +## Getting started +In order to use this online store, you'll need to run `pip install 'feast[singlestore]'`. You can get started by then running `feast init` and then setting the `feature_store.yaml` as described below. + +## Example + +{% code title="feature_store.yaml" %} +```yaml +project: my_feature_repo +registry: data/registry.db +provider: local +online_store: + type: singlestore + host: DB_HOST + port: DB_PORT + database: DB_NAME + user: DB_USERNAME + password: DB_PASSWORD +``` +{% endcode %} + +## Functionality Matrix + +The set of functionality supported by online stores is described in detail [here](overview.md#functionality). +Below is a matrix indicating which functionality is supported by the SingleStore online store. + +| | SingleStore | +| :-------------------------------------------------------- | :----------- | +| write feature values to the online store | yes | +| read feature values from the online store | yes | +| update infrastructure (e.g. tables) in the online store | yes | +| teardown infrastructure (e.g. tables) in the online store | yes | +| generate a plan of infrastructure changes | no | +| support for on-demand transforms | yes | +| readable by Python SDK | yes | +| readable by Java | no | +| readable by Go | no | +| support for entityless feature views | yes | +| support for concurrent writing to the same key | no | +| support for ttl (time to live) at retrieval | no | +| support for deleting expired data | no | +| collocated by feature view | yes | +| collocated by feature service | no | +| collocated by entity key | no | + +To compare this set of functionality against other online stores, please see the full [functionality matrix](overview.md#functionality-matrix). 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 d614438e3d..9d301fcd0d 100644 --- a/sdk/python/docs/source/feast.infra.online_stores.contrib.rst +++ b/sdk/python/docs/source/feast.infra.online_stores.contrib.rst @@ -89,6 +89,14 @@ feast.infra.online\_stores.contrib.postgres\_repo\_configuration module :undoc-members: :show-inheritance: +feast.infra.online\_stores.contrib.singlestore\_repo\_configuration module +-------------------------------------------------------------------------- + +.. automodule:: feast.infra.online_stores.contrib.singlestore_repo_configuration + :members: + :undoc-members: + :show-inheritance: + Module contents --------------- diff --git a/sdk/python/docs/source/feast.infra.registry.contrib.postgres.rst b/sdk/python/docs/source/feast.infra.registry.contrib.postgres.rst new file mode 100644 index 0000000000..3f31990805 --- /dev/null +++ b/sdk/python/docs/source/feast.infra.registry.contrib.postgres.rst @@ -0,0 +1,21 @@ +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.contrib.rst b/sdk/python/docs/source/feast.infra.registry.contrib.rst index 83417109b8..44b89736ad 100644 --- a/sdk/python/docs/source/feast.infra.registry.contrib.rst +++ b/sdk/python/docs/source/feast.infra.registry.contrib.rst @@ -8,6 +8,7 @@ Subpackages :maxdepth: 4 feast.infra.registry.contrib.azure + feast.infra.registry.contrib.postgres Module contents --------------- 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 new file mode 100644 index 0000000000..e17a059c1a --- /dev/null +++ b/sdk/python/feast/infra/online_stores/contrib/singlestore_online_store/singlestore.py @@ -0,0 +1,235 @@ +from __future__ import absolute_import + +from collections import defaultdict +from datetime import datetime +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 +from singlestoredb.exceptions import InterfaceError + +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 as EntityKeyProto +from feast.protos.feast.types.Value_pb2 import Value as ValueProto +from feast.repo_config import FeastConfigBaseModel + + +class SingleStoreOnlineStoreConfig(FeastConfigBaseModel): + """ + Configuration for the SingleStore online store. + NOTE: The class *must* end with the `OnlineStoreConfig` suffix. + """ + + type: Literal["singlestore"] = "singlestore" + + host: Optional[StrictStr] = None + user: Optional[StrictStr] = None + password: Optional[StrictStr] = None + database: Optional[StrictStr] = None + port: Optional[int] = None + + +class SingleStoreOnlineStore(OnlineStore): + """ + An online store implementation that uses SingleStore. + NOTE: The class *must* end with the `OnlineStore` suffix. + """ + + _conn: Optional[Connection] = None + + def _init_conn(self, config: RepoConfig) -> Connection: + online_store_config = config.online_store + assert isinstance(online_store_config, SingleStoreOnlineStoreConfig) + return singlestoredb.connect( + host=online_store_config.host or "127.0.0.1", + user=online_store_config.user or "test", + password=online_store_config.password or "test", + database=online_store_config.database or "feast", + port=online_store_config.port or 3306, + autocommit=True, + ) + + def _get_cursor(self, config: RepoConfig) -> Any: + # This will try to reconnect also. + # In case it fails, we will have to create a new connection. + if not self._conn: + self._conn = self._init_conn(config) + try: + self._conn.ping(reconnect=True) + except InterfaceError: + self._conn = self._init_conn(config) + return self._conn.cursor() + + 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: + project = config.project + with self._get_cursor(config) as cur: + insert_values = [] + for entity_key, values, timestamp, created_ts in data: + entity_key_bin = serialize_entity_key( + entity_key, + entity_key_serialization_version=2, + ).hex() + timestamp = _to_naive_utc(timestamp) + if created_ts is not None: + created_ts = _to_naive_utc(created_ts) + + for feature_name, val in values.items(): + insert_values.append( + ( + entity_key_bin, + feature_name, + val.SerializeToString(), + timestamp, + created_ts, + ) + ) + # Control the batch so that we can update the progress + batch_size = 50000 + for i in range(0, len(insert_values), batch_size): + current_batch = insert_values[i : i + batch_size] + cur.executemany( + f""" + INSERT INTO {_table_id(project, table)} + (entity_key, feature_name, value, event_ts, created_ts) + values (%s, %s, %s, %s, %s) + ON DUPLICATE KEY UPDATE + value = VALUES(value), + event_ts = VALUES(event_ts), + created_ts = VALUES(created_ts); + """, + current_batch, + ) + if progress: + progress(len(current_batch)) + + 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]]]]: + project = config.project + result: List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]] = [] + with self._get_cursor(config) as cur: + keys = [] + for entity_key in entity_keys: + keys.append( + serialize_entity_key( + entity_key, + entity_key_serialization_version=2, + ).hex() + ) + + if not requested_features: + entity_key_placeholders = ",".join(["%s" for _ in keys]) + cur.execute( + f""" + SELECT entity_key, feature_name, value, event_ts FROM {_table_id(project, table)} + WHERE entity_key IN ({entity_key_placeholders}) + ORDER BY event_ts; + """, + tuple(keys), + ) + else: + entity_key_placeholders = ",".join(["%s" for _ in keys]) + requested_features_placeholders = ",".join( + ["%s" for _ in requested_features] + ) + cur.execute( + f""" + SELECT entity_key, feature_name, value, event_ts FROM {_table_id(project, table)} + WHERE entity_key IN ({entity_key_placeholders}) and feature_name IN ({requested_features_placeholders}) + ORDER BY event_ts; + """, + tuple(keys + requested_features), + ) + rows = cur.fetchall() or [] + + # Since we don't know the order returned from MySQL we'll need + # to construct a dict to be able to quickly look up the correct row + # when we iterate through the keys since they are in the correct order + values_dict = defaultdict(list) + for row in rows: + values_dict[row[0]].append(row[1:]) + + for key in keys: + if key in values_dict: + key_values = values_dict[key] + res = {} + res_ts: Optional[datetime] = None + for feature_name, value_bin, event_ts in key_values: + val = ValueProto() + val.ParseFromString(bytes(value_bin)) + res[feature_name] = val + res_ts = event_ts + result.append((res_ts, res)) + else: + result.append((None, None)) + return result + + 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, + ) -> None: + project = config.project + with self._get_cursor(config) as cur: + # We don't create any special state for the entities in this implementation. + for table in tables_to_keep: + cur.execute( + f"""CREATE TABLE IF NOT EXISTS {_table_id(project, table)} (entity_key VARCHAR(512), + feature_name VARCHAR(256), + value BLOB, + event_ts timestamp NULL DEFAULT NULL, + created_ts timestamp NULL DEFAULT NULL, + PRIMARY KEY(entity_key, feature_name), + INDEX {_table_id(project, table)}_ek (entity_key))""" + ) + + for table in tables_to_delete: + _drop_table_and_index(cur, project, table) + + def teardown( + self, + config: RepoConfig, + tables: Sequence[FeatureView], + entities: Sequence[Entity], + ) -> None: + project = config.project + with self._get_cursor(config) as cur: + for table in tables: + _drop_table_and_index(cur, project, table) + + +def _drop_table_and_index(cur: Cursor, project: str, table: FeatureView) -> None: + table_name = _table_id(project, table) + cur.execute(f"DROP INDEX {table_name}_ek ON {table_name};") + cur.execute(f"DROP TABLE IF EXISTS {table_name}") + + +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/singlestore_repo_configuration.py b/sdk/python/feast/infra/online_stores/contrib/singlestore_repo_configuration.py new file mode 100644 index 0000000000..2debe0f0ee --- /dev/null +++ b/sdk/python/feast/infra/online_stores/contrib/singlestore_repo_configuration.py @@ -0,0 +1,10 @@ +from tests.integration.feature_repos.integration_test_repo_config import ( + IntegrationTestRepoConfig, +) +from tests.integration.feature_repos.universal.online_store.singlestore import ( + SingleStoreOnlineStoreCreator, +) + +FULL_REPO_CONFIGS = [ + IntegrationTestRepoConfig(online_store_creator=SingleStoreOnlineStoreCreator), +] diff --git a/sdk/python/feast/repo_config.py b/sdk/python/feast/repo_config.py index 99b90a09a5..f3c379020d 100644 --- a/sdk/python/feast/repo_config.py +++ b/sdk/python/feast/repo_config.py @@ -63,6 +63,7 @@ "ikv": "feast.infra.online_stores.contrib.ikv_online_store.ikv.IKVOnlineStore", "elasticsearch": "feast.infra.online_stores.contrib.elasticsearch.ElasticSearchOnlineStore", "remote": "feast.infra.online_stores.remote.RemoteOnlineStore", + "singlestore": "feast.infra.online_stores.contrib.singlestore_online_store.singlestore.SingleStoreOnlineStore", } OFFLINE_STORE_CLASS_FOR_TYPE = { diff --git a/sdk/python/requirements/py3.10-ci-requirements.txt b/sdk/python/requirements/py3.10-ci-requirements.txt index 97bdfc159b..a0faf3d9ef 100644 --- a/sdk/python/requirements/py3.10-ci-requirements.txt +++ b/sdk/python/requirements/py3.10-ci-requirements.txt @@ -68,7 +68,9 @@ botocore==1.34.99 # moto # s3transfer build==1.2.1 - # via pip-tools + # via + # pip-tools + # singlestoredb cachecontrol==0.14.0 # via firebase-admin cachetools==5.3.3 @@ -505,6 +507,8 @@ pandas==2.2.2 # snowflake-connector-python pandocfilters==1.5.1 # via nbconvert +parsimonious==0.10.0 + # via singlestoredb parso==0.8.4 # via jedi parsy==2.1 @@ -610,6 +614,7 @@ pygments==2.18.0 pyjwt[crypto]==2.8.0 # via # msal + # singlestoredb # snowflake-connector-python pymssql==2.3.0 pymysql==1.1.1 @@ -705,6 +710,7 @@ requests==2.31.0 # msal # requests-oauthlib # responses + # singlestoredb # snowflake-connector-python # sphinx # trino @@ -745,8 +751,10 @@ setuptools==70.0.0 # grpcio-tools # kubernetes # pip-tools + # singlestoredb shellingham==1.5.4 # via typer +singlestoredb==1.3.1 six==1.16.0 # via # asttokens @@ -794,6 +802,8 @@ sqlalchemy-views==0.3.2 sqlglot==20.11.0 # via ibis-framework sqlite-vec==0.0.1a10 +sqlparams==6.0.1 + # via singlestoredb stack-data==0.6.3 # via ipython starlette==0.37.2 @@ -821,6 +831,7 @@ tomli==2.0.1 # pip-tools # pytest # pytest-env + # singlestoredb tomlkit==0.12.5 # via snowflake-connector-python toolz==0.12.1 @@ -946,7 +957,9 @@ websockets==12.0 werkzeug==3.0.3 # via moto wheel==0.43.0 - # via pip-tools + # via + # pip-tools + # singlestoredb widgetsnbextension==4.0.11 # via ipywidgets wrapt==1.16.0 diff --git a/sdk/python/requirements/py3.11-ci-requirements.txt b/sdk/python/requirements/py3.11-ci-requirements.txt index f6db0af6bc..cea8cc22d0 100644 --- a/sdk/python/requirements/py3.11-ci-requirements.txt +++ b/sdk/python/requirements/py3.11-ci-requirements.txt @@ -64,7 +64,9 @@ botocore==1.34.99 # moto # s3transfer build==1.2.1 - # via pip-tools + # via + # pip-tools + # singlestoredb cachecontrol==0.14.0 # via firebase-admin cachetools==5.3.3 @@ -496,6 +498,8 @@ pandas==2.2.2 # snowflake-connector-python pandocfilters==1.5.1 # via nbconvert +parsimonious==0.10.0 + # via singlestoredb parso==0.8.4 # via jedi parsy==2.1 @@ -601,6 +605,7 @@ pygments==2.18.0 pyjwt[crypto]==2.8.0 # via # msal + # singlestoredb # snowflake-connector-python pymssql==2.3.0 pymysql==1.1.1 @@ -696,6 +701,7 @@ requests==2.31.0 # msal # requests-oauthlib # responses + # singlestoredb # snowflake-connector-python # sphinx # trino @@ -736,8 +742,10 @@ setuptools==70.0.0 # grpcio-tools # kubernetes # pip-tools + # singlestoredb shellingham==1.5.4 # via typer +singlestoredb==1.3.1 six==1.16.0 # via # asttokens @@ -785,6 +793,8 @@ sqlalchemy-views==0.3.2 sqlglot==20.11.0 # via ibis-framework sqlite-vec==0.0.1a10 +sqlparams==6.0.1 + # via singlestoredb stack-data==0.6.3 # via ipython starlette==0.37.2 @@ -925,7 +935,9 @@ websockets==12.0 werkzeug==3.0.3 # via moto wheel==0.43.0 - # via pip-tools + # via + # pip-tools + # singlestoredb widgetsnbextension==4.0.11 # via ipywidgets wrapt==1.16.0 diff --git a/sdk/python/requirements/py3.9-ci-requirements.txt b/sdk/python/requirements/py3.9-ci-requirements.txt index 135b65a0cc..d7df488a88 100644 --- a/sdk/python/requirements/py3.9-ci-requirements.txt +++ b/sdk/python/requirements/py3.9-ci-requirements.txt @@ -68,7 +68,9 @@ botocore==1.34.99 # moto # s3transfer build==1.2.1 - # via pip-tools + # via + # pip-tools + # singlestoredb cachecontrol==0.14.0 # via firebase-admin cachetools==5.3.3 @@ -514,6 +516,8 @@ pandas==2.2.2 # snowflake-connector-python pandocfilters==1.5.1 # via nbconvert +parsimonious==0.10.0 + # via singlestoredb parso==0.8.4 # via jedi parsy==2.1 @@ -619,6 +623,7 @@ pygments==2.18.0 pyjwt[crypto]==2.8.0 # via # msal + # singlestoredb # snowflake-connector-python pymssql==2.3.0 pymysql==1.1.1 @@ -714,6 +719,7 @@ requests==2.31.0 # msal # requests-oauthlib # responses + # singlestoredb # snowflake-connector-python # sphinx # trino @@ -756,8 +762,10 @@ setuptools==70.0.0 # grpcio-tools # kubernetes # pip-tools + # singlestoredb shellingham==1.5.4 # via typer +singlestoredb==1.3.1 six==1.16.0 # via # asttokens @@ -805,6 +813,8 @@ sqlalchemy-views==0.3.2 sqlglot==20.11.0 # via ibis-framework sqlite-vec==0.0.1a10 +sqlparams==6.0.1 + # via singlestoredb stack-data==0.6.3 # via ipython starlette==0.37.2 @@ -832,6 +842,7 @@ tomli==2.0.1 # pip-tools # pytest # pytest-env + # singlestoredb tomlkit==0.12.5 # via snowflake-connector-python toolz==0.12.1 @@ -960,7 +971,9 @@ websockets==12.0 werkzeug==3.0.3 # via moto wheel==0.43.0 - # via pip-tools + # via + # pip-tools + # singlestoredb widgetsnbextension==4.0.11 # via ipywidgets wrapt==1.16.0 diff --git a/sdk/python/tests/integration/feature_repos/universal/online_store/singlestore.py b/sdk/python/tests/integration/feature_repos/universal/online_store/singlestore.py new file mode 100644 index 0000000000..d3a02421d0 --- /dev/null +++ b/sdk/python/tests/integration/feature_repos/universal/online_store/singlestore.py @@ -0,0 +1,43 @@ +import subprocess +import time +from typing import Dict + +from testcontainers.core.container import DockerContainer + +from tests.integration.feature_repos.universal.online_store_creator import ( + OnlineStoreCreator, +) + + +class SingleStoreOnlineStoreCreator(OnlineStoreCreator): + def __init__(self, project_name: str, **kwargs): + super().__init__(project_name) + self.container = ( + DockerContainer("ghcr.io/singlestore-labs/singlestoredb-dev:latest") + .with_exposed_ports(3306) + .with_env("USER", "root") + .with_env("ROOT_PASSWORD", "test") + # this license key is authorized solely for use in SingleStore Feast tests and is subject to strict usage restrictions + # if you want a free SingleStore license for your own use please visit https://www.singlestore.com/cloud-trial/ + .with_env( + "LICENSE_KEY", + "BGIxODZiYTg1YWUxYjRlODRhYzRjMGFmYTA1OTkxYzgyAAAAAAAAAAABAAAAAAAAACgwNQIZANx4NIXJ7CWvKYYb3wIyRXxBY7fdAnLeSwIYLy2Q0jA124GAkl04yuGrD59Zpv85DVYXAA==", + ) + ) + + def create_online_store(self) -> Dict[str, str]: + self.container.start() + time.sleep(30) + exposed_port = self.container.get_exposed_port("3306") + command = f"mysql -uroot -ptest -P {exposed_port} -e 'CREATE DATABASE feast;'" + subprocess.run(command, shell=True, check=True) + return { + "type": "singlestore", + "user": "root", + "password": "test", + "database": "feast", + "port": exposed_port, + } + + def teardown(self): + self.container.stop() diff --git a/setup.py b/setup.py index f954f19898..cffd91a0c5 100644 --- a/setup.py +++ b/setup.py @@ -155,6 +155,8 @@ ELASTICSEARCH_REQUIRED = ["elasticsearch>=8.13.0"] +SINGLESTORE_REQUIRED = ["singlestoredb"] + CI_REQUIRED = ( [ "build", @@ -218,6 +220,7 @@ + DELTA_REQUIRED + ELASTICSEARCH_REQUIRED + SQLITE_VEC_REQUIRED + + SINGLESTORE_REQUIRED ) DOCS_REQUIRED = CI_REQUIRED @@ -386,6 +389,7 @@ def run(self): "delta": DELTA_REQUIRED, "elasticsearch": ELASTICSEARCH_REQUIRED, "sqlite_vec": SQLITE_VEC_REQUIRED, + "singlestore": SINGLESTORE_REQUIRED, }, include_package_data=True, license="Apache", From 43e198f6945c5e868ade341309f2c5ca39ac563e Mon Sep 17 00:00:00 2001 From: "bdodla@expedia.com" <13788369+EXPEbdodla@users.noreply.github.com> Date: Fri, 28 Jun 2024 17:58:35 -0700 Subject: [PATCH 11/59] fix: CGO Memory leak issue in GO Feature server (#4291) --- go/internal/feast/server/http_server.go | 24 +++++++++++++++---- .../feast/transformation/transformation.go | 12 ++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/go/internal/feast/server/http_server.go b/go/internal/feast/server/http_server.go index 75cdbe9929..7ebab429e7 100644 --- a/go/internal/feast/server/http_server.go +++ b/go/internal/feast/server/http_server.go @@ -4,14 +4,16 @@ import ( "context" "encoding/json" "fmt" + "net/http" + "time" + "github.com/feast-dev/feast/go/internal/feast" "github.com/feast-dev/feast/go/internal/feast/model" + "github.com/feast-dev/feast/go/internal/feast/onlineserving" "github.com/feast-dev/feast/go/internal/feast/server/logging" "github.com/feast-dev/feast/go/protos/feast/serving" prototypes "github.com/feast-dev/feast/go/protos/feast/types" "github.com/feast-dev/feast/go/types" - "net/http" - "time" ) type httpServer struct { @@ -210,6 +212,8 @@ func (s *httpServer) getOnlineFeatures(w http.ResponseWriter, r *http.Request) { "results": results, } + w.Header().Set("Content-Type", "application/json") + err = json.NewEncoder(w).Encode(response) if err != nil { @@ -217,8 +221,6 @@ func (s *httpServer) getOnlineFeatures(w http.ResponseWriter, r *http.Request) { return } - w.Header().Set("Content-Type", "application/json") - if featureService != nil && featureService.LoggingConfig != nil && s.loggingService != nil { logger, err := s.loggingService.GetOrCreateLogger(featureService) if err != nil { @@ -250,11 +252,19 @@ func (s *httpServer) getOnlineFeatures(w http.ResponseWriter, r *http.Request) { return } } + go releaseCGOMemory(featureVectors) +} + +func releaseCGOMemory(featureVectors []*onlineserving.FeatureVector) { + for _, vector := range featureVectors { + vector.Values.Release() + } } func (s *httpServer) Serve(host string, port int) error { s.server = &http.Server{Addr: fmt.Sprintf("%s:%d", host, port), Handler: nil} http.HandleFunc("/get-online-features", s.getOnlineFeatures) + http.HandleFunc("/health", healthCheckHandler) err := s.server.ListenAndServe() // Don't return the error if it's caused by graceful shutdown using Stop() if err == http.ErrServerClosed { @@ -262,6 +272,12 @@ func (s *httpServer) Serve(host string, port int) error { } return err } + +func healthCheckHandler(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, "Healthy") +} + func (s *httpServer) Stop() error { if s.server != nil { return s.server.Shutdown(context.Background()) diff --git a/go/internal/feast/transformation/transformation.go b/go/internal/feast/transformation/transformation.go index 1cf1dd3311..7e63aec224 100644 --- a/go/internal/feast/transformation/transformation.go +++ b/go/internal/feast/transformation/transformation.go @@ -46,6 +46,7 @@ func AugmentResponseWithOnDemandTransforms( for name, values := range requestData { requestContextArrow[name], err = types.ProtoValuesToArrowArray(values.Val, arrowMemory, numRows) if err != nil { + ReleaseArrowContext(requestContextArrow) return nil, err } } @@ -53,6 +54,7 @@ func AugmentResponseWithOnDemandTransforms( for name, values := range entityRows { requestContextArrow[name], err = types.ProtoValuesToArrowArray(values.Val, arrowMemory, numRows) if err != nil { + ReleaseArrowContext(requestContextArrow) return nil, err } } @@ -71,14 +73,24 @@ func AugmentResponseWithOnDemandTransforms( fullFeatureNames, ) if err != nil { + ReleaseArrowContext(requestContextArrow) return nil, err } result = append(result, onDemandFeatures...) + + ReleaseArrowContext(requestContextArrow) } return result, nil } +func ReleaseArrowContext(requestContextArrow map[string]arrow.Array) { + // Release memory used by requestContextArrow + for _, arrowArray := range requestContextArrow { + arrowArray.Release() + } +} + func CallTransformations( featureView *model.OnDemandFeatureView, retrievedFeatures map[string]arrow.Array, From 9451d9ca15f234e8e16e81351294fd63b33c1af2 Mon Sep 17 00:00:00 2001 From: Job Almekinders <55230856+job-almekinders@users.noreply.github.com> Date: Mon, 1 Jul 2024 23:04:59 +0200 Subject: [PATCH 12/59] feat: Bump psycopg2 to psycopg3 for all Postgres components (#4303) * Makefile: Formatting Signed-off-by: Job Almekinders * Makefile: Exclude Snowflake tests for postgres offline store tests Signed-off-by: Job Almekinders * Bootstrap: Use conninfo Signed-off-by: Job Almekinders * Tests: Make connection string compatible with psycopg3 Signed-off-by: Job Almekinders * Tests: Test connection type pool and singleton Signed-off-by: Job Almekinders * Global: Replace conn.set_session() calls to be psycopg3 compatible Set connection read only Signed-off-by: Job Almekinders * Offline: Use psycopg3 Signed-off-by: Job Almekinders * Online: Use psycopg3 Signed-off-by: Job Almekinders * Online: Restructure online_write_batch Addition Signed-off-by: Job Almekinders * Online: Use correct placeholder Signed-off-by: Job Almekinders * Online: Handle bytes properly in online_read() Signed-off-by: Job Almekinders * Online: Whitespace Signed-off-by: Job Almekinders * Online: Open ConnectionPool Signed-off-by: Job Almekinders * Online: Add typehint Signed-off-by: Job Almekinders * Utils: Use psycopg3 Use new ConnectionPool Pass kwargs as named argument Use executemany over execute_values Remove not-required open argument in psycopg.connect Improve Use SpooledTemporaryFile Use max_size and add docstring Properly write with StringIO Utils: Use SpooledTemporaryFile over StringIO object Add replace Fix df_to_postgres_table Remove import Utils Signed-off-by: Job Almekinders * Lint: Raise exceptions if cursor returned no columns or rows Add log statement Lint: Fix _to_arrow_internal Lint: Fix _get_entity_df_event_timestamp_range Update exception Use ZeroColumnQueryResult Signed-off-by: Job Almekinders * Add comment on +psycopg string Signed-off-by: Job Almekinders * Docs: Remove mention of psycopg2 Signed-off-by: Job Almekinders * Lint: Fix Signed-off-by: Job Almekinders * Default to postgresql+psycopg and log warning Update warning Fix Format warning Add typehints Use better variable name Signed-off-by: Job Almekinders * Solve merge conflicts Signed-off-by: Job Almekinders --------- Signed-off-by: Job Almekinders --- Makefile | 7 +- docs/tutorials/using-scalable-registry.md | 2 +- sdk/python/feast/errors.py | 10 + .../postgres_offline_store/postgres.py | 27 +- .../postgres_offline_store/postgres_source.py | 8 +- .../infra/online_stores/contrib/postgres.py | 120 +++---- .../infra/utils/postgres/connection_utils.py | 85 ++--- sdk/python/feast/repo_config.py | 16 + .../feast/templates/postgres/bootstrap.py | 16 +- .../requirements/py3.10-ci-requirements.txt | 297 ++++++++++++------ .../requirements/py3.10-requirements.txt | 48 ++- .../requirements/py3.11-ci-requirements.txt | 295 ++++++++++++----- .../requirements/py3.11-requirements.txt | 48 ++- .../requirements/py3.9-ci-requirements.txt | 291 ++++++++++++----- .../requirements/py3.9-requirements.txt | 48 ++- .../online_store/test_universal_online.py | 9 +- .../registration/test_universal_registry.py | 4 +- setup.py | 2 +- 18 files changed, 925 insertions(+), 408 deletions(-) diff --git a/Makefile b/Makefile index 39406cc17d..d2fbb34e1f 100644 --- a/Makefile +++ b/Makefile @@ -65,7 +65,7 @@ install-python: 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)-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" @@ -164,7 +164,7 @@ test-python-universal-mssql: sdk/python/tests -# To use Athena as an offline store, you need to create an Athena database and an S3 bucket on AWS. +# To use Athena as an offline store, you need to create an Athena database and an S3 bucket on AWS. # https://docs.aws.amazon.com/athena/latest/ug/getting-started.html # Modify environment variables ATHENA_REGION, ATHENA_DATA_SOURCE, ATHENA_DATABASE, ATHENA_WORKGROUP or # ATHENA_S3_BUCKET_NAME according to your needs. If tests fail with the pytest -n 8 option, change the number to 1. @@ -191,7 +191,7 @@ test-python-universal-athena: not s3_registry and \ not test_snowflake" \ sdk/python/tests - + test-python-universal-postgres-offline: PYTHONPATH='.' \ FULL_REPO_CONFIGS_MODULE=sdk.python.feast.infra.offline_stores.contrib.postgres_repo_configuration \ @@ -209,6 +209,7 @@ test-python-universal-postgres-offline: not test_push_features_to_offline_store and \ not gcs_registry and \ not s3_registry and \ + not test_snowflake and \ not test_universal_types" \ sdk/python/tests diff --git a/docs/tutorials/using-scalable-registry.md b/docs/tutorials/using-scalable-registry.md index 30b8e01ed5..25746f60e2 100644 --- a/docs/tutorials/using-scalable-registry.md +++ b/docs/tutorials/using-scalable-registry.md @@ -49,7 +49,7 @@ When this happens, your database is likely using what is referred to as an in `SQLAlchemy` terminology. See your database's documentation for examples on how to set its scheme in the Database URL. -`Psycopg2`, which is the database library leveraged by the online and offline +`Psycopg`, which is the database library leveraged by the online and offline stores, is not impacted by the need to speak a particular dialect, and so the following only applies to the registry. diff --git a/sdk/python/feast/errors.py b/sdk/python/feast/errors.py index 6083b3d554..c4c1157626 100644 --- a/sdk/python/feast/errors.py +++ b/sdk/python/feast/errors.py @@ -389,3 +389,13 @@ def __init__(self, input_dict: dict): super().__init__( f"Failed to serialize the provided dictionary into a pandas DataFrame: {input_dict.keys()}" ) + + +class ZeroRowsQueryResult(Exception): + def __init__(self, query: str): + super().__init__(f"This query returned zero rows:\n{query}") + + +class ZeroColumnQueryResult(Exception): + def __init__(self, query: str): + super().__init__(f"This query returned zero columns:\n{query}") 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 cb08b5f016..c4740a960e 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 @@ -19,11 +19,11 @@ import pandas as pd import pyarrow as pa from jinja2 import BaseLoader, Environment -from psycopg2 import sql +from psycopg import sql from pytz import utc from feast.data_source import DataSource -from feast.errors import InvalidEntityType +from feast.errors import InvalidEntityType, ZeroColumnQueryResult, ZeroRowsQueryResult from feast.feature_view import DUMMY_ENTITY_ID, DUMMY_ENTITY_VAL, FeatureView from feast.infra.offline_stores import offline_utils from feast.infra.offline_stores.contrib.postgres_offline_store.postgres_source import ( @@ -274,8 +274,10 @@ def to_sql(self) -> str: def _to_arrow_internal(self, timeout: Optional[int] = None) -> pa.Table: with self._query_generator() as query: with _get_conn(self.config.offline_store) as conn, conn.cursor() as cur: - conn.set_session(readonly=True) + conn.read_only = True cur.execute(query) + if not cur.description: + raise ZeroColumnQueryResult(query) fields = [ (c.name, pg_type_code_to_arrow(c.type_code)) for c in cur.description @@ -331,16 +333,19 @@ def _get_entity_df_event_timestamp_range( entity_df_event_timestamp.max().to_pydatetime(), ) elif isinstance(entity_df, str): - # If the entity_df is a string (SQL query), determine range - # from table + # If the entity_df is a string (SQL query), determine range from table with _get_conn(config.offline_store) as conn, conn.cursor() as cur: - ( - cur.execute( - f"SELECT MIN({entity_df_event_timestamp_col}) AS min, MAX({entity_df_event_timestamp_col}) AS max FROM ({entity_df}) as tmp_alias" - ), - ) + query = f""" + SELECT + MIN({entity_df_event_timestamp_col}) AS min, + MAX({entity_df_event_timestamp_col}) AS max + FROM ({entity_df}) AS tmp_alias + """ + cur.execute(query) res = cur.fetchone() - entity_df_event_timestamp_range = (res[0], res[1]) + if not res: + raise ZeroRowsQueryResult(query) + entity_df_event_timestamp_range = (res[0], res[1]) else: raise InvalidEntityType(type(entity_df)) diff --git a/sdk/python/feast/infra/offline_stores/contrib/postgres_offline_store/postgres_source.py b/sdk/python/feast/infra/offline_stores/contrib/postgres_offline_store/postgres_source.py index bbb3f768fd..c216328b8d 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/postgres_offline_store/postgres_source.py +++ b/sdk/python/feast/infra/offline_stores/contrib/postgres_offline_store/postgres_source.py @@ -4,7 +4,7 @@ from typeguard import typechecked from feast.data_source import DataSource -from feast.errors import DataSourceNoNameException +from feast.errors import DataSourceNoNameException, ZeroColumnQueryResult from feast.infra.utils.postgres.connection_utils import _get_conn from feast.protos.feast.core.DataSource_pb2 import DataSource as DataSourceProto from feast.protos.feast.core.SavedDataset_pb2 import ( @@ -111,7 +111,11 @@ def get_table_column_names_and_types( self, config: RepoConfig ) -> Iterable[Tuple[str, str]]: with _get_conn(config.offline_store) as conn, conn.cursor() as cur: - cur.execute(f"SELECT * FROM {self.get_table_query_string()} AS sub LIMIT 0") + query = f"SELECT * FROM {self.get_table_query_string()} AS sub LIMIT 0" + cur.execute(query) + if not cur.description: + raise ZeroColumnQueryResult(query) + return ( (c.name, pg_type_code_to_pg_type(c.type_code)) for c in cur.description ) diff --git a/sdk/python/feast/infra/online_stores/contrib/postgres.py b/sdk/python/feast/infra/online_stores/contrib/postgres.py index 3eddd8ba20..8715f0f65b 100644 --- a/sdk/python/feast/infra/online_stores/contrib/postgres.py +++ b/sdk/python/feast/infra/online_stores/contrib/postgres.py @@ -2,13 +2,22 @@ import logging from collections import defaultdict from datetime import datetime -from typing import Any, Callable, Dict, List, Literal, Optional, Sequence, Tuple +from typing import ( + Any, + Callable, + Dict, + Generator, + List, + Literal, + Optional, + Sequence, + Tuple, +) -import psycopg2 import pytz -from psycopg2 import sql -from psycopg2.extras import execute_values -from psycopg2.pool import SimpleConnectionPool +from psycopg import sql +from psycopg.connection import Connection +from psycopg_pool import ConnectionPool from feast import Entity from feast.feature_view import FeatureView @@ -39,15 +48,17 @@ class PostgreSQLOnlineStoreConfig(PostgreSQLConfig): class PostgreSQLOnlineStore(OnlineStore): - _conn: Optional[psycopg2._psycopg.connection] = None - _conn_pool: Optional[SimpleConnectionPool] = None + _conn: Optional[Connection] = None + _conn_pool: Optional[ConnectionPool] = None @contextlib.contextmanager - def _get_conn(self, config: RepoConfig): + def _get_conn(self, config: RepoConfig) -> Generator[Connection, Any, Any]: assert config.online_store.type == "postgres" + if config.online_store.conn_type == ConnectionType.pool: if not self._conn_pool: self._conn_pool = _get_connection_pool(config.online_store) + self._conn_pool.open() connection = self._conn_pool.getconn() yield connection self._conn_pool.putconn(connection) @@ -64,57 +75,56 @@ def online_write_batch( Tuple[EntityKeyProto, Dict[str, ValueProto], datetime, Optional[datetime]] ], progress: Optional[Callable[[int], Any]], + batch_size: int = 5000, ) -> None: - project = config.project + # Format insert values + insert_values = [] + for entity_key, values, timestamp, created_ts in data: + entity_key_bin = serialize_entity_key( + entity_key, + entity_key_serialization_version=config.entity_key_serialization_version, + ) + timestamp = _to_naive_utc(timestamp) + if created_ts is not None: + created_ts = _to_naive_utc(created_ts) - with self._get_conn(config) as conn, conn.cursor() as cur: - insert_values = [] - for entity_key, values, timestamp, created_ts in data: - entity_key_bin = serialize_entity_key( - entity_key, - entity_key_serialization_version=config.entity_key_serialization_version, - ) - timestamp = _to_naive_utc(timestamp) - if created_ts is not None: - created_ts = _to_naive_utc(created_ts) - - for feature_name, val in values.items(): - vector_val = None - if config.online_store.pgvector_enabled: - vector_val = get_list_val_str(val) - insert_values.append( - ( - entity_key_bin, - feature_name, - val.SerializeToString(), - vector_val, - timestamp, - created_ts, - ) + for feature_name, val in values.items(): + vector_val = None + if config.online_store.pgvector_enabled: + vector_val = get_list_val_str(val) + insert_values.append( + ( + entity_key_bin, + feature_name, + val.SerializeToString(), + vector_val, + timestamp, + created_ts, ) - # Control the batch so that we can update the progress - batch_size = 5000 + ) + + # Create insert query + sql_query = sql.SQL( + """ + INSERT INTO {} + (entity_key, feature_name, value, vector_value, event_ts, created_ts) + VALUES (%s, %s, %s, %s, %s, %s) + ON CONFLICT (entity_key, feature_name) DO + UPDATE SET + value = EXCLUDED.value, + vector_value = EXCLUDED.vector_value, + event_ts = EXCLUDED.event_ts, + created_ts = EXCLUDED.created_ts; + """ + ).format(sql.Identifier(_table_id(config.project, table))) + + # Push data in batches to online store + with self._get_conn(config) as conn, conn.cursor() as cur: for i in range(0, len(insert_values), batch_size): cur_batch = insert_values[i : i + batch_size] - execute_values( - cur, - sql.SQL( - """ - INSERT INTO {} - (entity_key, feature_name, value, vector_value, event_ts, created_ts) - VALUES %s - ON CONFLICT (entity_key, feature_name) DO - UPDATE SET - value = EXCLUDED.value, - vector_value = EXCLUDED.vector_value, - event_ts = EXCLUDED.event_ts, - created_ts = EXCLUDED.created_ts; - """, - ).format(sql.Identifier(_table_id(project, table))), - cur_batch, - page_size=batch_size, - ) + cur.executemany(sql_query, cur_batch) conn.commit() + if progress: progress(len(cur_batch)) @@ -172,7 +182,9 @@ def online_read( # when we iterate through the keys since they are in the correct order values_dict = defaultdict(list) for row in rows if rows is not None else []: - values_dict[row[0].tobytes()].append(row[1:]) + values_dict[ + row[0] if isinstance(row[0], bytes) else row[0].tobytes() + ].append(row[1:]) for key in keys: if key in values_dict: diff --git a/sdk/python/feast/infra/utils/postgres/connection_utils.py b/sdk/python/feast/infra/utils/postgres/connection_utils.py index 0d99c8ab99..e0599019b9 100644 --- a/sdk/python/feast/infra/utils/postgres/connection_utils.py +++ b/sdk/python/feast/infra/utils/postgres/connection_utils.py @@ -1,50 +1,59 @@ -from typing import Dict +from typing import Any, Dict import numpy as np import pandas as pd -import psycopg2 -import psycopg2.extras +import psycopg import pyarrow as pa -from psycopg2.pool import SimpleConnectionPool +from psycopg.connection import Connection +from psycopg_pool import ConnectionPool from feast.infra.utils.postgres.postgres_config import PostgreSQLConfig from feast.type_map import arrow_to_pg_type -def _get_conn(config: PostgreSQLConfig): - conn = psycopg2.connect( - dbname=config.database, - host=config.host, - port=int(config.port), - user=config.user, - password=config.password, - sslmode=config.sslmode, - sslkey=config.sslkey_path, - sslcert=config.sslcert_path, - sslrootcert=config.sslrootcert_path, - options="-c search_path={}".format(config.db_schema or config.user), +def _get_conn(config: PostgreSQLConfig) -> Connection: + """Get a psycopg `Connection`.""" + conn = psycopg.connect( + conninfo=_get_conninfo(config), keepalives_idle=config.keepalives_idle, + **_get_conn_kwargs(config), ) return conn -def _get_connection_pool(config: PostgreSQLConfig): - return SimpleConnectionPool( - config.min_conn, - config.max_conn, - dbname=config.database, - host=config.host, - port=int(config.port), - user=config.user, - password=config.password, - sslmode=config.sslmode, - sslkey=config.sslkey_path, - sslcert=config.sslcert_path, - sslrootcert=config.sslrootcert_path, - options="-c search_path={}".format(config.db_schema or config.user), +def _get_connection_pool(config: PostgreSQLConfig) -> ConnectionPool: + """Get a psycopg `ConnectionPool`.""" + return ConnectionPool( + conninfo=_get_conninfo(config), + min_size=config.min_conn, + max_size=config.max_conn, + open=False, + kwargs=_get_conn_kwargs(config), ) +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}" + ) + + +def _get_conn_kwargs(config: PostgreSQLConfig) -> Dict[str, Any]: + """Get the additional `kwargs` required for connection objects.""" + return { + "sslmode": config.sslmode, + "sslkey": config.sslkey_path, + "sslcert": config.sslcert_path, + "sslrootcert": config.sslrootcert_path, + "options": "-c search_path={}".format(config.db_schema or config.user), + } + + def _df_to_create_table_sql(entity_df, table_name) -> str: pa_table = pa.Table.from_pandas(entity_df) columns = [ @@ -63,16 +72,14 @@ def df_to_postgres_table( """ Create a table for the data frame, insert all the values, and return the table schema """ + nr_columns = df.shape[1] + placeholders = ", ".join(["%s"] * nr_columns) + query = f"INSERT INTO {table_name} VALUES ({placeholders})" + values = df.replace({np.NaN: None}).to_numpy().tolist() + with _get_conn(config) as conn, conn.cursor() as cur: cur.execute(_df_to_create_table_sql(df, table_name)) - psycopg2.extras.execute_values( - cur, - f""" - INSERT INTO {table_name} - VALUES %s - """, - df.replace({np.NaN: None}).to_numpy(), - ) + cur.executemany(query, values) return dict(zip(df.columns, df.dtypes)) @@ -82,7 +89,7 @@ def get_query_schema(config: PostgreSQLConfig, sql_query: str) -> Dict[str, np.d new table """ with _get_conn(config) as conn: - conn.set_session(readonly=True) + conn.read_only = True df = pd.read_sql( f"SELECT * FROM {sql_query} LIMIT 0", conn, diff --git a/sdk/python/feast/repo_config.py b/sdk/python/feast/repo_config.py index f3c379020d..8d6bff2818 100644 --- a/sdk/python/feast/repo_config.py +++ b/sdk/python/feast/repo_config.py @@ -12,6 +12,7 @@ StrictInt, StrictStr, ValidationError, + ValidationInfo, field_validator, model_validator, ) @@ -123,6 +124,21 @@ class RegistryConfig(FeastBaseModel): sqlalchemy_config_kwargs: Dict[str, Any] = {} """ Dict[str, Any]: Extra arguments to pass to SQLAlchemy.create_engine. """ + @field_validator("path") + def validate_path(cls, path: str, values: ValidationInfo) -> str: + if values.data.get("registry_type") == "sql": + if path.startswith("postgresql://"): + _logger.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`." + ) + return path.replace("postgresql://", "postgresql+psycopg://") + return path + class RepoConfig(FeastBaseModel): """Repo config. Typically loaded from `feature_store.yaml`""" diff --git a/sdk/python/feast/templates/postgres/bootstrap.py b/sdk/python/feast/templates/postgres/bootstrap.py index 9f6e8a988d..6ed13e4e39 100644 --- a/sdk/python/feast/templates/postgres/bootstrap.py +++ b/sdk/python/feast/templates/postgres/bootstrap.py @@ -1,5 +1,5 @@ import click -import psycopg2 +import psycopg from feast.file_utils import replace_str_in_file from feast.infra.utils.postgres.connection_utils import df_to_postgres_table @@ -34,12 +34,14 @@ def bootstrap(): 'Should I upload example data to Postgres (overwriting "feast_driver_hourly_stats" table)?', default=True, ): - db_connection = psycopg2.connect( - dbname=postgres_database, - host=postgres_host, - port=int(postgres_port), - user=postgres_user, - password=postgres_password, + db_connection = psycopg.connect( + conninfo=( + f"postgresql://{postgres_user}" + f":{postgres_password}" + f"@{postgres_host}" + f":{int(postgres_port)}" + f"/{postgres_database}" + ), options=f"-c search_path={postgres_schema}", ) diff --git a/sdk/python/requirements/py3.10-ci-requirements.txt b/sdk/python/requirements/py3.10-ci-requirements.txt index a0faf3d9ef..3aa7130ccf 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.0 +aiobotocore==2.13.1 + # via feast (setup.py) aiohttp==3.9.5 # via aiobotocore aioitertools==0.11.0 @@ -11,14 +12,16 @@ alabaster==0.7.16 # via sphinx altair==4.2.2 # via great-expectations -annotated-types==0.6.0 +annotated-types==0.7.0 # via pydantic -anyio==4.3.0 +anyio==4.4.0 # via # httpx # 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 @@ -43,12 +47,14 @@ attrs==23.2.0 # aiohttp # jsonschema # referencing -azure-core==1.30.1 +azure-core==1.30.2 # via # azure-identity # azure-storage-blob -azure-identity==1.16.0 +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 @@ -59,9 +65,11 @@ bidict==0.23.1 # via ibis-framework bleach==6.1.0 # via nbconvert -boto3==1.34.99 - # via moto -botocore==1.34.99 +boto3==1.34.131 + # via + # feast (setup.py) + # moto +botocore==1.34.131 # via # aiobotocore # boto3 @@ -69,6 +77,7 @@ botocore==1.34.99 # s3transfer build==1.2.1 # via + # feast (setup.py) # pip-tools # singlestoredb cachecontrol==0.14.0 @@ -76,7 +85,8 @@ cachecontrol==0.14.0 cachetools==5.3.3 # via google-auth cassandra-driver==3.29.1 -certifi==2024.2.2 + # via feast (setup.py) +certifi==2024.6.2 # via # elastic-transport # httpcore @@ -98,6 +108,7 @@ charset-normalizer==3.3.2 # snowflake-connector-python click==8.1.7 # via + # feast (setup.py) # dask # geomet # great-expectations @@ -107,15 +118,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.5.3 +coverage[toml]==7.5.4 # via pytest-cov -cryptography==42.0.7 +cryptography==42.0.8 # via + # feast (setup.py) # azure-identity # azure-storage-blob # great-expectations @@ -126,20 +140,24 @@ cryptography==42.0.7 # snowflake-connector-python # types-pyopenssl # types-redis -dask[dataframe]==2024.5.0 - # via dask-expr -dask-expr==1.1.0 +dask[dataframe]==2024.6.2 + # via + # feast (setup.py) + # dask-expr +dask-expr==1.1.6 # via dask db-dtypes==1.2.0 # via google-cloud-bigquery -debugpy==1.8.1 +debugpy==1.8.2 # via ipykernel decorator==5.1.1 # via ipython defusedxml==0.7.1 # via nbconvert -deltalake==0.17.3 +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 @@ -152,12 +170,13 @@ duckdb==0.10.3 # via # duckdb-engine # ibis-framework -duckdb-engine==0.12.1 +duckdb-engine==0.13.0 # via ibis-framework elastic-transport==8.13.1 # via elasticsearch -elasticsearch==8.13.2 -email-validator==2.1.1 +elasticsearch==8.14.0 + # via feast (setup.py) +email-validator==2.2.0 # via fastapi entrypoints==0.4 # via altair @@ -171,16 +190,17 @@ execnet==2.1.1 executing==2.0.1 # via stack-data fastapi==0.111.0 - # via fastapi-cli -fastapi-cli==0.0.2 + # via feast (setup.py) +fastapi-cli==0.0.4 # via fastapi -fastjsonschema==2.19.1 +fastjsonschema==2.20.0 # via nbformat -filelock==3.14.0 +filelock==3.15.4 # via # snowflake-connector-python # virtualenv firebase-admin==5.4.0 + # via feast (setup.py) fqdn==1.5.1 # via jsonschema frozenlist==1.4.1 @@ -188,13 +208,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.0 +google-api-core[grpc]==2.19.1 # via + # feast (setup.py) # firebase-admin # google-api-python-client # google-cloud-bigquery @@ -204,9 +227,9 @@ google-api-core[grpc]==2.19.0 # google-cloud-datastore # google-cloud-firestore # google-cloud-storage -google-api-python-client==2.131.0 +google-api-python-client==2.134.0 # via firebase-admin -google-auth==2.29.0 +google-auth==2.30.0 # via # google-api-core # google-api-python-client @@ -219,8 +242,11 @@ google-auth==2.29.0 google-auth-httplib2==0.2.0 # via google-api-python-client google-cloud-bigquery[pandas]==3.12.0 + # via feast (setup.py) google-cloud-bigquery-storage==2.25.0 -google-cloud-bigtable==2.23.1 + # via feast (setup.py) +google-cloud-bigtable==2.24.0 + # via feast (setup.py) google-cloud-core==2.4.1 # via # google-cloud-bigquery @@ -229,30 +255,34 @@ google-cloud-core==2.4.1 # google-cloud-firestore # google-cloud-storage google-cloud-datastore==2.19.0 + # via feast (setup.py) google-cloud-firestore==2.16.0 # via firebase-admin -google-cloud-storage==2.16.0 - # via firebase-admin +google-cloud-storage==2.17.0 + # via + # feast (setup.py) + # firebase-admin google-crc32c==1.5.0 # via # google-cloud-storage # google-resumable-media -google-resumable-media==2.7.0 +google-resumable-media==2.7.1 # via # google-cloud-bigquery # google-cloud-storage -googleapis-common-protos[grpc]==1.63.0 +googleapis-common-protos[grpc]==1.63.2 # via + # feast (setup.py) # google-api-core # grpc-google-iam-v1 # grpcio-status -great-expectations==0.18.15 -greenlet==3.0.3 - # via sqlalchemy -grpc-google-iam-v1==0.13.0 +great-expectations==0.18.16 + # via feast (setup.py) +grpc-google-iam-v1==0.13.1 # via google-cloud-bigtable -grpcio==1.64.0 +grpcio==1.64.1 # via + # feast (setup.py) # google-api-core # google-cloud-bigquery # googleapis-common-protos @@ -263,19 +293,27 @@ grpcio==1.64.0 # 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 httplib2==0.22.0 @@ -286,11 +324,15 @@ httptools==0.6.1 # via uvicorn httpx==0.27.0 # via + # feast (setup.py) # fastapi # jupyterlab ibis-framework[duckdb]==8.0.0 - # via ibis-substrait + # via + # feast (setup.py) + # ibis-substrait ibis-substrait==3.2.0 + # via feast (setup.py) identify==2.5.36 # via pre-commit idna==3.7 @@ -304,7 +346,7 @@ idna==3.7 # yarl imagesize==1.4.1 # via sphinx -importlib-metadata==7.1.0 +importlib-metadata==8.0.0 # via dask iniconfig==2.0.0 # via pytest @@ -325,6 +367,7 @@ jedi==0.19.1 # via ipython jinja2==3.1.4 # via + # feast (setup.py) # altair # fastapi # great-expectations @@ -342,12 +385,13 @@ json5==0.9.25 # via jupyterlab-server jsonpatch==1.33 # via great-expectations -jsonpointer==2.4 +jsonpointer==3.0.0 # via # jsonpatch # jsonschema jsonschema[format-nongpl]==4.22.0 # via + # feast (setup.py) # altair # great-expectations # jupyter-events @@ -382,7 +426,7 @@ jupyter-server==2.14.1 # notebook-shim jupyter-server-terminals==0.5.3 # via jupyter-server -jupyterlab==4.2.1 +jupyterlab==4.2.3 # via notebook jupyterlab-pygments==0.3.0 # via nbconvert @@ -393,6 +437,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 @@ -404,7 +449,7 @@ markupsafe==2.1.5 # jinja2 # nbconvert # werkzeug -marshmallow==3.21.2 +marshmallow==3.21.3 # via great-expectations matplotlib-inline==0.1.7 # via @@ -413,18 +458,22 @@ 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 -msal==1.28.0 + # via feast (setup.py) +msal==1.29.0 # via # azure-identity # msal-extensions -msal-extensions==1.1.0 +msal-extensions==1.2.0 # via azure-identity msgpack==1.0.8 # via cachecontrol @@ -434,11 +483,14 @@ multidict==6.0.5 # yarl multipledispatch==1.0.0 # via ibis-framework -mypy==1.10.0 - # via sqlalchemy +mypy==1.10.1 + # 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 @@ -451,9 +503,9 @@ nbformat==5.10.4 # nbconvert nest-asyncio==1.6.0 # via ipykernel -nodeenv==1.9.0 +nodeenv==1.9.1 # via pre-commit -notebook==7.2.0 +notebook==7.2.1 # via great-expectations notebook-shim==0.2.4 # via @@ -461,6 +513,7 @@ notebook-shim==0.2.4 # notebook numpy==1.26.4 # via + # feast (setup.py) # altair # dask # db-dtypes @@ -471,11 +524,11 @@ numpy==1.26.4 # scipy oauthlib==3.2.2 # via requests-oauthlib -orjson==3.10.3 +orjson==3.10.5 # via fastapi overrides==7.7.0 # via jupyter-server -packaging==24.0 +packaging==24.1 # via # build # dask @@ -490,13 +543,13 @@ packaging==24.0 # jupyterlab # jupyterlab-server # marshmallow - # msal-extensions # nbconvert # pytest # snowflake-connector-python # sphinx pandas==2.2.2 # via + # feast (setup.py) # altair # dask # dask-expr @@ -519,9 +572,10 @@ pbr==6.0.0 # via mock pexpect==4.9.0 # via ipython -pip==24.0 +pip==24.1.1 # via pip-tools pip-tools==7.4.1 + # via feast (setup.py) platformdirs==3.11.0 # via # jupyter-core @@ -531,14 +585,15 @@ pluggy==1.5.0 # via pytest ply==3.11 # via thriftpy2 -portalocker==2.8.2 +portalocker==2.10.0 # via msal-extensions pre-commit==3.3.1 + # via feast (setup.py) prometheus-client==0.20.0 # via jupyter-server -prompt-toolkit==3.0.45 +prompt-toolkit==3.0.47 # via ipython -proto-plus==1.23.0 +proto-plus==1.24.0 # via # google-api-core # google-cloud-bigquery @@ -548,6 +603,7 @@ proto-plus==1.23.0 # google-cloud-firestore protobuf==4.25.3 # via + # feast (setup.py) # google-api-core # google-cloud-bigquery # google-cloud-bigquery-storage @@ -565,8 +621,15 @@ protobuf==4.25.3 # proto-plus # substrait psutil==5.9.0 - # via ipykernel -psycopg2-binary==2.9.9 + # 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 + # via psycopg ptyprocess==0.7.0 # via # pexpect @@ -574,12 +637,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 @@ -597,16 +662,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.1 +pydantic==2.7.4 # via + # feast (setup.py) # fastapi # great-expectations -pydantic-core==2.18.2 +pydantic-core==2.18.4 # via pydantic pygments==2.18.0 # via + # feast (setup.py) # ipython # nbconvert # rich @@ -617,8 +685,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 @@ -630,8 +701,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 @@ -641,13 +714,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 @@ -676,6 +757,7 @@ pytz==2024.1 # trino pyyaml==6.0.1 # via + # feast (setup.py) # dask # ibis-substrait # jupyter-events @@ -689,14 +771,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.4.28 -requests==2.31.0 +regex==2024.5.15 # via + # feast (setup.py) + # parsimonious +requests==2.32.3 + # via + # feast (setup.py) # azure-core # cachecontrol # docker @@ -716,7 +803,7 @@ requests==2.31.0 # trino requests-oauthlib==2.0.0 # via kubernetes -responses==0.25.0 +responses==0.25.3 # via moto rfc3339-validator==0.1.4 # via @@ -731,6 +818,7 @@ rich==13.7.1 # ibis-framework # typer rockset==2.1.2 + # via feast (setup.py) rpds-py==0.18.1 # via # jsonschema @@ -739,22 +827,25 @@ rsa==4.9 # via google-auth ruamel-yaml==0.17.17 # via great-expectations -ruff==0.4.6 -s3transfer==0.10.1 +ruff==0.4.10 + # via feast (setup.py) +s3transfer==0.10.2 # via boto3 -scipy==1.13.1 +scipy==1.14.0 # via great-expectations send2trash==1.8.3 # via jupyter-server -setuptools==70.0.0 +setuptools==70.1.1 # via # grpcio-tools + # jupyterlab # kubernetes # pip-tools # singlestoredb shellingham==1.5.4 # via typer -singlestoredb==1.3.1 +singlestoredb==1.4.0 + # via feast (setup.py) six==1.16.0 # via # asttokens @@ -774,12 +865,14 @@ sniffio==1.3.1 # httpx snowballstemmer==2.2.0 # via sphinx -snowflake-connector-python[pandas]==3.10.1 +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 @@ -792,8 +885,9 @@ sphinxcontrib-qthelp==1.0.7 # via sphinx sphinxcontrib-serializinghtml==1.1.10 # via sphinx -sqlalchemy[mypy]==2.0.30 +sqlalchemy[mypy]==2.0.31 # via + # feast (setup.py) # duckdb-engine # ibis-framework # sqlalchemy-views @@ -802,6 +896,7 @@ sqlalchemy-views==0.3.2 sqlglot==20.11.0 # via ibis-framework sqlite-vec==0.0.1a10 + # via feast (setup.py) sqlparams==6.0.1 # via singlestoredb stack-data==0.6.3 @@ -811,17 +906,21 @@ starlette==0.37.2 substrait==0.19.0 # via ibis-substrait tabulate==0.9.0 -tenacity==8.3.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 -thriftpy2==0.5.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 @@ -849,7 +948,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 @@ -866,38 +967,55 @@ traitlets==5.14.3 # nbconvert # nbformat trino==0.328.0 -typeguard==4.2.1 + # 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 -types-pymysql==1.1.0.20240425 + # 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 -types-setuptools==70.0.0.20240524 - # via types-cffi + # via feast (setup.py) +types-setuptools==70.1.0.20240627 + # 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.11.0 +typing-extensions==4.12.2 # via # anyio # async-lru # azure-core + # azure-identity # azure-storage-blob # fastapi # great-expectations # ibis-framework # ipython # mypy + # psycopg + # psycopg-pool # pydantic # pydantic-core # snowflake-connector-python @@ -912,14 +1030,15 @@ tzlocal==5.2 # via # great-expectations # trino -ujson==5.9.0 +ujson==5.10.0 # via fastapi uri-template==1.3.0 # via jsonschema uritemplate==4.1.1 # via google-api-python-client -urllib3==1.26.18 +urllib3==1.26.19 # via + # feast (setup.py) # botocore # docker # elastic-transport @@ -930,19 +1049,21 @@ urllib3==1.26.18 # responses # rockset # testcontainers -uvicorn[standard]==0.29.0 +uvicorn[standard]==0.30.1 # via + # feast (setup.py) # fastapi - # fastapi-cli uvloop==0.19.0 # via uvicorn virtualenv==20.23.0 - # via pre-commit -watchfiles==0.21.0 + # via + # feast (setup.py) + # pre-commit +watchfiles==0.22.0 # via uvicorn wcwidth==0.2.13 # via prompt-toolkit -webcolors==1.13 +webcolors==24.6.0 # via jsonschema webencodings==0.5.1 # via @@ -970,5 +1091,5 @@ xmltodict==0.13.0 # via moto yarl==1.9.4 # via aiohttp -zipp==3.18.1 +zipp==3.19.2 # via importlib-metadata diff --git a/sdk/python/requirements/py3.10-requirements.txt b/sdk/python/requirements/py3.10-requirements.txt index 99c9bfc3fe..72124636b6 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,14 +43,15 @@ 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 # via dask -greenlet==3.0.3 - # via sqlalchemy gunicorn==22.0.0 + # via feast (setup.py) h11==0.14.0 # via # httpcore @@ -65,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 @@ -78,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 @@ -96,20 +108,29 @@ packaging==24.0 # gunicorn pandas==2.2.2 # via + # feast (setup.py) # dask # dask-expr partd==1.4.2 # via dask protobuf==4.25.3 - # via mypy-protobuf + # via + # feast (setup.py) + # mypy-protobuf 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 +141,7 @@ pytz==2024.1 # via pandas pyyaml==6.0.1 # via + # feast (setup.py) # dask # uvicorn referencing==0.35.1 @@ -127,6 +149,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 +165,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 +181,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 +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.11-ci-requirements.txt b/sdk/python/requirements/py3.11-ci-requirements.txt index cea8cc22d0..673047b5c7 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.0 +aiobotocore==2.13.1 + # via feast (setup.py) aiohttp==3.9.5 # via aiobotocore aioitertools==0.11.0 @@ -11,14 +12,16 @@ alabaster==0.7.16 # via sphinx altair==4.2.2 # via great-expectations -annotated-types==0.6.0 +annotated-types==0.7.0 # via pydantic -anyio==4.3.0 +anyio==4.4.0 # via # httpx # 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 @@ -39,12 +43,14 @@ attrs==23.2.0 # aiohttp # jsonschema # referencing -azure-core==1.30.1 +azure-core==1.30.2 # via # azure-identity # azure-storage-blob -azure-identity==1.16.0 +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 @@ -55,9 +61,11 @@ bidict==0.23.1 # via ibis-framework bleach==6.1.0 # via nbconvert -boto3==1.34.99 - # via moto -botocore==1.34.99 +boto3==1.34.131 + # via + # feast (setup.py) + # moto +botocore==1.34.131 # via # aiobotocore # boto3 @@ -65,6 +73,7 @@ botocore==1.34.99 # s3transfer build==1.2.1 # via + # feast (setup.py) # pip-tools # singlestoredb cachecontrol==0.14.0 @@ -72,7 +81,8 @@ cachecontrol==0.14.0 cachetools==5.3.3 # via google-auth cassandra-driver==3.29.1 -certifi==2024.2.2 + # via feast (setup.py) +certifi==2024.6.2 # via # elastic-transport # httpcore @@ -94,6 +104,7 @@ charset-normalizer==3.3.2 # snowflake-connector-python click==8.1.7 # via + # feast (setup.py) # dask # geomet # great-expectations @@ -103,15 +114,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.5.3 +coverage[toml]==7.5.4 # via pytest-cov -cryptography==42.0.7 +cryptography==42.0.8 # via + # feast (setup.py) # azure-identity # azure-storage-blob # great-expectations @@ -122,20 +136,24 @@ cryptography==42.0.7 # snowflake-connector-python # types-pyopenssl # types-redis -dask[dataframe]==2024.5.0 - # via dask-expr -dask-expr==1.1.0 +dask[dataframe]==2024.6.2 + # via + # feast (setup.py) + # dask-expr +dask-expr==1.1.6 # via dask db-dtypes==1.2.0 # via google-cloud-bigquery -debugpy==1.8.1 +debugpy==1.8.2 # via ipykernel decorator==5.1.1 # via ipython defusedxml==0.7.1 # via nbconvert -deltalake==0.17.4 +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 @@ -148,12 +166,13 @@ duckdb==0.10.3 # via # duckdb-engine # ibis-framework -duckdb-engine==0.12.1 +duckdb-engine==0.13.0 # via ibis-framework elastic-transport==8.13.1 # via elasticsearch -elasticsearch==8.13.2 -email-validator==2.1.1 +elasticsearch==8.14.0 + # via feast (setup.py) +email-validator==2.2.0 # via fastapi entrypoints==0.4 # via altair @@ -162,16 +181,17 @@ execnet==2.1.1 executing==2.0.1 # via stack-data fastapi==0.111.0 - # via fastapi-cli -fastapi-cli==0.0.2 + # via feast (setup.py) +fastapi-cli==0.0.4 # via fastapi -fastjsonschema==2.19.1 +fastjsonschema==2.20.0 # via nbformat -filelock==3.14.0 +filelock==3.15.4 # via # snowflake-connector-python # virtualenv firebase-admin==5.4.0 + # via feast (setup.py) fqdn==1.5.1 # via jsonschema frozenlist==1.4.1 @@ -179,13 +199,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.0 +google-api-core[grpc]==2.19.1 # via + # feast (setup.py) # firebase-admin # google-api-python-client # google-cloud-bigquery @@ -195,9 +218,9 @@ google-api-core[grpc]==2.19.0 # google-cloud-datastore # google-cloud-firestore # google-cloud-storage -google-api-python-client==2.131.0 +google-api-python-client==2.134.0 # via firebase-admin -google-auth==2.29.0 +google-auth==2.30.0 # via # google-api-core # google-api-python-client @@ -210,8 +233,11 @@ google-auth==2.29.0 google-auth-httplib2==0.2.0 # via google-api-python-client google-cloud-bigquery[pandas]==3.12.0 + # via feast (setup.py) google-cloud-bigquery-storage==2.25.0 -google-cloud-bigtable==2.23.1 + # via feast (setup.py) +google-cloud-bigtable==2.24.0 + # via feast (setup.py) google-cloud-core==2.4.1 # via # google-cloud-bigquery @@ -220,30 +246,34 @@ google-cloud-core==2.4.1 # google-cloud-firestore # google-cloud-storage google-cloud-datastore==2.19.0 + # via feast (setup.py) google-cloud-firestore==2.16.0 # via firebase-admin -google-cloud-storage==2.16.0 - # via firebase-admin +google-cloud-storage==2.17.0 + # via + # feast (setup.py) + # firebase-admin google-crc32c==1.5.0 # via # google-cloud-storage # google-resumable-media -google-resumable-media==2.7.0 +google-resumable-media==2.7.1 # via # google-cloud-bigquery # google-cloud-storage -googleapis-common-protos[grpc]==1.63.0 +googleapis-common-protos[grpc]==1.63.2 # via + # feast (setup.py) # google-api-core # grpc-google-iam-v1 # grpcio-status -great-expectations==0.18.15 -greenlet==3.0.3 - # via sqlalchemy -grpc-google-iam-v1==0.13.0 +great-expectations==0.18.16 + # via feast (setup.py) +grpc-google-iam-v1==0.13.1 # via google-cloud-bigtable -grpcio==1.64.0 +grpcio==1.64.1 # via + # feast (setup.py) # google-api-core # google-cloud-bigquery # googleapis-common-protos @@ -254,19 +284,27 @@ grpcio==1.64.0 # 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 httplib2==0.22.0 @@ -277,11 +315,15 @@ httptools==0.6.1 # via uvicorn httpx==0.27.0 # via + # feast (setup.py) # fastapi # jupyterlab ibis-framework[duckdb]==8.0.0 - # via ibis-substrait + # via + # feast (setup.py) + # ibis-substrait ibis-substrait==3.2.0 + # via feast (setup.py) identify==2.5.36 # via pre-commit idna==3.7 @@ -295,7 +337,7 @@ idna==3.7 # yarl imagesize==1.4.1 # via sphinx -importlib-metadata==7.1.0 +importlib-metadata==8.0.0 # via dask iniconfig==2.0.0 # via pytest @@ -316,6 +358,7 @@ jedi==0.19.1 # via ipython jinja2==3.1.4 # via + # feast (setup.py) # altair # fastapi # great-expectations @@ -333,12 +376,13 @@ json5==0.9.25 # via jupyterlab-server jsonpatch==1.33 # via great-expectations -jsonpointer==2.4 +jsonpointer==3.0.0 # via # jsonpatch # jsonschema jsonschema[format-nongpl]==4.22.0 # via + # feast (setup.py) # altair # great-expectations # jupyter-events @@ -373,7 +417,7 @@ jupyter-server==2.14.1 # notebook-shim jupyter-server-terminals==0.5.3 # via jupyter-server -jupyterlab==4.2.1 +jupyterlab==4.2.3 # via notebook jupyterlab-pygments==0.3.0 # via nbconvert @@ -384,6 +428,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 @@ -395,7 +440,7 @@ markupsafe==2.1.5 # jinja2 # nbconvert # werkzeug -marshmallow==3.21.2 +marshmallow==3.21.3 # via great-expectations matplotlib-inline==0.1.7 # via @@ -404,18 +449,22 @@ 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 -msal==1.28.0 + # via feast (setup.py) +msal==1.29.0 # via # azure-identity # msal-extensions -msal-extensions==1.1.0 +msal-extensions==1.2.0 # via azure-identity msgpack==1.0.8 # via cachecontrol @@ -425,11 +474,14 @@ multidict==6.0.5 # yarl multipledispatch==1.0.0 # via ibis-framework -mypy==1.10.0 - # via sqlalchemy +mypy==1.10.1 + # 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 @@ -442,9 +494,9 @@ nbformat==5.10.4 # nbconvert nest-asyncio==1.6.0 # via ipykernel -nodeenv==1.9.0 +nodeenv==1.9.1 # via pre-commit -notebook==7.2.0 +notebook==7.2.1 # via great-expectations notebook-shim==0.2.4 # via @@ -452,6 +504,7 @@ notebook-shim==0.2.4 # notebook numpy==1.26.4 # via + # feast (setup.py) # altair # dask # db-dtypes @@ -462,11 +515,11 @@ numpy==1.26.4 # scipy oauthlib==3.2.2 # via requests-oauthlib -orjson==3.10.3 +orjson==3.10.5 # via fastapi overrides==7.7.0 # via jupyter-server -packaging==24.0 +packaging==24.1 # via # build # dask @@ -481,13 +534,13 @@ packaging==24.0 # jupyterlab # jupyterlab-server # marshmallow - # msal-extensions # nbconvert # pytest # snowflake-connector-python # sphinx pandas==2.2.2 # via + # feast (setup.py) # altair # dask # dask-expr @@ -510,9 +563,10 @@ pbr==6.0.0 # via mock pexpect==4.9.0 # via ipython -pip==24.0 +pip==24.1.1 # via pip-tools pip-tools==7.4.1 + # via feast (setup.py) platformdirs==3.11.0 # via # jupyter-core @@ -522,14 +576,15 @@ pluggy==1.5.0 # via pytest ply==3.11 # via thriftpy2 -portalocker==2.8.2 +portalocker==2.10.0 # via msal-extensions pre-commit==3.3.1 + # via feast (setup.py) prometheus-client==0.20.0 # via jupyter-server -prompt-toolkit==3.0.45 +prompt-toolkit==3.0.47 # via ipython -proto-plus==1.23.0 +proto-plus==1.24.0 # via # google-api-core # google-cloud-bigquery @@ -539,6 +594,7 @@ proto-plus==1.23.0 # google-cloud-firestore protobuf==4.25.3 # via + # feast (setup.py) # google-api-core # google-cloud-bigquery # google-cloud-bigquery-storage @@ -556,8 +612,15 @@ protobuf==4.25.3 # proto-plus # substrait psutil==5.9.0 - # via ipykernel -psycopg2-binary==2.9.9 + # 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 + # via psycopg ptyprocess==0.7.0 # via # pexpect @@ -565,12 +628,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 @@ -588,16 +653,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.1 +pydantic==2.7.4 # via + # feast (setup.py) # fastapi # great-expectations -pydantic-core==2.18.2 +pydantic-core==2.18.4 # via pydantic pygments==2.18.0 # via + # feast (setup.py) # ipython # nbconvert # rich @@ -608,8 +676,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 @@ -621,8 +692,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 @@ -632,13 +705,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 @@ -667,6 +748,7 @@ pytz==2024.1 # trino pyyaml==6.0.1 # via + # feast (setup.py) # dask # ibis-substrait # jupyter-events @@ -680,14 +762,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 -requests==2.31.0 # via + # feast (setup.py) + # parsimonious +requests==2.32.3 + # via + # feast (setup.py) # azure-core # cachecontrol # docker @@ -707,7 +794,7 @@ requests==2.31.0 # trino requests-oauthlib==2.0.0 # via kubernetes -responses==0.25.0 +responses==0.25.3 # via moto rfc3339-validator==0.1.4 # via @@ -722,6 +809,7 @@ rich==13.7.1 # ibis-framework # typer rockset==2.1.2 + # via feast (setup.py) rpds-py==0.18.1 # via # jsonschema @@ -730,22 +818,25 @@ rsa==4.9 # via google-auth ruamel-yaml==0.17.17 # via great-expectations -ruff==0.4.6 -s3transfer==0.10.1 +ruff==0.4.10 + # via feast (setup.py) +s3transfer==0.10.2 # via boto3 -scipy==1.13.1 +scipy==1.14.0 # via great-expectations send2trash==1.8.3 # via jupyter-server -setuptools==70.0.0 +setuptools==70.1.1 # via # grpcio-tools + # jupyterlab # kubernetes # pip-tools # singlestoredb shellingham==1.5.4 # via typer -singlestoredb==1.3.1 +singlestoredb==1.4.0 + # via feast (setup.py) six==1.16.0 # via # asttokens @@ -765,12 +856,14 @@ sniffio==1.3.1 # httpx snowballstemmer==2.2.0 # via sphinx -snowflake-connector-python[pandas]==3.10.1 +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 @@ -783,8 +876,9 @@ sphinxcontrib-qthelp==1.0.7 # via sphinx sphinxcontrib-serializinghtml==1.1.10 # via sphinx -sqlalchemy[mypy]==2.0.30 +sqlalchemy[mypy]==2.0.31 # via + # feast (setup.py) # duckdb-engine # ibis-framework # sqlalchemy-views @@ -793,6 +887,7 @@ sqlalchemy-views==0.3.2 sqlglot==20.11.0 # via ibis-framework sqlite-vec==0.0.1a10 + # via feast (setup.py) sqlparams==6.0.1 # via singlestoredb stack-data==0.6.3 @@ -802,17 +897,21 @@ starlette==0.37.2 substrait==0.19.0 # via ibis-substrait tabulate==0.9.0 -tenacity==8.3.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 -thriftpy2==0.5.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 @@ -830,7 +929,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 @@ -847,36 +948,53 @@ traitlets==5.14.3 # nbconvert # nbformat trino==0.328.0 -typeguard==4.2.1 + # 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 -types-pymysql==1.1.0.20240425 + # 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 -types-setuptools==70.0.0.20240524 - # via types-cffi + # via feast (setup.py) +types-setuptools==70.1.0.20240627 + # 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.11.0 +typing-extensions==4.12.2 # via # azure-core + # azure-identity # azure-storage-blob # fastapi # great-expectations # ibis-framework # ipython # mypy + # psycopg + # psycopg-pool # pydantic # pydantic-core # snowflake-connector-python @@ -890,14 +1008,15 @@ tzlocal==5.2 # via # great-expectations # trino -ujson==5.9.0 +ujson==5.10.0 # via fastapi uri-template==1.3.0 # via jsonschema uritemplate==4.1.1 # via google-api-python-client -urllib3==1.26.18 +urllib3==1.26.19 # via + # feast (setup.py) # botocore # docker # elastic-transport @@ -908,19 +1027,21 @@ urllib3==1.26.18 # responses # rockset # testcontainers -uvicorn[standard]==0.29.0 +uvicorn[standard]==0.30.1 # via + # feast (setup.py) # fastapi - # fastapi-cli uvloop==0.19.0 # via uvicorn virtualenv==20.23.0 - # via pre-commit -watchfiles==0.21.0 + # via + # feast (setup.py) + # pre-commit +watchfiles==0.22.0 # via uvicorn wcwidth==0.2.13 # via prompt-toolkit -webcolors==1.13 +webcolors==24.6.0 # via jsonschema webencodings==0.5.1 # via @@ -948,5 +1069,5 @@ xmltodict==0.13.0 # via moto yarl==1.9.4 # via aiohttp -zipp==3.18.1 +zipp==3.19.2 # via importlib-metadata diff --git a/sdk/python/requirements/py3.11-requirements.txt b/sdk/python/requirements/py3.11-requirements.txt index c34b610d14..408f392515 100644 --- a/sdk/python/requirements/py3.11-requirements.txt +++ b/sdk/python/requirements/py3.11-requirements.txt @@ -20,30 +20,36 @@ 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 # via dask -greenlet==3.0.3 - # via sqlalchemy gunicorn==22.0.0 + # via feast (setup.py) h11==0.14.0 # via # httpcore @@ -63,8 +69,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 +85,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 +106,29 @@ packaging==24.0 # gunicorn pandas==2.2.2 # via + # feast (setup.py) # dask # dask-expr partd==1.4.2 # via dask protobuf==4.25.3 - # via mypy-protobuf + # via + # feast (setup.py) + # mypy-protobuf 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 +139,7 @@ pytz==2024.1 # via pandas pyyaml==6.0.1 # via + # feast (setup.py) # dask # uvicorn referencing==0.35.1 @@ -125,6 +147,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 +163,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 +201,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 d7df488a88..83009f8730 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.0 +aiobotocore==2.13.1 + # via feast (setup.py) aiohttp==3.9.5 # via aiobotocore aioitertools==0.11.0 @@ -11,14 +12,16 @@ alabaster==0.7.16 # via sphinx altair==4.2.2 # via great-expectations -annotated-types==0.6.0 +annotated-types==0.7.0 # via pydantic -anyio==4.3.0 +anyio==4.4.0 # via # httpx # 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 @@ -43,12 +47,14 @@ attrs==23.2.0 # aiohttp # jsonschema # referencing -azure-core==1.30.1 +azure-core==1.30.2 # via # azure-identity # azure-storage-blob -azure-identity==1.16.0 +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 @@ -59,9 +65,11 @@ bidict==0.23.1 # via ibis-framework bleach==6.1.0 # via nbconvert -boto3==1.34.99 - # via moto -botocore==1.34.99 +boto3==1.34.131 + # via + # feast (setup.py) + # moto +botocore==1.34.131 # via # aiobotocore # boto3 @@ -69,6 +77,7 @@ botocore==1.34.99 # s3transfer build==1.2.1 # via + # feast (setup.py) # pip-tools # singlestoredb cachecontrol==0.14.0 @@ -76,7 +85,8 @@ cachecontrol==0.14.0 cachetools==5.3.3 # via google-auth cassandra-driver==3.29.1 -certifi==2024.2.2 + # via feast (setup.py) +certifi==2024.6.2 # via # elastic-transport # httpcore @@ -98,6 +108,7 @@ charset-normalizer==3.3.2 # snowflake-connector-python click==8.1.7 # via + # feast (setup.py) # dask # geomet # great-expectations @@ -107,15 +118,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.5.3 +coverage[toml]==7.5.4 # via pytest-cov -cryptography==42.0.7 +cryptography==42.0.8 # via + # feast (setup.py) # azure-identity # azure-storage-blob # great-expectations @@ -126,20 +140,24 @@ cryptography==42.0.7 # snowflake-connector-python # types-pyopenssl # types-redis -dask[dataframe]==2024.5.0 - # via dask-expr -dask-expr==1.1.0 +dask[dataframe]==2024.6.2 + # via + # feast (setup.py) + # dask-expr +dask-expr==1.1.6 # via dask db-dtypes==1.2.0 # via google-cloud-bigquery -debugpy==1.8.1 +debugpy==1.8.2 # via ipykernel decorator==5.1.1 # via ipython defusedxml==0.7.1 # via nbconvert -deltalake==0.17.4 +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 @@ -152,12 +170,13 @@ duckdb==0.10.3 # via # duckdb-engine # ibis-framework -duckdb-engine==0.12.1 +duckdb-engine==0.13.0 # via ibis-framework elastic-transport==8.13.1 # via elasticsearch -elasticsearch==8.13.2 -email-validator==2.1.1 +elasticsearch==8.14.0 + # via feast (setup.py) +email-validator==2.2.0 # via fastapi entrypoints==0.4 # via altair @@ -171,16 +190,17 @@ execnet==2.1.1 executing==2.0.1 # via stack-data fastapi==0.111.0 - # via fastapi-cli -fastapi-cli==0.0.2 + # via feast (setup.py) +fastapi-cli==0.0.4 # via fastapi -fastjsonschema==2.19.1 +fastjsonschema==2.20.0 # via nbformat -filelock==3.14.0 +filelock==3.15.4 # via # snowflake-connector-python # virtualenv firebase-admin==5.4.0 + # via feast (setup.py) fqdn==1.5.1 # via jsonschema frozenlist==1.4.1 @@ -188,13 +208,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.0 +google-api-core[grpc]==2.19.1 # via + # feast (setup.py) # firebase-admin # google-api-python-client # google-cloud-bigquery @@ -204,9 +227,9 @@ google-api-core[grpc]==2.19.0 # google-cloud-datastore # google-cloud-firestore # google-cloud-storage -google-api-python-client==2.131.0 +google-api-python-client==2.134.0 # via firebase-admin -google-auth==2.29.0 +google-auth==2.30.0 # via # google-api-core # google-api-python-client @@ -219,8 +242,11 @@ google-auth==2.29.0 google-auth-httplib2==0.2.0 # via google-api-python-client google-cloud-bigquery[pandas]==3.12.0 + # via feast (setup.py) google-cloud-bigquery-storage==2.25.0 -google-cloud-bigtable==2.23.1 + # via feast (setup.py) +google-cloud-bigtable==2.24.0 + # via feast (setup.py) google-cloud-core==2.4.1 # via # google-cloud-bigquery @@ -229,30 +255,34 @@ google-cloud-core==2.4.1 # google-cloud-firestore # google-cloud-storage google-cloud-datastore==2.19.0 + # via feast (setup.py) google-cloud-firestore==2.16.0 # via firebase-admin -google-cloud-storage==2.16.0 - # via firebase-admin +google-cloud-storage==2.17.0 + # via + # feast (setup.py) + # firebase-admin google-crc32c==1.5.0 # via # google-cloud-storage # google-resumable-media -google-resumable-media==2.7.0 +google-resumable-media==2.7.1 # via # google-cloud-bigquery # google-cloud-storage -googleapis-common-protos[grpc]==1.63.0 +googleapis-common-protos[grpc]==1.63.2 # via + # feast (setup.py) # google-api-core # grpc-google-iam-v1 # grpcio-status -great-expectations==0.18.15 -greenlet==3.0.3 - # via sqlalchemy -grpc-google-iam-v1==0.13.0 +great-expectations==0.18.16 + # via feast (setup.py) +grpc-google-iam-v1==0.13.1 # via google-cloud-bigtable -grpcio==1.64.0 +grpcio==1.64.1 # via + # feast (setup.py) # google-api-core # google-cloud-bigquery # googleapis-common-protos @@ -263,19 +293,27 @@ grpcio==1.64.0 # 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 httplib2==0.22.0 @@ -286,11 +324,15 @@ httptools==0.6.1 # via uvicorn httpx==0.27.0 # via + # feast (setup.py) # fastapi # jupyterlab ibis-framework[duckdb]==8.0.0 - # via ibis-substrait + # via + # feast (setup.py) + # ibis-substrait ibis-substrait==3.2.0 + # via feast (setup.py) identify==2.5.36 # via pre-commit idna==3.7 @@ -304,7 +346,7 @@ idna==3.7 # yarl imagesize==1.4.1 # via sphinx -importlib-metadata==7.1.0 +importlib-metadata==8.0.0 # via # build # dask @@ -334,6 +376,7 @@ jedi==0.19.1 # via ipython jinja2==3.1.4 # via + # feast (setup.py) # altair # fastapi # great-expectations @@ -351,12 +394,13 @@ json5==0.9.25 # via jupyterlab-server jsonpatch==1.33 # via great-expectations -jsonpointer==2.4 +jsonpointer==3.0.0 # via # jsonpatch # jsonschema jsonschema[format-nongpl]==4.22.0 # via + # feast (setup.py) # altair # great-expectations # jupyter-events @@ -391,7 +435,7 @@ jupyter-server==2.14.1 # notebook-shim jupyter-server-terminals==0.5.3 # via jupyter-server -jupyterlab==4.2.1 +jupyterlab==4.2.3 # via notebook jupyterlab-pygments==0.3.0 # via nbconvert @@ -402,6 +446,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 @@ -413,7 +458,7 @@ markupsafe==2.1.5 # jinja2 # nbconvert # werkzeug -marshmallow==3.21.2 +marshmallow==3.21.3 # via great-expectations matplotlib-inline==0.1.7 # via @@ -422,18 +467,22 @@ 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 -msal==1.28.0 + # via feast (setup.py) +msal==1.29.0 # via # azure-identity # msal-extensions -msal-extensions==1.1.0 +msal-extensions==1.2.0 # via azure-identity msgpack==1.0.8 # via cachecontrol @@ -443,11 +492,14 @@ multidict==6.0.5 # yarl multipledispatch==1.0.0 # via ibis-framework -mypy==1.10.0 - # via sqlalchemy +mypy==1.10.1 + # 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 @@ -460,9 +512,9 @@ nbformat==5.10.4 # nbconvert nest-asyncio==1.6.0 # via ipykernel -nodeenv==1.9.0 +nodeenv==1.9.1 # via pre-commit -notebook==7.2.0 +notebook==7.2.1 # via great-expectations notebook-shim==0.2.4 # via @@ -470,6 +522,7 @@ notebook-shim==0.2.4 # notebook numpy==1.26.4 # via + # feast (setup.py) # altair # dask # db-dtypes @@ -480,11 +533,11 @@ numpy==1.26.4 # scipy oauthlib==3.2.2 # via requests-oauthlib -orjson==3.10.3 +orjson==3.10.5 # via fastapi overrides==7.7.0 # via jupyter-server -packaging==24.0 +packaging==24.1 # via # build # dask @@ -499,13 +552,13 @@ packaging==24.0 # jupyterlab # jupyterlab-server # marshmallow - # msal-extensions # nbconvert # pytest # snowflake-connector-python # sphinx pandas==2.2.2 # via + # feast (setup.py) # altair # dask # dask-expr @@ -528,9 +581,10 @@ pbr==6.0.0 # via mock pexpect==4.9.0 # via ipython -pip==24.0 +pip==24.1.1 # via pip-tools pip-tools==7.4.1 + # via feast (setup.py) platformdirs==3.11.0 # via # jupyter-core @@ -540,14 +594,15 @@ pluggy==1.5.0 # via pytest ply==3.11 # via thriftpy2 -portalocker==2.8.2 +portalocker==2.10.0 # via msal-extensions pre-commit==3.3.1 + # via feast (setup.py) prometheus-client==0.20.0 # via jupyter-server -prompt-toolkit==3.0.45 +prompt-toolkit==3.0.47 # via ipython -proto-plus==1.23.0 +proto-plus==1.24.0 # via # google-api-core # google-cloud-bigquery @@ -557,6 +612,7 @@ proto-plus==1.23.0 # google-cloud-firestore protobuf==4.25.3 # via + # feast (setup.py) # google-api-core # google-cloud-bigquery # google-cloud-bigquery-storage @@ -574,8 +630,15 @@ protobuf==4.25.3 # proto-plus # substrait psutil==5.9.0 - # via ipykernel -psycopg2-binary==2.9.9 + # 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 ptyprocess==0.7.0 # via # pexpect @@ -583,12 +646,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 @@ -606,16 +671,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.1 +pydantic==2.7.4 # via + # feast (setup.py) # fastapi # great-expectations -pydantic-core==2.18.2 +pydantic-core==2.18.4 # via pydantic pygments==2.18.0 # via + # feast (setup.py) # ipython # nbconvert # rich @@ -626,8 +694,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 @@ -639,8 +710,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 @@ -650,13 +723,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 @@ -685,6 +766,7 @@ pytz==2024.1 # trino pyyaml==6.0.1 # via + # feast (setup.py) # dask # ibis-substrait # jupyter-events @@ -698,14 +780,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 -requests==2.31.0 # via + # feast (setup.py) + # parsimonious +requests==2.32.3 + # via + # feast (setup.py) # azure-core # cachecontrol # docker @@ -725,7 +812,7 @@ requests==2.31.0 # trino requests-oauthlib==2.0.0 # via kubernetes -responses==0.25.0 +responses==0.25.3 # via moto rfc3339-validator==0.1.4 # via @@ -740,6 +827,7 @@ rich==13.7.1 # ibis-framework # typer rockset==2.1.2 + # via feast (setup.py) rpds-py==0.18.1 # via # jsonschema @@ -750,22 +838,25 @@ ruamel-yaml==0.17.17 # via great-expectations ruamel-yaml-clib==0.2.8 # via ruamel-yaml -ruff==0.4.6 -s3transfer==0.10.1 +ruff==0.4.10 + # via feast (setup.py) +s3transfer==0.10.2 # via boto3 scipy==1.13.1 # via great-expectations send2trash==1.8.3 # via jupyter-server -setuptools==70.0.0 +setuptools==70.1.1 # via # grpcio-tools + # jupyterlab # kubernetes # pip-tools # singlestoredb shellingham==1.5.4 # via typer -singlestoredb==1.3.1 +singlestoredb==1.4.0 + # via feast (setup.py) six==1.16.0 # via # asttokens @@ -785,12 +876,14 @@ sniffio==1.3.1 # httpx snowballstemmer==2.2.0 # via sphinx -snowflake-connector-python[pandas]==3.10.1 +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 @@ -803,8 +896,9 @@ sphinxcontrib-qthelp==1.0.7 # via sphinx sphinxcontrib-serializinghtml==1.1.10 # via sphinx -sqlalchemy[mypy]==2.0.30 +sqlalchemy[mypy]==2.0.31 # via + # feast (setup.py) # duckdb-engine # ibis-framework # sqlalchemy-views @@ -813,6 +907,7 @@ sqlalchemy-views==0.3.2 sqlglot==20.11.0 # via ibis-framework sqlite-vec==0.0.1a10 + # via feast (setup.py) sqlparams==6.0.1 # via singlestoredb stack-data==0.6.3 @@ -822,17 +917,21 @@ starlette==0.37.2 substrait==0.19.0 # via ibis-substrait tabulate==0.9.0 -tenacity==8.3.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 -thriftpy2==0.5.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 @@ -860,7 +959,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 @@ -877,39 +978,56 @@ traitlets==5.14.3 # nbconvert # nbformat trino==0.328.0 -typeguard==4.2.1 + # 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 -types-setuptools==70.0.0.20240524 - # via types-cffi + # via feast (setup.py) +types-setuptools==70.1.0.20240627 + # 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.11.0 +typing-extensions==4.12.2 # via # aioitertools # anyio # async-lru # azure-core + # azure-identity # azure-storage-blob # fastapi # great-expectations # ibis-framework # ipython # mypy + # psycopg + # psycopg-pool # pydantic # pydantic-core # snowflake-connector-python @@ -925,14 +1043,15 @@ tzlocal==5.2 # via # great-expectations # trino -ujson==5.9.0 +ujson==5.10.0 # via fastapi uri-template==1.3.0 # via jsonschema uritemplate==4.1.1 # via google-api-python-client -urllib3==1.26.18 +urllib3==1.26.19 # via + # feast (setup.py) # botocore # docker # elastic-transport @@ -944,19 +1063,21 @@ urllib3==1.26.18 # rockset # snowflake-connector-python # testcontainers -uvicorn[standard]==0.29.0 +uvicorn[standard]==0.30.1 # via + # feast (setup.py) # fastapi - # fastapi-cli uvloop==0.19.0 # via uvicorn virtualenv==20.23.0 - # via pre-commit -watchfiles==0.21.0 + # via + # feast (setup.py) + # pre-commit +watchfiles==0.22.0 # via uvicorn wcwidth==0.2.13 # via prompt-toolkit -webcolors==1.13 +webcolors==24.6.0 # via jsonschema webencodings==0.5.1 # via @@ -984,5 +1105,5 @@ xmltodict==0.13.0 # via moto yarl==1.9.4 # via aiohttp -zipp==3.18.1 +zipp==3.19.2 # via importlib-metadata diff --git a/sdk/python/requirements/py3.9-requirements.txt b/sdk/python/requirements/py3.9-requirements.txt index 149a96626e..3c833438de 100644 --- a/sdk/python/requirements/py3.9-requirements.txt +++ b/sdk/python/requirements/py3.9-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,14 +43,15 @@ 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 # via dask -greenlet==3.0.3 - # via sqlalchemy gunicorn==22.0.0 + # via feast (setup.py) h11==0.14.0 # via # httpcore @@ -67,8 +73,11 @@ importlib-metadata==7.1.0 # 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 +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 @@ -98,20 +110,29 @@ packaging==24.0 # gunicorn pandas==2.2.2 # via + # feast (setup.py) # dask # dask-expr partd==1.4.2 # via dask protobuf==4.25.3 - # via mypy-protobuf + # via + # feast (setup.py) + # mypy-protobuf 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 +143,7 @@ pytz==2024.1 # via pandas pyyaml==6.0.1 # via + # feast (setup.py) # dask # uvicorn referencing==0.35.1 @@ -129,6 +151,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 +167,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 +183,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 +210,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/tests/integration/online_store/test_universal_online.py b/sdk/python/tests/integration/online_store/test_universal_online.py index e78c1053bf..c6b034e2aa 100644 --- a/sdk/python/tests/integration/online_store/test_universal_online.py +++ b/sdk/python/tests/integration/online_store/test_universal_online.py @@ -39,13 +39,18 @@ @pytest.mark.integration @pytest.mark.universal_online_stores(only=["postgres"]) +@pytest.mark.parametrize( + "conn_type", + [ConnectionType.singleton, ConnectionType.pool], + ids=lambda v: f"conn_type:{v}", +) def test_connection_pool_online_stores( - environment, universal_data_sources, fake_ingest_data + environment, universal_data_sources, fake_ingest_data, conn_type ): if os.getenv("FEAST_IS_LOCAL_TEST", "False") == "True": return fs = environment.feature_store - fs.config.online_store.conn_type = ConnectionType.pool + fs.config.online_store.conn_type = conn_type fs.config.online_store.min_conn = 1 fs.config.online_store.max_conn = 10 diff --git a/sdk/python/tests/integration/registration/test_universal_registry.py b/sdk/python/tests/integration/registration/test_universal_registry.py index 24ba9fe42a..c119ae800a 100644 --- a/sdk/python/tests/integration/registration/test_universal_registry.py +++ b/sdk/python/tests/integration/registration/test_universal_registry.py @@ -149,7 +149,9 @@ def pg_registry(): registry_config = RegistryConfig( registry_type="sql", - path=f"postgresql://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{container_host}:{container_port}/{POSTGRES_DB}", + # 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}", sqlalchemy_config_kwargs={"echo": False, "pool_pre_ping": True}, ) diff --git a/setup.py b/setup.py index cffd91a0c5..958e93799d 100644 --- a/setup.py +++ b/setup.py @@ -102,7 +102,7 @@ TRINO_REQUIRED = ["trino>=0.305.0,<0.400.0", "regex"] POSTGRES_REQUIRED = [ - "psycopg2-binary>=2.8.3,<3", + "psycopg[binary,pool]>=3.0.0,<4", ] MYSQL_REQUIRED = ["pymysql", "types-PyMySQL"] From 7072fd0e2e1d2f4d9a3e8f02d04ae042b3d9c0d4 Mon Sep 17 00:00:00 2001 From: Tornike Gurgenidze Date: Tue, 2 Jul 2024 01:05:44 +0400 Subject: [PATCH 13/59] feat: Move get_online_features to OnlineStore interface (#4319) * move get_online_features to OnlineStore interface Signed-off-by: tokoko * fix pydantic warnings Signed-off-by: tokoko * run ruff format Signed-off-by: tokoko --------- Signed-off-by: tokoko --- sdk/python/feast/feature_store.py | 185 +----------------- .../kubernetes/k8s_materialization_engine.py | 4 +- .../feast/infra/online_stores/online_store.py | 182 ++++++++++++++++- .../feast/infra/passthrough_provider.py | 46 ++++- sdk/python/feast/infra/provider.py | 34 +++- sdk/python/tests/foo_provider.py | 32 ++- .../universal/data_sources/file.py | 2 +- 7 files changed, 303 insertions(+), 182 deletions(-) diff --git a/sdk/python/feast/feature_store.py b/sdk/python/feast/feature_store.py index b7e4ef619f..6476af5ac8 100644 --- a/sdk/python/feast/feature_store.py +++ b/sdk/python/feast/feature_store.py @@ -1559,75 +1559,16 @@ def get_online_features( ... ) >>> online_response_dict = online_response.to_dict() """ - if isinstance(entity_rows, list): - columnar: Dict[str, List[Any]] = {k: [] for k in entity_rows[0].keys()} - for entity_row in entity_rows: - for key, value in entity_row.items(): - try: - columnar[key].append(value) - except KeyError as e: - raise ValueError( - "All entity_rows must have the same keys." - ) from e - - entity_rows = columnar + provider = self._get_provider() - ( - join_key_values, - grouped_refs, - entity_name_to_join_key_map, - requested_on_demand_feature_views, - feature_refs, - requested_result_row_names, - online_features_response, - ) = utils._prepare_entities_to_read_from_online_store( + return provider.get_online_features( + config=self.config, + features=features, + entity_rows=entity_rows, registry=self._registry, project=self.project, - features=features, - entity_values=entity_rows, full_feature_names=full_feature_names, - native_entity_values=True, - ) - - provider = self._get_provider() - for table, requested_features in grouped_refs: - # Get the correct set of entity values with the correct join keys. - table_entity_values, idxs = utils._get_unique_entities( - table, - join_key_values, - entity_name_to_join_key_map, - ) - - # Fetch feature data for the minimum set of Entities. - feature_data = self._read_from_online_store( - table_entity_values, - provider, - requested_features, - table, - ) - - # Populate the result_rows with the Features from the OnlineStore inplace. - utils._populate_response_from_feature_data( - feature_data, - idxs, - online_features_response, - full_feature_names, - requested_features, - table, - ) - - if requested_on_demand_feature_views: - utils._augment_response_with_on_demand_transforms( - online_features_response, - feature_refs, - requested_on_demand_feature_views, - full_feature_names, - ) - - utils._drop_unneeded_columns( - online_features_response, requested_result_row_names ) - return OnlineResponse(online_features_response) async def get_online_features_async( self, @@ -1664,75 +1605,16 @@ async def get_online_features_async( Raises: Exception: No entity with the specified name exists. """ - if isinstance(entity_rows, list): - columnar: Dict[str, List[Any]] = {k: [] for k in entity_rows[0].keys()} - for entity_row in entity_rows: - for key, value in entity_row.items(): - try: - columnar[key].append(value) - except KeyError as e: - raise ValueError( - "All entity_rows must have the same keys." - ) from e - - entity_rows = columnar + provider = self._get_provider() - ( - join_key_values, - grouped_refs, - entity_name_to_join_key_map, - requested_on_demand_feature_views, - feature_refs, - requested_result_row_names, - online_features_response, - ) = utils._prepare_entities_to_read_from_online_store( + return await provider.get_online_features_async( + config=self.config, + features=features, + entity_rows=entity_rows, registry=self._registry, project=self.project, - features=features, - entity_values=entity_rows, full_feature_names=full_feature_names, - native_entity_values=True, - ) - - provider = self._get_provider() - for table, requested_features in grouped_refs: - # Get the correct set of entity values with the correct join keys. - table_entity_values, idxs = utils._get_unique_entities( - table, - join_key_values, - entity_name_to_join_key_map, - ) - - # Fetch feature data for the minimum set of Entities. - feature_data = await self._read_from_online_store_async( - table_entity_values, - provider, - requested_features, - table, - ) - - # Populate the result_rows with the Features from the OnlineStore inplace. - utils._populate_response_from_feature_data( - feature_data, - idxs, - online_features_response, - full_feature_names, - requested_features, - table, - ) - - if requested_on_demand_feature_views: - utils._augment_response_with_on_demand_transforms( - online_features_response, - feature_refs, - requested_on_demand_feature_views, - full_feature_names, - ) - - utils._drop_unneeded_columns( - online_features_response, requested_result_row_names ) - return OnlineResponse(online_features_response) def retrieve_online_documents( self, @@ -1806,53 +1688,6 @@ def retrieve_online_documents( ) return OnlineResponse(online_features_response) - def _read_from_online_store( - self, - entity_rows: Iterable[Mapping[str, Value]], - provider: Provider, - requested_features: List[str], - table: FeatureView, - ) -> List[Tuple[List[Timestamp], List["FieldStatus.ValueType"], List[Value]]]: - """Read and process data from the OnlineStore for a given FeatureView. - - This method guarantees that the order of the data in each element of the - List returned is the same as the order of `requested_features`. - - This method assumes that `provider.online_read` returns data for each - combination of Entities in `entity_rows` in the same order as they - are provided. - """ - entity_key_protos = utils._get_entity_key_protos(entity_rows) - - # Fetch data for Entities. - read_rows = provider.online_read( - config=self.config, - table=table, - entity_keys=entity_key_protos, - requested_features=requested_features, - ) - - return utils._convert_rows_to_protobuf(requested_features, read_rows) - - async def _read_from_online_store_async( - self, - entity_rows: Iterable[Mapping[str, Value]], - provider: Provider, - requested_features: List[str], - table: FeatureView, - ) -> List[Tuple[List[Timestamp], List["FieldStatus.ValueType"], List[Value]]]: - entity_key_protos = utils._get_entity_key_protos(entity_rows) - - # Fetch data for Entities. - read_rows = await provider.online_read_async( - config=self.config, - table=table, - entity_keys=entity_key_protos, - requested_features=requested_features, - ) - - return utils._convert_rows_to_protobuf(requested_features, read_rows) - def _retrieve_from_online_store( self, provider: Provider, 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 2e7129b037..510b6b4e4c 100644 --- a/sdk/python/feast/infra/materialization/kubernetes/k8s_materialization_engine.py +++ b/sdk/python/feast/infra/materialization/kubernetes/k8s_materialization_engine.py @@ -306,7 +306,9 @@ def _create_kubernetes_job(self, job_id, paths, feature_view): def _create_configuration_map(self, job_id, paths, feature_view, namespace): """Create a Kubernetes configmap for this job""" - feature_store_configuration = yaml.dump(self.repo_config.dict(by_alias=True)) + feature_store_configuration = yaml.dump( + self.repo_config.model_dump(by_alias=True) + ) materialization_config = yaml.dump( {"paths": paths, "feature_view": feature_view.name} diff --git a/sdk/python/feast/infra/online_stores/online_store.py b/sdk/python/feast/infra/online_stores/online_store.py index 05983a494c..9cf2ef95f6 100644 --- a/sdk/python/feast/infra/online_stores/online_store.py +++ b/sdk/python/feast/infra/online_stores/online_store.py @@ -14,13 +14,17 @@ from abc import ABC, abstractmethod from datetime import datetime -from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple +from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Tuple, Union -from feast import Entity +from feast import Entity, utils +from feast.feature_service import FeatureService from feast.feature_view import FeatureView from feast.infra.infra_object import InfraObject +from feast.infra.registry.base_registry import BaseRegistry +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 +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 @@ -105,6 +109,180 @@ async def online_read_async( f"Online store {self.__class__.__name__} does not support online read async" ) + def get_online_features( + self, + config: RepoConfig, + features: Union[List[str], FeatureService], + entity_rows: Union[ + List[Dict[str, Any]], + Mapping[str, Union[Sequence[Any], Sequence[ValueProto], RepeatedValue]], + ], + registry: BaseRegistry, + project: str, + full_feature_names: bool = False, + ) -> OnlineResponse: + if isinstance(entity_rows, list): + columnar: Dict[str, List[Any]] = {k: [] for k in entity_rows[0].keys()} + for entity_row in entity_rows: + for key, value in entity_row.items(): + try: + columnar[key].append(value) + except KeyError as e: + raise ValueError( + "All entity_rows must have the same keys." + ) from e + + entity_rows = columnar + + ( + join_key_values, + grouped_refs, + entity_name_to_join_key_map, + requested_on_demand_feature_views, + feature_refs, + requested_result_row_names, + online_features_response, + ) = utils._prepare_entities_to_read_from_online_store( + registry=registry, + project=project, + features=features, + entity_values=entity_rows, + full_feature_names=full_feature_names, + native_entity_values=True, + ) + + for table, requested_features in grouped_refs: + # Get the correct set of entity values with the correct join keys. + table_entity_values, idxs = utils._get_unique_entities( + table, + join_key_values, + entity_name_to_join_key_map, + ) + + entity_key_protos = utils._get_entity_key_protos(table_entity_values) + + # Fetch data for Entities. + read_rows = self.online_read( + config=config, + table=table, + entity_keys=entity_key_protos, + requested_features=requested_features, + ) + + feature_data = utils._convert_rows_to_protobuf( + requested_features, read_rows + ) + + # Populate the result_rows with the Features from the OnlineStore inplace. + utils._populate_response_from_feature_data( + feature_data, + idxs, + online_features_response, + full_feature_names, + requested_features, + table, + ) + + if requested_on_demand_feature_views: + utils._augment_response_with_on_demand_transforms( + online_features_response, + feature_refs, + requested_on_demand_feature_views, + full_feature_names, + ) + + utils._drop_unneeded_columns( + online_features_response, requested_result_row_names + ) + return OnlineResponse(online_features_response) + + async def get_online_features_async( + self, + config: RepoConfig, + features: Union[List[str], FeatureService], + entity_rows: Union[ + List[Dict[str, Any]], + Mapping[str, Union[Sequence[Any], Sequence[ValueProto], RepeatedValue]], + ], + registry: BaseRegistry, + project: str, + full_feature_names: bool = False, + ) -> OnlineResponse: + if isinstance(entity_rows, list): + columnar: Dict[str, List[Any]] = {k: [] for k in entity_rows[0].keys()} + for entity_row in entity_rows: + for key, value in entity_row.items(): + try: + columnar[key].append(value) + except KeyError as e: + raise ValueError( + "All entity_rows must have the same keys." + ) from e + + entity_rows = columnar + + ( + join_key_values, + grouped_refs, + entity_name_to_join_key_map, + requested_on_demand_feature_views, + feature_refs, + requested_result_row_names, + online_features_response, + ) = utils._prepare_entities_to_read_from_online_store( + registry=registry, + project=project, + features=features, + entity_values=entity_rows, + full_feature_names=full_feature_names, + native_entity_values=True, + ) + + for table, requested_features in grouped_refs: + # Get the correct set of entity values with the correct join keys. + table_entity_values, idxs = utils._get_unique_entities( + table, + join_key_values, + entity_name_to_join_key_map, + ) + + entity_key_protos = utils._get_entity_key_protos(table_entity_values) + + # Fetch data for Entities. + read_rows = await self.online_read_async( + config=config, + table=table, + entity_keys=entity_key_protos, + requested_features=requested_features, + ) + + feature_data = utils._convert_rows_to_protobuf( + requested_features, read_rows + ) + + # Populate the result_rows with the Features from the OnlineStore inplace. + utils._populate_response_from_feature_data( + feature_data, + idxs, + online_features_response, + full_feature_names, + requested_features, + table, + ) + + if requested_on_demand_feature_views: + utils._augment_response_with_on_demand_transforms( + online_features_response, + feature_refs, + requested_on_demand_feature_views, + full_feature_names, + ) + + utils._drop_unneeded_columns( + online_features_response, requested_result_row_names + ) + return OnlineResponse(online_features_response) + @abstractmethod def update( self, diff --git a/sdk/python/feast/infra/passthrough_provider.py b/sdk/python/feast/infra/passthrough_provider.py index bad6f86cc6..c3c3048a89 100644 --- a/sdk/python/feast/infra/passthrough_provider.py +++ b/sdk/python/feast/infra/passthrough_provider.py @@ -1,5 +1,5 @@ from datetime import datetime, timedelta -from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union +from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Tuple, Union import pandas as pd import pyarrow as pa @@ -23,8 +23,10 @@ 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.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 +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 BATCH_ENGINE_CLASS_FOR_TYPE, RepoConfig from feast.saved_dataset import SavedDataset @@ -193,6 +195,48 @@ def online_read( ) return result + def get_online_features( + self, + config: RepoConfig, + features: Union[List[str], FeatureService], + entity_rows: Union[ + List[Dict[str, Any]], + Mapping[str, Union[Sequence[Any], Sequence[ValueProto], RepeatedValue]], + ], + registry: BaseRegistry, + project: str, + full_feature_names: bool = False, + ) -> OnlineResponse: + return self.online_store.get_online_features( + config=config, + features=features, + entity_rows=entity_rows, + registry=registry, + project=project, + full_feature_names=full_feature_names, + ) + + async def get_online_features_async( + self, + config: RepoConfig, + features: Union[List[str], FeatureService], + entity_rows: Union[ + List[Dict[str, Any]], + Mapping[str, Union[Sequence[Any], Sequence[ValueProto], RepeatedValue]], + ], + registry: BaseRegistry, + project: str, + full_feature_names: bool = False, + ) -> OnlineResponse: + return await self.online_store.get_online_features_async( + config=config, + features=features, + entity_rows=entity_rows, + registry=registry, + project=project, + full_feature_names=full_feature_names, + ) + async def online_read_async( self, config: RepoConfig, diff --git a/sdk/python/feast/infra/provider.py b/sdk/python/feast/infra/provider.py index 75afd6bba8..9940af1d02 100644 --- a/sdk/python/feast/infra/provider.py +++ b/sdk/python/feast/infra/provider.py @@ -1,7 +1,7 @@ from abc import ABC, abstractmethod from datetime import datetime from pathlib import Path -from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union +from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Tuple, Union import pandas as pd import pyarrow @@ -15,8 +15,10 @@ 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.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 +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.saved_dataset import SavedDataset @@ -230,6 +232,36 @@ def online_read( """ pass + @abstractmethod + def get_online_features( + self, + config: RepoConfig, + features: Union[List[str], FeatureService], + entity_rows: Union[ + List[Dict[str, Any]], + Mapping[str, Union[Sequence[Any], Sequence[ValueProto], RepeatedValue]], + ], + registry: BaseRegistry, + project: str, + full_feature_names: bool = False, + ) -> OnlineResponse: + pass + + @abstractmethod + async def get_online_features_async( + self, + config: RepoConfig, + features: Union[List[str], FeatureService], + entity_rows: Union[ + List[Dict[str, Any]], + Mapping[str, Union[Sequence[Any], Sequence[ValueProto], RepeatedValue]], + ], + registry: BaseRegistry, + project: str, + full_feature_names: bool = False, + ) -> OnlineResponse: + pass + @abstractmethod async def online_read_async( self, diff --git a/sdk/python/tests/foo_provider.py b/sdk/python/tests/foo_provider.py index bd1e247a7b..8e8f54db24 100644 --- a/sdk/python/tests/foo_provider.py +++ b/sdk/python/tests/foo_provider.py @@ -1,6 +1,6 @@ from datetime import datetime from pathlib import Path -from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union +from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Tuple, Union import pandas import pyarrow @@ -11,7 +11,9 @@ 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.online_response import OnlineResponse from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto +from feast.protos.feast.types.Value_pb2 import RepeatedValue from feast.protos.feast.types.Value_pb2 import Value as ValueProto from feast.saved_dataset import SavedDataset @@ -138,3 +140,31 @@ def validate_data_source( data_source: DataSource, ): pass + + def get_online_features( + self, + config: RepoConfig, + features: Union[List[str], FeatureService], + entity_rows: Union[ + List[Dict[str, Any]], + Mapping[str, Union[Sequence[Any], Sequence[ValueProto], RepeatedValue]], + ], + registry: BaseRegistry, + project: str, + full_feature_names: bool = False, + ) -> OnlineResponse: + pass + + async def get_online_features_async( + self, + config: RepoConfig, + features: Union[List[str], FeatureService], + entity_rows: Union[ + List[Dict[str, Any]], + Mapping[str, Union[Sequence[Any], Sequence[ValueProto], RepeatedValue]], + ], + registry: BaseRegistry, + project: str, + full_feature_names: bool = False, + ) -> OnlineResponse: + pass 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 f7ab55d868..4a4a7360d8 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 @@ -381,7 +381,7 @@ def setup(self, registry: RegistryConfig): repo_path = Path(tempfile.mkdtemp()) with open(repo_path / "feature_store.yaml", "w") as outfile: - yaml.dump(config.dict(by_alias=True), outfile) + yaml.dump(config.model_dump(by_alias=True), outfile) repo_path = str(repo_path.resolve()) self.server_port = free_port() From 398ea3b86c83605963124404ff4baa95162dc1f4 Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Mon, 1 Jul 2024 17:06:27 -0400 Subject: [PATCH 14/59] fix: Fix SQLite import issue (#4294) adding try and except block for import Co-authored-by: Francisco Javier Arceo --- .../feast/infra/online_stores/sqlite.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/sdk/python/feast/infra/online_stores/sqlite.py b/sdk/python/feast/infra/online_stores/sqlite.py index 41af14aaf1..9896b766d4 100644 --- a/sdk/python/feast/infra/online_stores/sqlite.py +++ b/sdk/python/feast/infra/online_stores/sqlite.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import itertools +import logging import os import sqlite3 import struct @@ -20,7 +21,6 @@ from pathlib import Path from typing import Any, Callable, Dict, List, Literal, Optional, Sequence, Tuple, Union -import sqlite_vec from google.protobuf.internal.containers import RepeatedScalarFieldContainer from pydantic import StrictStr @@ -84,7 +84,9 @@ 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): + if sys.version_info[0:2] == (3, 10) and config.online_store.vec_enabled: + import sqlite_vec # noqa: F401 + self._conn.enable_load_extension(True) # type: ignore sqlite_vec.load(self._conn) @@ -410,6 +412,10 @@ def retrieve_online_documents( def _initialize_conn(db_path: str): + try: + import sqlite_vec # noqa: F401 + except ModuleNotFoundError: + logging.warning("Cannot use sqlite_vec for vector search") Path(db_path).parent.mkdir(exist_ok=True) return sqlite3.connect( db_path, @@ -482,8 +488,13 @@ def from_proto(sqlite_table_proto: SqliteTableProto) -> Any: def update(self): if sys.version_info[0:2] == (3, 10): - self.conn.enable_load_extension(True) - sqlite_vec.load(self.conn) + try: + import sqlite_vec # noqa: F401 + + self.conn.enable_load_extension(True) + sqlite_vec.load(self.conn) + except ModuleNotFoundError: + logging.warning("Cannot use sqlite_vec for vector search") self.conn.execute( f"CREATE TABLE IF NOT EXISTS {self.name} (entity_key BLOB, feature_name TEXT, value BLOB, vector_value BLOB, event_ts timestamp, created_ts timestamp, PRIMARY KEY(entity_key, feature_name))" ) From 98ff63cd389207998b3452ec46e5a2f0fc70485c Mon Sep 17 00:00:00 2001 From: Shuchu Han Date: Mon, 1 Jul 2024 22:54:28 -0400 Subject: [PATCH 15/59] fix: Using one single function call for utcnow(). (#4307) Signed-off-by: Shuchu Han --- sdk/python/feast/feature_store.py | 9 +++--- .../feast/infra/offline_stores/bigquery.py | 4 +-- .../trino_offline_store/trino_queries.py | 6 ++-- .../feast/infra/online_stores/datastore.py | 6 ++-- .../feast/infra/registry/caching_registry.py | 9 +++--- .../contrib/azure/azure_registry_store.py | 4 +-- sdk/python/feast/infra/registry/file.py | 4 +-- sdk/python/feast/infra/registry/gcs.py | 4 +-- sdk/python/feast/infra/registry/registry.py | 21 ++++++------ sdk/python/feast/infra/registry/s3.py | 4 +-- sdk/python/feast/infra/registry/snowflake.py | 22 ++++++------- sdk/python/feast/infra/registry/sql.py | 9 +++--- sdk/python/feast/on_demand_feature_view.py | 6 ++-- sdk/python/feast/utils.py | 4 +++ sdk/python/tests/conftest.py | 11 ++++--- sdk/python/tests/data/data_creator.py | 15 +++++---- sdk/python/tests/doctest/test_all.py | 7 ++-- .../feature_repos/repo_configuration.py | 3 +- .../materialization/test_snowflake.py | 5 +-- .../offline_store/test_offline_write.py | 9 +++--- .../test_push_features_to_offline_store.py | 5 ++- .../test_universal_historical_retrieval.py | 13 ++++---- .../offline_store/test_validation.py | 5 ++- .../test_push_features_to_online_store.py | 7 ++-- .../test_python_feature_server.py | 10 +++--- .../online_store/test_remote_online_store.py | 4 +-- .../online_store/test_universal_online.py | 13 ++++---- .../test_universal_odfv_feature_inference.py | 7 ++-- .../registration/test_universal_registry.py | 7 ++-- .../registration/test_universal_types.py | 3 +- sdk/python/tests/unit/cli/test_cli_chdir.py | 5 +-- .../tests/unit/local_feast_tests/test_init.py | 5 +-- .../online_store/test_online_retrieval.py | 32 +++++++++---------- sdk/python/tests/unit/test_datetime.py | 6 ++++ sdk/python/tests/unit/test_feature_views.py | 10 +++--- .../tests/unit/test_stream_feature_view.py | 10 +++--- .../tests/utils/basic_read_write_test.py | 7 ++-- .../tests/utils/dynamo_table_creator.py | 5 ++- sdk/python/tests/utils/e2e_test_validation.py | 3 +- .../tests/utils/online_write_benchmark.py | 6 ++-- sdk/python/tests/utils/test_log_creator.py | 8 ++--- 41 files changed, 176 insertions(+), 157 deletions(-) create mode 100644 sdk/python/tests/unit/test_datetime.py diff --git a/sdk/python/feast/feature_store.py b/sdk/python/feast/feature_store.py index 6476af5ac8..9600732e17 100644 --- a/sdk/python/feast/feature_store.py +++ b/sdk/python/feast/feature_store.py @@ -86,6 +86,7 @@ from feast.repo_contents import RepoContents from feast.saved_dataset import SavedDataset, SavedDatasetStorage, ValidationReference from feast.stream_feature_view import StreamFeatureView +from feast.utils import _utc_now from feast.version import get_version warnings.simplefilter("once", DeprecationWarning) @@ -1246,7 +1247,7 @@ def materialize_incremental( >>> from feast import FeatureStore, RepoConfig >>> from datetime import datetime, timedelta >>> fs = FeatureStore(repo_path="project/feature_repo") - >>> fs.materialize_incremental(end_date=datetime.utcnow() - timedelta(minutes=5)) + >>> fs.materialize_incremental(end_date=_utc_now() - timedelta(minutes=5)) Materializing... ... @@ -1270,7 +1271,7 @@ def materialize_incremental( f" either a ttl to be set or for materialize() to have been run at least once." ) elif feature_view.ttl.total_seconds() > 0: - start_date = datetime.utcnow() - feature_view.ttl + start_date = _utc_now() - feature_view.ttl else: # TODO(felixwang9817): Find the earliest timestamp for this specific feature # view from the offline store, and set the start date to that timestamp. @@ -1278,7 +1279,7 @@ def materialize_incremental( f"Since the ttl is 0 for feature view {Style.BRIGHT + Fore.GREEN}{feature_view.name}{Style.RESET_ALL}, " "the start date will be set to 1 year before the current time." ) - start_date = datetime.utcnow() - timedelta(weeks=52) + start_date = _utc_now() - timedelta(weeks=52) provider = self._get_provider() print( f"{Style.BRIGHT + Fore.GREEN}{feature_view.name}{Style.RESET_ALL}" @@ -1335,7 +1336,7 @@ def materialize( >>> from datetime import datetime, timedelta >>> fs = FeatureStore(repo_path="project/feature_repo") >>> fs.materialize( - ... start_date=datetime.utcnow() - timedelta(hours=3), end_date=datetime.utcnow() - timedelta(minutes=10) + ... start_date=_utc_now() - timedelta(hours=3), end_date=_utc_now() - timedelta(minutes=10) ... ) Materializing... diff --git a/sdk/python/feast/infra/offline_stores/bigquery.py b/sdk/python/feast/infra/offline_stores/bigquery.py index 36334b606d..3e4a0f1b99 100644 --- a/sdk/python/feast/infra/offline_stores/bigquery.py +++ b/sdk/python/feast/infra/offline_stores/bigquery.py @@ -45,7 +45,7 @@ from feast.on_demand_feature_view import OnDemandFeatureView from feast.repo_config import FeastConfigBaseModel, RepoConfig from feast.saved_dataset import SavedDatasetStorage -from feast.utils import get_user_agent +from feast.utils import _utc_now, get_user_agent from .bigquery_source import ( BigQueryLoggingDestination, @@ -701,7 +701,7 @@ def _upload_entity_df( # Ensure that the table expires after some time table = client.get_table(table=table_name) - table.expires = datetime.utcnow() + timedelta(minutes=30) + table.expires = _utc_now() + timedelta(minutes=30) client.update_table(table, ["expires"]) return table diff --git a/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/trino_queries.py b/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/trino_queries.py index 50472407bc..3a26583af2 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/trino_queries.py +++ b/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/trino_queries.py @@ -1,6 +1,5 @@ from __future__ import annotations -import datetime import signal from dataclasses import dataclass from enum import Enum @@ -16,6 +15,7 @@ from feast.infra.offline_stores.contrib.trino_offline_store.trino_type_map import ( trino_to_pa_value_type, ) +from feast.utils import _utc_now class QueryStatus(Enum): @@ -97,12 +97,12 @@ def __init__(self, query_text: str, cursor: Cursor): def execute(self) -> Results: try: self.status = QueryStatus.RUNNING - start_time = datetime.datetime.utcnow() + start_time = _utc_now() self._cursor.execute(operation=self.query_text) rows = self._cursor.fetchall() - end_time = datetime.datetime.utcnow() + end_time = _utc_now() self.execution_time = end_time - start_time self.status = QueryStatus.COMPLETED diff --git a/sdk/python/feast/infra/online_stores/datastore.py b/sdk/python/feast/infra/online_stores/datastore.py index b33767cea5..9ae10792f5 100644 --- a/sdk/python/feast/infra/online_stores/datastore.py +++ b/sdk/python/feast/infra/online_stores/datastore.py @@ -44,7 +44,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, RepoConfig -from feast.utils import get_user_agent +from feast.utils import _utc_now, get_user_agent LOGGER = logging.getLogger(__name__) @@ -122,7 +122,7 @@ def update( entity = datastore.Entity( key=key, exclude_from_indexes=("created_ts", "event_ts", "values") ) - entity.update({"created_ts": datetime.utcnow()}) + entity.update({"created_ts": _utc_now()}) client.put(entity) for table in tables_to_delete: @@ -457,7 +457,7 @@ def update(self): entity = datastore.Entity( key=key, exclude_from_indexes=("created_ts", "event_ts", "values") ) - entity.update({"created_ts": datetime.utcnow()}) + entity.update({"created_ts": _utc_now()}) client.put(entity) def teardown(self): diff --git a/sdk/python/feast/infra/registry/caching_registry.py b/sdk/python/feast/infra/registry/caching_registry.py index 6336dd7fee..f7eab7d70a 100644 --- a/sdk/python/feast/infra/registry/caching_registry.py +++ b/sdk/python/feast/infra/registry/caching_registry.py @@ -1,6 +1,6 @@ import logging from abc import abstractmethod -from datetime import datetime, timedelta +from datetime import timedelta from threading import Lock from typing import List, Optional @@ -15,6 +15,7 @@ from feast.project_metadata import ProjectMetadata from feast.saved_dataset import SavedDataset, ValidationReference from feast.stream_feature_view import StreamFeatureView +from feast.utils import _utc_now logger = logging.getLogger(__name__) @@ -27,7 +28,7 @@ def __init__( ): self.cached_registry_proto = self.proto() proto_registry_utils.init_project_metadata(self.cached_registry_proto, project) - self.cached_registry_proto_created = datetime.utcnow() + self.cached_registry_proto_created = _utc_now() self._refresh_lock = Lock() self.cached_registry_proto_ttl = timedelta( seconds=cache_ttl_seconds if cache_ttl_seconds is not None else 0 @@ -318,7 +319,7 @@ def refresh(self, project: Optional[str] = None): self.cached_registry_proto, project ) self.cached_registry_proto = self.proto() - self.cached_registry_proto_created = datetime.utcnow() + self.cached_registry_proto_created = _utc_now() def _refresh_cached_registry_if_necessary(self): with self._refresh_lock: @@ -329,7 +330,7 @@ def _refresh_cached_registry_if_necessary(self): self.cached_registry_proto_ttl.total_seconds() > 0 # 0 ttl means infinity and ( - datetime.utcnow() + _utc_now() > ( self.cached_registry_proto_created + self.cached_registry_proto_ttl diff --git a/sdk/python/feast/infra/registry/contrib/azure/azure_registry_store.py b/sdk/python/feast/infra/registry/contrib/azure/azure_registry_store.py index 9c00170b0f..f9317bf7a4 100644 --- a/sdk/python/feast/infra/registry/contrib/azure/azure_registry_store.py +++ b/sdk/python/feast/infra/registry/contrib/azure/azure_registry_store.py @@ -3,7 +3,6 @@ import os import uuid -from datetime import datetime from pathlib import Path from tempfile import TemporaryFile from urllib.parse import urlparse @@ -11,6 +10,7 @@ from feast.infra.registry.registry import RegistryConfig from feast.infra.registry.registry_store import RegistryStore from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto +from feast.utils import _utc_now REGISTRY_SCHEMA_VERSION = "1" @@ -89,7 +89,7 @@ def teardown(self): def _write_registry(self, registry_proto: RegistryProto): registry_proto.version_id = str(uuid.uuid4()) - registry_proto.last_updated.FromDatetime(datetime.utcnow()) + registry_proto.last_updated.FromDatetime(_utc_now()) file_obj = TemporaryFile() file_obj.write(registry_proto.SerializeToString()) diff --git a/sdk/python/feast/infra/registry/file.py b/sdk/python/feast/infra/registry/file.py index 7117a0d2c6..ae783bf82c 100644 --- a/sdk/python/feast/infra/registry/file.py +++ b/sdk/python/feast/infra/registry/file.py @@ -1,10 +1,10 @@ import uuid -from datetime import datetime from pathlib import Path from feast.infra.registry.registry_store import RegistryStore from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto from feast.repo_config import RegistryConfig +from feast.utils import _utc_now class FileRegistryStore(RegistryStore): @@ -37,7 +37,7 @@ def teardown(self): def _write_registry(self, registry_proto: RegistryProto): registry_proto.version_id = str(uuid.uuid4()) - registry_proto.last_updated.FromDatetime(datetime.utcnow()) + registry_proto.last_updated.FromDatetime(_utc_now()) file_dir = self._filepath.parent file_dir.mkdir(exist_ok=True) with open(self._filepath, mode="wb", buffering=0) as f: diff --git a/sdk/python/feast/infra/registry/gcs.py b/sdk/python/feast/infra/registry/gcs.py index 7e4b7104cf..72498ad054 100644 --- a/sdk/python/feast/infra/registry/gcs.py +++ b/sdk/python/feast/infra/registry/gcs.py @@ -1,5 +1,4 @@ import uuid -from datetime import datetime from pathlib import Path from tempfile import TemporaryFile from urllib.parse import urlparse @@ -7,6 +6,7 @@ from feast.infra.registry.registry_store import RegistryStore from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto from feast.repo_config import RegistryConfig +from feast.utils import _utc_now class GCSRegistryStore(RegistryStore): @@ -62,7 +62,7 @@ def teardown(self): def _write_registry(self, registry_proto: RegistryProto): registry_proto.version_id = str(uuid.uuid4()) - registry_proto.last_updated.FromDatetime(datetime.utcnow()) + registry_proto.last_updated.FromDatetime(_utc_now()) # we have already checked the bucket exists so no need to do it again gs_bucket = self.gcs_client.get_bucket(self._bucket) blob = gs_bucket.blob(self._blob) diff --git a/sdk/python/feast/infra/registry/registry.py b/sdk/python/feast/infra/registry/registry.py index 4d6bff4cc7..fe44e6253a 100644 --- a/sdk/python/feast/infra/registry/registry.py +++ b/sdk/python/feast/infra/registry/registry.py @@ -47,6 +47,7 @@ from feast.repo_contents import RepoContents from feast.saved_dataset import SavedDataset, ValidationReference from feast.stream_feature_view import StreamFeatureView +from feast.utils import _utc_now REGISTRY_SCHEMA_VERSION = "1" @@ -217,7 +218,7 @@ def clone(self) -> "Registry": if self.cached_registry_proto else RegistryProto() ) - new_registry.cached_registry_proto_created = datetime.utcnow() + new_registry.cached_registry_proto_created = _utc_now() new_registry._registry_store = NoopRegistryStore() return new_registry @@ -248,7 +249,7 @@ def get_infra(self, project: str, allow_cache: bool = False) -> Infra: def apply_entity(self, entity: Entity, project: str, commit: bool = True): entity.is_valid() - now = datetime.utcnow() + now = _utc_now() if not entity.created_timestamp: entity.created_timestamp = now entity.last_updated_timestamp = now @@ -334,7 +335,7 @@ def delete_data_source(self, name: str, project: str, commit: bool = True): def apply_feature_service( self, feature_service: FeatureService, project: str, commit: bool = True ): - now = datetime.utcnow() + now = _utc_now() if not feature_service.created_timestamp: feature_service.created_timestamp = now feature_service.last_updated_timestamp = now @@ -390,7 +391,7 @@ def apply_feature_view( ): feature_view.ensure_valid() - now = datetime.utcnow() + now = _utc_now() if not feature_view.created_timestamp: feature_view.created_timestamp = now feature_view.last_updated_timestamp = now @@ -517,7 +518,7 @@ def apply_materialization( existing_feature_view.materialization_intervals.append( (start_date, end_date) ) - existing_feature_view.last_updated_timestamp = datetime.utcnow() + existing_feature_view.last_updated_timestamp = _utc_now() feature_view_proto = existing_feature_view.to_proto() feature_view_proto.spec.project = project del self.cached_registry_proto.feature_views[idx] @@ -539,7 +540,7 @@ def apply_materialization( existing_stream_feature_view.materialization_intervals.append( (start_date, end_date) ) - existing_stream_feature_view.last_updated_timestamp = datetime.utcnow() + existing_stream_feature_view.last_updated_timestamp = _utc_now() stream_feature_view_proto = existing_stream_feature_view.to_proto() stream_feature_view_proto.spec.project = project del self.cached_registry_proto.stream_feature_views[idx] @@ -664,7 +665,7 @@ def apply_saved_dataset( project: str, commit: bool = True, ): - now = datetime.utcnow() + now = _utc_now() if not saved_dataset.created_timestamp: saved_dataset.created_timestamp = now saved_dataset.last_updated_timestamp = now @@ -812,7 +813,7 @@ def _prepare_registry_for_changes(self, project: str): registry_proto = RegistryProto() registry_proto.registry_schema_version = REGISTRY_SCHEMA_VERSION self.cached_registry_proto = registry_proto - self.cached_registry_proto_created = datetime.utcnow() + self.cached_registry_proto_created = _utc_now() # Initialize project metadata if needed assert self.cached_registry_proto @@ -848,7 +849,7 @@ def _get_registry_proto( self.cached_registry_proto_ttl.total_seconds() > 0 # 0 ttl means infinity and ( - datetime.utcnow() + _utc_now() > ( self.cached_registry_proto_created + self.cached_registry_proto_ttl @@ -871,7 +872,7 @@ def _get_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 = datetime.utcnow() + self.cached_registry_proto_created = _utc_now() if not project: return registry_proto diff --git a/sdk/python/feast/infra/registry/s3.py b/sdk/python/feast/infra/registry/s3.py index cbae3af11c..8aac4d52ee 100644 --- a/sdk/python/feast/infra/registry/s3.py +++ b/sdk/python/feast/infra/registry/s3.py @@ -1,6 +1,5 @@ import os import uuid -from datetime import datetime from pathlib import Path from tempfile import TemporaryFile from urllib.parse import urlparse @@ -9,6 +8,7 @@ from feast.infra.registry.registry_store import RegistryStore from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto from feast.repo_config import RegistryConfig +from feast.utils import _utc_now try: import boto3 @@ -70,7 +70,7 @@ def teardown(self): def _write_registry(self, registry_proto: RegistryProto): registry_proto.version_id = str(uuid.uuid4()) - registry_proto.last_updated.FromDatetime(datetime.utcnow()) + registry_proto.last_updated.FromDatetime(_utc_now()) # we have already checked the bucket exists so no need to do it again file_obj = TemporaryFile() file_obj.write(registry_proto.SerializeToString()) diff --git a/sdk/python/feast/infra/registry/snowflake.py b/sdk/python/feast/infra/registry/snowflake.py index d7ab67e7d0..f2bc09e7e4 100644 --- a/sdk/python/feast/infra/registry/snowflake.py +++ b/sdk/python/feast/infra/registry/snowflake.py @@ -10,7 +10,6 @@ from pydantic import ConfigDict, Field, StrictStr import feast -from feast import utils from feast.base_feature_view import BaseFeatureView from feast.data_source import DataSource from feast.entity import Entity @@ -54,6 +53,7 @@ from feast.repo_config import RegistryConfig from feast.saved_dataset import SavedDataset, ValidationReference from feast.stream_feature_view import StreamFeatureView +from feast.utils import _utc_now, has_all_tags logger = logging.getLogger(__name__) @@ -126,16 +126,15 @@ def __init__( with GetSnowflakeConnection(self.registry_config) as conn: sql_function_file = f"{os.path.dirname(feast.__file__)}/infra/utils/snowflake/registry/snowflake_table_creation.sql" with open(sql_function_file, "r") as file: - sqlFile = file.read() - - sqlCommands = sqlFile.split(";") - for command in sqlCommands: + sql_file = file.read() + sql_cmds = sql_file.split(";") + for command in sql_cmds: query = command.replace("REGISTRY_PATH", f"{self.registry_path}") execute_snowflake_statement(conn, query) self.cached_registry_proto = self.proto() proto_registry_utils.init_project_metadata(self.cached_registry_proto, project) - self.cached_registry_proto_created = datetime.utcnow() + self.cached_registry_proto_created = _utc_now() self._refresh_lock = Lock() self.cached_registry_proto_ttl = timedelta( seconds=registry_config.cache_ttl_seconds @@ -154,7 +153,7 @@ def refresh(self, project: Optional[str] = None): self.cached_registry_proto, project ) self.cached_registry_proto = self.proto() - self.cached_registry_proto_created = datetime.utcnow() + self.cached_registry_proto_created = _utc_now() def _refresh_cached_registry_if_necessary(self): with self._refresh_lock: @@ -165,7 +164,7 @@ def _refresh_cached_registry_if_necessary(self): self.cached_registry_proto_ttl.total_seconds() > 0 # 0 ttl means infinity and ( - datetime.utcnow() + _utc_now() > ( self.cached_registry_proto_created + self.cached_registry_proto_ttl @@ -182,7 +181,6 @@ def teardown(self): sql_function_file = f"{os.path.dirname(feast.__file__)}/infra/utils/snowflake/registry/snowflake_table_deletion.sql" with open(sql_function_file, "r") as file: sqlFile = file.read() - sqlCommands = sqlFile.split(";") for command in sqlCommands: query = command.replace("REGISTRY_PATH", f"{self.registry_path}") @@ -281,7 +279,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}" - update_datetime = datetime.utcnow() + update_datetime = _utc_now() if hasattr(obj, "last_updated_timestamp"): obj.last_updated_timestamp = update_datetime @@ -416,7 +414,7 @@ def _delete_object( if cursor.rowcount < 1 and not_found_exception: # type: ignore raise not_found_exception(name, project) - self._set_last_updated_metadata(datetime.utcnow(), project) + self._set_last_updated_metadata(_utc_now(), project) return cursor.rowcount @@ -787,7 +785,7 @@ def _list_objects( obj = python_class.from_proto( proto_class.FromString(row[1][proto_field_name]) ) - if utils.has_all_tags(obj.tags, tags): + if has_all_tags(obj.tags, tags): objects.append(obj) return objects return [] diff --git a/sdk/python/feast/infra/registry/sql.py b/sdk/python/feast/infra/registry/sql.py index 239898677c..6ef08989b7 100644 --- a/sdk/python/feast/infra/registry/sql.py +++ b/sdk/python/feast/infra/registry/sql.py @@ -60,6 +60,7 @@ from feast.repo_config import RegistryConfig from feast.saved_dataset import SavedDataset, ValidationReference from feast.stream_feature_view import StreamFeatureView +from feast.utils import _utc_now metadata = MetaData() @@ -591,7 +592,7 @@ def apply_user_metadata( table.c.project_id == project, ) row = conn.execute(stmt).first() - update_datetime = datetime.utcnow() + update_datetime = _utc_now() update_time = int(update_datetime.timestamp()) if row: values = { @@ -703,7 +704,7 @@ def _apply_object( assert name, f"name needs to be provided for {obj}" with self.engine.begin() as conn: - update_datetime = datetime.utcnow() + update_datetime = _utc_now() update_time = int(update_datetime.timestamp()) stmt = select(table).where( getattr(table.c, id_field_name) == name, table.c.project_id == project @@ -770,7 +771,7 @@ def _apply_object( def _maybe_init_project_metadata(self, project): # Initialize project metadata if needed with self.engine.begin() as conn: - update_datetime = datetime.utcnow() + update_datetime = _utc_now() update_time = int(update_datetime.timestamp()) stmt = select(feast_metadata).where( feast_metadata.c.metadata_key == FeastMetadataKeys.PROJECT_UUID.value, @@ -803,7 +804,7 @@ 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(datetime.utcnow(), project) + self._set_last_updated_metadata(_utc_now(), project) return rows.rowcount diff --git a/sdk/python/feast/on_demand_feature_view.py b/sdk/python/feast/on_demand_feature_view.py index 839ce4d64c..586f5d1bac 100644 --- a/sdk/python/feast/on_demand_feature_view.py +++ b/sdk/python/feast/on_demand_feature_view.py @@ -2,7 +2,6 @@ import functools import inspect import warnings -from datetime import datetime from types import FunctionType from typing import Any, Optional, Union @@ -34,6 +33,7 @@ from feast.transformation.pandas_transformation import PandasTransformation from feast.transformation.python_transformation import PythonTransformation from feast.transformation.substrait_transformation import SubstraitTransformation +from feast.utils import _utc_now from feast.value_type import ValueType warnings.simplefilter("once", DeprecationWarning) @@ -549,7 +549,7 @@ def _construct_random_input(self) -> dict[str, list[Any]]: ValueType.DOUBLE: [1.0], ValueType.FLOAT: [1.0], ValueType.BOOL: [True], - ValueType.UNIX_TIMESTAMP: [datetime.utcnow()], + ValueType.UNIX_TIMESTAMP: [_utc_now()], ValueType.BYTES_LIST: [[str.encode("hello world")]], ValueType.STRING_LIST: [["hello world"]], ValueType.INT32_LIST: [[1]], @@ -557,7 +557,7 @@ def _construct_random_input(self) -> dict[str, list[Any]]: ValueType.DOUBLE_LIST: [[1.0]], ValueType.FLOAT_LIST: [[1.0]], ValueType.BOOL_LIST: [[True]], - ValueType.UNIX_TIMESTAMP_LIST: [[datetime.utcnow()]], + ValueType.UNIX_TIMESTAMP_LIST: [[_utc_now()]], } feature_dict = {} diff --git a/sdk/python/feast/utils.py b/sdk/python/feast/utils.py index a6c893c954..1a1d757fc1 100644 --- a/sdk/python/feast/utils.py +++ b/sdk/python/feast/utils.py @@ -1052,3 +1052,7 @@ def tags_str_to_dict(tags: str = "") -> dict[str, str]: cast(tuple[str, str], tag.split(":", 1)) for tag in tags_list if ":" in tag ).items() } + + +def _utc_now() -> datetime: + return datetime.utcnow() diff --git a/sdk/python/tests/conftest.py b/sdk/python/tests/conftest.py index fb6b7e5608..1fd510d104 100644 --- a/sdk/python/tests/conftest.py +++ b/sdk/python/tests/conftest.py @@ -15,7 +15,7 @@ import multiprocessing import os import random -from datetime import datetime, timedelta +from datetime import timedelta from multiprocessing import Process from sys import platform from typing import Any, Dict, List, Tuple, no_type_check @@ -27,6 +27,7 @@ from feast.data_source import DataSource 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, @@ -133,7 +134,7 @@ def pytest_collection_modifyitems(config, items: List[Item]): @pytest.fixture def simple_dataset_1() -> pd.DataFrame: - now = datetime.utcnow() + now = _utc_now() ts = pd.Timestamp(now).round("ms") data = { "id_join_key": [1, 2, 1, 3, 3], @@ -153,7 +154,7 @@ def simple_dataset_1() -> pd.DataFrame: @pytest.fixture def simple_dataset_2() -> pd.DataFrame: - now = datetime.utcnow() + now = _utc_now() ts = pd.Timestamp(now).round("ms") data = { "id_join_key": ["a", "b", "c", "d", "e"], @@ -391,8 +392,8 @@ def fake_ingest_data(): "conv_rate": [0.5], "acc_rate": [0.6], "avg_daily_trips": [4], - "event_timestamp": [pd.Timestamp(datetime.utcnow()).round("ms")], - "created": [pd.Timestamp(datetime.utcnow()).round("ms")], + "event_timestamp": [pd.Timestamp(_utc_now()).round("ms")], + "created": [pd.Timestamp(_utc_now()).round("ms")], } return pd.DataFrame(data) diff --git a/sdk/python/tests/data/data_creator.py b/sdk/python/tests/data/data_creator.py index 1be96f753a..15d09c5a40 100644 --- a/sdk/python/tests/data/data_creator.py +++ b/sdk/python/tests/data/data_creator.py @@ -5,6 +5,7 @@ from pytz import timezone, utc from feast.types import FeastType, Float32, Int32, Int64, String +from feast.utils import _utc_now def create_basic_driver_dataset( @@ -13,7 +14,7 @@ def create_basic_driver_dataset( feature_is_list: bool = False, list_has_empty_list: bool = False, ) -> pd.DataFrame: - now = datetime.utcnow().replace(microsecond=0, second=0, minute=0) + now = _utc_now().replace(microsecond=0, second=0, minute=0) ts = pd.Timestamp(now).round("ms") data = { "driver_id": get_entities_for_feast_type(entity_type), @@ -86,14 +87,14 @@ def create_document_dataset() -> pd.DataFrame: "embedding_float": [[4.0, 5.0], [1.0, 2.0], [3.0, 4.0]], "embedding_double": [[4.0, 5.0], [1.0, 2.0], [3.0, 4.0]], "ts": [ - pd.Timestamp(datetime.utcnow()).round("ms"), - pd.Timestamp(datetime.utcnow()).round("ms"), - pd.Timestamp(datetime.utcnow()).round("ms"), + pd.Timestamp(_utc_now()).round("ms"), + pd.Timestamp(_utc_now()).round("ms"), + pd.Timestamp(_utc_now()).round("ms"), ], "created_ts": [ - pd.Timestamp(datetime.utcnow()).round("ms"), - pd.Timestamp(datetime.utcnow()).round("ms"), - pd.Timestamp(datetime.utcnow()).round("ms"), + pd.Timestamp(_utc_now()).round("ms"), + pd.Timestamp(_utc_now()).round("ms"), + pd.Timestamp(_utc_now()).round("ms"), ], } return pd.DataFrame(data) diff --git a/sdk/python/tests/doctest/test_all.py b/sdk/python/tests/doctest/test_all.py index 814a7ca798..52348e7da4 100644 --- a/sdk/python/tests/doctest/test_all.py +++ b/sdk/python/tests/doctest/test_all.py @@ -6,13 +6,14 @@ import unittest import feast +from feast.utils import _utc_now FILES_TO_IGNORE = {"app"} def setup_feature_store(): """Prepares the local environment for a FeatureStore docstring test.""" - from datetime import datetime, timedelta + from datetime import timedelta from feast import Entity, FeatureStore, FeatureView, Field, FileSource from feast.repo_operations import init_repo @@ -42,8 +43,8 @@ def setup_feature_store(): ) fs.apply([driver_hourly_stats_view, driver]) fs.materialize( - start_date=datetime.utcnow() - timedelta(hours=3), - end_date=datetime.utcnow() - timedelta(minutes=10), + start_date=_utc_now() - timedelta(hours=3), + end_date=_utc_now() - timedelta(minutes=10), ) diff --git a/sdk/python/tests/integration/feature_repos/repo_configuration.py b/sdk/python/tests/integration/feature_repos/repo_configuration.py index 9e3c02b9c0..48f5070f1e 100644 --- a/sdk/python/tests/integration/feature_repos/repo_configuration.py +++ b/sdk/python/tests/integration/feature_repos/repo_configuration.py @@ -21,6 +21,7 @@ ) from feast.infra.feature_servers.local_process.config import LocalFeatureServerConfig from feast.repo_config import RegistryConfig, RepoConfig +from feast.utils import _utc_now from tests.integration.feature_repos.integration_test_repo_config import ( IntegrationTestRepoConfig, RegistryLocation, @@ -412,7 +413,7 @@ class Environment: fixture_request: Optional[pytest.FixtureRequest] = None def __post_init__(self): - self.end_date = datetime.utcnow().replace(microsecond=0, second=0, minute=0) + self.end_date = _utc_now().replace(microsecond=0, second=0, minute=0) self.start_date: datetime = self.end_date - timedelta(days=3) def setup(self): diff --git a/sdk/python/tests/integration/materialization/test_snowflake.py b/sdk/python/tests/integration/materialization/test_snowflake.py index adb2bd7e7d..f12191363b 100644 --- a/sdk/python/tests/integration/materialization/test_snowflake.py +++ b/sdk/python/tests/integration/materialization/test_snowflake.py @@ -8,6 +8,7 @@ from feast.entity import Entity from feast.feature_view import 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 from tests.integration.feature_repos.integration_test_repo_config import ( IntegrationTestRepoConfig, @@ -146,7 +147,7 @@ def test_snowflake_materialization_consistency_internal_with_lists( split_dt = df["ts_1"][4].to_pydatetime() - timedelta(seconds=1) print(f"Split datetime: {split_dt}") - now = datetime.utcnow() + now = _utc_now() full_feature_names = True start_date = (now - timedelta(hours=5)).replace(tzinfo=utc) @@ -231,7 +232,7 @@ def test_snowflake_materialization_entityless_fv(): print(f"Split datetime: {split_dt}") - now = datetime.utcnow() + now = _utc_now() start_date = (now - timedelta(hours=5)).replace(tzinfo=utc) end_date = split_dt diff --git a/sdk/python/tests/integration/offline_store/test_offline_write.py b/sdk/python/tests/integration/offline_store/test_offline_write.py index b8c465946d..63bdc4755a 100644 --- a/sdk/python/tests/integration/offline_store/test_offline_write.py +++ b/sdk/python/tests/integration/offline_store/test_offline_write.py @@ -1,5 +1,5 @@ import random -from datetime import datetime, timedelta +from datetime import timedelta import numpy as np import pandas as pd @@ -7,6 +7,7 @@ from feast import FeatureView, Field from feast.types import Float32, Int32 +from feast.utils import _utc_now from tests.integration.feature_repos.repo_configuration import ( construct_universal_feature_views, ) @@ -23,7 +24,7 @@ def test_reorder_columns(environment, universal_data_sources): driver_fv = feature_views.driver store.apply([driver(), driver_fv]) - now = datetime.utcnow() + now = _utc_now() ts = pd.Timestamp(now).round("ms") # This dataframe has columns in the wrong order. @@ -53,7 +54,7 @@ def test_writing_incorrect_schema_fails(environment, universal_data_sources): driver_fv = feature_views.driver store.apply([driver(), driver_fv]) - now = datetime.utcnow() + now = _utc_now() ts = pd.Timestamp(now).round("ms") expected_df = pd.DataFrame.from_dict( @@ -91,7 +92,7 @@ def test_writing_consecutively_to_offline_store(environment, universal_data_sour ), # This is to make sure all offline store data is out of date since get_historical_features() only searches backwards for a ttl window. ) - now = datetime.utcnow() + now = _utc_now() ts = pd.Timestamp(now, unit="ns") entity_df = pd.DataFrame.from_dict( diff --git a/sdk/python/tests/integration/offline_store/test_push_features_to_offline_store.py b/sdk/python/tests/integration/offline_store/test_push_features_to_offline_store.py index 0b1db9011a..5e3d72e671 100644 --- a/sdk/python/tests/integration/offline_store/test_push_features_to_offline_store.py +++ b/sdk/python/tests/integration/offline_store/test_push_features_to_offline_store.py @@ -1,10 +1,9 @@ -import datetime - import numpy as np import pandas as pd import pytest from feast.data_source import PushMode +from feast.utils import _utc_now from tests.integration.feature_repos.repo_configuration import ( construct_universal_feature_views, ) @@ -20,7 +19,7 @@ def test_push_features_and_read(environment, universal_data_sources): location_fv = feature_views.pushed_locations store.apply([location(), location_fv]) - now = pd.Timestamp(datetime.datetime.utcnow()).round("ms") + now = pd.Timestamp(_utc_now()).round("ms") entity_df = pd.DataFrame.from_dict({"location_id": [1], "event_timestamp": [now]}) before_df = store.get_historical_features( 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 bfb8a56200..ecaa5f40db 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 @@ -15,6 +15,7 @@ DEFAULT_ENTITY_DF_EVENT_TIMESTAMP_COL, ) from feast.types import Float32, Int32 +from feast.utils import _utc_now from tests.integration.feature_repos.repo_configuration import ( construct_universal_feature_views, table_name_from_data_source, @@ -144,11 +145,11 @@ def test_historical_features_main( files = job_from_df.to_remote_storage() assert len(files) # 0 # This test should be way more detailed - start_time = datetime.utcnow() + start_time = _utc_now() actual_df_from_df_entities = job_from_df.to_df() print(f"actual_df_from_df_entities shape: {actual_df_from_df_entities.shape}") - end_time = datetime.utcnow() + end_time = _utc_now() print(str(f"Time to execute job_from_df.to_df() = '{(end_time - start_time)}'\n")) assert sorted(expected_df.columns) == sorted(actual_df_from_df_entities.columns) @@ -303,9 +304,9 @@ def test_historical_features_with_entities_from_query( full_feature_names=full_feature_names, ) - start_time = datetime.utcnow() + start_time = _utc_now() actual_df_from_sql_entities = job_from_sql.to_df() - end_time = datetime.utcnow() + end_time = _utc_now() print(str(f"\nTime to execute job_from_sql.to_df() = '{(end_time - start_time)}'")) event_timestamp = ( @@ -618,11 +619,11 @@ def test_historical_features_containing_backfills(environment): full_feature_names=False, ) - start_time = datetime.utcnow() + start_time = _utc_now() actual_df = offline_job.to_df() print(f"actual_df shape: {actual_df.shape}") - end_time = datetime.utcnow() + end_time = _utc_now() print(str(f"Time to execute job_from_df.to_df() = '{(end_time - start_time)}'\n")) assert sorted(expected_df.columns) == sorted(actual_df.columns) diff --git a/sdk/python/tests/integration/offline_store/test_validation.py b/sdk/python/tests/integration/offline_store/test_validation.py index 1731f823c8..6f0496e8c8 100644 --- a/sdk/python/tests/integration/offline_store/test_validation.py +++ b/sdk/python/tests/integration/offline_store/test_validation.py @@ -16,7 +16,7 @@ LoggingConfig, ) from feast.protos.feast.serving.ServingService_pb2 import FieldStatus -from feast.utils import make_tzaware +from feast.utils import _utc_now, make_tzaware from feast.wait import wait_retry_backoff from tests.integration.feature_repos.repo_configuration import ( construct_universal_feature_views, @@ -316,8 +316,7 @@ def test_e2e_validation_via_cli(environment, universal_data_sources): "avg_passenger_count": [0], "lifetime_trip_count": [0], "event_timestamp": [ - make_tzaware(datetime.datetime.utcnow()) - - datetime.timedelta(hours=1) + make_tzaware(_utc_now()) - datetime.timedelta(hours=1) ], } ) 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 42561563f9..98fe3ab1ec 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 @@ -1,8 +1,7 @@ -import datetime - import pandas as pd import pytest +from feast.utils import _utc_now from tests.integration.feature_repos.repo_configuration import ( construct_universal_feature_views, ) @@ -21,8 +20,8 @@ def test_push_features_and_read(environment, universal_data_sources): data = { "location_id": [1], "temperature": [4], - "event_timestamp": [pd.Timestamp(datetime.datetime.utcnow()).round("ms")], - "created": [pd.Timestamp(datetime.datetime.utcnow()).round("ms")], + "event_timestamp": [pd.Timestamp(_utc_now()).round("ms")], + "created": [pd.Timestamp(_utc_now()).round("ms")], } df_ingest = pd.DataFrame(data) 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 089efd7a56..1010e73178 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 @@ -1,5 +1,4 @@ import json -from datetime import datetime from typing import List import pytest @@ -7,6 +6,7 @@ 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, ) @@ -67,8 +67,8 @@ def test_push(python_fs_client): "df": { "location_id": [1], "temperature": [initial_temp * 100], - "event_timestamp": [str(datetime.utcnow())], - "created": [str(datetime.utcnow())], + "event_timestamp": [str(_utc_now())], + "created": [str(_utc_now())], }, } ) @@ -98,8 +98,8 @@ def test_push_source_does_not_exist(python_fs_client): "df": { "location_id": [1], "temperature": [initial_temp * 100], - "event_timestamp": [str(datetime.utcnow())], - "created": [str(datetime.utcnow())], + "event_timestamp": [str(_utc_now())], + "created": [str(_utc_now())], }, } ), 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 759a9c7a87..1d5dd0fca0 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,12 +1,12 @@ import os import subprocess import tempfile -from datetime import datetime from textwrap import dedent import pytest from feast.feature_store import FeatureStore +from feast.utils import _utc_now from feast.wait import wait_retry_backoff from tests.utils.cli_repo_creator import CliRunner from tests.utils.http_server import check_port_open, free_port @@ -150,7 +150,7 @@ def _default_store(temp_dir, project_name) -> FeatureStore: fs = FeatureStore(repo_path=repo_path) fs.materialize_incremental( - end_date=datetime.utcnow(), feature_views=["driver_hourly_stats"] + end_date=_utc_now(), feature_views=["driver_hourly_stats"] ) return 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 c6b034e2aa..38656b90a9 100644 --- a/sdk/python/tests/integration/online_store/test_universal_online.py +++ b/sdk/python/tests/integration/online_store/test_universal_online.py @@ -22,6 +22,7 @@ from feast.infra.utils.postgres.postgres_config import ConnectionType from feast.online_response import TIMESTAMP_POSTFIX from feast.types import Float32, Int32, String +from feast.utils import _utc_now from feast.wait import wait_retry_backoff from tests.integration.feature_repos.repo_configuration import ( Environment, @@ -136,9 +137,9 @@ def test_write_to_online_store_event_check(environment): fs = environment.feature_store # write same data points 3 with different timestamps - now = pd.Timestamp(datetime.datetime.utcnow()).round("ms") - hour_ago = pd.Timestamp(datetime.datetime.utcnow() - timedelta(hours=1)).round("ms") - latest = pd.Timestamp(datetime.datetime.utcnow() + timedelta(seconds=1)).round("ms") + now = pd.Timestamp(_utc_now()).round("ms") + hour_ago = pd.Timestamp(_utc_now() - timedelta(hours=1)).round("ms") + latest = pd.Timestamp(_utc_now() + timedelta(seconds=1)).round("ms") data = { "id": [123, 567, 890], @@ -221,7 +222,7 @@ def test_write_to_online_store_event_check(environment): # writes to online store via datasource (dataframe_source) materialization fs.materialize( start_date=datetime.datetime.now() - timedelta(hours=12), - end_date=datetime.datetime.utcnow(), + end_date=_utc_now(), ) df = fs.get_online_features( @@ -250,8 +251,8 @@ def test_write_to_online_store(environment, universal_data_sources): "conv_rate": [0.85], "acc_rate": [0.91], "avg_daily_trips": [14], - "event_timestamp": [pd.Timestamp(datetime.datetime.utcnow()).round("ms")], - "created": [pd.Timestamp(datetime.datetime.utcnow()).round("ms")], + "event_timestamp": [pd.Timestamp(_utc_now()).round("ms")], + "created": [pd.Timestamp(_utc_now()).round("ms")], } df_data = pd.DataFrame(data) diff --git a/sdk/python/tests/integration/registration/test_universal_odfv_feature_inference.py b/sdk/python/tests/integration/registration/test_universal_odfv_feature_inference.py index ce960b9c35..151f629289 100644 --- a/sdk/python/tests/integration/registration/test_universal_odfv_feature_inference.py +++ b/sdk/python/tests/integration/registration/test_universal_odfv_feature_inference.py @@ -1,5 +1,3 @@ -from datetime import datetime - import pandas as pd import pytest @@ -7,6 +5,7 @@ from feast.errors import SpecifiedFeaturesNotPresentError from feast.infra.offline_stores.file_source import FileSource from feast.types import Float64 +from feast.utils import _utc_now from tests.integration.feature_repos.universal.entities import customer, driver, item from tests.integration.feature_repos.universal.feature_views import ( conv_rate_plus_100_feature_view, @@ -50,8 +49,8 @@ def test_infer_odfv_list_features(environment, infer_features, tmp_path): "item_id": [0], "embedding_float": [fake_embedding], "embedding_double": [fake_embedding], - "event_timestamp": [pd.Timestamp(datetime.utcnow())], - "created": [pd.Timestamp(datetime.utcnow())], + "event_timestamp": [pd.Timestamp(_utc_now())], + "created": [pd.Timestamp(_utc_now())], } ) output_path = f"{tmp_path}/items.parquet" diff --git a/sdk/python/tests/integration/registration/test_universal_registry.py b/sdk/python/tests/integration/registration/test_universal_registry.py index c119ae800a..c06ccf2d4d 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 datetime, timedelta +from datetime import timedelta from tempfile import mkstemp from unittest import mock @@ -46,6 +46,7 @@ from feast.repo_config import RegistryConfig from feast.stream_feature_view import Aggregation, StreamFeatureView from feast.types import Array, Bytes, Float32, Int32, Int64, String +from feast.utils import _utc_now from feast.value_type import ValueType from tests.integration.feature_repos.universal.entities import driver @@ -745,7 +746,7 @@ def odfv1(feature_df: pd.DataFrame) -> pd.DataFrame: ) # Simulate materialization - current_date = datetime.utcnow() + current_date = _utc_now() end_date = current_date.replace(tzinfo=utc) start_date = (current_date - timedelta(days=1)).replace(tzinfo=utc) test_registry.apply_materialization(feature_view, project, start_date, end_date) @@ -814,7 +815,7 @@ def odfv1(feature_df: pd.DataFrame) -> pd.DataFrame: ) # Simulate materialization a second time - current_date = datetime.utcnow() + current_date = _utc_now() end_date_1 = current_date.replace(tzinfo=utc) start_date_1 = (current_date - timedelta(days=1)).replace(tzinfo=utc) test_registry.apply_materialization( diff --git a/sdk/python/tests/integration/registration/test_universal_types.py b/sdk/python/tests/integration/registration/test_universal_types.py index ca15681c9b..928d05ad31 100644 --- a/sdk/python/tests/integration/registration/test_universal_types.py +++ b/sdk/python/tests/integration/registration/test_universal_types.py @@ -20,6 +20,7 @@ String, UnixTimestamp, ) +from feast.utils import _utc_now from tests.data.data_creator import create_basic_driver_dataset from tests.integration.feature_repos.universal.entities import driver from tests.integration.feature_repos.universal.feature_views import driver_feature_view @@ -93,7 +94,7 @@ def test_feature_get_historical_features_types_match( entity_df = pd.DataFrame() entity_df["driver_id"] = [1, 3] - ts = pd.Timestamp(datetime.utcnow()).round("ms") + ts = pd.Timestamp(_utc_now()).round("ms") entity_df["ts"] = [ ts - timedelta(hours=4), ts - timedelta(hours=2), diff --git a/sdk/python/tests/unit/cli/test_cli_chdir.py b/sdk/python/tests/unit/cli/test_cli_chdir.py index 12ca8f6b08..dd592db074 100644 --- a/sdk/python/tests/unit/cli/test_cli_chdir.py +++ b/sdk/python/tests/unit/cli/test_cli_chdir.py @@ -1,7 +1,8 @@ import tempfile -from datetime import datetime, timedelta +from datetime import timedelta from pathlib import Path +from feast.utils import _utc_now from tests.utils.cli_repo_creator import CliRunner @@ -29,7 +30,7 @@ def test_cli_chdir() -> None: ) assert result.returncode == 0 - end_date = datetime.utcnow() + end_date = _utc_now() start_date = end_date - timedelta(days=100) result = runner.run( [ diff --git a/sdk/python/tests/unit/local_feast_tests/test_init.py b/sdk/python/tests/unit/local_feast_tests/test_init.py index c5d3cbe57d..4543a23979 100644 --- a/sdk/python/tests/unit/local_feast_tests/test_init.py +++ b/sdk/python/tests/unit/local_feast_tests/test_init.py @@ -1,8 +1,9 @@ import tempfile -from datetime import datetime, timedelta +from datetime import timedelta from pathlib import Path from textwrap import dedent +from feast.utils import _utc_now from tests.utils.cli_repo_creator import CliRunner @@ -20,7 +21,7 @@ def test_repo_init() -> None: result = runner.run(["apply"], cwd=repo_path) assert result.returncode == 0 - end_date = datetime.utcnow() + end_date = _utc_now() start_date = end_date - timedelta(days=100) result = runner.run( ["materialize", start_date.isoformat(), end_date.isoformat()], cwd=repo_path 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 1e8cf45dcc..0b552c0453 100644 --- a/sdk/python/tests/unit/online_store/test_online_retrieval.py +++ b/sdk/python/tests/unit/online_store/test_online_retrieval.py @@ -3,7 +3,6 @@ import sqlite3 import sys import time -from datetime import datetime import numpy as np import pandas as pd @@ -17,6 +16,7 @@ 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 RegistryConfig +from feast.utils import _utc_now from tests.integration.feature_repos.universal.feature_views import TAGS from tests.utils.cli_repo_creator import CliRunner, get_example_repo @@ -51,8 +51,8 @@ def test_get_online_features() -> None: "lat": ValueProto(double_val=0.1), "lon": ValueProto(string_val="1.0"), }, - datetime.utcnow(), - datetime.utcnow(), + _utc_now(), + _utc_now(), ) ], progress=None, @@ -72,8 +72,8 @@ def test_get_online_features() -> None: "name": ValueProto(string_val="John"), "age": ValueProto(int64_val=3), }, - datetime.utcnow(), - datetime.utcnow(), + _utc_now(), + _utc_now(), ) ], progress=None, @@ -90,8 +90,8 @@ def test_get_online_features() -> None: ( customer_key, {"trips": ValueProto(int64_val=7)}, - datetime.utcnow(), - datetime.utcnow(), + _utc_now(), + _utc_now(), ) ], progress=None, @@ -318,8 +318,8 @@ def test_online_to_df(): "lat": ValueProto(double_val=d * lat_multiply), "lon": ValueProto(string_val=str(d * lon_multiply)), }, - datetime.utcnow(), - datetime.utcnow(), + _utc_now(), + _utc_now(), ) ], progress=None, @@ -348,8 +348,8 @@ def test_online_to_df(): "name": ValueProto(string_val=name + str(c)), "age": ValueProto(int64_val=c * age_multiply), }, - datetime.utcnow(), - datetime.utcnow(), + _utc_now(), + _utc_now(), ) ], progress=None, @@ -372,8 +372,8 @@ def test_online_to_df(): ( combo_keys, {"trips": ValueProto(int64_val=c * d)}, - datetime.utcnow(), - datetime.utcnow(), + _utc_now(), + _utc_now(), ) ], progress=None, @@ -468,8 +468,8 @@ def test_sqlite_get_online_documents() -> None: ) ) }, - datetime.utcnow(), - datetime.utcnow(), + _utc_now(), + _utc_now(), ) ) @@ -488,7 +488,7 @@ def test_sqlite_get_online_documents() -> None: ) for i in range(n) ], - "event_timestamp": [datetime.utcnow() for _ in range(n)], + "event_timestamp": [_utc_now() for _ in range(n)], } ) diff --git a/sdk/python/tests/unit/test_datetime.py b/sdk/python/tests/unit/test_datetime.py new file mode 100644 index 0000000000..aaab507ed0 --- /dev/null +++ b/sdk/python/tests/unit/test_datetime.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- + + +""" +Test the retirement of datetime.utcnow() function. +""" diff --git a/sdk/python/tests/unit/test_feature_views.py b/sdk/python/tests/unit/test_feature_views.py index b387f55d8b..981968df0d 100644 --- a/sdk/python/tests/unit/test_feature_views.py +++ b/sdk/python/tests/unit/test_feature_views.py @@ -1,9 +1,8 @@ -from datetime import datetime, timedelta +from datetime import timedelta import pytest from typeguard import TypeCheckError -from feast import utils from feast.batch_feature_view import BatchFeatureView from feast.data_format import AvroFormat from feast.data_source import KafkaSource @@ -13,6 +12,7 @@ from feast.infra.offline_stores.file_source import FileSource from feast.protos.feast.types.Value_pb2 import ValueType from feast.types import Float32 +from feast.utils import _utc_now, make_tzaware def test_create_feature_view_with_conflicting_entities(): @@ -143,9 +143,9 @@ def test_update_materialization_intervals(): ) assert len(updated_feature_view.materialization_intervals) == 0 - current_time = datetime.utcnow() - start_date = utils.make_tzaware(current_time - timedelta(days=1)) - end_date = utils.make_tzaware(current_time) + current_time = _utc_now() + start_date = make_tzaware(current_time - timedelta(days=1)) + end_date = make_tzaware(current_time) updated_feature_view.materialization_intervals.append((start_date, end_date)) # Update the Feature View, i.e. simply update the name diff --git a/sdk/python/tests/unit/test_stream_feature_view.py b/sdk/python/tests/unit/test_stream_feature_view.py index 77431666c3..4f93691028 100644 --- a/sdk/python/tests/unit/test_stream_feature_view.py +++ b/sdk/python/tests/unit/test_stream_feature_view.py @@ -1,9 +1,8 @@ import copy -from datetime import datetime, timedelta +from datetime import timedelta import pytest -from feast import utils from feast.aggregation import Aggregation from feast.batch_feature_view import BatchFeatureView from feast.data_format import AvroFormat @@ -16,6 +15,7 @@ ) from feast.stream_feature_view import StreamFeatureView, stream_feature_view from feast.types import Float32 +from feast.utils import _utc_now, make_tzaware def test_create_batch_feature_view(): @@ -286,9 +286,9 @@ def test_update_materialization_intervals(): udf=simple_udf, tags={}, ) - current_time = datetime.utcnow() - start_date = utils.make_tzaware(current_time - timedelta(days=1)) - end_date = utils.make_tzaware(current_time) + current_time = _utc_now() + start_date = make_tzaware(current_time - timedelta(days=1)) + end_date = make_tzaware(current_time) stored_stream_feature_view.materialization_intervals.append((start_date, end_date)) # Update the stream feature view i.e. here it's simply the name diff --git a/sdk/python/tests/utils/basic_read_write_test.py b/sdk/python/tests/utils/basic_read_write_test.py index 5a93a05a1f..c09a94083f 100644 --- a/sdk/python/tests/utils/basic_read_write_test.py +++ b/sdk/python/tests/utils/basic_read_write_test.py @@ -1,9 +1,10 @@ -from datetime import datetime, timedelta +from datetime import timedelta from typing import Optional from feast.feature_store import FeatureStore from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto from feast.protos.feast.types.Value_pb2 import Value as ValueProto +from feast.utils import _utc_now def basic_rw_test( @@ -65,13 +66,13 @@ def _driver_rw_test(event_ts, created_ts, write, expect_read): """ 1. Basic test: write value, read it back """ - time_1 = datetime.utcnow() + time_1 = _utc_now() _driver_rw_test( event_ts=time_1, created_ts=time_1, write=(1.1, "3.1"), expect_read=(1.1, "3.1") ) """ Values with an new event_ts should overwrite older ones """ - time_3 = datetime.utcnow() + time_3 = _utc_now() _driver_rw_test( event_ts=time_1 + timedelta(hours=1), created_ts=time_3, diff --git a/sdk/python/tests/utils/dynamo_table_creator.py b/sdk/python/tests/utils/dynamo_table_creator.py index 20bac122b3..0ebc939dc1 100644 --- a/sdk/python/tests/utils/dynamo_table_creator.py +++ b/sdk/python/tests/utils/dynamo_table_creator.py @@ -1,11 +1,10 @@ -from datetime import datetime - import boto3 from feast import utils from feast.infra.online_stores.helpers import compute_entity_id from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto from feast.protos.feast.types.Value_pb2 import Value as ValueProto +from feast.utils import _utc_now def create_n_customer_test_samples(n=10): @@ -19,7 +18,7 @@ def create_n_customer_test_samples(n=10): "name": ValueProto(string_val="John"), "age": ValueProto(int64_val=3), }, - datetime.utcnow(), + _utc_now(), None, ) for i in range(n) diff --git a/sdk/python/tests/utils/e2e_test_validation.py b/sdk/python/tests/utils/e2e_test_validation.py index d9104bae42..1a8bedc796 100644 --- a/sdk/python/tests/utils/e2e_test_validation.py +++ b/sdk/python/tests/utils/e2e_test_validation.py @@ -10,6 +10,7 @@ from pytz import utc from feast import FeatureStore, FeatureView, RepoConfig +from feast.utils import _utc_now from tests.integration.feature_repos.integration_test_repo_config import ( IntegrationTestRepoConfig, ) @@ -31,7 +32,7 @@ def validate_offline_online_store_consistency( fs: FeatureStore, fv: FeatureView, split_dt: datetime ) -> None: - now = datetime.utcnow() + now = _utc_now() full_feature_names = True check_offline_store: bool = True diff --git a/sdk/python/tests/utils/online_write_benchmark.py b/sdk/python/tests/utils/online_write_benchmark.py index 8a138f41db..9b1a4eb0b2 100644 --- a/sdk/python/tests/utils/online_write_benchmark.py +++ b/sdk/python/tests/utils/online_write_benchmark.py @@ -2,7 +2,7 @@ import random import string import tempfile -from datetime import datetime, timedelta +from datetime import timedelta import click import pyarrow as pa @@ -16,7 +16,7 @@ from feast.field import Field from feast.repo_config import RepoConfig from feast.types import Float32, Int32 -from feast.utils import _convert_arrow_to_proto +from feast.utils import _convert_arrow_to_proto, _utc_now def create_driver_hourly_stats_feature_view(source): @@ -69,7 +69,7 @@ def benchmark_writes(): provider = store._get_provider() - end_date = datetime.utcnow() + end_date = _utc_now() start_date = end_date - timedelta(days=14) customers = list(range(100)) data = create_driver_hourly_stats_df(customers, start_date, end_date) diff --git a/sdk/python/tests/utils/test_log_creator.py b/sdk/python/tests/utils/test_log_creator.py index ec0d92814c..f072f4c886 100644 --- a/sdk/python/tests/utils/test_log_creator.py +++ b/sdk/python/tests/utils/test_log_creator.py @@ -8,12 +8,12 @@ import numpy as np import pandas as pd import pyarrow -import pytz from feast import FeatureService, FeatureStore, FeatureView from feast.errors import FeatureViewNotFoundException from feast.feature_logging import LOG_DATE_FIELD, LOG_TIMESTAMP_FIELD, REQUEST_ID_FIELD from feast.protos.feast.serving.ServingService_pb2 import FieldStatus +from feast.utils import _utc_now def get_latest_rows( @@ -64,9 +64,7 @@ def generate_expected_logs( logs[f"{col}__status"] = FieldStatus.PRESENT if feature_view.ttl: logs[f"{col}__status"] = logs[f"{col}__status"].mask( - df[timestamp_column] - < datetime.datetime.utcnow().replace(tzinfo=pytz.UTC) - - feature_view.ttl, + df[timestamp_column] < _utc_now() - feature_view.ttl, FieldStatus.OUTSIDE_MAX_AGE, ) @@ -119,7 +117,7 @@ def prepare_logs( f"{destination_field}__status" ].mask( logs_df[f"{destination_field}__timestamp"] - < (datetime.datetime.utcnow() - view.ttl), + < (_utc_now() - view.ttl), FieldStatus.OUTSIDE_MAX_AGE, ) From b0dc6832ff446429390a916aa9e0e61066cbde1d Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Thu, 4 Jul 2024 12:39:46 -0400 Subject: [PATCH 16/59] chore: Upgrading sqlite_vec to latest package (#4332) Signed-off-by: Francisco Javier Arceo Co-authored-by: Francisco Javier Arceo --- infra/scripts/pixi/pixi.lock | 306 ++++++++++++++++++ infra/scripts/pixi/pixi.toml | 2 +- .../requirements/py3.10-ci-requirements.txt | 2 + .../requirements/py3.10-requirements.txt | 2 + .../requirements/py3.11-ci-requirements.txt | 2 + .../requirements/py3.11-requirements.txt | 2 + .../requirements/py3.9-ci-requirements.txt | 2 + .../requirements/py3.9-requirements.txt | 2 + 8 files changed, 319 insertions(+), 1 deletion(-) diff --git a/infra/scripts/pixi/pixi.lock b/infra/scripts/pixi/pixi.lock index f1ce2d2658..1ca8742026 100644 --- a/infra/scripts/pixi/pixi.lock +++ b/infra/scripts/pixi/pixi.lock @@ -11,6 +11,9 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-13.2.0-h807b86a_5.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-h95c4c6d_6.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/uv-0.1.39-h0ea3d13_0.conda + osx-64: + - conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-17.0.6-heb59cac_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/uv-0.1.45-h4e38c46_0.conda osx-arm64: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-17.0.6-h5f092b4_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/uv-0.1.45-hc069d6b_0.conda @@ -41,6 +44,21 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/uv-0.1.39-h0ea3d13_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xz-5.2.6-h166bdaf_0.tar.bz2 + osx-64: + - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h10d778d_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ca-certificates-2024.7.4-h8857fd0_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-17.0.6-heb59cac_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.4.2-h0d85af4_5.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.46.0-h1b8f9f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.1-h87427d6_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.5-h5846eda_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.3.1-h87427d6_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.10.14-h00d2728_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.2-h9e318b2_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h1abcd95_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/uv-0.1.45-h4e38c46_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/xz-5.2.6-h775f41a_0.tar.bz2 osx-arm64: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h93a5062_5.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ca-certificates-2024.2.2-hf0a4a13_0.conda @@ -84,6 +102,22 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/uv-0.1.39-h0ea3d13_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xz-5.2.6-h166bdaf_0.tar.bz2 + osx-64: + - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h10d778d_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ca-certificates-2024.7.4-h8857fd0_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-17.0.6-heb59cac_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.6.2-h73e2aa4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.4.2-h0d85af4_5.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.46.0-h1b8f9f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.1-h87427d6_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.5-h5846eda_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.3.1-h87427d6_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.11.9-h657bba9_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.2-h9e318b2_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h1abcd95_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/uv-0.1.45-h4e38c46_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/xz-5.2.6-h775f41a_0.tar.bz2 osx-arm64: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h93a5062_5.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ca-certificates-2024.2.2-hf0a4a13_0.conda @@ -127,6 +161,21 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/uv-0.1.39-h0ea3d13_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xz-5.2.6-h166bdaf_0.tar.bz2 + osx-64: + - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h10d778d_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ca-certificates-2024.7.4-h8857fd0_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-17.0.6-heb59cac_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.4.2-h0d85af4_5.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.46.0-h1b8f9f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.1-h87427d6_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.5-h5846eda_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.3.1-h87427d6_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.9.19-h7a9c478_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.2-h9e318b2_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h1abcd95_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/uv-0.1.45-h4e38c46_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/xz-5.2.6-h775f41a_0.tar.bz2 osx-arm64: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h93a5062_5.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ca-certificates-2024.2.2-hf0a4a13_0.conda @@ -172,6 +221,19 @@ packages: license_family: BSD size: 23621 timestamp: 1650670423406 +- kind: conda + name: bzip2 + version: 1.0.8 + build: h10d778d_5 + build_number: 5 + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h10d778d_5.conda + sha256: 61fb2b488928a54d9472113e1280b468a309561caa54f33825a3593da390b242 + md5: 6097a6ca9ada32699b5fc4312dd6ef18 + license: bzip2-1.0.6 + license_family: BSD + size: 127885 + timestamp: 1699280178474 - kind: conda name: bzip2 version: 1.0.8 @@ -222,6 +284,17 @@ packages: license: ISC size: 155725 timestamp: 1706844034242 +- kind: conda + name: ca-certificates + version: 2024.7.4 + build: h8857fd0_0 + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/ca-certificates-2024.7.4-h8857fd0_0.conda + sha256: d16f46c489cb3192305c7d25b795333c5fc17bb0986de20598ed519f8c9cc9e4 + md5: 7df874a4b05b2d2b82826190170eaa0f + license: ISC + size: 154473 + timestamp: 1720077510541 - kind: conda name: ld_impl_linux-64 version: '2.40' @@ -264,6 +337,21 @@ packages: license_family: Apache size: 1248885 timestamp: 1715020154867 +- kind: conda + name: libcxx + version: 17.0.6 + build: heb59cac_3 + build_number: 3 + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/libcxx-17.0.6-heb59cac_3.conda + sha256: 9df841c64b19a3843869467ff8ff2eb3f6c5491ebaac8fd94fb8029a5b00dcbf + md5: ef15f182e353155497e13726b915bfc4 + depends: + - __osx >=10.13 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache + size: 1250659 + timestamp: 1720040263499 - kind: conda name: libexpat version: 2.6.2 @@ -280,6 +368,20 @@ packages: license_family: MIT size: 73730 timestamp: 1710362120304 +- kind: conda + name: libexpat + version: 2.6.2 + build: h73e2aa4_0 + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.6.2-h73e2aa4_0.conda + sha256: a188a77b275d61159a32ab547f7d17892226e7dac4518d2c6ac3ac8fc8dfde92 + md5: 3d1d51c8f716d97c864d12f7af329526 + constrains: + - expat 2.6.2.* + license: MIT + license_family: MIT + size: 69246 + timestamp: 1710362566073 - kind: conda name: libexpat version: 2.6.2 @@ -294,6 +396,19 @@ packages: license_family: MIT size: 63655 timestamp: 1710362424980 +- kind: conda + name: libffi + version: 3.4.2 + build: h0d85af4_5 + build_number: 5 + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.4.2-h0d85af4_5.tar.bz2 + sha256: 7a2d27a936ceee6942ea4d397f9c7d136f12549d86f7617e8b6bad51e01a941f + md5: ccb34fb14960ad8b125962d3d79b31a9 + license: MIT + license_family: MIT + size: 51348 + timestamp: 1636488394370 - kind: conda name: libffi version: 3.4.2 @@ -429,6 +544,20 @@ packages: license: Unlicense size: 859858 timestamp: 1713367435849 +- kind: conda + name: libsqlite + version: 3.46.0 + build: h1b8f9f3_0 + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.46.0-h1b8f9f3_0.conda + sha256: 63af1a9e3284c7e4952364bafe7267e41e2d9d8bcc0e85a4ea4b0ec02d3693f6 + md5: 5dadfbc1a567fe6e475df4ce3148be09 + depends: + - __osx >=10.13 + - libzlib >=1.2.13,<2.0a0 + license: Unlicense + size: 908643 + timestamp: 1718050720117 - kind: conda name: libstdcxx-ng version: 13.2.0 @@ -504,6 +633,23 @@ packages: license_family: Other size: 46768 timestamp: 1716874151980 +- kind: conda + name: libzlib + version: 1.3.1 + build: h87427d6_1 + build_number: 1 + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.1-h87427d6_1.conda + sha256: 80a62db652b1da0ccc100812a1d86e94f75028968991bfb17f9536f3aa72d91d + md5: b7575b5aa92108dcc9aaab0f05f2dbce + depends: + - __osx >=10.13 + constrains: + - zlib 1.3.1 *_1 + license: Zlib + license_family: Other + size: 57372 + timestamp: 1716874211519 - kind: conda name: ncurses version: 6.4.20240210 @@ -517,6 +663,17 @@ packages: license: X11 AND BSD-3-Clause size: 895669 timestamp: 1710866638986 +- kind: conda + name: ncurses + version: '6.5' + build: h5846eda_0 + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.5-h5846eda_0.conda + sha256: 6ecc73db0e49143092c0934355ac41583a5d5a48c6914c5f6ca48e562d3a4b79 + md5: 02a888433d165c99bf09784a7b14d900 + license: X11 AND BSD-3-Clause + size: 823601 + timestamp: 1715195267791 - kind: conda name: ncurses version: '6.5' @@ -581,6 +738,24 @@ packages: license_family: Apache size: 2893954 timestamp: 1716468329572 +- kind: conda + name: openssl + version: 3.3.1 + build: h87427d6_1 + build_number: 1 + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.3.1-h87427d6_1.conda + sha256: 60eed5d771207bcef05e0547c8f93a61d0ad1dcf75e19f8f8d9ded8094d78477 + md5: d838ffe9ec3c6d971f110e04487466ff + depends: + - __osx >=10.13 + - ca-certificates + constrains: + - pyopenssl >=22.1 + license: Apache-2.0 + license_family: Apache + size: 2551950 + timestamp: 1719364820943 - kind: conda name: python version: 3.9.19 @@ -610,6 +785,30 @@ packages: license: Python-2.0 size: 23800555 timestamp: 1710940120866 +- kind: conda + name: python + version: 3.9.19 + build: h7a9c478_0_cpython + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/python-3.9.19-h7a9c478_0_cpython.conda + sha256: 58b76be84683bc03112b3ed7e377e99af24844ebf7d7568f6466a2dae7a887fe + md5: 7d53d366acd9dbfb498c69326ccb520a + depends: + - bzip2 >=1.0.8,<2.0a0 + - libffi >=3.4,<4.0a0 + - libsqlite >=3.45.2,<4.0a0 + - libzlib >=1.2.13,<2.0.0a0 + - ncurses >=6.4.20240210,<7.0a0 + - openssl >=3.2.1,<4.0a0 + - readline >=8.2,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - xz >=5.2.6,<6.0a0 + constrains: + - python_abi 3.9.* *_cp39 + license: Python-2.0 + size: 12372436 + timestamp: 1710940037648 - kind: conda name: python version: 3.9.19 @@ -634,6 +833,30 @@ packages: license: Python-2.0 size: 11847835 timestamp: 1710939779164 +- kind: conda + name: python + version: 3.10.14 + build: h00d2728_0_cpython + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/python-3.10.14-h00d2728_0_cpython.conda + sha256: 00c1de2d46ede26609ef4e84a44b83be7876ba6a0215b7c83bff41a0656bf694 + md5: 0a1cddc4382c5c171e791c70740546dd + depends: + - bzip2 >=1.0.8,<2.0a0 + - libffi >=3.4,<4.0a0 + - libsqlite >=3.45.2,<4.0a0 + - libzlib >=1.2.13,<2.0.0a0 + - ncurses >=6.4.20240210,<7.0a0 + - openssl >=3.2.1,<4.0a0 + - readline >=8.2,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - xz >=5.2.6,<6.0a0 + constrains: + - python_abi 3.10.* *_cp310 + license: Python-2.0 + size: 11890228 + timestamp: 1710940046031 - kind: conda name: python version: 3.10.14 @@ -687,6 +910,32 @@ packages: license: Python-2.0 size: 25517742 timestamp: 1710939725109 +- kind: conda + name: python + version: 3.11.9 + build: h657bba9_0_cpython + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/python-3.11.9-h657bba9_0_cpython.conda + sha256: 3b50a5abb3b812875beaa9ab792dbd1bf44f335c64e9f9fedcf92d953995651c + md5: 612763bc5ede9552e4233ec518b9c9fb + depends: + - __osx >=10.9 + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.6.2,<3.0a0 + - libffi >=3.4,<4.0a0 + - libsqlite >=3.45.3,<4.0a0 + - libzlib >=1.2.13,<2.0.0a0 + - ncurses >=6.4.20240210,<7.0a0 + - openssl >=3.2.1,<4.0a0 + - readline >=8.2,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - xz >=5.2.6,<6.0a0 + constrains: + - python_abi 3.11.* *_cp311 + license: Python-2.0 + size: 15503226 + timestamp: 1713553747073 - kind: conda name: python version: 3.11.9 @@ -774,6 +1023,36 @@ packages: license_family: GPL size: 250351 timestamp: 1679532511311 +- kind: conda + name: readline + version: '8.2' + build: h9e318b2_1 + build_number: 1 + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/readline-8.2-h9e318b2_1.conda + sha256: 41e7d30a097d9b060037f0c6a2b1d4c4ae7e942c06c943d23f9d481548478568 + md5: f17f77f2acf4d344734bda76829ce14e + depends: + - ncurses >=6.3,<7.0a0 + license: GPL-3.0-only + license_family: GPL + size: 255870 + timestamp: 1679532707590 +- kind: conda + name: tk + version: 8.6.13 + build: h1abcd95_1 + build_number: 1 + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h1abcd95_1.conda + sha256: 30412b2e9de4ff82d8c2a7e5d06a15f4f4fef1809a72138b6ccb53a33b26faf5 + md5: bf830ba5afc507c6232d4ef0fb1a882d + depends: + - libzlib >=1.2.13,<2.0.0a0 + license: TCL + license_family: BSD + size: 3270220 + timestamp: 1699202389792 - kind: conda name: tk version: 8.6.13 @@ -831,6 +1110,22 @@ packages: license: Apache-2.0 OR MIT size: 11891252 timestamp: 1714233659570 +- kind: conda + name: uv + version: 0.1.45 + build: h4e38c46_0 + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/uv-0.1.45-h4e38c46_0.conda + sha256: 8c11774ca1940dcd90187ce240afea26b76e2942f9b18d65f6d4b483534193fd + md5: 754ce8a22c94a30c7bbd42274c7fae31 + depends: + - __osx >=10.13 + - libcxx >=16 + constrains: + - __osx >=10.12 + license: Apache-2.0 OR MIT + size: 8937335 + timestamp: 1716265195083 - kind: conda name: uv version: 0.1.45 @@ -871,3 +1166,14 @@ packages: license: LGPL-2.1 and GPL-2.0 size: 235693 timestamp: 1660346961024 +- kind: conda + name: xz + version: 5.2.6 + build: h775f41a_0 + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/xz-5.2.6-h775f41a_0.tar.bz2 + sha256: eb09823f34cc2dd663c0ec4ab13f246f45dcd52e5b8c47b9864361de5204a1c8 + md5: a72f9d4ea13d55d745ff1ed594747f10 + license: LGPL-2.1 and GPL-2.0 + size: 238119 + timestamp: 1660346964847 diff --git a/infra/scripts/pixi/pixi.toml b/infra/scripts/pixi/pixi.toml index 10179339f7..487c6f7def 100644 --- a/infra/scripts/pixi/pixi.toml +++ b/infra/scripts/pixi/pixi.toml @@ -1,7 +1,7 @@ [project] name = "pixi-feast" channels = ["conda-forge"] -platforms = ["linux-64", "osx-arm64"] +platforms = ["linux-64", "osx-arm64", "osx-64"] [tasks] diff --git a/sdk/python/requirements/py3.10-ci-requirements.txt b/sdk/python/requirements/py3.10-ci-requirements.txt index 3aa7130ccf..0eaababc0d 100644 --- a/sdk/python/requirements/py3.10-ci-requirements.txt +++ b/sdk/python/requirements/py3.10-ci-requirements.txt @@ -278,6 +278,8 @@ 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 diff --git a/sdk/python/requirements/py3.10-requirements.txt b/sdk/python/requirements/py3.10-requirements.txt index 72124636b6..308123600c 100644 --- a/sdk/python/requirements/py3.10-requirements.txt +++ b/sdk/python/requirements/py3.10-requirements.txt @@ -50,6 +50,8 @@ 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 673047b5c7..d0663a2bea 100644 --- a/sdk/python/requirements/py3.11-ci-requirements.txt +++ b/sdk/python/requirements/py3.11-ci-requirements.txt @@ -269,6 +269,8 @@ 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 diff --git a/sdk/python/requirements/py3.11-requirements.txt b/sdk/python/requirements/py3.11-requirements.txt index 408f392515..f21afdb5b1 100644 --- a/sdk/python/requirements/py3.11-requirements.txt +++ b/sdk/python/requirements/py3.11-requirements.txt @@ -48,6 +48,8 @@ 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 83009f8730..f09c666f42 100644 --- a/sdk/python/requirements/py3.9-ci-requirements.txt +++ b/sdk/python/requirements/py3.9-ci-requirements.txt @@ -278,6 +278,8 @@ 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 diff --git a/sdk/python/requirements/py3.9-requirements.txt b/sdk/python/requirements/py3.9-requirements.txt index 3c833438de..52ff8a0f4f 100644 --- a/sdk/python/requirements/py3.9-requirements.txt +++ b/sdk/python/requirements/py3.9-requirements.txt @@ -50,6 +50,8 @@ 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 From 0d89d1519fc6b8ddd05a2588138e2e85f5a921b1 Mon Sep 17 00:00:00 2001 From: Tom Steenbergen <41334387+TomSteenbergen@users.noreply.github.com> Date: Mon, 8 Jul 2024 12:27:39 +0200 Subject: [PATCH 17/59] fix: Remove redundant batching in PostgreSQLOnlineStore.online_write_batch and fix progress bar (#4331) * Remove batching and fix tqdm progress bar Signed-off-by: TomSteenbergen * Comment Signed-off-by: TomSteenbergen * Remove test changes Signed-off-by: TomSteenbergen * Update comment Signed-off-by: TomSteenbergen --------- Signed-off-by: TomSteenbergen --- .../feast/infra/online_stores/contrib/postgres.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/sdk/python/feast/infra/online_stores/contrib/postgres.py b/sdk/python/feast/infra/online_stores/contrib/postgres.py index 8715f0f65b..330b50bc78 100644 --- a/sdk/python/feast/infra/online_stores/contrib/postgres.py +++ b/sdk/python/feast/infra/online_stores/contrib/postgres.py @@ -75,7 +75,6 @@ def online_write_batch( Tuple[EntityKeyProto, Dict[str, ValueProto], datetime, Optional[datetime]] ], progress: Optional[Callable[[int], Any]], - batch_size: int = 5000, ) -> None: # Format insert values insert_values = [] @@ -118,15 +117,13 @@ def online_write_batch( """ ).format(sql.Identifier(_table_id(config.project, table))) - # Push data in batches to online store + # Push data into the online store with self._get_conn(config) as conn, conn.cursor() as cur: - for i in range(0, len(insert_values), batch_size): - cur_batch = insert_values[i : i + batch_size] - cur.executemany(sql_query, cur_batch) - conn.commit() + cur.executemany(sql_query, insert_values) + conn.commit() - if progress: - progress(len(cur_batch)) + if progress: + progress(len(data)) def online_read( self, From cea52e9fb02cb9e0b8f48206278474f5a5fa167e Mon Sep 17 00:00:00 2001 From: Tom Steenbergen <41334387+TomSteenbergen@users.noreply.github.com> Date: Mon, 8 Jul 2024 12:29:43 +0200 Subject: [PATCH 18/59] feat: Add async feature retrieval for Postgres Online Store (#4327) * Add async retrieval for postgres Signed-off-by: TomSteenbergen * Format Signed-off-by: TomSteenbergen * Update _prepare_keys method Signed-off-by: TomSteenbergen * Fix typo Signed-off-by: TomSteenbergen --------- Signed-off-by: TomSteenbergen --- .../infra/online_stores/contrib/postgres.py | 186 ++++++++++++------ .../infra/utils/postgres/connection_utils.py | 25 ++- .../online_store/test_universal_online.py | 2 +- 3 files changed, 150 insertions(+), 63 deletions(-) diff --git a/sdk/python/feast/infra/online_stores/contrib/postgres.py b/sdk/python/feast/infra/online_stores/contrib/postgres.py index 330b50bc78..ff73a4a347 100644 --- a/sdk/python/feast/infra/online_stores/contrib/postgres.py +++ b/sdk/python/feast/infra/online_stores/contrib/postgres.py @@ -4,6 +4,7 @@ from datetime import datetime from typing import ( Any, + AsyncGenerator, Callable, Dict, Generator, @@ -12,18 +13,24 @@ Optional, Sequence, Tuple, + Union, ) import pytz -from psycopg import sql +from psycopg import AsyncConnection, sql from psycopg.connection import Connection -from psycopg_pool import ConnectionPool +from psycopg_pool import AsyncConnectionPool, ConnectionPool 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.online_store import OnlineStore -from feast.infra.utils.postgres.connection_utils import _get_conn, _get_connection_pool +from feast.infra.utils.postgres.connection_utils import ( + _get_conn, + _get_conn_async, + _get_connection_pool, + _get_connection_pool_async, +) from feast.infra.utils.postgres.postgres_config import ConnectionType, PostgreSQLConfig from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto from feast.protos.feast.types.Value_pb2 import Value as ValueProto @@ -51,6 +58,9 @@ class PostgreSQLOnlineStore(OnlineStore): _conn: Optional[Connection] = None _conn_pool: Optional[ConnectionPool] = None + _conn_async: Optional[AsyncConnection] = None + _conn_pool_async: Optional[AsyncConnectionPool] = None + @contextlib.contextmanager def _get_conn(self, config: RepoConfig) -> Generator[Connection, Any, Any]: assert config.online_store.type == "postgres" @@ -67,6 +77,24 @@ def _get_conn(self, config: RepoConfig) -> Generator[Connection, Any, Any]: self._conn = _get_conn(config.online_store) yield self._conn + @contextlib.asynccontextmanager + async def _get_conn_async( + self, config: RepoConfig + ) -> AsyncGenerator[AsyncConnection, Any]: + if config.online_store.conn_type == ConnectionType.pool: + if not self._conn_pool_async: + self._conn_pool_async = await _get_connection_pool_async( + config.online_store + ) + await self._conn_pool_async.open() + connection = await self._conn_pool_async.getconn() + yield connection + await self._conn_pool_async.putconn(connection) + else: + if not self._conn_async: + self._conn_async = await _get_conn_async(config.online_store) + yield self._conn_async + def online_write_batch( self, config: RepoConfig, @@ -132,69 +160,107 @@ def online_read( entity_keys: List[EntityKeyProto], requested_features: Optional[List[str]] = None, ) -> List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]]: - result: List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]] = [] + keys = self._prepare_keys(entity_keys, config.entity_key_serialization_version) + query, params = self._construct_query_and_params( + config, table, keys, requested_features + ) - project = config.project with self._get_conn(config) as conn, conn.cursor() as cur: - # Collecting all the keys to a list allows us to make fewer round trips - # to PostgreSQL - keys = [] - for entity_key in entity_keys: - keys.append( - serialize_entity_key( - entity_key, - entity_key_serialization_version=config.entity_key_serialization_version, - ) - ) + cur.execute(query, params) + rows = cur.fetchall() - if not requested_features: - cur.execute( - sql.SQL( - """ - SELECT entity_key, feature_name, value, event_ts - FROM {} WHERE entity_key = ANY(%s); - """ - ).format( - sql.Identifier(_table_id(project, table)), - ), - (keys,), - ) - else: - cur.execute( - sql.SQL( - """ - SELECT entity_key, feature_name, value, event_ts - FROM {} WHERE entity_key = ANY(%s) and feature_name = ANY(%s); - """ - ).format( - sql.Identifier(_table_id(project, table)), - ), - (keys, requested_features), - ) + return self._process_rows(keys, rows) - rows = cur.fetchall() + async def online_read_async( + self, + config: RepoConfig, + table: FeatureView, + entity_keys: List[EntityKeyProto], + requested_features: Optional[List[str]] = None, + ) -> List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]]: + keys = self._prepare_keys(entity_keys, config.entity_key_serialization_version) + query, params = self._construct_query_and_params( + config, table, keys, requested_features + ) - # Since we don't know the order returned from PostgreSQL we'll need - # to construct a dict to be able to quickly look up the correct row - # when we iterate through the keys since they are in the correct order - values_dict = defaultdict(list) - for row in rows if rows is not None else []: - values_dict[ - row[0] if isinstance(row[0], bytes) else row[0].tobytes() - ].append(row[1:]) - - for key in keys: - if key in values_dict: - value = values_dict[key] - res = {} - for feature_name, value_bin, event_ts in value: - val = ValueProto() - val.ParseFromString(bytes(value_bin)) - res[feature_name] = val - result.append((event_ts, res)) - else: - result.append((None, None)) + async with self._get_conn_async(config) as conn: + async with conn.cursor() as cur: + await cur.execute(query, params) + rows = await cur.fetchall() + + return self._process_rows(keys, rows) + + @staticmethod + def _construct_query_and_params( + config: RepoConfig, + table: FeatureView, + keys: List[bytes], + requested_features: Optional[List[str]] = None, + ) -> Tuple[sql.Composed, Union[Tuple[List[bytes], List[str]], Tuple[List[bytes]]]]: + """Construct the SQL query based on the given parameters.""" + if requested_features: + query = sql.SQL( + """ + SELECT entity_key, feature_name, value, event_ts + FROM {} WHERE entity_key = ANY(%s) AND feature_name = ANY(%s); + """ + ).format( + sql.Identifier(_table_id(config.project, table)), + ) + params = (keys, requested_features) + else: + query = sql.SQL( + """ + SELECT entity_key, feature_name, value, event_ts + FROM {} WHERE entity_key = ANY(%s); + """ + ).format( + sql.Identifier(_table_id(config.project, table)), + ) + params = (keys, []) + return query, params + + @staticmethod + def _prepare_keys( + entity_keys: List[EntityKeyProto], entity_key_serialization_version: int + ) -> List[bytes]: + """Prepare all keys in a list to make fewer round trips to the database.""" + return [ + serialize_entity_key( + entity_key, + entity_key_serialization_version=entity_key_serialization_version, + ) + for entity_key in entity_keys + ] + + @staticmethod + def _process_rows( + keys: List[bytes], rows: List[Tuple] + ) -> List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]]: + """Transform the retrieved rows in the desired output. + PostgreSQL may return rows in an unpredictable order. Therefore, `values_dict` + is created to quickly look up the correct row using the keys, since these are + actually in the correct order. + """ + values_dict = defaultdict(list) + for row in rows if rows is not None else []: + values_dict[ + row[0] if isinstance(row[0], bytes) else row[0].tobytes() + ].append(row[1:]) + + result: List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]] = [] + for key in keys: + if key in values_dict: + value = values_dict[key] + res = {} + for feature_name, value_bin, event_ts in value: + val = ValueProto() + val.ParseFromString(bytes(value_bin)) + res[feature_name] = val + result.append((event_ts, res)) + else: + result.append((None, None)) return result def update( diff --git a/sdk/python/feast/infra/utils/postgres/connection_utils.py b/sdk/python/feast/infra/utils/postgres/connection_utils.py index e0599019b9..7b37ea981f 100644 --- a/sdk/python/feast/infra/utils/postgres/connection_utils.py +++ b/sdk/python/feast/infra/utils/postgres/connection_utils.py @@ -4,8 +4,8 @@ import pandas as pd import psycopg import pyarrow as pa -from psycopg.connection import Connection -from psycopg_pool import ConnectionPool +from psycopg import AsyncConnection, Connection +from psycopg_pool import AsyncConnectionPool, ConnectionPool from feast.infra.utils.postgres.postgres_config import PostgreSQLConfig from feast.type_map import arrow_to_pg_type @@ -21,6 +21,16 @@ def _get_conn(config: PostgreSQLConfig) -> Connection: return conn +async def _get_conn_async(config: PostgreSQLConfig) -> AsyncConnection: + """Get a psycopg `AsyncConnection`.""" + conn = await psycopg.AsyncConnection.connect( + conninfo=_get_conninfo(config), + keepalives_idle=config.keepalives_idle, + **_get_conn_kwargs(config), + ) + return conn + + def _get_connection_pool(config: PostgreSQLConfig) -> ConnectionPool: """Get a psycopg `ConnectionPool`.""" return ConnectionPool( @@ -32,6 +42,17 @@ def _get_connection_pool(config: PostgreSQLConfig) -> ConnectionPool: ) +async def _get_connection_pool_async(config: PostgreSQLConfig) -> AsyncConnectionPool: + """Get a psycopg `AsyncConnectionPool`.""" + return AsyncConnectionPool( + conninfo=_get_conninfo(config), + min_size=config.min_conn, + max_size=config.max_conn, + open=False, + kwargs=_get_conn_kwargs(config), + ) + + def _get_conninfo(config: PostgreSQLConfig) -> str: """Get the `conninfo` argument required for connection objects.""" return ( 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 38656b90a9..2ffe869ef5 100644 --- a/sdk/python/tests/integration/online_store/test_universal_online.py +++ b/sdk/python/tests/integration/online_store/test_universal_online.py @@ -488,7 +488,7 @@ def test_online_retrieval_with_event_timestamps(environment, universal_data_sour @pytest.mark.integration -@pytest.mark.universal_online_stores(only=["redis", "dynamodb"]) +@pytest.mark.universal_online_stores(only=["redis", "dynamodb", "postgres"]) def test_async_online_retrieval_with_event_timestamps( environment, universal_data_sources ): From f5697863669a6bb9dbd491f79192e8ddd0073388 Mon Sep 17 00:00:00 2001 From: stanconia <36575269+stanconia@users.noreply.github.com> Date: Tue, 9 Jul 2024 05:25:29 -0400 Subject: [PATCH 19/59] feat: Add Async refresh to Sql Registry (#4251) * Add sql registry async refresh Signed-off-by: Stanley Opara * make refresh code a daemon thread Signed-off-by: Stanley Opara * Change RegistryConfig to cacheMode Signed-off-by: Stanley Opara * Only run async when ttl > 0 Signed-off-by: Stanley Opara * make refresh async run in a loop Signed-off-by: Stanley Opara * make refresh async run in a loop Signed-off-by: Stanley Opara * Reorder async refresh call Signed-off-by: Stanley Opara * Add documentation Signed-off-by: Stanley Opara * Update test_universal_registry.py Signed-off-by: Stanley Opara * Force rerun of tests Signed-off-by: Stanley Opara * Force rerun of tests Signed-off-by: Stanley Opara * Format repo config file Signed-off-by: Stanley Opara --------- Signed-off-by: Stanley Opara Co-authored-by: Stanley Opara --- .../feast/infra/registry/caching_registry.py | 57 +++++---- sdk/python/feast/infra/registry/sql.py | 4 +- sdk/python/feast/repo_config.py | 3 + .../registration/test_universal_registry.py | 113 ++++++++++++++++-- 4 files changed, 145 insertions(+), 32 deletions(-) diff --git a/sdk/python/feast/infra/registry/caching_registry.py b/sdk/python/feast/infra/registry/caching_registry.py index f7eab7d70a..298639028d 100644 --- a/sdk/python/feast/infra/registry/caching_registry.py +++ b/sdk/python/feast/infra/registry/caching_registry.py @@ -1,4 +1,6 @@ +import atexit import logging +import threading from abc import abstractmethod from datetime import timedelta from threading import Lock @@ -21,11 +23,7 @@ class CachingRegistry(BaseRegistry): - def __init__( - self, - project: str, - cache_ttl_seconds: int, - ): + 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() @@ -33,6 +31,10 @@ def __init__( self.cached_registry_proto_ttl = timedelta( seconds=cache_ttl_seconds if cache_ttl_seconds is not None else 0 ) + self.cache_mode = cache_mode + if cache_mode == "thread": + self._start_thread_async_refresh(cache_ttl_seconds) + atexit.register(self._exit_handler) @abstractmethod def _get_data_source(self, name: str, project: str) -> DataSource: @@ -322,22 +324,35 @@ def refresh(self, project: Optional[str] = None): self.cached_registry_proto_created = _utc_now() def _refresh_cached_registry_if_necessary(self): - 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.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 expired: + logger.info("Registry cache expired, so refreshing") + self.refresh() + + def _start_thread_async_refresh(self, cache_ttl_seconds): + self.refresh() + if cache_ttl_seconds <= 0: + return + 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.start() - if expired: - logger.info("Registry cache expired, so refreshing") - self.refresh() + def _exit_handler(self): + self.registry_refresh_thread.cancel() diff --git a/sdk/python/feast/infra/registry/sql.py b/sdk/python/feast/infra/registry/sql.py index 6ef08989b7..a2b16a3a09 100644 --- a/sdk/python/feast/infra/registry/sql.py +++ b/sdk/python/feast/infra/registry/sql.py @@ -193,7 +193,9 @@ def __init__( ) metadata.create_all(self.engine) super().__init__( - project=project, cache_ttl_seconds=registry_config.cache_ttl_seconds + project=project, + cache_ttl_seconds=registry_config.cache_ttl_seconds, + cache_mode=registry_config.cache_mode, ) def teardown(self): diff --git a/sdk/python/feast/repo_config.py b/sdk/python/feast/repo_config.py index 8d6bff2818..137023ef22 100644 --- a/sdk/python/feast/repo_config.py +++ b/sdk/python/feast/repo_config.py @@ -124,6 +124,9 @@ class RegistryConfig(FeastBaseModel): 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)""" + @field_validator("path") def validate_path(cls, path: str, values: ValidationInfo) -> str: if values.data.get("registry_type") == "sql": diff --git a/sdk/python/tests/integration/registration/test_universal_registry.py b/sdk/python/tests/integration/registration/test_universal_registry.py index c06ccf2d4d..b0738c8419 100644 --- a/sdk/python/tests/integration/registration/test_universal_registry.py +++ b/sdk/python/tests/integration/registration/test_universal_registry.py @@ -125,7 +125,7 @@ def minio_registry() -> Registry: logger = logging.getLogger(__name__) -@pytest.fixture(scope="session") +@pytest.fixture(scope="function") def pg_registry(): container = ( DockerContainer("postgres:latest") @@ -137,6 +137,35 @@ def pg_registry(): container.start() + registry_config = _given_registry_config_for_pg_sql(container) + + yield SqlRegistry(registry_config, "project", None) + + container.stop() + + +@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) + ) + + container.start() + + registry_config = _given_registry_config_for_pg_sql(container, 2, "thread") + + yield SqlRegistry(registry_config, "project", None) + + container.stop() + + +def _given_registry_config_for_pg_sql( + container, cache_ttl_seconds=2, cache_mode="sync" +): log_string_to_wait_for = "database system is ready to accept connections" waited = wait_for_logs( container=container, @@ -148,25 +177,42 @@ def pg_registry(): container_port = container.get_exposed_port(5432) container_host = container.get_container_host_ip() - registry_config = RegistryConfig( + return RegistryConfig( registry_type="sql", + cache_ttl_seconds=cache_ttl_seconds, + cache_mode=cache_mode, # 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}", sqlalchemy_config_kwargs={"echo": False, "pool_pre_ping": True}, ) + +@pytest.fixture(scope="function") +def mysql_registry(): + container = MySqlContainer("mysql:latest") + container.start() + + registry_config = _given_registry_config_for_mysql(container) + yield SqlRegistry(registry_config, "project", None) container.stop() -@pytest.fixture(scope="session") -def mysql_registry(): +@pytest.fixture(scope="function") +def mysql_registry_async(): container = MySqlContainer("mysql:latest") container.start() - # testing for the database to exist and ready to connect and start testing. + registry_config = _given_registry_config_for_mysql(container, 2, "thread") + + yield SqlRegistry(registry_config, "project", None) + + container.stop() + + +def _given_registry_config_for_mysql(container, cache_ttl_seconds=2, cache_mode="sync"): import sqlalchemy engine = sqlalchemy.create_engine( @@ -174,16 +220,14 @@ def mysql_registry(): ) engine.connect() - registry_config = RegistryConfig( + return RegistryConfig( 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}, ) - yield SqlRegistry(registry_config, "project", None) - - container.stop() - @pytest.fixture(scope="session") def sqlite_registry(): @@ -269,6 +313,17 @@ def mock_remote_registry(): lazy_fixture("sqlite_registry"), ] +async_sql_fixtures = [ + pytest.param( + lazy_fixture("pg_registry_async"), + marks=pytest.mark.xdist_group(name="pg_registry_async"), + ), + pytest.param( + lazy_fixture("mysql_registry_async"), + marks=pytest.mark.xdist_group(name="mysql_registry_async"), + ), +] + @pytest.mark.integration @pytest.mark.parametrize("test_registry", all_fixtures) @@ -999,6 +1054,44 @@ def test_registry_cache(test_registry): test_registry.teardown() +@pytest.mark.integration +@pytest.mark.parametrize( + "test_registry", + async_sql_fixtures, +) +def test_registry_cache_thread_async(test_registry): + # Create Feature View + batch_source = FileSource( + name="test_source", + file_format=ParquetFormat(), + path="file://feast/*", + timestamp_field="ts_col", + created_timestamp_column="timestamp", + ) + + project = "project" + + # Register data source + test_registry.apply_data_source(batch_source, project) + registry_data_sources_cached = test_registry.list_data_sources( + project, allow_cache=True + ) + # async ttl yet to expire, so there will be a cache miss + assert len(registry_data_sources_cached) == 0 + + # Wait for cache to be refreshed + time.sleep(4) + # Now objects exist + registry_data_sources_cached = test_registry.list_data_sources( + project, allow_cache=True + ) + assert len(registry_data_sources_cached) == 1 + registry_data_source = registry_data_sources_cached[0] + assert registry_data_source == batch_source + + test_registry.teardown() + + @pytest.mark.integration @pytest.mark.parametrize( "test_registry", From 8e8c1f2ff9a77738e71542cbaab9531f321842a4 Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Wed, 10 Jul 2024 09:20:41 -0400 Subject: [PATCH 20/59] feat: Add Tornike to maintainers.md (#4339) * Update maintainers.md * Update maintainers.md * Update maintainers.md --- community/maintainers.md | 1 + 1 file changed, 1 insertion(+) diff --git a/community/maintainers.md b/community/maintainers.md index 0b3d4ab648..5ccd347be0 100644 --- a/community/maintainers.md +++ b/community/maintainers.md @@ -16,6 +16,7 @@ In alphabetical order | Shuchu Han | `shuchu` | shuchu.han@gmail.com | Independent | | Willem Pienaar | `woop` | will.pienaar@gmail.com | Cleric | | Zhiling Chen | `zhilingc` | chnzhlng@gmail.com | GetGround | +| Tornike Gurgenidze | `tokoko` | togurgenidze@gmail.com | Bank of Georgia | ## Emeritus Maintainers From 71afd1c31d2a0ebf033f72aaf27eba8b9d66d4db Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Wed, 10 Jul 2024 10:10:40 -0400 Subject: [PATCH 21/59] docs: Update SUMMARY.md (#4340) Update SUMMARY.md --- docs/SUMMARY.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index a40c60d97c..5a82a190fe 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -121,7 +121,8 @@ * [\[Alpha\] Go feature server](reference/feature-servers/go-feature-server.md) * [Offline Feature Server](reference/feature-servers/offline-feature-server) * [\[Beta\] Web UI](reference/alpha-web-ui.md) -* [\[Alpha\] On demand feature view](reference/alpha-on-demand-feature-view.md) +* [\[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) * [Feast CLI reference](reference/feast-cli-commands.md) * [Python API reference](http://rtd.feast.dev) From 96613c108ad3f42ca38f72c25130319eef2568c6 Mon Sep 17 00:00:00 2001 From: Tornike Gurgenidze Date: Wed, 10 Jul 2024 19:19:55 +0400 Subject: [PATCH 22/59] chore: Upgrade ibis to 9.0 (#4330) upgrade ibis version Signed-off-by: tokoko --- sdk/python/feast/infra/offline_stores/ibis.py | 7 +- .../requirements/py3.10-ci-requirements.txt | 167 ++---------------- .../requirements/py3.10-requirements.txt | 46 +---- .../requirements/py3.11-ci-requirements.txt | 167 ++---------------- .../requirements/py3.11-requirements.txt | 46 +---- .../requirements/py3.9-ci-requirements.txt | 167 ++---------------- .../requirements/py3.9-requirements.txt | 46 +---- setup.py | 6 +- 8 files changed, 80 insertions(+), 572 deletions(-) diff --git a/sdk/python/feast/infra/offline_stores/ibis.py b/sdk/python/feast/infra/offline_stores/ibis.py index 6cc1606a45..dd81fd1d4e 100644 --- a/sdk/python/feast/infra/offline_stores/ibis.py +++ b/sdk/python/feast/infra/offline_stores/ibis.py @@ -335,11 +335,8 @@ def deduplicate( if created_timestamp_col: order_by_fields.append(ibis.desc(table[created_timestamp_col])) - table = ( - table.group_by(by=group_by_cols) - .order_by(order_by_fields) - .mutate(rn=ibis.row_number()) - ) + window = ibis.window(group_by=group_by_cols, order_by=order_by_fields, following=0) + table = table.mutate(rn=ibis.row_number().over(window)) return table.filter(table["rn"] == ibis.literal(0)).drop("rn") diff --git a/sdk/python/requirements/py3.10-ci-requirements.txt b/sdk/python/requirements/py3.10-ci-requirements.txt index 0eaababc0d..e6e66ac2ee 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,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 @@ -52,9 +48,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,9 +60,7 @@ bidict==0.23.1 bleach==6.1.0 # via nbconvert boto3==1.34.131 - # via - # feast (setup.py) - # moto + # via moto botocore==1.34.131 # via # aiobotocore @@ -77,7 +69,6 @@ botocore==1.34.131 # s3transfer build==1.2.1 # via - # feast (setup.py) # pip-tools # singlestoredb cachecontrol==0.14.0 @@ -85,7 +76,6 @@ cachecontrol==0.14.0 cachetools==5.3.3 # via google-auth cassandra-driver==3.29.1 - # via feast (setup.py) certifi==2024.6.2 # via # elastic-transport @@ -108,7 +98,6 @@ charset-normalizer==3.3.2 # snowflake-connector-python click==8.1.7 # via - # feast (setup.py) # dask # geomet # great-expectations @@ -118,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 @@ -129,7 +116,6 @@ coverage[toml]==7.5.4 # via pytest-cov cryptography==42.0.8 # via - # feast (setup.py) # azure-identity # azure-storage-blob # great-expectations @@ -141,9 +127,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 @@ -155,9 +139,7 @@ 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 @@ -167,15 +149,10 @@ docker==7.1.0 docutils==0.19 # via sphinx duckdb==0.10.3 - # via - # duckdb-engine - # ibis-framework -duckdb-engine==0.13.0 # via ibis-framework 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 @@ -190,7 +167,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 @@ -200,7 +176,6 @@ filelock==3.15.4 # snowflake-connector-python # virtualenv firebase-admin==5.4.0 - # via feast (setup.py) fqdn==1.5.1 # via jsonschema frozenlist==1.4.1 @@ -208,16 +183,13 @@ frozenlist==1.4.1 # aiohttp # aiosignal fsspec==2023.12.2 - # via - # feast (setup.py) - # dask + # via 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) # firebase-admin # google-api-python-client # google-cloud-bigquery @@ -242,11 +214,8 @@ google-auth==2.30.0 google-auth-httplib2==0.2.0 # via google-api-python-client google-cloud-bigquery[pandas]==3.12.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 @@ -255,13 +224,10 @@ google-cloud-core==2.4.1 # google-cloud-firestore # google-cloud-storage google-cloud-datastore==2.19.0 - # via feast (setup.py) google-cloud-firestore==2.16.0 # via firebase-admin google-cloud-storage==2.17.0 - # via - # feast (setup.py) - # firebase-admin + # via firebase-admin google-crc32c==1.5.0 # via # google-cloud-storage @@ -272,19 +238,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 @@ -295,27 +258,19 @@ 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 httplib2==0.22.0 @@ -326,15 +281,11 @@ httptools==0.6.1 # via uvicorn httpx==0.27.0 # via - # feast (setup.py) # fastapi # jupyterlab -ibis-framework[duckdb]==8.0.0 - # via - # feast (setup.py) - # ibis-substrait -ibis-substrait==3.2.0 - # via feast (setup.py) +ibis-framework[duckdb]==9.1.0 + # via ibis-substrait +ibis-substrait==4.0.0 identify==2.5.36 # via pre-commit idna==3.7 @@ -369,7 +320,6 @@ jedi==0.19.1 # via ipython jinja2==3.1.4 # via - # feast (setup.py) # altair # fastapi # great-expectations @@ -393,7 +343,6 @@ jsonpointer==3.0.0 # jsonschema jsonschema[format-nongpl]==4.22.0 # via - # feast (setup.py) # altair # great-expectations # jupyter-events @@ -439,7 +388,6 @@ 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 @@ -460,17 +408,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 @@ -483,16 +427,11 @@ multidict==6.0.5 # via # aiohttp # yarl -multipledispatch==1.0.0 - # via ibis-framework 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 @@ -515,7 +454,6 @@ notebook-shim==0.2.4 # notebook numpy==1.26.4 # via - # feast (setup.py) # altair # dask # db-dtypes @@ -535,7 +473,6 @@ packaging==24.1 # build # dask # db-dtypes - # duckdb-engine # google-cloud-bigquery # great-expectations # gunicorn @@ -551,7 +488,6 @@ packaging==24.1 # sphinx pandas==2.2.2 # via - # feast (setup.py) # altair # dask # dask-expr @@ -577,7 +513,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 @@ -590,7 +525,6 @@ 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 prompt-toolkit==3.0.47 @@ -605,7 +539,6 @@ proto-plus==1.24.0 # google-cloud-firestore protobuf==4.25.3 # via - # feast (setup.py) # google-api-core # google-cloud-bigquery # google-cloud-bigquery-storage @@ -623,11 +556,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 @@ -639,14 +569,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 @@ -664,19 +592,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 @@ -687,11 +612,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 @@ -703,10 +625,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 @@ -716,21 +636,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 @@ -759,7 +671,6 @@ pytz==2024.1 # trino pyyaml==6.0.1 # via - # feast (setup.py) # dask # ibis-substrait # jupyter-events @@ -773,19 +684,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 # cachecontrol # docker @@ -820,7 +727,6 @@ rich==13.7.1 # ibis-framework # typer rockset==2.1.2 - # via feast (setup.py) rpds-py==0.18.1 # via # jsonschema @@ -830,7 +736,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 @@ -847,7 +752,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 @@ -868,13 +772,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 @@ -888,17 +790,9 @@ sphinxcontrib-qthelp==1.0.7 sphinxcontrib-serializinghtml==1.1.10 # via sphinx sqlalchemy[mypy]==2.0.31 - # via - # feast (setup.py) - # duckdb-engine - # ibis-framework - # sqlalchemy-views -sqlalchemy-views==0.3.2 - # via ibis-framework -sqlglot==20.11.0 +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 @@ -908,21 +802,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 @@ -950,9 +840,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 @@ -969,39 +857,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 @@ -1040,7 +914,6 @@ uritemplate==4.1.1 # via google-api-python-client urllib3==1.26.19 # via - # feast (setup.py) # botocore # docker # elastic-transport @@ -1052,15 +925,11 @@ urllib3==1.26.19 # 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.10-requirements.txt b/sdk/python/requirements/py3.10-requirements.txt index 308123600c..99c9bfc3fe 100644 --- a/sdk/python/requirements/py3.10-requirements.txt +++ b/sdk/python/requirements/py3.10-requirements.txt @@ -20,22 +20,17 @@ 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,9 +38,7 @@ 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 @@ -53,7 +46,6 @@ fsspec==2024.3.1 greenlet==3.0.3 # via sqlalchemy gunicorn==22.0.0 - # via feast (setup.py) h11==0.14.0 # via # httpcore @@ -73,11 +65,8 @@ 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 locket==1.0.0 @@ -89,16 +78,13 @@ 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 @@ -110,29 +96,20 @@ packaging==24.0 # gunicorn pandas==2.2.2 # via - # feast (setup.py) # dask # dask-expr partd==1.4.2 # via dask protobuf==4.25.3 - # via - # feast (setup.py) - # mypy-protobuf + # via mypy-protobuf pyarrow==16.0.0 - # via - # feast (setup.py) - # dask-expr + # via dask-expr 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 python-dateutil==2.9.0.post0 # via pandas python-dotenv==1.0.1 @@ -143,7 +120,6 @@ pytz==2024.1 # via pandas pyyaml==6.0.1 # via - # feast (setup.py) # dask # uvicorn referencing==0.35.1 @@ -151,7 +127,6 @@ 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 @@ -167,15 +142,11 @@ 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 @@ -183,9 +154,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 @@ -209,7 +178,6 @@ 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 d0663a2bea..fa1f24a586 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 @@ -48,9 +44,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,9 +56,7 @@ bidict==0.23.1 bleach==6.1.0 # via nbconvert boto3==1.34.131 - # via - # feast (setup.py) - # moto + # via moto botocore==1.34.131 # via # aiobotocore @@ -73,7 +65,6 @@ botocore==1.34.131 # s3transfer build==1.2.1 # via - # feast (setup.py) # pip-tools # singlestoredb cachecontrol==0.14.0 @@ -81,7 +72,6 @@ cachecontrol==0.14.0 cachetools==5.3.3 # via google-auth cassandra-driver==3.29.1 - # via feast (setup.py) certifi==2024.6.2 # via # elastic-transport @@ -104,7 +94,6 @@ charset-normalizer==3.3.2 # snowflake-connector-python click==8.1.7 # via - # feast (setup.py) # dask # geomet # great-expectations @@ -114,9 +103,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 @@ -125,7 +112,6 @@ coverage[toml]==7.5.4 # via pytest-cov cryptography==42.0.8 # via - # feast (setup.py) # azure-identity # azure-storage-blob # great-expectations @@ -137,9 +123,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 @@ -151,9 +135,7 @@ 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 @@ -163,15 +145,10 @@ docker==7.1.0 docutils==0.19 # via sphinx duckdb==0.10.3 - # via - # duckdb-engine - # ibis-framework -duckdb-engine==0.13.0 # via ibis-framework 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 @@ -181,7 +158,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 @@ -191,7 +167,6 @@ filelock==3.15.4 # snowflake-connector-python # virtualenv firebase-admin==5.4.0 - # via feast (setup.py) fqdn==1.5.1 # via jsonschema frozenlist==1.4.1 @@ -199,16 +174,13 @@ frozenlist==1.4.1 # aiohttp # aiosignal fsspec==2023.12.2 - # via - # feast (setup.py) - # dask + # via 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) # firebase-admin # google-api-python-client # google-cloud-bigquery @@ -233,11 +205,8 @@ google-auth==2.30.0 google-auth-httplib2==0.2.0 # via google-api-python-client google-cloud-bigquery[pandas]==3.12.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 @@ -246,13 +215,10 @@ google-cloud-core==2.4.1 # google-cloud-firestore # google-cloud-storage google-cloud-datastore==2.19.0 - # via feast (setup.py) google-cloud-firestore==2.16.0 # via firebase-admin google-cloud-storage==2.17.0 - # via - # feast (setup.py) - # firebase-admin + # via firebase-admin google-crc32c==1.5.0 # via # google-cloud-storage @@ -263,19 +229,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 @@ -286,27 +249,19 @@ 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 httplib2==0.22.0 @@ -317,15 +272,11 @@ httptools==0.6.1 # via uvicorn httpx==0.27.0 # via - # feast (setup.py) # fastapi # jupyterlab -ibis-framework[duckdb]==8.0.0 - # via - # feast (setup.py) - # ibis-substrait -ibis-substrait==3.2.0 - # via feast (setup.py) +ibis-framework[duckdb]==9.1.0 + # via ibis-substrait +ibis-substrait==4.0.0 identify==2.5.36 # via pre-commit idna==3.7 @@ -360,7 +311,6 @@ jedi==0.19.1 # via ipython jinja2==3.1.4 # via - # feast (setup.py) # altair # fastapi # great-expectations @@ -384,7 +334,6 @@ jsonpointer==3.0.0 # jsonschema jsonschema[format-nongpl]==4.22.0 # via - # feast (setup.py) # altair # great-expectations # jupyter-events @@ -430,7 +379,6 @@ 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 @@ -451,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==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 @@ -474,16 +418,11 @@ multidict==6.0.5 # via # aiohttp # yarl -multipledispatch==1.0.0 - # via ibis-framework 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 @@ -506,7 +445,6 @@ notebook-shim==0.2.4 # notebook numpy==1.26.4 # via - # feast (setup.py) # altair # dask # db-dtypes @@ -526,7 +464,6 @@ packaging==24.1 # build # dask # db-dtypes - # duckdb-engine # google-cloud-bigquery # great-expectations # gunicorn @@ -542,7 +479,6 @@ packaging==24.1 # sphinx pandas==2.2.2 # via - # feast (setup.py) # altair # dask # dask-expr @@ -568,7 +504,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 @@ -581,7 +516,6 @@ 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 prompt-toolkit==3.0.47 @@ -596,7 +530,6 @@ proto-plus==1.24.0 # google-cloud-firestore protobuf==4.25.3 # via - # feast (setup.py) # google-api-core # google-cloud-bigquery # google-cloud-bigquery-storage @@ -614,11 +547,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 @@ -630,14 +560,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 @@ -655,19 +583,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 @@ -678,11 +603,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 @@ -694,10 +616,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 @@ -707,21 +627,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 @@ -750,7 +662,6 @@ pytz==2024.1 # trino pyyaml==6.0.1 # via - # feast (setup.py) # dask # ibis-substrait # jupyter-events @@ -764,19 +675,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 # cachecontrol # docker @@ -811,7 +718,6 @@ rich==13.7.1 # ibis-framework # typer rockset==2.1.2 - # via feast (setup.py) rpds-py==0.18.1 # via # jsonschema @@ -821,7 +727,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 @@ -838,7 +743,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 +763,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,17 +781,9 @@ sphinxcontrib-qthelp==1.0.7 sphinxcontrib-serializinghtml==1.1.10 # via sphinx sqlalchemy[mypy]==2.0.31 - # via - # feast (setup.py) - # duckdb-engine - # ibis-framework - # sqlalchemy-views -sqlalchemy-views==0.3.2 - # via ibis-framework -sqlglot==20.11.0 +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 @@ -899,21 +793,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 @@ -931,9 +821,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 @@ -950,39 +838,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 @@ -1018,7 +892,6 @@ uritemplate==4.1.1 # via google-api-python-client urllib3==1.26.19 # via - # feast (setup.py) # botocore # docker # elastic-transport @@ -1030,15 +903,11 @@ urllib3==1.26.19 # 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 f21afdb5b1..c34b610d14 100644 --- a/sdk/python/requirements/py3.11-requirements.txt +++ b/sdk/python/requirements/py3.11-requirements.txt @@ -20,30 +20,23 @@ 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 @@ -51,7 +44,6 @@ fsspec==2024.3.1 greenlet==3.0.3 # via sqlalchemy gunicorn==22.0.0 - # via feast (setup.py) h11==0.14.0 # via # httpcore @@ -71,11 +63,8 @@ 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 locket==1.0.0 @@ -87,16 +76,13 @@ 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 @@ -108,29 +94,20 @@ packaging==24.0 # gunicorn pandas==2.2.2 # via - # feast (setup.py) # dask # dask-expr partd==1.4.2 # via dask protobuf==4.25.3 - # via - # feast (setup.py) - # mypy-protobuf + # via mypy-protobuf pyarrow==16.0.0 - # via - # feast (setup.py) - # dask-expr + # via dask-expr 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 python-dateutil==2.9.0.post0 # via pandas python-dotenv==1.0.1 @@ -141,7 +118,6 @@ pytz==2024.1 # via pandas pyyaml==6.0.1 # via - # feast (setup.py) # dask # uvicorn referencing==0.35.1 @@ -149,7 +125,6 @@ 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 @@ -165,23 +140,17 @@ 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 @@ -203,7 +172,6 @@ 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 f09c666f42..ba4c6c989d 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,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 @@ -52,9 +48,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,9 +60,7 @@ bidict==0.23.1 bleach==6.1.0 # via nbconvert boto3==1.34.131 - # via - # feast (setup.py) - # moto + # via moto botocore==1.34.131 # via # aiobotocore @@ -77,7 +69,6 @@ botocore==1.34.131 # s3transfer build==1.2.1 # via - # feast (setup.py) # pip-tools # singlestoredb cachecontrol==0.14.0 @@ -85,7 +76,6 @@ cachecontrol==0.14.0 cachetools==5.3.3 # via google-auth cassandra-driver==3.29.1 - # via feast (setup.py) certifi==2024.6.2 # via # elastic-transport @@ -108,7 +98,6 @@ charset-normalizer==3.3.2 # snowflake-connector-python click==8.1.7 # via - # feast (setup.py) # dask # geomet # great-expectations @@ -118,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 @@ -129,7 +116,6 @@ coverage[toml]==7.5.4 # via pytest-cov cryptography==42.0.8 # via - # feast (setup.py) # azure-identity # azure-storage-blob # great-expectations @@ -141,9 +127,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 @@ -155,9 +139,7 @@ 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 @@ -167,15 +149,10 @@ docker==7.1.0 docutils==0.19 # via sphinx duckdb==0.10.3 - # via - # duckdb-engine - # ibis-framework -duckdb-engine==0.13.0 # via ibis-framework 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 @@ -190,7 +167,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 @@ -200,7 +176,6 @@ filelock==3.15.4 # snowflake-connector-python # virtualenv firebase-admin==5.4.0 - # via feast (setup.py) fqdn==1.5.1 # via jsonschema frozenlist==1.4.1 @@ -208,16 +183,13 @@ frozenlist==1.4.1 # aiohttp # aiosignal fsspec==2023.12.2 - # via - # feast (setup.py) - # dask + # via 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) # firebase-admin # google-api-python-client # google-cloud-bigquery @@ -242,11 +214,8 @@ google-auth==2.30.0 google-auth-httplib2==0.2.0 # via google-api-python-client google-cloud-bigquery[pandas]==3.12.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 @@ -255,13 +224,10 @@ google-cloud-core==2.4.1 # google-cloud-firestore # google-cloud-storage google-cloud-datastore==2.19.0 - # via feast (setup.py) google-cloud-firestore==2.16.0 # via firebase-admin google-cloud-storage==2.17.0 - # via - # feast (setup.py) - # firebase-admin + # via firebase-admin google-crc32c==1.5.0 # via # google-cloud-storage @@ -272,19 +238,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 @@ -295,27 +258,19 @@ 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 httplib2==0.22.0 @@ -326,15 +281,11 @@ httptools==0.6.1 # via uvicorn httpx==0.27.0 # via - # feast (setup.py) # fastapi # jupyterlab -ibis-framework[duckdb]==8.0.0 - # via - # feast (setup.py) - # ibis-substrait -ibis-substrait==3.2.0 - # via feast (setup.py) +ibis-framework[duckdb]==9.0.0 + # via ibis-substrait +ibis-substrait==4.0.0 identify==2.5.36 # via pre-commit idna==3.7 @@ -378,7 +329,6 @@ jedi==0.19.1 # via ipython jinja2==3.1.4 # via - # feast (setup.py) # altair # fastapi # great-expectations @@ -402,7 +352,6 @@ jsonpointer==3.0.0 # jsonschema jsonschema[format-nongpl]==4.22.0 # via - # feast (setup.py) # altair # great-expectations # jupyter-events @@ -448,7 +397,6 @@ 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 @@ -469,17 +417,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 @@ -492,16 +436,11 @@ multidict==6.0.5 # via # aiohttp # yarl -multipledispatch==1.0.0 - # via ibis-framework 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 @@ -524,7 +463,6 @@ notebook-shim==0.2.4 # notebook numpy==1.26.4 # via - # feast (setup.py) # altair # dask # db-dtypes @@ -544,7 +482,6 @@ packaging==24.1 # build # dask # db-dtypes - # duckdb-engine # google-cloud-bigquery # great-expectations # gunicorn @@ -560,7 +497,6 @@ packaging==24.1 # sphinx pandas==2.2.2 # via - # feast (setup.py) # altair # dask # dask-expr @@ -586,7 +522,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 @@ -599,7 +534,6 @@ 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 prompt-toolkit==3.0.47 @@ -614,7 +548,6 @@ proto-plus==1.24.0 # google-cloud-firestore protobuf==4.25.3 # via - # feast (setup.py) # google-api-core # google-cloud-bigquery # google-cloud-bigquery-storage @@ -632,11 +565,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 @@ -648,14 +578,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 @@ -673,19 +601,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 @@ -696,11 +621,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 @@ -712,10 +634,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 @@ -725,21 +645,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 @@ -768,7 +680,6 @@ pytz==2024.1 # trino pyyaml==6.0.1 # via - # feast (setup.py) # dask # ibis-substrait # jupyter-events @@ -782,19 +693,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 # cachecontrol # docker @@ -829,7 +736,6 @@ rich==13.7.1 # ibis-framework # typer rockset==2.1.2 - # via feast (setup.py) rpds-py==0.18.1 # via # jsonschema @@ -841,7 +747,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 @@ -858,7 +763,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 @@ -879,13 +783,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 @@ -899,17 +801,9 @@ sphinxcontrib-qthelp==1.0.7 sphinxcontrib-serializinghtml==1.1.10 # via sphinx sqlalchemy[mypy]==2.0.31 - # via - # feast (setup.py) - # duckdb-engine - # ibis-framework - # sqlalchemy-views -sqlalchemy-views==0.3.2 - # via ibis-framework -sqlglot==20.11.0 +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 @@ -919,21 +813,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 @@ -961,9 +851,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 @@ -980,39 +868,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 @@ -1053,7 +927,6 @@ uritemplate==4.1.1 # via google-api-python-client urllib3==1.26.19 # via - # feast (setup.py) # botocore # docker # elastic-transport @@ -1066,15 +939,11 @@ urllib3==1.26.19 # 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 52ff8a0f4f..149a96626e 100644 --- a/sdk/python/requirements/py3.9-requirements.txt +++ b/sdk/python/requirements/py3.9-requirements.txt @@ -20,22 +20,17 @@ 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,9 +38,7 @@ 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 @@ -53,7 +46,6 @@ fsspec==2024.3.1 greenlet==3.0.3 # via sqlalchemy gunicorn==22.0.0 - # via feast (setup.py) h11==0.14.0 # via # httpcore @@ -75,11 +67,8 @@ importlib-metadata==7.1.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 locket==1.0.0 @@ -91,16 +80,13 @@ 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 @@ -112,29 +98,20 @@ packaging==24.0 # gunicorn pandas==2.2.2 # via - # feast (setup.py) # dask # dask-expr partd==1.4.2 # via dask protobuf==4.25.3 - # via - # feast (setup.py) - # mypy-protobuf + # via mypy-protobuf pyarrow==16.0.0 - # via - # feast (setup.py) - # dask-expr + # via dask-expr 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 python-dateutil==2.9.0.post0 # via pandas python-dotenv==1.0.1 @@ -145,7 +122,6 @@ pytz==2024.1 # via pandas pyyaml==6.0.1 # via - # feast (setup.py) # dask # uvicorn referencing==0.35.1 @@ -153,7 +129,6 @@ 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 @@ -169,15 +144,11 @@ 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 @@ -185,9 +156,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 @@ -212,7 +181,6 @@ 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/setup.py b/setup.py index 958e93799d..f7fa610fbd 100644 --- a/setup.py +++ b/setup.py @@ -138,8 +138,8 @@ ] IBIS_REQUIRED = [ - "ibis-framework>=8.0.0,<9", - "ibis-substrait<=3.2.0", + "ibis-framework>=9.0.0,<10", + "ibis-substrait>=4.0.0", ] GRPCIO_REQUIRED = [ @@ -149,7 +149,7 @@ "grpcio-health-checking>=1.56.2,<2", ] -DUCKDB_REQUIRED = ["ibis-framework[duckdb]>=8.0.0,<9"] +DUCKDB_REQUIRED = ["ibis-framework[duckdb]>=9.0.0,<10"] DELTA_REQUIRED = ["deltalake"] From a639d617c047030f75c6950e9bfa6e5cfe63daaa Mon Sep 17 00:00:00 2001 From: Hao Xu Date: Wed, 10 Jul 2024 08:22:13 -0700 Subject: [PATCH 23/59] fix: Update dask version to support pandas 1.x (#4326) * update dask version to support pandas 1.x Signed-off-by: cmuhao * update dask version to support pandas 1.x Signed-off-by: cmuhao * update dask version to support pandas 1.x Signed-off-by: cmuhao --------- Signed-off-by: cmuhao --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f7fa610fbd..a543262821 100644 --- a/setup.py +++ b/setup.py @@ -65,7 +65,7 @@ "fastapi>=0.68.0", "uvicorn[standard]>=0.14.0,<1", "gunicorn; platform_system != 'Windows'", - "dask[dataframe]>=2024.4.2", + "dask[dataframe]>=2024.2.1", ] GCP_REQUIRED = [ From c45ff72f821404c595477e696ab4be1b888090cc Mon Sep 17 00:00:00 2001 From: Alex Mirrington <34053287+alexmirrington@users.noreply.github.com> Date: Thu, 11 Jul 2024 03:18:28 +1000 Subject: [PATCH 24/59] fix: OnDemandFeatureView type inference for array types (#4310) Fix OnDemandFeatureView type inference for array types Signed-off-by: Alex Mirrington --- .../transformation/pandas_transformation.py | 28 +- .../transformation/python_transformation.py | 27 +- .../substrait_transformation.py | 32 ++- sdk/python/feast/type_map.py | 1 + .../test_on_demand_pandas_transformation.py | 254 +++++++++++++++++- .../test_on_demand_python_transformation.py | 243 ++++++++++++++++- 6 files changed, 555 insertions(+), 30 deletions(-) diff --git a/sdk/python/feast/transformation/pandas_transformation.py b/sdk/python/feast/transformation/pandas_transformation.py index e9dab72160..41e437fb6b 100644 --- a/sdk/python/feast/transformation/pandas_transformation.py +++ b/sdk/python/feast/transformation/pandas_transformation.py @@ -40,15 +40,27 @@ def infer_features(self, random_input: dict[str, list[Any]]) -> list[Field]: df = pd.DataFrame.from_dict(random_input) output_df: pd.DataFrame = self.transform(df) - return [ - Field( - name=f, - dtype=from_value_type( - python_type_to_feast_value_type(f, type_name=str(dt)) - ), + fields = [] + for feature_name, feature_type in zip(output_df.columns, output_df.dtypes): + feature_value = output_df[feature_name].tolist() + if len(feature_value) <= 0: + raise TypeError( + f"Failed to infer type for feature '{feature_name}' with value " + + f"'{feature_value}' since no items were returned by the UDF." + ) + fields.append( + Field( + name=feature_name, + dtype=from_value_type( + python_type_to_feast_value_type( + feature_name, + value=feature_value[0], + type_name=str(feature_type), + ) + ), + ) ) - for f, dt in zip(output_df.columns, output_df.dtypes) - ] + return fields def __eq__(self, other): if not isinstance(other, PandasTransformation): diff --git a/sdk/python/feast/transformation/python_transformation.py b/sdk/python/feast/transformation/python_transformation.py index 2a9c7db876..d828890b1e 100644 --- a/sdk/python/feast/transformation/python_transformation.py +++ b/sdk/python/feast/transformation/python_transformation.py @@ -40,15 +40,26 @@ def transform(self, input_dict: dict) -> dict: def infer_features(self, random_input: dict[str, list[Any]]) -> list[Field]: output_dict: dict[str, list[Any]] = self.transform(random_input) - return [ - Field( - name=f, - dtype=from_value_type( - python_type_to_feast_value_type(f, type_name=type(dt[0]).__name__) - ), + fields = [] + for feature_name, feature_value in output_dict.items(): + if len(feature_value) <= 0: + raise TypeError( + f"Failed to infer type for feature '{feature_name}' with value " + + f"'{feature_value}' since no items were returned by the UDF." + ) + fields.append( + Field( + name=feature_name, + dtype=from_value_type( + python_type_to_feast_value_type( + feature_name, + value=feature_value[0], + type_name=type(feature_value[0]).__name__, + ) + ), + ) ) - for f, dt in output_dict.items() - ] + return fields def __eq__(self, other): if not isinstance(other, PythonTransformation): diff --git a/sdk/python/feast/transformation/substrait_transformation.py b/sdk/python/feast/transformation/substrait_transformation.py index 17c40cf0a1..1de60aed00 100644 --- a/sdk/python/feast/transformation/substrait_transformation.py +++ b/sdk/python/feast/transformation/substrait_transformation.py @@ -60,16 +60,28 @@ def infer_features(self, random_input: dict[str, list[Any]]) -> list[Field]: df = pd.DataFrame.from_dict(random_input) output_df: pd.DataFrame = self.transform(df) - return [ - Field( - name=f, - dtype=from_value_type( - python_type_to_feast_value_type(f, type_name=str(dt)) - ), - ) - for f, dt in zip(output_df.columns, output_df.dtypes) - if f not in random_input - ] + fields = [] + for feature_name, feature_type in zip(output_df.columns, output_df.dtypes): + feature_value = output_df[feature_name].tolist() + if len(feature_value) <= 0: + raise TypeError( + f"Failed to infer type for feature '{feature_name}' with value " + + f"'{feature_value}' since no items were returned by the UDF." + ) + if feature_name not in random_input: + fields.append( + Field( + name=feature_name, + dtype=from_value_type( + python_type_to_feast_value_type( + feature_name, + value=feature_value[0], + type_name=str(feature_type), + ) + ), + ) + ) + return fields def __eq__(self, other): if not isinstance(other, SubstraitTransformation): diff --git a/sdk/python/feast/type_map.py b/sdk/python/feast/type_map.py index a0859f2f7a..6ba61fc8c5 100644 --- a/sdk/python/feast/type_map.py +++ b/sdk/python/feast/type_map.py @@ -155,6 +155,7 @@ def python_type_to_feast_value_type( "uint16": ValueType.INT32, "uint8": ValueType.INT32, "int8": ValueType.INT32, + "bool_": ValueType.BOOL, # np.bool_ "bool": ValueType.BOOL, "boolean": ValueType.BOOL, "timedelta": ValueType.UNIX_TIMESTAMP, diff --git a/sdk/python/tests/unit/test_on_demand_pandas_transformation.py b/sdk/python/tests/unit/test_on_demand_pandas_transformation.py index c5f066dd83..1a04a466fb 100644 --- a/sdk/python/tests/unit/test_on_demand_pandas_transformation.py +++ b/sdk/python/tests/unit/test_on_demand_pandas_transformation.py @@ -1,15 +1,31 @@ import os +import re import tempfile from datetime import datetime, timedelta import pandas as pd +import pytest -from feast import Entity, FeatureStore, FeatureView, FileSource, RepoConfig +from feast import ( + Entity, + FeatureStore, + FeatureView, + FileSource, + RepoConfig, + RequestSource, +) from feast.driver_test_data import create_driver_hourly_stats_df 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 Float32, Float64, Int64 +from feast.types import ( + Array, + Bool, + Float32, + Float64, + Int64, + String, +) def test_pandas_transformation(): @@ -91,3 +107,237 @@ def pandas_view(inputs: pd.DataFrame) -> pd.DataFrame: assert online_response["conv_rate_plus_acc"].equals( online_response["conv_rate"] + online_response["acc_rate"] ) + + +def test_pandas_transformation_returning_all_data_types(): + with tempfile.TemporaryDirectory() as data_dir: + 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", + ) + + 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, + ) + + request_source = RequestSource( + name="request_source", + schema=[ + Field(name="avg_daily_trip_rank_thresholds", dtype=Array(Int64)), + Field(name="avg_daily_trip_rank_names", dtype=Array(String)), + ], + ) + + @on_demand_feature_view( + sources=[request_source, driver_stats_fv], + schema=[ + Field(name="highest_achieved_rank", dtype=String), + Field(name="avg_daily_trips_plus_one", dtype=Int64), + Field(name="conv_rate_plus_acc", dtype=Float64), + Field(name="is_highest_rank", dtype=Bool), + Field(name="achieved_ranks", dtype=Array(String)), + Field(name="trips_until_next_rank_int", dtype=Array(Int64)), + Field(name="trips_until_next_rank_float", dtype=Array(Float64)), + Field(name="achieved_ranks_mask", dtype=Array(Bool)), + ], + mode="pandas", + ) + def pandas_view(inputs: pd.DataFrame) -> pd.DataFrame: + df = pd.DataFrame() + df["conv_rate_plus_acc"] = inputs["conv_rate"] + inputs["acc_rate"] + df["avg_daily_trips_plus_one"] = inputs["avg_daily_trips"] + 1 + + df["trips_until_next_rank_int"] = inputs[ + ["avg_daily_trips", "avg_daily_trip_rank_thresholds"] + ].apply( + lambda x: [max(threshold - x.iloc[0], 0) for threshold in x.iloc[1]], + axis=1, + ) + df["trips_until_next_rank_float"] = df["trips_until_next_rank_int"].map( + lambda values: [float(value) for value in values] + ) + df["achieved_ranks_mask"] = df["trips_until_next_rank_int"].map( + lambda values: [value <= 0 for value in values] + ) + + temp = pd.concat( + [df[["achieved_ranks_mask"]], inputs[["avg_daily_trip_rank_names"]]], + axis=1, + ) + df["achieved_ranks"] = temp.apply( + lambda x: [ + rank if achieved else "Locked" + for achieved, rank in zip(x.iloc[0], x.iloc[1]) + ], + axis=1, + ) + df["highest_achieved_rank"] = ( + df["achieved_ranks"] + .map( + lambda ranks: str( + ([rank for rank in ranks if rank != "Locked"][-1:] or ["None"])[ + 0 + ] + ) + ) + .astype("string") + ) + df["is_highest_rank"] = df["achieved_ranks"].map( + lambda ranks: ranks[-1] != "Locked" + ) + return df + + store.apply([driver, driver_stats_source, driver_stats_fv, pandas_view]) + + entity_rows = [ + { + "driver_id": 1001, + "avg_daily_trip_rank_thresholds": [100, 250, 500, 1000], + "avg_daily_trip_rank_names": ["Bronze", "Silver", "Gold", "Platinum"], + } + ] + store.write_to_online_store( + feature_view_name="driver_hourly_stats", df=driver_df + ) + + online_response = store.get_online_features( + entity_rows=entity_rows, + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "driver_hourly_stats:avg_daily_trips", + "pandas_view:avg_daily_trips_plus_one", + "pandas_view:conv_rate_plus_acc", + "pandas_view:trips_until_next_rank_int", + "pandas_view:trips_until_next_rank_float", + "pandas_view:achieved_ranks_mask", + "pandas_view:achieved_ranks", + "pandas_view:highest_achieved_rank", + "pandas_view:is_highest_rank", + ], + ).to_df() + # We use to_df here to ensure we use the pandas backend, but convert to a dict for comparisons + result = online_response.to_dict(orient="records")[0] + + # Type assertions + # Materialized view + assert type(result["conv_rate"]) == float + assert type(result["acc_rate"]) == float + assert type(result["avg_daily_trips"]) == int + # On-demand view + assert type(result["avg_daily_trips_plus_one"]) == int + assert type(result["conv_rate_plus_acc"]) == float + assert type(result["highest_achieved_rank"]) == str + assert type(result["is_highest_rank"]) == bool + + assert type(result["trips_until_next_rank_int"]) == list + assert all([type(e) == int for e in result["trips_until_next_rank_int"]]) + + assert type(result["trips_until_next_rank_float"]) == list + assert all([type(e) == float for e in result["trips_until_next_rank_float"]]) + + assert type(result["achieved_ranks"]) == list + assert all([type(e) == str for e in result["achieved_ranks"]]) + + assert type(result["achieved_ranks_mask"]) == list + assert all([type(e) == bool for e in result["achieved_ranks_mask"]]) + + # Value assertions + expected_trips_until_next_rank = [ + max(threshold - result["avg_daily_trips"], 0) + for threshold in entity_rows[0]["avg_daily_trip_rank_thresholds"] + ] + expected_mask = [value <= 0 for value in expected_trips_until_next_rank] + expected_ranks = [ + rank if achieved else "Locked" + for achieved, rank in zip( + expected_mask, entity_rows[0]["avg_daily_trip_rank_names"] + ) + ] + highest_rank = ( + [rank for rank in expected_ranks if rank != "Locked"][-1:] or ["None"] + )[0] + + assert result["conv_rate_plus_acc"] == result["conv_rate"] + result["acc_rate"] + assert result["avg_daily_trips_plus_one"] == result["avg_daily_trips"] + 1 + assert result["highest_achieved_rank"] == highest_rank + assert result["is_highest_rank"] == (expected_ranks[-1] != "Locked") + + assert result["trips_until_next_rank_int"] == expected_trips_until_next_rank + assert result["trips_until_next_rank_float"] == [ + float(value) for value in expected_trips_until_next_rank + ] + assert result["achieved_ranks_mask"] == expected_mask + assert result["achieved_ranks"] == expected_ranks + + +def test_invalid_pandas_transformation_raises_type_error_on_apply(): + with tempfile.TemporaryDirectory() as data_dir: + 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") + ), + ) + ) + + request_source = RequestSource( + name="request_source", + schema=[ + Field(name="driver_name", dtype=String), + ], + ) + + @on_demand_feature_view( + sources=[request_source], + schema=[Field(name="driver_name_lower", dtype=String)], + mode="pandas", + ) + def pandas_view(inputs: pd.DataFrame) -> pd.DataFrame: + return pd.DataFrame({"driver_name_lower": []}) + + with pytest.raises( + TypeError, + match=re.escape( + "Failed to infer type for feature 'driver_name_lower' with value '[]' since no items were returned by the UDF." + ), + ): + store.apply([request_source, pandas_view]) 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 72e9b53a10..c5bd68d6a8 100644 --- a/sdk/python/tests/unit/test_on_demand_python_transformation.py +++ b/sdk/python/tests/unit/test_on_demand_python_transformation.py @@ -1,4 +1,5 @@ import os +import re import tempfile import unittest from datetime import datetime, timedelta @@ -7,12 +8,19 @@ import pandas as pd import pytest -from feast import Entity, FeatureStore, FeatureView, FileSource, RepoConfig +from feast import ( + Entity, + FeatureStore, + FeatureView, + FileSource, + RepoConfig, + RequestSource, +) from feast.driver_test_data import create_driver_hourly_stats_df 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 Float32, Float64, Int64 +from feast.types import Array, Bool, Float32, Float64, Int64, String class TestOnDemandPythonTransformation(unittest.TestCase): @@ -248,3 +256,234 @@ def test_python_docs_demo(self): + online_python_response["acc_rate"][0] == online_python_response["conv_rate_plus_val2_python"][0] ) + + +class TestOnDemandPythonTransformationAllDataTypes(unittest.TestCase): + def setUp(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", + ) + + 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, + ) + + request_source = RequestSource( + name="request_source", + schema=[ + Field(name="avg_daily_trip_rank_thresholds", dtype=Array(Int64)), + Field(name="avg_daily_trip_rank_names", dtype=Array(String)), + ], + ) + + @on_demand_feature_view( + sources=[request_source, driver_stats_fv], + schema=[ + Field(name="highest_achieved_rank", dtype=String), + Field(name="avg_daily_trips_plus_one", dtype=Int64), + Field(name="conv_rate_plus_acc", dtype=Float64), + Field(name="is_highest_rank", dtype=Bool), + Field(name="achieved_ranks", dtype=Array(String)), + Field(name="trips_until_next_rank_int", dtype=Array(Int64)), + Field(name="trips_until_next_rank_float", dtype=Array(Float64)), + Field(name="achieved_ranks_mask", dtype=Array(Bool)), + ], + mode="python", + ) + def python_view(inputs: dict[str, Any]) -> dict[str, Any]: + output = {} + trips_until_next_rank = [ + [max(threshold - row[1], 0) for threshold in row[0]] + for row in zip( + inputs["avg_daily_trip_rank_thresholds"], + inputs["avg_daily_trips"], + ) + ] + mask = [[value <= 0 for value in row] for row in trips_until_next_rank] + ranks = [ + [rank if mask else "Locked" for mask, rank in zip(*row)] + for row in zip(mask, inputs["avg_daily_trip_rank_names"]) + ] + highest_rank = [ + ([rank for rank in row if rank != "Locked"][-1:] or ["None"])[0] + for row in ranks + ] + + output["conv_rate_plus_acc"] = [ + sum(row) for row in zip(inputs["conv_rate"], inputs["acc_rate"]) + ] + output["avg_daily_trips_plus_one"] = [ + row + 1 for row in inputs["avg_daily_trips"] + ] + output["highest_achieved_rank"] = highest_rank + output["is_highest_rank"] = [row[-1] != "Locked" for row in ranks] + + output["trips_until_next_rank_int"] = trips_until_next_rank + output["trips_until_next_rank_float"] = [ + [float(value) for value in row] for row in trips_until_next_rank + ] + output["achieved_ranks_mask"] = mask + output["achieved_ranks"] = ranks + return output + + 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 + ) + + def test_python_transformation_returning_all_data_types(self): + entity_rows = [ + { + "driver_id": 1001, + "avg_daily_trip_rank_thresholds": [100, 250, 500, 1000], + "avg_daily_trip_rank_names": ["Bronze", "Silver", "Gold", "Platinum"], + } + ] + online_response = self.store.get_online_features( + entity_rows=entity_rows, + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "driver_hourly_stats:avg_daily_trips", + "python_view:avg_daily_trips_plus_one", + "python_view:conv_rate_plus_acc", + "python_view:trips_until_next_rank_int", + "python_view:trips_until_next_rank_float", + "python_view:achieved_ranks_mask", + "python_view:achieved_ranks", + "python_view:highest_achieved_rank", + "python_view:is_highest_rank", + ], + ).to_dict() + result = {name: value[0] for name, value in online_response.items()} + + # Type assertions + # Materialized view + assert type(result["conv_rate"]) == float + assert type(result["acc_rate"]) == float + assert type(result["avg_daily_trips"]) == int + # On-demand view + assert type(result["avg_daily_trips_plus_one"]) == int + assert type(result["conv_rate_plus_acc"]) == float + assert type(result["highest_achieved_rank"]) == str + assert type(result["is_highest_rank"]) == bool + + assert type(result["trips_until_next_rank_int"]) == list + assert all([type(e) == int for e in result["trips_until_next_rank_int"]]) + + assert type(result["trips_until_next_rank_float"]) == list + assert all([type(e) == float for e in result["trips_until_next_rank_float"]]) + + assert type(result["achieved_ranks"]) == list + assert all([type(e) == str for e in result["achieved_ranks"]]) + + assert type(result["achieved_ranks_mask"]) == list + assert all([type(e) == bool for e in result["achieved_ranks_mask"]]) + + # Value assertions + expected_trips_until_next_rank = [ + max(threshold - result["avg_daily_trips"], 0) + for threshold in entity_rows[0]["avg_daily_trip_rank_thresholds"] + ] + expected_mask = [value <= 0 for value in expected_trips_until_next_rank] + expected_ranks = [ + rank if achieved else "Locked" + for achieved, rank in zip( + expected_mask, entity_rows[0]["avg_daily_trip_rank_names"] + ) + ] + highest_rank = ( + [rank for rank in expected_ranks if rank != "Locked"][-1:] or ["None"] + )[0] + + assert result["conv_rate_plus_acc"] == result["conv_rate"] + result["acc_rate"] + assert result["avg_daily_trips_plus_one"] == result["avg_daily_trips"] + 1 + assert result["highest_achieved_rank"] == highest_rank + assert result["is_highest_rank"] == (expected_ranks[-1] != "Locked") + + assert result["trips_until_next_rank_int"] == expected_trips_until_next_rank + assert result["trips_until_next_rank_float"] == [ + float(value) for value in expected_trips_until_next_rank + ] + assert result["achieved_ranks_mask"] == expected_mask + assert result["achieved_ranks"] == expected_ranks + + +def test_invalid_python_transformation_raises_type_error_on_apply(): + with tempfile.TemporaryDirectory() as data_dir: + 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") + ), + ) + ) + + request_source = RequestSource( + name="request_source", + schema=[ + Field(name="driver_name", dtype=String), + ], + ) + + @on_demand_feature_view( + sources=[request_source], + schema=[Field(name="driver_name_lower", dtype=String)], + mode="python", + ) + def python_view(inputs: dict[str, Any]) -> dict[str, Any]: + return {"driver_name_lower": []} + + with pytest.raises( + TypeError, + match=re.escape( + "Failed to infer type for feature 'driver_name_lower' with value '[]' since no items were returned by the UDF." + ), + ): + store.apply([request_source, python_view]) From 5c07bd80ce729c5aef90e2b07710cd06c0334b1e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jul 2024 21:19:13 +0400 Subject: [PATCH 25/59] chore: Bump zipp from 3.18.1 to 3.19.1 in /sdk/python/requirements (#4337) Bumps [zipp](https://github.com/jaraco/zipp) from 3.18.1 to 3.19.1. - [Release notes](https://github.com/jaraco/zipp/releases) - [Changelog](https://github.com/jaraco/zipp/blob/main/NEWS.rst) - [Commits](https://github.com/jaraco/zipp/compare/v3.18.1...v3.19.1) --- updated-dependencies: - dependency-name: zipp 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 e6e66ac2ee..a9ac50711a 100644 --- a/sdk/python/requirements/py3.10-ci-requirements.txt +++ b/sdk/python/requirements/py3.10-ci-requirements.txt @@ -962,5 +962,5 @@ xmltodict==0.13.0 # via moto yarl==1.9.4 # via aiohttp -zipp==3.19.2 +zipp==3.19.1 # via importlib-metadata diff --git a/sdk/python/requirements/py3.10-requirements.txt b/sdk/python/requirements/py3.10-requirements.txt index 99c9bfc3fe..b9d913c48a 100644 --- a/sdk/python/requirements/py3.10-requirements.txt +++ b/sdk/python/requirements/py3.10-requirements.txt @@ -186,5 +186,5 @@ watchfiles==0.21.0 # via uvicorn websockets==12.0 # via uvicorn -zipp==3.18.1 +zipp==3.19.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 fa1f24a586..7f85ceb547 100644 --- a/sdk/python/requirements/py3.11-ci-requirements.txt +++ b/sdk/python/requirements/py3.11-ci-requirements.txt @@ -940,5 +940,5 @@ xmltodict==0.13.0 # via moto yarl==1.9.4 # via aiohttp -zipp==3.19.2 +zipp==3.19.1 # via importlib-metadata diff --git a/sdk/python/requirements/py3.11-requirements.txt b/sdk/python/requirements/py3.11-requirements.txt index c34b610d14..2bf521cda9 100644 --- a/sdk/python/requirements/py3.11-requirements.txt +++ b/sdk/python/requirements/py3.11-requirements.txt @@ -180,5 +180,5 @@ watchfiles==0.21.0 # via uvicorn websockets==12.0 # via uvicorn -zipp==3.18.1 +zipp==3.19.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 ba4c6c989d..bbeb7367e7 100644 --- a/sdk/python/requirements/py3.9-ci-requirements.txt +++ b/sdk/python/requirements/py3.9-ci-requirements.txt @@ -976,5 +976,5 @@ xmltodict==0.13.0 # via moto yarl==1.9.4 # via aiohttp -zipp==3.19.2 +zipp==3.19.1 # via importlib-metadata diff --git a/sdk/python/requirements/py3.9-requirements.txt b/sdk/python/requirements/py3.9-requirements.txt index 149a96626e..9c4450cb45 100644 --- a/sdk/python/requirements/py3.9-requirements.txt +++ b/sdk/python/requirements/py3.9-requirements.txt @@ -189,5 +189,5 @@ watchfiles==0.21.0 # via uvicorn websockets==12.0 # via uvicorn -zipp==3.18.1 +zipp==3.19.1 # via importlib-metadata From aba317cf33b1c17bd94c8724522abaf15e5683ce Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jul 2024 21:27:54 -0400 Subject: [PATCH 26/59] chore: Bump braces from 3.0.2 to 3.0.3 in /sdk/python/feast/ui (#4287) --- sdk/python/feast/ui/yarn.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/sdk/python/feast/ui/yarn.lock b/sdk/python/feast/ui/yarn.lock index 005035db2d..7c01b6c1e8 100644 --- a/sdk/python/feast/ui/yarn.lock +++ b/sdk/python/feast/ui/yarn.lock @@ -3560,11 +3560,11 @@ brace-expansion@^2.0.1: balanced-match "^1.0.0" braces@^3.0.2, braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: - fill-range "^7.0.1" + fill-range "^7.1.1" broadcast-channel@^3.4.1: version "3.7.0" @@ -5400,10 +5400,10 @@ filesize@^8.0.6: resolved "https://registry.yarnpkg.com/filesize/-/filesize-8.0.7.tgz#695e70d80f4e47012c132d57a059e80c6b580bd8" integrity sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ== -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" From dc363472db75c2971c7711147f824257235c6673 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jul 2024 21:28:42 -0400 Subject: [PATCH 27/59] chore: Bump ws from 7.5.6 to 7.5.10 in /ui (#4292) --- ui/yarn.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ui/yarn.lock b/ui/yarn.lock index 89107de0b8..26c833fa11 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -11640,14 +11640,14 @@ write-file-atomic@^3.0.0: typedarray-to-buffer "^3.1.5" ws@^7.4.6: - version "7.5.6" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.6.tgz#e59fc509fb15ddfb65487ee9765c5a51dec5fe7b" - integrity sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA== + version "7.5.10" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" + integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== ws@^8.1.0: - version "8.4.2" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.4.2.tgz#18e749868d8439f2268368829042894b6907aa0b" - integrity sha512-Kbk4Nxyq7/ZWqr/tarI9yIt/+iNNFOjBXEWgTb4ydaNHBNGgvf2QHbS9fdfsndfjFlFwEd4Al+mw83YkaD10ZA== + version "8.17.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" + integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== xml-name-validator@^3.0.0: version "3.0.0" From 660df6ec9b1191d1ea1b673c8a4fd5845c05f717 Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Fri, 12 Jul 2024 01:26:03 -0400 Subject: [PATCH 28/59] chore: Updating docs (#4346) --- docs/README.md | 8 ++++---- ui/feature_repo/features.py | 22 +++++++++++----------- ui/package.json | 2 +- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/README.md b/docs/README.md index 66c7548440..eea372ded0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -39,10 +39,10 @@ Feast is likely **not** the right tool if you ### 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 (though it offers experimental light-weight transformations). Users usually integrate Feast with upstream systems (e.g. existing ETL/ELT pipelines). [Tecton](http://tecton.ai/) is a more fully featured feature platform which addresses these needs. -* **native streaming feature integration:** Feast enables users to push streaming features, but does not pull from streaming sources or manage streaming pipelines. [Tecton](http://tecton.ai/) is a more fully featured feature platform which orchestrates end to end 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). [Tecton](http://tecton.ai/) also more robustly addresses these needs. -* **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). [Tecton](http://tecton.ai/) captures more end-to-end lineage by also managing feature transformations. +* **batch + streaming feature engineering**: Feast primarily processes already transformed feature values but is investing in supporting batch and streaming 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. ## Example use cases diff --git a/ui/feature_repo/features.py b/ui/feature_repo/features.py index e02bb3de5d..40a42a9e99 100644 --- a/ui/feature_repo/features.py +++ b/ui/feature_repo/features.py @@ -11,7 +11,7 @@ name="zipcode", description="A zipcode", tags={ - "owner": "danny@tecton.ai", + "owner": "danny@feast.ai", "team": "hack week", }, ) @@ -40,7 +40,7 @@ tags={ "date_added": "2022-02-7", "experiments": "experiment-A,experiment-B,experiment-C", - "access_group": "feast-team@tecton.ai", + "access_group": "feast-team@feast.ai", }, online=True, ) @@ -62,7 +62,7 @@ tags={ "date_added": "2022-02-7", "experiments": "experiment-A,experiment-B,experiment-C", - "access_group": "feast-team@tecton.ai", + "access_group": "feast-team@feast.ai", }, online=True, ) @@ -80,7 +80,7 @@ tags={ "date_added": "2022-02-7", "experiments": "experiment-A,experiment-B,experiment-C", - "access_group": "feast-team@tecton.ai", + "access_group": "feast-team@feast.ai", }, online=True, ) @@ -89,7 +89,7 @@ name="dob_ssn", description="Date of birth and last four digits of social security number", tags={ - "owner": "tony@tecton.ai", + "owner": "tony@feast.ai", "team": "hack week", }, ) @@ -121,7 +121,7 @@ tags={ "date_added": "2022-02-6", "experiments": "experiment-A", - "access_group": "feast-team@tecton.ai", + "access_group": "feast-team@feast.ai", }, online=True, ) @@ -157,7 +157,7 @@ def transaction_gt_last_credit_card_due(inputs: pd.DataFrame) -> pd.DataFrame: credit_history[["credit_card_due", "missed_payments_1y"]], zipcode_features, ], - tags={"owner": "tony@tecton.ai", "stage": "staging"}, + tags={"owner": "tony@feast.ai", "stage": "staging"}, description="Credit scoring model", ) @@ -167,7 +167,7 @@ def transaction_gt_last_credit_card_due(inputs: pd.DataFrame) -> pd.DataFrame: credit_history[["mortgage_due", "credit_card_due", "missed_payments_1y"]], zipcode_features, ], - tags={"owner": "tony@tecton.ai", "stage": "prod"}, + tags={"owner": "tony@feast.ai", "stage": "prod"}, description="Credit scoring model", ) @@ -178,7 +178,7 @@ def transaction_gt_last_credit_card_due(inputs: pd.DataFrame) -> pd.DataFrame: zipcode_features, transaction_gt_last_credit_card_due, ], - tags={"owner": "tony@tecton.ai", "stage": "dev"}, + tags={"owner": "tony@feast.ai", "stage": "dev"}, description="Credit scoring model", ) @@ -187,7 +187,7 @@ def transaction_gt_last_credit_card_due(inputs: pd.DataFrame) -> pd.DataFrame: features=[ zipcode_features, ], - tags={"owner": "amanda@tecton.ai", "stage": "dev"}, + tags={"owner": "amanda@feast.ai", "stage": "dev"}, description="Location model", ) @@ -196,6 +196,6 @@ def transaction_gt_last_credit_card_due(inputs: pd.DataFrame) -> pd.DataFrame: features=[ zipcode_money_features, ], - tags={"owner": "amanda@tecton.ai", "stage": "dev"}, + tags={"owner": "amanda@feast.ai", "stage": "dev"}, description="Location model", ) diff --git a/ui/package.json b/ui/package.json index de37f4394a..a380c65cfc 100644 --- a/ui/package.json +++ b/ui/package.json @@ -118,7 +118,7 @@ "Feature", "Store" ], - "author": "tony@tecton.ai", + "author": "tony@feast.ai", "license": "Apache-2.0", "bugs": { "url": "https://github.com/feast-dev/feast/issues" From 1ce65bc1f5528a8bfa33e36e8852f4fd198d3ce7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Jul 2024 14:11:39 +0400 Subject: [PATCH 29/59] chore: Bump certifi from 2024.2.2 to 2024.7.4 in /sdk/python/requirements (#4334) chore: Bump certifi in /sdk/python/requirements Bumps [certifi](https://github.com/certifi/python-certifi) from 2024.2.2 to 2024.7.4. - [Commits](https://github.com/certifi/python-certifi/compare/2024.02.02...2024.07.04) --- updated-dependencies: - dependency-name: certifi 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 a9ac50711a..33709a1ef0 100644 --- a/sdk/python/requirements/py3.10-ci-requirements.txt +++ b/sdk/python/requirements/py3.10-ci-requirements.txt @@ -76,7 +76,7 @@ cachecontrol==0.14.0 cachetools==5.3.3 # via google-auth cassandra-driver==3.29.1 -certifi==2024.6.2 +certifi==2024.7.4 # via # elastic-transport # httpcore diff --git a/sdk/python/requirements/py3.10-requirements.txt b/sdk/python/requirements/py3.10-requirements.txt index b9d913c48a..0cca106863 100644 --- a/sdk/python/requirements/py3.10-requirements.txt +++ b/sdk/python/requirements/py3.10-requirements.txt @@ -11,7 +11,7 @@ attrs==23.2.0 # via # jsonschema # referencing -certifi==2024.2.2 +certifi==2024.7.4 # via # httpcore # httpx diff --git a/sdk/python/requirements/py3.11-ci-requirements.txt b/sdk/python/requirements/py3.11-ci-requirements.txt index 7f85ceb547..09e9e8eeea 100644 --- a/sdk/python/requirements/py3.11-ci-requirements.txt +++ b/sdk/python/requirements/py3.11-ci-requirements.txt @@ -72,7 +72,7 @@ cachecontrol==0.14.0 cachetools==5.3.3 # via google-auth cassandra-driver==3.29.1 -certifi==2024.6.2 +certifi==2024.7.4 # via # elastic-transport # httpcore diff --git a/sdk/python/requirements/py3.11-requirements.txt b/sdk/python/requirements/py3.11-requirements.txt index 2bf521cda9..687e4bfe52 100644 --- a/sdk/python/requirements/py3.11-requirements.txt +++ b/sdk/python/requirements/py3.11-requirements.txt @@ -11,7 +11,7 @@ attrs==23.2.0 # via # jsonschema # referencing -certifi==2024.2.2 +certifi==2024.7.4 # via # httpcore # httpx diff --git a/sdk/python/requirements/py3.9-ci-requirements.txt b/sdk/python/requirements/py3.9-ci-requirements.txt index bbeb7367e7..6f5d0220bc 100644 --- a/sdk/python/requirements/py3.9-ci-requirements.txt +++ b/sdk/python/requirements/py3.9-ci-requirements.txt @@ -76,7 +76,7 @@ cachecontrol==0.14.0 cachetools==5.3.3 # via google-auth cassandra-driver==3.29.1 -certifi==2024.6.2 +certifi==2024.7.4 # via # elastic-transport # httpcore diff --git a/sdk/python/requirements/py3.9-requirements.txt b/sdk/python/requirements/py3.9-requirements.txt index 9c4450cb45..096f54ab1f 100644 --- a/sdk/python/requirements/py3.9-requirements.txt +++ b/sdk/python/requirements/py3.9-requirements.txt @@ -11,7 +11,7 @@ attrs==23.2.0 # via # jsonschema # referencing -certifi==2024.2.2 +certifi==2024.7.4 # via # httpcore # httpx From 92d17def8cdff2bebfa622a4b3846d5bdc3e58d8 Mon Sep 17 00:00:00 2001 From: Shuchu Han Date: Sat, 13 Jul 2024 22:19:12 -0400 Subject: [PATCH 30/59] fix: Remove typo. (#4351) --- docs/reference/feature-servers/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/reference/feature-servers/README.md b/docs/reference/feature-servers/README.md index 124834f8a7..2ceaf5807f 100644 --- a/docs/reference/feature-servers/README.md +++ b/docs/reference/feature-servers/README.md @@ -8,7 +8,6 @@ Feast users can choose to retrieve features from a feature server, as opposed to {% content-ref url="go-feature-server.md" %} [go-feature-server.md](go-feature-server.md) -======= {% endcontent-ref %} {% content-ref url="offline-feature-server.md" %} From b9696efb128b9591ca5b2a41e7a9a26e196ebac4 Mon Sep 17 00:00:00 2001 From: Tornike Gurgenidze Date: Mon, 15 Jul 2024 02:47:01 +0400 Subject: [PATCH 31/59] chore: Rename FileOfflineStore to DaskOfflineStore (#4349) rename file offline store to dask Signed-off-by: tokoko --- docs/SUMMARY.md | 4 +-- .../offline-stores/{file.md => dask.md} | 17 ++++++----- docs/reference/offline-stores/overview.md | 6 ++-- .../docs/source/feast.infra.contrib.rst | 8 ------ .../source/feast.infra.feature_servers.rst | 2 -- .../source/feast.infra.offline_stores.rst | 12 ++++---- .../source/feast.infra.registry.contrib.rst | 1 - sdk/python/docs/source/feast.infra.rst | 24 ---------------- .../infra/offline_stores/{file.py => dask.py} | 28 +++++++++---------- sdk/python/feast/repo_config.py | 7 +++-- .../universal/data_sources/file.py | 6 ++-- .../offline_stores/test_offline_store.py | 8 +++--- .../test_dynamodb_online_store.py | 4 +-- 13 files changed, 46 insertions(+), 81 deletions(-) rename docs/reference/offline-stores/{file.md => dask.md} (87%) rename sdk/python/feast/infra/offline_stores/{file.py => dask.py} (97%) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 5a82a190fe..1173a693ef 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -76,7 +76,7 @@ * [Azure Synapse + Azure SQL (contrib)](reference/data-sources/mssql.md) * [Offline stores](reference/offline-stores/README.md) * [Overview](reference/offline-stores/overview.md) - * [File](reference/offline-stores/file.md) + * [Dask](reference/offline-stores/dask.md) * [Snowflake](reference/offline-stores/snowflake.md) * [BigQuery](reference/offline-stores/bigquery.md) * [Redshift](reference/offline-stores/redshift.md) @@ -119,7 +119,7 @@ * [Feature servers](reference/feature-servers/README.md) * [Python feature server](reference/feature-servers/python-feature-server.md) * [\[Alpha\] Go feature server](reference/feature-servers/go-feature-server.md) - * [Offline Feature Server](reference/feature-servers/offline-feature-server) + * [Offline Feature Server](reference/feature-servers/offline-feature-server.md) * [\[Beta\] Web UI](reference/alpha-web-ui.md) * [\[Beta\] On demand feature view](reference/beta-on-demand-feature-view.md) * [\[Alpha\] Vector Database](reference/alpha-vector-database.md) diff --git a/docs/reference/offline-stores/file.md b/docs/reference/offline-stores/dask.md similarity index 87% rename from docs/reference/offline-stores/file.md rename to docs/reference/offline-stores/dask.md index 4b76d9af90..d8698ba544 100644 --- a/docs/reference/offline-stores/file.md +++ b/docs/reference/offline-stores/dask.md @@ -1,9 +1,8 @@ -# File offline store +# Dask offline store ## Description -The file offline store provides support for reading [FileSources](../data-sources/file.md). -It uses Dask as the compute engine. +The Dask offline store provides support for reading [FileSources](../data-sources/file.md). {% hint style="warning" %} All data is downloaded and joined using Python and therefore may not scale to production workloads. @@ -17,18 +16,18 @@ project: my_feature_repo registry: data/registry.db provider: local offline_store: - type: file + type: dask ``` {% endcode %} -The full set of configuration options is available in [FileOfflineStoreConfig](https://rtd.feast.dev/en/latest/#feast.infra.offline_stores.file.FileOfflineStoreConfig). +The full set of configuration options is available in [DaskOfflineStoreConfig](https://rtd.feast.dev/en/latest/#feast.infra.offline_stores.dask.DaskOfflineStoreConfig). ## Functionality Matrix The set of functionality supported by offline stores is described in detail [here](overview.md#functionality). -Below is a matrix indicating which functionality is supported by the file offline store. +Below is a matrix indicating which functionality is supported by the dask offline store. -| | File | +| | Dask | | :-------------------------------- | :-- | | `get_historical_features` (point-in-time correct join) | yes | | `pull_latest_from_table_or_query` (retrieve latest feature values) | yes | @@ -36,9 +35,9 @@ Below is a matrix indicating which functionality is supported by the file offlin | `offline_write_batch` (persist dataframes to offline store) | yes | | `write_logged_features` (persist logged features to offline store) | yes | -Below is a matrix indicating which functionality is supported by `FileRetrievalJob`. +Below is a matrix indicating which functionality is supported by `DaskRetrievalJob`. -| | File | +| | Dask | | --------------------------------- | --- | | export to dataframe | yes | | export to arrow table | yes | diff --git a/docs/reference/offline-stores/overview.md b/docs/reference/offline-stores/overview.md index 4d7681e38c..182eac6586 100644 --- a/docs/reference/offline-stores/overview.md +++ b/docs/reference/offline-stores/overview.md @@ -25,13 +25,13 @@ The first three of these methods all return a `RetrievalJob` specific to an offl ## Functionality Matrix -There are currently four core offline store implementations: `FileOfflineStore`, `BigQueryOfflineStore`, `SnowflakeOfflineStore`, and `RedshiftOfflineStore`. +There are currently four core offline store implementations: `DaskOfflineStore`, `BigQueryOfflineStore`, `SnowflakeOfflineStore`, and `RedshiftOfflineStore`. There are several additional implementations contributed by the Feast community (`PostgreSQLOfflineStore`, `SparkOfflineStore`, and `TrinoOfflineStore`), which are not guaranteed to be stable or to match the functionality of the core implementations. Details for each specific offline store, such as how to configure it in a `feature_store.yaml`, can be found [here](README.md). Below is a matrix indicating which offline stores support which methods. -| | File | BigQuery | Snowflake | Redshift | Postgres | Spark | Trino | +| | Dask | BigQuery | Snowflake | Redshift | Postgres | Spark | Trino | | :-------------------------------- | :-- | :-- | :-- | :-- | :-- | :-- | :-- | | `get_historical_features` | yes | yes | yes | yes | yes | yes | yes | | `pull_latest_from_table_or_query` | yes | yes | yes | yes | yes | yes | yes | @@ -42,7 +42,7 @@ Below is a matrix indicating which offline stores support which methods. Below is a matrix indicating which `RetrievalJob`s support what functionality. -| | File | BigQuery | Snowflake | Redshift | Postgres | Spark | Trino | DuckDB | +| | Dask | BigQuery | Snowflake | Redshift | Postgres | Spark | Trino | DuckDB | | --------------------------------- | --- | --- | --- | --- | --- | --- | --- | --- | | export to dataframe | yes | yes | yes | yes | yes | yes | yes | yes | | export to arrow table | yes | yes | yes | yes | yes | yes | yes | yes | diff --git a/sdk/python/docs/source/feast.infra.contrib.rst b/sdk/python/docs/source/feast.infra.contrib.rst index 7b2fa3cc9c..1f46ff0abf 100644 --- a/sdk/python/docs/source/feast.infra.contrib.rst +++ b/sdk/python/docs/source/feast.infra.contrib.rst @@ -4,14 +4,6 @@ feast.infra.contrib package Submodules ---------- -feast.infra.contrib.azure\_provider module ------------------------------------------- - -.. automodule:: feast.infra.contrib.azure_provider - :members: - :undoc-members: - :show-inheritance: - feast.infra.contrib.grpc\_server module --------------------------------------- diff --git a/sdk/python/docs/source/feast.infra.feature_servers.rst b/sdk/python/docs/source/feast.infra.feature_servers.rst index 334b585905..ca5203504d 100644 --- a/sdk/python/docs/source/feast.infra.feature_servers.rst +++ b/sdk/python/docs/source/feast.infra.feature_servers.rst @@ -7,8 +7,6 @@ Subpackages .. toctree:: :maxdepth: 4 - feast.infra.feature_servers.aws_lambda - feast.infra.feature_servers.gcp_cloudrun feast.infra.feature_servers.local_process feast.infra.feature_servers.multicloud diff --git a/sdk/python/docs/source/feast.infra.offline_stores.rst b/sdk/python/docs/source/feast.infra.offline_stores.rst index 052a114cfb..c770e5c13b 100644 --- a/sdk/python/docs/source/feast.infra.offline_stores.rst +++ b/sdk/python/docs/source/feast.infra.offline_stores.rst @@ -28,18 +28,18 @@ feast.infra.offline\_stores.bigquery\_source module :undoc-members: :show-inheritance: -feast.infra.offline\_stores.duckdb module ------------------------------------------ +feast.infra.offline\_stores.dask module +--------------------------------------- -.. automodule:: feast.infra.offline_stores.duckdb +.. automodule:: feast.infra.offline_stores.dask :members: :undoc-members: :show-inheritance: -feast.infra.offline\_stores.file module ---------------------------------------- +feast.infra.offline\_stores.duckdb module +----------------------------------------- -.. automodule:: feast.infra.offline_stores.file +.. automodule:: feast.infra.offline_stores.duckdb :members: :undoc-members: :show-inheritance: diff --git a/sdk/python/docs/source/feast.infra.registry.contrib.rst b/sdk/python/docs/source/feast.infra.registry.contrib.rst index 44b89736ad..83417109b8 100644 --- a/sdk/python/docs/source/feast.infra.registry.contrib.rst +++ b/sdk/python/docs/source/feast.infra.registry.contrib.rst @@ -8,7 +8,6 @@ Subpackages :maxdepth: 4 feast.infra.registry.contrib.azure - feast.infra.registry.contrib.postgres Module contents --------------- diff --git a/sdk/python/docs/source/feast.infra.rst b/sdk/python/docs/source/feast.infra.rst index a1dfc86492..b0046a2719 100644 --- a/sdk/python/docs/source/feast.infra.rst +++ b/sdk/python/docs/source/feast.infra.rst @@ -19,22 +19,6 @@ Subpackages Submodules ---------- -feast.infra.aws module ----------------------- - -.. automodule:: feast.infra.aws - :members: - :undoc-members: - :show-inheritance: - -feast.infra.gcp module ----------------------- - -.. automodule:: feast.infra.gcp - :members: - :undoc-members: - :show-inheritance: - feast.infra.infra\_object module -------------------------------- @@ -51,14 +35,6 @@ feast.infra.key\_encoding\_utils module :undoc-members: :show-inheritance: -feast.infra.local module ------------------------- - -.. automodule:: feast.infra.local - :members: - :undoc-members: - :show-inheritance: - feast.infra.passthrough\_provider module ---------------------------------------- diff --git a/sdk/python/feast/infra/offline_stores/file.py b/sdk/python/feast/infra/offline_stores/dask.py similarity index 97% rename from sdk/python/feast/infra/offline_stores/file.py rename to sdk/python/feast/infra/offline_stores/dask.py index af2570ebc0..4a63baf646 100644 --- a/sdk/python/feast/infra/offline_stores/file.py +++ b/sdk/python/feast/infra/offline_stores/dask.py @@ -39,20 +39,20 @@ from feast.saved_dataset import SavedDatasetStorage from feast.utils import _get_requested_feature_views_to_features_dict -# FileRetrievalJob will cast string objects to string[pyarrow] from dask version 2023.7.1 +# DaskRetrievalJob will cast string objects to string[pyarrow] from dask version 2023.7.1 # This is not the desired behavior for our use case, so we set the convert-string option to False # See (https://github.com/dask/dask/issues/10881#issuecomment-1923327936) dask.config.set({"dataframe.convert-string": False}) -class FileOfflineStoreConfig(FeastConfigBaseModel): - """Offline store config for local (file-based) store""" +class DaskOfflineStoreConfig(FeastConfigBaseModel): + """Offline store config for dask store""" - type: Literal["file"] = "file" + type: Union[Literal["dask"], Literal["file"]] = "dask" """ Offline store type selector""" -class FileRetrievalJob(RetrievalJob): +class DaskRetrievalJob(RetrievalJob): def __init__( self, evaluation_function: Callable, @@ -122,7 +122,7 @@ def supports_remote_storage_export(self) -> bool: return False -class FileOfflineStore(OfflineStore): +class DaskOfflineStore(OfflineStore): @staticmethod def get_historical_features( config: RepoConfig, @@ -133,7 +133,7 @@ def get_historical_features( project: str, full_feature_names: bool = False, ) -> RetrievalJob: - assert isinstance(config.offline_store, FileOfflineStoreConfig) + assert isinstance(config.offline_store, DaskOfflineStoreConfig) for fv in feature_views: assert isinstance(fv.batch_source, FileSource) @@ -283,7 +283,7 @@ def evaluate_historical_retrieval(): return entity_df_with_features.persist() - job = FileRetrievalJob( + job = DaskRetrievalJob( evaluation_function=evaluate_historical_retrieval, full_feature_names=full_feature_names, on_demand_feature_views=OnDemandFeatureView.get_requested_odfvs( @@ -309,7 +309,7 @@ def pull_latest_from_table_or_query( start_date: datetime, end_date: datetime, ) -> RetrievalJob: - assert isinstance(config.offline_store, FileOfflineStoreConfig) + assert isinstance(config.offline_store, DaskOfflineStoreConfig) assert isinstance(data_source, FileSource) # Create lazy function that is only called from the RetrievalJob object @@ -372,7 +372,7 @@ def evaluate_offline_job(): return source_df[list(columns_to_extract)].persist() # When materializing a single feature view, we don't need full feature names. On demand transforms aren't materialized - return FileRetrievalJob( + return DaskRetrievalJob( evaluation_function=evaluate_offline_job, full_feature_names=False, ) @@ -387,10 +387,10 @@ def pull_all_from_table_or_query( start_date: datetime, end_date: datetime, ) -> RetrievalJob: - assert isinstance(config.offline_store, FileOfflineStoreConfig) + assert isinstance(config.offline_store, DaskOfflineStoreConfig) assert isinstance(data_source, FileSource) - return FileOfflineStore.pull_latest_from_table_or_query( + return DaskOfflineStore.pull_latest_from_table_or_query( config=config, data_source=data_source, join_key_columns=join_key_columns @@ -410,7 +410,7 @@ def write_logged_features( logging_config: LoggingConfig, registry: BaseRegistry, ): - assert isinstance(config.offline_store, FileOfflineStoreConfig) + assert isinstance(config.offline_store, DaskOfflineStoreConfig) destination = logging_config.destination assert isinstance(destination, FileLoggingDestination) @@ -441,7 +441,7 @@ def offline_write_batch( table: pyarrow.Table, progress: Optional[Callable[[int], Any]], ): - assert isinstance(config.offline_store, FileOfflineStoreConfig) + assert isinstance(config.offline_store, DaskOfflineStoreConfig) assert isinstance(feature_view.batch_source, FileSource) pa_schema, column_names = get_pyarrow_schema_from_batch_source( diff --git a/sdk/python/feast/repo_config.py b/sdk/python/feast/repo_config.py index 137023ef22..fc2792e323 100644 --- a/sdk/python/feast/repo_config.py +++ b/sdk/python/feast/repo_config.py @@ -68,7 +68,8 @@ } OFFLINE_STORE_CLASS_FOR_TYPE = { - "file": "feast.infra.offline_stores.file.FileOfflineStore", + "file": "feast.infra.offline_stores.dask.DaskOfflineStore", + "dask": "feast.infra.offline_stores.dask.DaskOfflineStore", "bigquery": "feast.infra.offline_stores.bigquery.BigQueryOfflineStore", "redshift": "feast.infra.offline_stores.redshift.RedshiftOfflineStore", "snowflake.offline": "feast.infra.offline_stores.snowflake.SnowflakeOfflineStore", @@ -205,7 +206,7 @@ def __init__(self, **data: Any): self.registry_config = data["registry"] self._offline_store = None - self.offline_config = data.get("offline_store", "file") + self.offline_config = data.get("offline_store", "dask") self._online_store = None self.online_config = data.get("online_store", "sqlite") @@ -348,7 +349,7 @@ def _validate_offline_store_config(cls, values: Any) -> Any: # Set the default type if "type" not in values["offline_store"]: - values["offline_store"]["type"] = "file" + values["offline_store"]["type"] = "dask" offline_store_type = values["offline_store"]["type"] 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 4a4a7360d8..5174e16046 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 @@ -20,8 +20,8 @@ from feast.data_format import DeltaFormat, ParquetFormat from feast.data_source import DataSource from feast.feature_logging import LoggingDestination +from feast.infra.offline_stores.dask import DaskOfflineStoreConfig from feast.infra.offline_stores.duckdb import DuckDBOfflineStoreConfig -from feast.infra.offline_stores.file import FileOfflineStoreConfig from feast.infra.offline_stores.file_source import ( FileLoggingDestination, SavedDatasetFileStorage, @@ -84,7 +84,7 @@ def get_prefixed_table_name(self, suffix: str) -> str: return f"{self.project_name}.{suffix}" def create_offline_store_config(self) -> FeastConfigBaseModel: - return FileOfflineStoreConfig() + return DaskOfflineStoreConfig() def create_logged_features_destination(self) -> LoggingDestination: d = tempfile.mkdtemp(prefix=self.project_name) @@ -334,7 +334,7 @@ def get_prefixed_table_name(self, suffix: str) -> str: return f"{suffix}" def create_offline_store_config(self) -> FeastConfigBaseModel: - return FileOfflineStoreConfig() + return DaskOfflineStoreConfig() def teardown(self): self.minio.stop() 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 3589c8a3fa..50f048928d 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 @@ -23,7 +23,7 @@ from feast.infra.offline_stores.contrib.trino_offline_store.trino import ( TrinoRetrievalJob, ) -from feast.infra.offline_stores.file import FileRetrievalJob +from feast.infra.offline_stores.dask import DaskRetrievalJob from feast.infra.offline_stores.offline_store import RetrievalJob, RetrievalMetadata from feast.infra.offline_stores.redshift import ( RedshiftOfflineStoreConfig, @@ -100,7 +100,7 @@ def metadata(self) -> Optional[RetrievalMetadata]: @pytest.fixture( params=[ MockRetrievalJob, - FileRetrievalJob, + DaskRetrievalJob, RedshiftRetrievalJob, SnowflakeRetrievalJob, AthenaRetrievalJob, @@ -112,8 +112,8 @@ def metadata(self) -> Optional[RetrievalMetadata]: ] ) def retrieval_job(request, environment): - if request.param is FileRetrievalJob: - return FileRetrievalJob(lambda: 1, full_feature_names=False) + if request.param is DaskRetrievalJob: + return DaskRetrievalJob(lambda: 1, full_feature_names=False) elif request.param is RedshiftRetrievalJob: offline_store_config = RedshiftOfflineStoreConfig( cluster_id="feast-int-bucket", 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 6045dbc6ce..6ff7b3c360 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 @@ -5,7 +5,7 @@ import pytest from moto import mock_dynamodb -from feast.infra.offline_stores.file import FileOfflineStoreConfig +from feast.infra.offline_stores.dask import DaskOfflineStoreConfig from feast.infra.online_stores.dynamodb import ( DynamoDBOnlineStore, DynamoDBOnlineStoreConfig, @@ -40,7 +40,7 @@ def repo_config(): provider=PROVIDER, online_store=DynamoDBOnlineStoreConfig(region=REGION), # online_store={"type": "dynamodb", "region": REGION}, - offline_store=FileOfflineStoreConfig(), + offline_store=DaskOfflineStoreConfig(), entity_key_serialization_version=2, ) From 40270e754660d0a8f57cc8a3bbfb1e1e346c3d86 Mon Sep 17 00:00:00 2001 From: Shuchu Han Date: Mon, 15 Jul 2024 03:47:12 -0400 Subject: [PATCH 32/59] fix: Avoid XSS attack from Jinjin2's Environment(). (#4355) Signed-off-by: Shuchu Han --- .../offline_stores/contrib/postgres_offline_store/postgres.py | 4 +++- sdk/python/feast/infra/offline_stores/offline_utils.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) 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..c3bbfd97bc 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 @@ -365,7 +365,9 @@ def build_point_in_time_query( full_feature_names: bool = False, ) -> str: """Build point-in-time query between each feature view table and the entity dataframe for PostgreSQL""" - template = Environment(loader=BaseLoader()).from_string(source=query_template) + template = Environment(autoescape=True, loader=BaseLoader()).from_string( + source=query_template + ) final_output_feature_names = list(entity_df_columns) final_output_feature_names.extend( diff --git a/sdk/python/feast/infra/offline_stores/offline_utils.py b/sdk/python/feast/infra/offline_stores/offline_utils.py index 2d4fa268e4..6036ba5472 100644 --- a/sdk/python/feast/infra/offline_stores/offline_utils.py +++ b/sdk/python/feast/infra/offline_stores/offline_utils.py @@ -186,7 +186,9 @@ def build_point_in_time_query( full_feature_names: bool = False, ) -> str: """Build point-in-time query between each feature view table and the entity dataframe for Bigquery and Redshift""" - template = Environment(loader=BaseLoader()).from_string(source=query_template) + template = Environment(autoescape=True, loader=BaseLoader()).from_string( + source=query_template + ) final_output_feature_names = list(entity_df_columns) final_output_feature_names.extend( From 38cae164000e116d08bb5b403d573efd03e34b6f Mon Sep 17 00:00:00 2001 From: Tornike Gurgenidze Date: Tue, 16 Jul 2024 09:05:34 +0400 Subject: [PATCH 33/59] chore: Bump google-cloud-datastore lower bound (#4348) bump google-cloud-datastore lower bound Signed-off-by: tokoko --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a543262821..b836200005 100644 --- a/setup.py +++ b/setup.py @@ -73,7 +73,7 @@ "googleapis-common-protos>=1.52.0,<2", "google-cloud-bigquery[pandas]>=2,<3.13.0", "google-cloud-bigquery-storage >= 2.0.0,<3", - "google-cloud-datastore>=2.1.0,<3", + "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", From cdeab486970ccb8c716499610f927a6e8eb14457 Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Tue, 16 Jul 2024 14:47:00 -0400 Subject: [PATCH 34/59] revert: Revert "fix: Avoid XSS attack from Jinjin2's Environment()." (#4357) Revert "fix: Avoid XSS attack from Jinjin2's Environment(). (#4355)" This reverts commit 40270e754660d0a8f57cc8a3bbfb1e1e346c3d86. --- .../offline_stores/contrib/postgres_offline_store/postgres.py | 4 +--- sdk/python/feast/infra/offline_stores/offline_utils.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) 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 c3bbfd97bc..c4740a960e 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 @@ -365,9 +365,7 @@ def build_point_in_time_query( full_feature_names: bool = False, ) -> str: """Build point-in-time query between each feature view table and the entity dataframe for PostgreSQL""" - template = Environment(autoescape=True, loader=BaseLoader()).from_string( - source=query_template - ) + template = Environment(loader=BaseLoader()).from_string(source=query_template) final_output_feature_names = list(entity_df_columns) final_output_feature_names.extend( diff --git a/sdk/python/feast/infra/offline_stores/offline_utils.py b/sdk/python/feast/infra/offline_stores/offline_utils.py index 6036ba5472..2d4fa268e4 100644 --- a/sdk/python/feast/infra/offline_stores/offline_utils.py +++ b/sdk/python/feast/infra/offline_stores/offline_utils.py @@ -186,9 +186,7 @@ def build_point_in_time_query( full_feature_names: bool = False, ) -> str: """Build point-in-time query between each feature view table and the entity dataframe for Bigquery and Redshift""" - template = Environment(autoescape=True, loader=BaseLoader()).from_string( - source=query_template - ) + template = Environment(loader=BaseLoader()).from_string(source=query_template) final_output_feature_names = list(entity_df_columns) final_output_feature_names.extend( From ce4f09b9d21f0b9315f1b8b79772901d2081813d Mon Sep 17 00:00:00 2001 From: Tornike Gurgenidze Date: Tue, 16 Jul 2024 23:05:40 +0400 Subject: [PATCH 35/59] chore: Remove distutils (#4356) * remove distutils Signed-off-by: tokoko * fix formatting Signed-off-by: tokoko --------- Signed-off-by: tokoko --- sdk/python/feast/repo_operations.py | 4 ++-- setup.py | 19 ++++++------------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/sdk/python/feast/repo_operations.py b/sdk/python/feast/repo_operations.py index 05a7d05e23..a3100ca9d7 100644 --- a/sdk/python/feast/repo_operations.py +++ b/sdk/python/feast/repo_operations.py @@ -384,8 +384,8 @@ def cli_check_repo(repo_path: Path, fs_yaml_file: Path): def init_repo(repo_name: str, template: str): import os - from distutils.dir_util import copy_tree from pathlib import Path + from shutil import copytree from colorama import Fore, Style @@ -412,7 +412,7 @@ def init_repo(repo_name: str, template: str): template_path = str(Path(Path(__file__).parent / "templates" / template).absolute()) if not os.path.exists(template_path): raise IOError(f"Could not find template {template}") - copy_tree(template_path, str(repo_path)) + copytree(template_path, str(repo_path), dirs_exist_ok=True) # Seed the repository bootstrap_path = repo_path / "bootstrap.py" diff --git a/setup.py b/setup.py index b836200005..4ac492b3bf 100644 --- a/setup.py +++ b/setup.py @@ -18,21 +18,14 @@ import shutil import subprocess import sys -from distutils.cmd import Command -from pathlib import Path - -from setuptools import find_packages -try: - from setuptools import 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 pathlib import Path -except ImportError: - from distutils.command.build_py import build_py - from distutils.core import setup +from setuptools import find_packages, setup, Command +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 NAME = "feast" DESCRIPTION = "Python SDK for Feast" From a8bc696010fa94fa0be44fba2570bee0eab83ba2 Mon Sep 17 00:00:00 2001 From: Shuchu Han Date: Tue, 16 Jul 2024 15:22:30 -0400 Subject: [PATCH 36/59] fix: Retire the datetime.utcnow(). (#4352) * fix: Retire the datetime.utcnow(). Signed-off-by: Shuchu Han * fix: Remove unnecessary unit test. Signed-off-by: Shuchu Han --------- Signed-off-by: Shuchu Han --- sdk/python/feast/driver_test_data.py | 43 ++++++++++++++++------ sdk/python/feast/type_map.py | 3 +- sdk/python/feast/utils.py | 4 +- sdk/python/tests/unit/test_datetime.py | 6 --- sdk/python/tests/utils/feature_records.py | 4 +- sdk/python/tests/utils/test_log_creator.py | 2 +- 6 files changed, 38 insertions(+), 24 deletions(-) delete mode 100644 sdk/python/tests/unit/test_datetime.py diff --git a/sdk/python/feast/driver_test_data.py b/sdk/python/feast/driver_test_data.py index 7959046e6e..defeb404a3 100644 --- a/sdk/python/feast/driver_test_data.py +++ b/sdk/python/feast/driver_test_data.py @@ -61,11 +61,11 @@ def create_orders_df( df["order_is_success"] = np.random.randint(0, 2, size=order_count).astype(np.int32) df[DEFAULT_ENTITY_DF_EVENT_TIMESTAMP_COL] = [ _convert_event_timestamp( - pd.Timestamp(dt, unit="ms", tz="UTC").round("ms"), + pd.Timestamp(dt, unit="ms").round("ms"), EventTimestampType(idx % 4), ) for idx, dt in enumerate( - pd.date_range(start=start_date, end=end_date, periods=order_count) + pd.date_range(start=start_date, end=end_date, periods=order_count, tz="UTC") ) ] df.sort_values( @@ -101,9 +101,13 @@ def create_driver_hourly_stats_df(drivers, start_date, end_date) -> pd.DataFrame df_hourly = pd.DataFrame( { "event_timestamp": [ - pd.Timestamp(dt, unit="ms", tz="UTC").round("ms") + pd.Timestamp(dt, unit="ms").round("ms") for dt in pd.date_range( - start=start_date, end=end_date, freq="1h", inclusive="left" + start=start_date, + end=end_date, + freq="1h", + inclusive="left", + tz="UTC", ) ] # include a fixed timestamp for get_historical_features in the quickstart @@ -162,9 +166,13 @@ def create_customer_daily_profile_df(customers, start_date, end_date) -> pd.Data df_daily = pd.DataFrame( { "event_timestamp": [ - pd.Timestamp(dt, unit="ms", tz="UTC").round("ms") + pd.Timestamp(dt, unit="ms").round("ms") for dt in pd.date_range( - start=start_date, end=end_date, freq="1D", inclusive="left" + start=start_date, + end=end_date, + freq="1D", + inclusive="left", + tz="UTC", ) ] } @@ -207,9 +215,13 @@ def create_location_stats_df(locations, start_date, end_date) -> pd.DataFrame: df_hourly = pd.DataFrame( { "event_timestamp": [ - pd.Timestamp(dt, unit="ms", tz="UTC").round("ms") + pd.Timestamp(dt, unit="ms").round("ms") for dt in pd.date_range( - start=start_date, end=end_date, freq="1h", inclusive="left" + start=start_date, + end=end_date, + freq="1h", + inclusive="left", + tz="UTC", ) ] } @@ -254,9 +266,16 @@ def create_global_daily_stats_df(start_date, end_date) -> pd.DataFrame: df_daily = pd.DataFrame( { "event_timestamp": [ - pd.Timestamp(dt, unit="ms", tz="UTC").round("ms") + pd.Timestamp( + dt, + unit="ms", + ).round("ms") for dt in pd.date_range( - start=start_date, end=end_date, freq="1D", inclusive="left" + start=start_date, + end=end_date, + freq="1D", + inclusive="left", + tz="UTC", ) ] } @@ -286,11 +305,11 @@ def create_field_mapping_df(start_date, end_date) -> pd.DataFrame: df["column_name"] = np.random.randint(1, 100, size=size).astype(np.int32) df[DEFAULT_ENTITY_DF_EVENT_TIMESTAMP_COL] = [ _convert_event_timestamp( - pd.Timestamp(dt, unit="ms", tz="UTC").round("ms"), + pd.Timestamp(dt, unit="ms").round("ms"), EventTimestampType(idx % 4), ) for idx, dt in enumerate( - pd.date_range(start=start_date, end=end_date, periods=size) + pd.date_range(start=start_date, end=end_date, periods=size, tz="UTC") ) ] df["created"] = pd.to_datetime(pd.Timestamp.now(tz=None).round("ms")) diff --git a/sdk/python/feast/type_map.py b/sdk/python/feast/type_map.py index 6ba61fc8c5..703c1dc7c5 100644 --- a/sdk/python/feast/type_map.py +++ b/sdk/python/feast/type_map.py @@ -162,7 +162,8 @@ def python_type_to_feast_value_type( "timestamp": ValueType.UNIX_TIMESTAMP, "datetime": ValueType.UNIX_TIMESTAMP, "datetime64[ns]": ValueType.UNIX_TIMESTAMP, - "datetime64[ns, tz]": ValueType.UNIX_TIMESTAMP, + "datetime64[ns, tz]": ValueType.UNIX_TIMESTAMP, # special dtype of pandas + "datetime64[ns, utc]": ValueType.UNIX_TIMESTAMP, "category": ValueType.STRING, } diff --git a/sdk/python/feast/utils.py b/sdk/python/feast/utils.py index 1a1d757fc1..0467393aa2 100644 --- a/sdk/python/feast/utils.py +++ b/sdk/python/feast/utils.py @@ -5,7 +5,7 @@ import typing import warnings from collections import Counter, defaultdict -from datetime import datetime +from datetime import datetime, timezone from pathlib import Path from typing import ( Any, @@ -1055,4 +1055,4 @@ def tags_str_to_dict(tags: str = "") -> dict[str, str]: def _utc_now() -> datetime: - return datetime.utcnow() + return datetime.now(tz=timezone.utc) diff --git a/sdk/python/tests/unit/test_datetime.py b/sdk/python/tests/unit/test_datetime.py deleted file mode 100644 index aaab507ed0..0000000000 --- a/sdk/python/tests/unit/test_datetime.py +++ /dev/null @@ -1,6 +0,0 @@ -# -*- coding: utf-8 -*- - - -""" -Test the retirement of datetime.utcnow() function. -""" diff --git a/sdk/python/tests/utils/feature_records.py b/sdk/python/tests/utils/feature_records.py index 2c26f3c000..bd3567c9ee 100644 --- a/sdk/python/tests/utils/feature_records.py +++ b/sdk/python/tests/utils/feature_records.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import Any, Dict, List, Optional import numpy as np @@ -520,7 +520,7 @@ def get_last_feature_row(df: pd.DataFrame, driver_id, max_date: datetime): """Manually extract last feature value from a dataframe for a given driver_id with up to `max_date` date""" filtered = df[ (df["driver_id"] == driver_id) - & (df["event_timestamp"] < max_date.replace(tzinfo=utc)) + & (df["event_timestamp"] < max_date.replace(tzinfo=timezone.utc)) ] max_ts = filtered.loc[filtered["event_timestamp"].idxmax()]["event_timestamp"] filtered_by_ts = filtered[filtered["event_timestamp"] == max_ts] diff --git a/sdk/python/tests/utils/test_log_creator.py b/sdk/python/tests/utils/test_log_creator.py index f072f4c886..987c8d77ef 100644 --- a/sdk/python/tests/utils/test_log_creator.py +++ b/sdk/python/tests/utils/test_log_creator.py @@ -117,7 +117,7 @@ def prepare_logs( f"{destination_field}__status" ].mask( logs_df[f"{destination_field}__timestamp"] - < (_utc_now() - view.ttl), + < (_utc_now() - view.ttl).replace(tzinfo=None), FieldStatus.OUTSIDE_MAX_AGE, ) From 5a1636431d44a7e109c64688fe3176741feccc1b Mon Sep 17 00:00:00 2001 From: camenares <32527085+camenares@users.noreply.github.com> Date: Tue, 16 Jul 2024 17:13:20 -0400 Subject: [PATCH 37/59] chore: Change arrow scalar ids usage (#4347) * Update google-cloud-storage Signed-off-by: Christopher Camenares * test tighter library restriction Signed-off-by: Christopher Camenares * fix lint Signed-off-by: Christopher Camenares * bump <4 again Signed-off-by: Christopher Camenares --------- Signed-off-by: Christopher Camenares --- .../feast/infra/offline_stores/bigquery.py | 15 +- .../requirements/py3.10-ci-requirements.txt | 152 +++++++++++++++--- .../requirements/py3.10-requirements.txt | 48 ++++-- .../requirements/py3.11-ci-requirements.txt | 152 +++++++++++++++--- .../requirements/py3.11-requirements.txt | 48 ++++-- .../requirements/py3.9-ci-requirements.txt | 152 +++++++++++++++--- .../requirements/py3.9-requirements.txt | 48 ++++-- setup.py | 2 +- 8 files changed, 532 insertions(+), 85 deletions(-) diff --git a/sdk/python/feast/infra/offline_stores/bigquery.py b/sdk/python/feast/infra/offline_stores/bigquery.py index 3e4a0f1b99..ef12eba442 100644 --- a/sdk/python/feast/infra/offline_stores/bigquery.py +++ b/sdk/python/feast/infra/offline_stores/bigquery.py @@ -59,7 +59,6 @@ from google.auth.exceptions import DefaultCredentialsError from google.cloud import bigquery from google.cloud.bigquery import Client, SchemaField, Table - from google.cloud.bigquery._pandas_helpers import ARROW_SCALAR_IDS_TO_BQ from google.cloud.storage import Client as StorageClient except ImportError as e: @@ -67,6 +66,16 @@ raise FeastExtrasDependencyImportError("gcp", str(e)) +try: + from google.cloud.bigquery._pyarrow_helpers import _ARROW_SCALAR_IDS_TO_BQ +except ImportError: + try: + from google.cloud.bigquery._pandas_helpers import ( # type: ignore + ARROW_SCALAR_IDS_TO_BQ as _ARROW_SCALAR_IDS_TO_BQ, + ) + except ImportError as e: + raise FeastExtrasDependencyImportError("gcp", str(e)) + def get_http_client_info(): return http_client_info.ClientInfo(user_agent=get_user_agent()) @@ -794,10 +803,10 @@ def arrow_schema_to_bq_schema(arrow_schema: pyarrow.Schema) -> List[SchemaField] for field in arrow_schema: if pyarrow.types.is_list(field.type): detected_mode = "REPEATED" - detected_type = ARROW_SCALAR_IDS_TO_BQ[field.type.value_type.id] + detected_type = _ARROW_SCALAR_IDS_TO_BQ[field.type.value_type.id] else: detected_mode = "NULLABLE" - detected_type = ARROW_SCALAR_IDS_TO_BQ[field.type.id] + detected_type = _ARROW_SCALAR_IDS_TO_BQ[field.type.id] bq_schema.append( SchemaField(name=field.name, field_type=detected_type, mode=detected_mode) diff --git a/sdk/python/requirements/py3.10-ci-requirements.txt b/sdk/python/requirements/py3.10-ci-requirements.txt index 33709a1ef0..a7f300a0ed 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,6 +77,7 @@ botocore==1.34.131 # s3transfer build==1.2.1 # via + # feast (setup.py) # pip-tools # singlestoredb cachecontrol==0.14.0 @@ -76,6 +85,7 @@ cachecontrol==0.14.0 cachetools==5.3.3 # via google-auth cassandra-driver==3.29.1 + # via feast (setup.py) certifi==2024.7.4 # via # elastic-transport @@ -98,6 +108,7 @@ charset-normalizer==3.3.2 # snowflake-connector-python click==8.1.7 # via + # feast (setup.py) # dask # geomet # great-expectations @@ -107,7 +118,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 +129,7 @@ coverage[toml]==7.5.4 # via pytest-cov cryptography==42.0.8 # via + # feast (setup.py) # azure-identity # azure-storage-blob # great-expectations @@ -127,7 +141,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 @@ -139,7 +155,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 @@ -153,6 +171,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 @@ -167,6 +186,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 @@ -176,6 +196,7 @@ filelock==3.15.4 # snowflake-connector-python # virtualenv firebase-admin==5.4.0 + # via feast (setup.py) fqdn==1.5.1 # via jsonschema frozenlist==1.4.1 @@ -183,13 +204,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) # firebase-admin # google-api-python-client # google-cloud-bigquery @@ -213,9 +237,12 @@ google-auth==2.30.0 # kubernetes google-auth-httplib2==0.2.0 # via google-api-python-client -google-cloud-bigquery[pandas]==3.12.0 +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 @@ -224,10 +251,13 @@ google-cloud-core==2.4.1 # google-cloud-firestore # google-cloud-storage google-cloud-datastore==2.19.0 + # via feast (setup.py) google-cloud-firestore==2.16.0 # via firebase-admin google-cloud-storage==2.17.0 - # via firebase-admin + # via + # feast (setup.py) + # firebase-admin google-crc32c==1.5.0 # via # google-cloud-storage @@ -238,16 +268,17 @@ 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 -greenlet==3.0.3 - # via sqlalchemy + # via feast (setup.py) 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 @@ -258,19 +289,27 @@ 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 httplib2==0.22.0 @@ -281,11 +320,15 @@ 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 @@ -320,6 +363,7 @@ jedi==0.19.1 # via ipython jinja2==3.1.4 # via + # feast (setup.py) # altair # fastapi # great-expectations @@ -343,6 +387,7 @@ jsonpointer==3.0.0 # jsonschema jsonschema[format-nongpl]==4.22.0 # via + # feast (setup.py) # altair # great-expectations # jupyter-events @@ -388,6 +433,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 @@ -408,13 +454,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 @@ -428,10 +478,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 @@ -454,6 +507,7 @@ notebook-shim==0.2.4 # notebook numpy==1.26.4 # via + # feast (setup.py) # altair # dask # db-dtypes @@ -488,6 +542,7 @@ packaging==24.1 # sphinx pandas==2.2.2 # via + # feast (setup.py) # altair # dask # dask-expr @@ -513,6 +568,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 @@ -525,6 +581,7 @@ 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 prompt-toolkit==3.0.47 @@ -539,6 +596,7 @@ proto-plus==1.24.0 # google-cloud-firestore protobuf==4.25.3 # via + # feast (setup.py) # google-api-core # google-cloud-bigquery # google-cloud-bigquery-storage @@ -556,8 +614,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 @@ -569,12 +630,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 @@ -592,16 +655,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 @@ -612,8 +678,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 @@ -625,8 +694,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 @@ -636,13 +707,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 @@ -671,6 +750,7 @@ pytz==2024.1 # trino pyyaml==6.0.1 # via + # feast (setup.py) # dask # ibis-substrait # jupyter-events @@ -684,15 +764,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 # cachecontrol # docker @@ -727,6 +811,7 @@ rich==13.7.1 # ibis-framework # typer rockset==2.1.2 + # via feast (setup.py) rpds-py==0.18.1 # via # jsonschema @@ -736,6 +821,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 @@ -752,6 +838,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 @@ -772,11 +859,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 @@ -790,9 +879,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 @@ -802,17 +893,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 @@ -840,7 +935,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 @@ -857,25 +954,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 @@ -914,6 +1025,7 @@ uritemplate==4.1.1 # via google-api-python-client urllib3==1.26.19 # via + # feast (setup.py) # botocore # docker # elastic-transport @@ -925,11 +1037,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..39a278818f 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,14 +43,15 @@ 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 # via dask -greenlet==3.0.3 - # via sqlalchemy gunicorn==22.0.0 + # via feast (setup.py) h11==0.14.0 # via # httpcore @@ -65,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 @@ -78,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 @@ -96,20 +108,29 @@ packaging==24.0 # gunicorn pandas==2.2.2 # via + # feast (setup.py) # dask # dask-expr partd==1.4.2 # via dask protobuf==4.25.3 - # via mypy-protobuf + # via + # feast (setup.py) + # mypy-protobuf 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 +141,7 @@ pytz==2024.1 # via pandas pyyaml==6.0.1 # via + # feast (setup.py) # dask # uvicorn referencing==0.35.1 @@ -127,6 +149,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 +165,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 +181,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 +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.11-ci-requirements.txt b/sdk/python/requirements/py3.11-ci-requirements.txt index 09e9e8eeea..4c1be0a5b4 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,6 +73,7 @@ botocore==1.34.131 # s3transfer build==1.2.1 # via + # feast (setup.py) # pip-tools # singlestoredb cachecontrol==0.14.0 @@ -72,6 +81,7 @@ cachecontrol==0.14.0 cachetools==5.3.3 # via google-auth cassandra-driver==3.29.1 + # via feast (setup.py) certifi==2024.7.4 # via # elastic-transport @@ -94,6 +104,7 @@ charset-normalizer==3.3.2 # snowflake-connector-python click==8.1.7 # via + # feast (setup.py) # dask # geomet # great-expectations @@ -103,7 +114,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 +125,7 @@ coverage[toml]==7.5.4 # via pytest-cov cryptography==42.0.8 # via + # feast (setup.py) # azure-identity # azure-storage-blob # great-expectations @@ -123,7 +137,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 @@ -135,7 +151,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 @@ -149,6 +167,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 @@ -158,6 +177,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 @@ -167,6 +187,7 @@ filelock==3.15.4 # snowflake-connector-python # virtualenv firebase-admin==5.4.0 + # via feast (setup.py) fqdn==1.5.1 # via jsonschema frozenlist==1.4.1 @@ -174,13 +195,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) # firebase-admin # google-api-python-client # google-cloud-bigquery @@ -204,9 +228,12 @@ google-auth==2.30.0 # kubernetes google-auth-httplib2==0.2.0 # via google-api-python-client -google-cloud-bigquery[pandas]==3.12.0 +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 @@ -215,10 +242,13 @@ google-cloud-core==2.4.1 # google-cloud-firestore # google-cloud-storage google-cloud-datastore==2.19.0 + # via feast (setup.py) google-cloud-firestore==2.16.0 # via firebase-admin google-cloud-storage==2.17.0 - # via firebase-admin + # via + # feast (setup.py) + # firebase-admin google-crc32c==1.5.0 # via # google-cloud-storage @@ -229,16 +259,17 @@ 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 -greenlet==3.0.3 - # via sqlalchemy + # via feast (setup.py) 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 @@ -249,19 +280,27 @@ 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 httplib2==0.22.0 @@ -272,11 +311,15 @@ 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 @@ -311,6 +354,7 @@ jedi==0.19.1 # via ipython jinja2==3.1.4 # via + # feast (setup.py) # altair # fastapi # great-expectations @@ -334,6 +378,7 @@ jsonpointer==3.0.0 # jsonschema jsonschema[format-nongpl]==4.22.0 # via + # feast (setup.py) # altair # great-expectations # jupyter-events @@ -379,6 +424,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 @@ -399,13 +445,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 @@ -419,10 +469,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 @@ -445,6 +498,7 @@ notebook-shim==0.2.4 # notebook numpy==1.26.4 # via + # feast (setup.py) # altair # dask # db-dtypes @@ -479,6 +533,7 @@ packaging==24.1 # sphinx pandas==2.2.2 # via + # feast (setup.py) # altair # dask # dask-expr @@ -504,6 +559,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 @@ -516,6 +572,7 @@ 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 prompt-toolkit==3.0.47 @@ -530,6 +587,7 @@ proto-plus==1.24.0 # google-cloud-firestore protobuf==4.25.3 # via + # feast (setup.py) # google-api-core # google-cloud-bigquery # google-cloud-bigquery-storage @@ -547,8 +605,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 @@ -560,12 +621,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 @@ -583,16 +646,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 @@ -603,8 +669,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 @@ -616,8 +685,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 @@ -627,13 +698,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 @@ -662,6 +741,7 @@ pytz==2024.1 # trino pyyaml==6.0.1 # via + # feast (setup.py) # dask # ibis-substrait # jupyter-events @@ -675,15 +755,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 # cachecontrol # docker @@ -718,6 +802,7 @@ rich==13.7.1 # ibis-framework # typer rockset==2.1.2 + # via feast (setup.py) rpds-py==0.18.1 # via # jsonschema @@ -727,6 +812,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 @@ -743,6 +829,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 @@ -763,11 +850,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 @@ -781,9 +870,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 @@ -793,17 +884,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 @@ -821,7 +916,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 @@ -838,25 +935,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 @@ -892,6 +1003,7 @@ uritemplate==4.1.1 # via google-api-python-client urllib3==1.26.19 # via + # feast (setup.py) # botocore # docker # elastic-transport @@ -903,11 +1015,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..44e658113a 100644 --- a/sdk/python/requirements/py3.11-requirements.txt +++ b/sdk/python/requirements/py3.11-requirements.txt @@ -20,30 +20,36 @@ 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 # via dask -greenlet==3.0.3 - # via sqlalchemy gunicorn==22.0.0 + # via feast (setup.py) h11==0.14.0 # via # httpcore @@ -63,8 +69,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 +85,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 +106,29 @@ packaging==24.0 # gunicorn pandas==2.2.2 # via + # feast (setup.py) # dask # dask-expr partd==1.4.2 # via dask protobuf==4.25.3 - # via mypy-protobuf + # via + # feast (setup.py) + # mypy-protobuf 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 +139,7 @@ pytz==2024.1 # via pandas pyyaml==6.0.1 # via + # feast (setup.py) # dask # uvicorn referencing==0.35.1 @@ -125,6 +147,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 +163,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 +201,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 6f5d0220bc..25cdea7a68 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,6 +77,7 @@ botocore==1.34.131 # s3transfer build==1.2.1 # via + # feast (setup.py) # pip-tools # singlestoredb cachecontrol==0.14.0 @@ -76,6 +85,7 @@ cachecontrol==0.14.0 cachetools==5.3.3 # via google-auth cassandra-driver==3.29.1 + # via feast (setup.py) certifi==2024.7.4 # via # elastic-transport @@ -98,6 +108,7 @@ charset-normalizer==3.3.2 # snowflake-connector-python click==8.1.7 # via + # feast (setup.py) # dask # geomet # great-expectations @@ -107,7 +118,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 +129,7 @@ coverage[toml]==7.5.4 # via pytest-cov cryptography==42.0.8 # via + # feast (setup.py) # azure-identity # azure-storage-blob # great-expectations @@ -127,7 +141,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 @@ -139,7 +155,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 @@ -153,6 +171,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 @@ -167,6 +186,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 @@ -176,6 +196,7 @@ filelock==3.15.4 # snowflake-connector-python # virtualenv firebase-admin==5.4.0 + # via feast (setup.py) fqdn==1.5.1 # via jsonschema frozenlist==1.4.1 @@ -183,13 +204,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) # firebase-admin # google-api-python-client # google-cloud-bigquery @@ -213,9 +237,12 @@ google-auth==2.30.0 # kubernetes google-auth-httplib2==0.2.0 # via google-api-python-client -google-cloud-bigquery[pandas]==3.12.0 +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 @@ -224,10 +251,13 @@ google-cloud-core==2.4.1 # google-cloud-firestore # google-cloud-storage google-cloud-datastore==2.19.0 + # via feast (setup.py) google-cloud-firestore==2.16.0 # via firebase-admin google-cloud-storage==2.17.0 - # via firebase-admin + # via + # feast (setup.py) + # firebase-admin google-crc32c==1.5.0 # via # google-cloud-storage @@ -238,16 +268,17 @@ 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 -greenlet==3.0.3 - # via sqlalchemy + # via feast (setup.py) 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 @@ -258,19 +289,27 @@ 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 httplib2==0.22.0 @@ -281,11 +320,15 @@ 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 @@ -329,6 +372,7 @@ jedi==0.19.1 # via ipython jinja2==3.1.4 # via + # feast (setup.py) # altair # fastapi # great-expectations @@ -352,6 +396,7 @@ jsonpointer==3.0.0 # jsonschema jsonschema[format-nongpl]==4.22.0 # via + # feast (setup.py) # altair # great-expectations # jupyter-events @@ -397,6 +442,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 @@ -417,13 +463,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 @@ -437,10 +487,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 @@ -463,6 +516,7 @@ notebook-shim==0.2.4 # notebook numpy==1.26.4 # via + # feast (setup.py) # altair # dask # db-dtypes @@ -497,6 +551,7 @@ packaging==24.1 # sphinx pandas==2.2.2 # via + # feast (setup.py) # altair # dask # dask-expr @@ -522,6 +577,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 @@ -534,6 +590,7 @@ 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 prompt-toolkit==3.0.47 @@ -548,6 +605,7 @@ proto-plus==1.24.0 # google-cloud-firestore protobuf==4.25.3 # via + # feast (setup.py) # google-api-core # google-cloud-bigquery # google-cloud-bigquery-storage @@ -565,8 +623,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 @@ -578,12 +639,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 @@ -601,16 +664,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 @@ -621,8 +687,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 @@ -634,8 +703,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 @@ -645,13 +716,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 @@ -680,6 +759,7 @@ pytz==2024.1 # trino pyyaml==6.0.1 # via + # feast (setup.py) # dask # ibis-substrait # jupyter-events @@ -693,15 +773,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 # cachecontrol # docker @@ -736,6 +820,7 @@ rich==13.7.1 # ibis-framework # typer rockset==2.1.2 + # via feast (setup.py) rpds-py==0.18.1 # via # jsonschema @@ -747,6 +832,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 @@ -763,6 +849,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 @@ -783,11 +870,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 @@ -801,9 +890,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 @@ -813,17 +904,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 @@ -851,7 +946,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 @@ -868,25 +965,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 @@ -927,6 +1038,7 @@ uritemplate==4.1.1 # via google-api-python-client urllib3==1.26.19 # via + # feast (setup.py) # botocore # docker # elastic-transport @@ -939,11 +1051,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..ea553bcae2 100644 --- a/sdk/python/requirements/py3.9-requirements.txt +++ b/sdk/python/requirements/py3.9-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,14 +43,15 @@ 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 # via dask -greenlet==3.0.3 - # via sqlalchemy gunicorn==22.0.0 + # via feast (setup.py) h11==0.14.0 # via # httpcore @@ -67,8 +73,11 @@ importlib-metadata==7.1.0 # 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 +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 @@ -98,20 +110,29 @@ packaging==24.0 # gunicorn pandas==2.2.2 # via + # feast (setup.py) # dask # dask-expr partd==1.4.2 # via dask protobuf==4.25.3 - # via mypy-protobuf + # via + # feast (setup.py) + # mypy-protobuf 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 +143,7 @@ pytz==2024.1 # via pandas pyyaml==6.0.1 # via + # feast (setup.py) # dask # uvicorn referencing==0.35.1 @@ -129,6 +151,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 +167,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 +183,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 +210,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/setup.py b/setup.py index 4ac492b3bf..2043bf1b3f 100644 --- a/setup.py +++ b/setup.py @@ -64,7 +64,7 @@ GCP_REQUIRED = [ "google-api-core>=1.23.0,<3", "googleapis-common-protos>=1.52.0,<2", - "google-cloud-bigquery[pandas]>=2,<3.13.0", + "google-cloud-bigquery[pandas]>=2,<4", "google-cloud-bigquery-storage >= 2.0.0,<3", "google-cloud-datastore>=2.16.0,<3", "google-cloud-storage>=1.34.0,<3", From 7914cbdaffeade727cf3cee538cf128cbfd86e06 Mon Sep 17 00:00:00 2001 From: Tornike Gurgenidze Date: Fri, 19 Jul 2024 01:46:43 +0400 Subject: [PATCH 38/59] feat: Port mssql contrib offline store to ibis (#4360) port mssql offline store to ibis Signed-off-by: tokoko --- Makefile | 5 +- .../contrib/mssql_offline_store/mssql.py | 742 ++++-------------- .../mssql_offline_store/mssqlserver_source.py | 42 +- .../mssql_offline_store/tests/data_source.py | 38 +- sdk/python/feast/infra/offline_stores/ibis.py | 31 +- sdk/python/feast/type_map.py | 2 + .../offline_stores/test_offline_store.py | 11 - setup.py | 3 + 8 files changed, 246 insertions(+), 628 deletions(-) diff --git a/Makefile b/Makefile index d2fbb34e1f..5e3bd0d913 100644 --- a/Makefile +++ b/Makefile @@ -160,7 +160,10 @@ test-python-universal-mssql: -k "not gcs_registry and \ not s3_registry and \ not test_lambda_materialization and \ - not test_snowflake" \ + not test_snowflake and \ + not test_historical_features_persisting and \ + not validation and \ + not test_feature_service_logging" \ sdk/python/tests diff --git a/sdk/python/feast/infra/offline_stores/contrib/mssql_offline_store/mssql.py b/sdk/python/feast/infra/offline_stores/contrib/mssql_offline_store/mssql.py index 5fe5857146..875d584568 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/mssql_offline_store/mssql.py +++ b/sdk/python/feast/infra/offline_stores/contrib/mssql_offline_store/mssql.py @@ -1,44 +1,109 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. -import warnings -from datetime import datetime +from datetime import datetime, timedelta from pathlib import Path -from typing import Any, Callable, Dict, List, Literal, Optional, Set, Tuple, Union +from typing import Any, Callable, Iterable, List, Literal, Optional, Tuple, Union +from urllib import parse -import numpy as np -import pandas +import ibis +import pandas as pd import pyarrow -import pyarrow as pa -import sqlalchemy -from pydantic.types import StrictStr -from sqlalchemy import create_engine -from sqlalchemy.engine import Engine -from sqlalchemy.orm import sessionmaker +from ibis.expr.types import Table +from pydantic import StrictStr -from feast import FileSource, errors from feast.data_source import DataSource -from feast.errors import InvalidEntityType from feast.feature_logging import LoggingConfig, LoggingSource from feast.feature_view import FeatureView -from feast.infra.offline_stores import offline_utils -from feast.infra.offline_stores.file_source import SavedDatasetFileStorage -from feast.infra.offline_stores.offline_store import OfflineStore, RetrievalMetadata -from feast.infra.offline_stores.offline_utils import ( - DEFAULT_ENTITY_DF_EVENT_TIMESTAMP_COL, - build_point_in_time_query, - get_feature_view_query_context, +from feast.infra.offline_stores.contrib.mssql_offline_store.mssqlserver_source import ( + MsSqlServerSource, ) -from feast.infra.provider import RetrievalJob +from feast.infra.offline_stores.ibis import ( + get_historical_features_ibis, + offline_write_batch_ibis, + pull_all_from_table_or_query_ibis, + pull_latest_from_table_or_query_ibis, + write_logged_features_ibis, +) +from feast.infra.offline_stores.offline_store import OfflineStore, RetrievalJob 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.saved_dataset import SavedDatasetStorage -from feast.type_map import pa_to_mssql_type -# Make sure warning doesn't raise more than once. -warnings.simplefilter("once", RuntimeWarning) -EntitySchema = Dict[str, np.dtype] +def get_ibis_connection(config: RepoConfig): + connection_params = parse.urlparse(config.offline_store.connection_string) + additional_kwargs = dict(parse.parse_qsl(connection_params.query)) + return ibis.mssql.connect( + user=connection_params.username, + password=connection_params.password, + host=connection_params.hostname, + port=connection_params.port, + database=connection_params.path.strip("/"), + **additional_kwargs, + ) + + +def get_table_column_names_and_types( + config: RepoConfig, data_source: MsSqlServerSource +) -> Iterable[Tuple[str, str]]: + con = get_ibis_connection(config) + + # assert isinstance(config.offline_store, MsSqlServerOfflineStoreConfig) + # conn = create_engine(config.offline_store.connection_string) + # self._mssqlserver_options.connection_str = ( + # config.offline_store.connection_string + # ) + name_type_pairs = [] + if len(data_source.table_ref.split(".")) == 2: + database, table_name = data_source.table_ref.split(".") + columns_query = f""" + SELECT COLUMN_NAME, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME = '{table_name}' and table_schema = '{database}' + """ + else: + columns_query = f""" + SELECT COLUMN_NAME, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME = '{data_source.table_ref}' + """ + + table_schema = con.sql(columns_query).execute() + + name_type_pairs.extend( + list( + zip( + table_schema["COLUMN_NAME"].to_list(), + table_schema["DATA_TYPE"].to_list(), + ) + ) + ) + return name_type_pairs + + +def _build_data_source_reader(config: RepoConfig): + con = get_ibis_connection(config) + + def _read_data_source(data_source: DataSource) -> Table: + assert isinstance(data_source, MsSqlServerSource) + return con.table(data_source.table_ref) + + return _read_data_source + + +def _build_data_source_writer(config: RepoConfig): + con = get_ibis_connection(config) + + def _write_data_source( + table: Table, + data_source: DataSource, + mode: str = "append", + allow_overwrite: bool = False, + ): + assert isinstance(data_source, MsSqlServerSource) + con.insert(table_name=data_source.table_ref, obj=table.to_pandas()) + + return _write_data_source + + +def mssql_event_expire_timestamp_fn(timestamp_field: str, ttl: timedelta) -> str: + ttl_seconds = int(ttl.total_seconds()) + return f"DATEADD(ss, {ttl_seconds}, {timestamp_field})" class MsSqlServerOfflineStoreConfig(FeastConfigBaseModel): @@ -52,18 +117,7 @@ class MsSqlServerOfflineStoreConfig(FeastConfigBaseModel): format: SQLAlchemy connection string, e.g. mssql+pyodbc://sa:yourStrong(!)Password@localhost:1433/feast_test?driver=ODBC+Driver+17+for+SQL+Server""" -def make_engine(config: MsSqlServerOfflineStoreConfig) -> Engine: - return create_engine(config.connection_string) - - class MsSqlServerOfflineStore(OfflineStore): - """ - Microsoft SQL Server based offline store, supporting Azure Synapse or Azure SQL. - - Note: to use this, you'll need to have Microsoft ODBC 17 installed. - See https://docs.microsoft.com/en-us/sql/connect/odbc/linux-mac/install-microsoft-odbc-driver-sql-server-macos?view=sql-server-ver15#17 - """ - @staticmethod def pull_latest_from_table_or_query( config: RepoConfig, @@ -75,43 +129,45 @@ def pull_latest_from_table_or_query( start_date: datetime, end_date: datetime, ) -> RetrievalJob: - warnings.warn( - "The Azure Synapse + Azure SQL offline store is an experimental feature in alpha development. " - "Some functionality may still be unstable so functionality can change in the future.", - RuntimeWarning, + return pull_latest_from_table_or_query_ibis( + config=config, + data_source=data_source, + join_key_columns=join_key_columns, + feature_name_columns=feature_name_columns, + timestamp_field=timestamp_field, + created_timestamp_column=created_timestamp_column, + start_date=start_date, + end_date=end_date, + data_source_reader=_build_data_source_reader(config), + data_source_writer=_build_data_source_writer(config), ) - assert type(data_source).__name__ == "MsSqlServerSource" - from_expression = data_source.get_table_query_string().replace("`", "") - - partition_by_join_key_string = ", ".join(join_key_columns) - if partition_by_join_key_string != "": - partition_by_join_key_string = ( - "PARTITION BY " + partition_by_join_key_string - ) - timestamps = [timestamp_field] - if created_timestamp_column: - timestamps.append(created_timestamp_column) - timestamp_desc_string = " DESC, ".join(timestamps) + " DESC" - field_string = ", ".join(join_key_columns + feature_name_columns + timestamps) - - query = f""" - SELECT {field_string} - FROM ( - SELECT {field_string}, - ROW_NUMBER() OVER({partition_by_join_key_string} ORDER BY {timestamp_desc_string}) AS _feast_row - FROM {from_expression} inner_t - WHERE {timestamp_field} BETWEEN CONVERT(DATETIMEOFFSET, '{start_date}', 120) AND CONVERT(DATETIMEOFFSET, '{end_date}', 120) - ) outer_t - WHERE outer_t._feast_row = 1 - """ - engine = make_engine(config.offline_store) - return MsSqlServerRetrievalJob( - query=query, - engine=engine, - config=config.offline_store, - full_feature_names=False, - on_demand_feature_views=None, + @staticmethod + def get_historical_features( + config: RepoConfig, + feature_views: List[FeatureView], + feature_refs: List[str], + entity_df: Union[pd.DataFrame, str], + registry: BaseRegistry, + project: str, + full_feature_names: bool = False, + ) -> RetrievalJob: + # TODO avoid this conversion + if type(entity_df) == str: + con = get_ibis_connection(config) + entity_df = con.sql(entity_df).execute() + + return get_historical_features_ibis( + config=config, + feature_views=feature_views, + feature_refs=feature_refs, + entity_df=entity_df, + registry=registry, + project=project, + full_feature_names=full_feature_names, + data_source_reader=_build_data_source_reader(config), + data_source_writer=_build_data_source_writer(config), + event_expire_timestamp_fn=mssql_event_expire_timestamp_fn, ) @staticmethod @@ -124,114 +180,32 @@ def pull_all_from_table_or_query( start_date: datetime, end_date: datetime, ) -> RetrievalJob: - assert type(data_source).__name__ == "MsSqlServerSource" - warnings.warn( - "The Azure Synapse + Azure SQL offline store is an experimental feature in alpha development. " - "Some functionality may still be unstable so functionality can change in the future.", - RuntimeWarning, - ) - from_expression = data_source.get_table_query_string().replace("`", "") - timestamps = [timestamp_field] - field_string = ", ".join(join_key_columns + feature_name_columns + timestamps) - - query = f""" - SELECT {field_string} - FROM ( - SELECT {field_string} - FROM {from_expression} - WHERE {timestamp_field} BETWEEN TIMESTAMP '{start_date}' AND TIMESTAMP '{end_date}' - ) - """ - engine = make_engine(config.offline_store) - - return MsSqlServerRetrievalJob( - query=query, - engine=engine, - config=config.offline_store, - full_feature_names=False, - on_demand_feature_views=None, + return pull_all_from_table_or_query_ibis( + config=config, + data_source=data_source, + join_key_columns=join_key_columns, + feature_name_columns=feature_name_columns, + timestamp_field=timestamp_field, + start_date=start_date, + end_date=end_date, + data_source_reader=_build_data_source_reader(config), + data_source_writer=_build_data_source_writer(config), ) @staticmethod - def get_historical_features( + def offline_write_batch( config: RepoConfig, - feature_views: List[FeatureView], - feature_refs: List[str], - entity_df: Union[pandas.DataFrame, str], - registry: BaseRegistry, - project: str, - full_feature_names: bool = False, - ) -> RetrievalJob: - warnings.warn( - "The Azure Synapse + Azure SQL offline store is an experimental feature in alpha development. " - "Some functionality may still be unstable so functionality can change in the future.", - RuntimeWarning, - ) - - expected_join_keys = _get_join_keys(project, feature_views, registry) - assert isinstance(config.offline_store, MsSqlServerOfflineStoreConfig) - engine = make_engine(config.offline_store) - if isinstance(entity_df, pandas.DataFrame): - entity_df_event_timestamp_col = ( - offline_utils.infer_event_timestamp_from_entity_df( - dict(zip(list(entity_df.columns), list(entity_df.dtypes))) - ) - ) - entity_df[entity_df_event_timestamp_col] = pandas.to_datetime( - entity_df[entity_df_event_timestamp_col], utc=True - ).fillna(pandas.Timestamp.now()) - - elif isinstance(entity_df, str): - raise ValueError( - "string entities are currently not supported in the MsSQL offline store." - ) - ( - table_schema, - table_name, - ) = _upload_entity_df_into_sqlserver_and_get_entity_schema( - engine, config, entity_df, full_feature_names=full_feature_names - ) - - _assert_expected_columns_in_sqlserver( - expected_join_keys, - entity_df_event_timestamp_col, - table_schema, - ) - - entity_df_event_timestamp_range = _get_entity_df_event_timestamp_range( - entity_df, - entity_df_event_timestamp_col, - engine, - ) - - # Build a query context containing all information required to template the SQL query - query_context = get_feature_view_query_context( - feature_refs, - feature_views, - registry, - project, - entity_df_timestamp_range=entity_df_event_timestamp_range, - ) - - # Generate the SQL query from the query context - query = build_point_in_time_query( - query_context, - left_table_query_string=table_name, - entity_df_event_timestamp_col=entity_df_event_timestamp_col, - entity_df_columns=table_schema.keys(), - full_feature_names=full_feature_names, - query_template=MULTIPLE_FEATURE_VIEW_POINT_IN_TIME_JOIN, - ) - query = query.replace("`", "") - - job = MsSqlServerRetrievalJob( - query=query, - engine=engine, - config=config.offline_store, - full_feature_names=full_feature_names, - on_demand_feature_views=registry.list_on_demand_feature_views(project), + feature_view: FeatureView, + table: pyarrow.Table, + progress: Optional[Callable[[int], Any]], + ): + offline_write_batch_ibis( + config=config, + feature_view=feature_view, + table=table, + progress=progress, + data_source_writer=_build_data_source_writer(config), ) - return job @staticmethod def write_logged_features( @@ -241,410 +215,10 @@ def write_logged_features( logging_config: LoggingConfig, registry: BaseRegistry, ): - raise NotImplementedError() - - @staticmethod - def offline_write_batch( - config: RepoConfig, - feature_view: FeatureView, - table: pyarrow.Table, - progress: Optional[Callable[[int], Any]], - ): - raise NotImplementedError() - - -def _assert_expected_columns_in_dataframe( - join_keys: Set[str], entity_df_event_timestamp_col: str, entity_df: pandas.DataFrame -): - entity_df_columns = set(entity_df.columns.values) - expected_columns = join_keys.copy() - expected_columns.add(entity_df_event_timestamp_col) - - missing_keys = expected_columns - entity_df_columns - - if len(missing_keys) != 0: - raise errors.FeastEntityDFMissingColumnsError(expected_columns, missing_keys) - - -def _assert_expected_columns_in_sqlserver( - join_keys: Set[str], entity_df_event_timestamp_col: str, table_schema: EntitySchema -): - entity_columns = set(table_schema.keys()) - expected_columns = join_keys.copy() - expected_columns.add(entity_df_event_timestamp_col) - - missing_keys = expected_columns - entity_columns - - if len(missing_keys) != 0: - raise errors.FeastEntityDFMissingColumnsError(expected_columns, missing_keys) - - -def _get_join_keys( - project: str, feature_views: List[FeatureView], registry: BaseRegistry -) -> Set[str]: - join_keys = set() - for feature_view in feature_views: - entities = feature_view.entities - for entity_name in entities: - entity = registry.get_entity(entity_name, project) - join_keys.add(entity.join_key) - return join_keys - - -def _infer_event_timestamp_from_sqlserver_schema(table_schema) -> str: - if any( - schema_field["COLUMN_NAME"] == DEFAULT_ENTITY_DF_EVENT_TIMESTAMP_COL - for schema_field in table_schema - ): - return DEFAULT_ENTITY_DF_EVENT_TIMESTAMP_COL - else: - datetime_columns = list( - filter( - lambda schema_field: schema_field["DATA_TYPE"] == "DATETIMEOFFSET", - table_schema, - ) - ) - if len(datetime_columns) == 1: - print( - f"Using {datetime_columns[0]['COLUMN_NAME']} as the event timestamp. To specify a column explicitly, please name it {DEFAULT_ENTITY_DF_EVENT_TIMESTAMP_COL}." - ) - return datetime_columns[0].name - else: - raise ValueError( - f"Please provide an entity_df with a column named {DEFAULT_ENTITY_DF_EVENT_TIMESTAMP_COL} representing the time of events." - ) - - -class MsSqlServerRetrievalJob(RetrievalJob): - def __init__( - self, - query: str, - engine: Engine, - config: MsSqlServerOfflineStoreConfig, - full_feature_names: bool, - on_demand_feature_views: Optional[List[OnDemandFeatureView]] = None, - metadata: Optional[RetrievalMetadata] = None, - drop_columns: Optional[List[str]] = None, - ): - self.query = query - self.engine = engine - self._config = config - self._full_feature_names = full_feature_names - self._on_demand_feature_views = on_demand_feature_views or [] - self._drop_columns = drop_columns - self._metadata = metadata - - @property - def full_feature_names(self) -> bool: - return self._full_feature_names - - @property - def on_demand_feature_views(self) -> List[OnDemandFeatureView]: - return self._on_demand_feature_views - - def _to_df_internal(self, timeout: Optional[int] = None) -> pandas.DataFrame: - return pandas.read_sql(self.query, con=self.engine).fillna(value=np.nan) - - def _to_arrow_internal(self, timeout: Optional[int] = None) -> pyarrow.Table: - result = pandas.read_sql(self.query, con=self.engine).fillna(value=np.nan) - return pyarrow.Table.from_pandas(result) - - ## Implements persist in Feast 0.18 - This persists to filestorage - ## ToDo: Persist to Azure Storage - def persist( - self, - storage: SavedDatasetStorage, - allow_overwrite: Optional[bool] = False, - timeout: Optional[int] = None, - ): - assert isinstance(storage, SavedDatasetFileStorage) - - filesystem, path = FileSource.create_filesystem_and_path( - storage.file_options.uri, - storage.file_options.s3_endpoint_override, + write_logged_features_ibis( + config=config, + data=data, + source=source, + logging_config=logging_config, + registry=registry, ) - - if path.endswith(".parquet"): - pyarrow.parquet.write_table( - self.to_arrow(), where=path, filesystem=filesystem - ) - else: - # otherwise assume destination is directory - pyarrow.parquet.write_to_dataset( - self.to_arrow(), root_path=path, filesystem=filesystem - ) - - def supports_remote_storage_export(self) -> bool: - return False - - def to_remote_storage(self) -> List[str]: - raise NotImplementedError() - - @property - def metadata(self) -> Optional[RetrievalMetadata]: - return self._metadata - - -def _upload_entity_df_into_sqlserver_and_get_entity_schema( - engine: sqlalchemy.engine.Engine, - config: RepoConfig, - entity_df: Union[pandas.DataFrame, str], - full_feature_names: bool, -) -> Tuple[Dict[Any, Any], str]: - """ - Uploads a Pandas entity dataframe into a SQL Server table and constructs the - schema from the original entity_df dataframe. - """ - table_id = offline_utils.get_temp_entity_table_name() - session = sessionmaker(bind=engine)() - - if type(entity_df) is str: - # TODO: This should be a temporary table, right? - session.execute(f"SELECT * INTO {table_id} FROM ({entity_df}) t") # type: ignore - - session.commit() - - limited_entity_df = MsSqlServerRetrievalJob( - f"SELECT TOP 1 * FROM {table_id}", - engine, - config.offline_store, - full_feature_names=full_feature_names, - on_demand_feature_views=None, - ).to_df() - - entity_schema = ( - dict(zip(limited_entity_df.columns, limited_entity_df.dtypes)), - table_id, - ) - - elif isinstance(entity_df, pandas.DataFrame): - # Drop the index so that we don't have unnecessary columns - engine.execute(_df_to_create_table_sql(entity_df, table_id)) # type: ignore - entity_df.to_sql(name=table_id, con=engine, index=False, if_exists="append") - entity_schema = dict(zip(entity_df.columns, entity_df.dtypes)), table_id - - else: - raise ValueError( - f"The entity dataframe you have provided must be a SQL Server SQL query," - f" or a Pandas dataframe. But we found: {type(entity_df)} " - ) - - return entity_schema - - -def _df_to_create_table_sql(df: pandas.DataFrame, table_name: str) -> str: - pa_table = pa.Table.from_pandas(df) - - columns = [f""""{f.name}" {pa_to_mssql_type(f.type)}""" for f in pa_table.schema] - - return f""" - CREATE TABLE "{table_name}" ( - {", ".join(columns)} - ); - """ - - -def _get_entity_df_event_timestamp_range( - entity_df: Union[pandas.DataFrame, str], - entity_df_event_timestamp_col: str, - engine: Engine, -) -> Tuple[datetime, datetime]: - if isinstance(entity_df, pandas.DataFrame): - entity_df_event_timestamp = entity_df.loc[ - :, entity_df_event_timestamp_col - ].infer_objects() - if pandas.api.types.is_string_dtype(entity_df_event_timestamp): - entity_df_event_timestamp = pandas.to_datetime( - entity_df_event_timestamp, utc=True - ) - entity_df_event_timestamp_range = ( - entity_df_event_timestamp.min().to_pydatetime(), - entity_df_event_timestamp.max().to_pydatetime(), - ) - elif isinstance(entity_df, str): - # If the entity_df is a string (SQL query), determine range - # from table - df = pandas.read_sql(entity_df, con=engine).fillna(value=np.nan) - entity_df_event_timestamp = df.loc[ - :, entity_df_event_timestamp_col - ].infer_objects() - if pandas.api.types.is_string_dtype(entity_df_event_timestamp): - entity_df_event_timestamp = pandas.to_datetime( - entity_df_event_timestamp, utc=True - ) - entity_df_event_timestamp_range = ( - entity_df_event_timestamp.min().to_pydatetime(), - entity_df_event_timestamp.max().to_pydatetime(), - ) - else: - raise InvalidEntityType(type(entity_df)) - - return entity_df_event_timestamp_range - - -# TODO: Optimizations -# * Use NEWID() instead of ROW_NUMBER(), or join on entity columns directly -# * Precompute ROW_NUMBER() so that it doesn't have to be recomputed for every query on entity_dataframe -# * Create temporary tables instead of keeping all tables in memory - -MULTIPLE_FEATURE_VIEW_POINT_IN_TIME_JOIN = """ -/* - Compute a deterministic hash for the `left_table_query_string` that will be used throughout - all the logic as the field to GROUP BY the data -*/ -WITH entity_dataframe AS ( - SELECT *, - {{entity_df_event_timestamp_col}} AS entity_timestamp - {% for featureview in featureviews %} - ,CONCAT( - {% for entity_key in unique_entity_keys %} - {{entity_key}}, - {% endfor %} - {{entity_df_event_timestamp_col}} - ) AS {{featureview.name}}__entity_row_unique_id - {% endfor %} - FROM {{ left_table_query_string }} -), - -{% for featureview in featureviews %} - -{{ featureview.name }}__entity_dataframe AS ( - SELECT - {{ featureview.entities | join(', ')}}{% if featureview.entities %},{% else %}{% endif %} - entity_timestamp, - {{featureview.name}}__entity_row_unique_id - FROM entity_dataframe - GROUP BY - {{ featureview.entities | join(', ')}}{% if featureview.entities %},{% else %}{% endif %} - entity_timestamp, - {{featureview.name}}__entity_row_unique_id -), - -/* - This query template performs the point-in-time correctness join for a single feature set table - to the provided entity table. - - 1. We first join the current feature_view to the entity dataframe that has been passed. - This JOIN has the following logic: - - For each row of the entity dataframe, only keep the rows where the timestamp_field` - is less than the one provided in the entity dataframe - - If there a TTL for the current feature_view, also keep the rows where the `timestamp_field` - is higher the the one provided minus the TTL - - For each row, Join on the entity key and retrieve the `entity_row_unique_id` that has been - computed previously - - The output of this CTE will contain all the necessary information and already filtered out most - of the data that is not relevant. -*/ - -{{ featureview.name }}__subquery AS ( - SELECT - {{ featureview.timestamp_field }} as event_timestamp, - {{ featureview.created_timestamp_column ~ ' as created_timestamp,' if featureview.created_timestamp_column else '' }} - {{ featureview.entity_selections | join(', ')}}{% if featureview.entity_selections %},{% else %}{% endif %} - {% for feature in featureview.features %} - {{ feature }} as {% if full_feature_names %}{{ featureview.name }}__{{featureview.field_mapping.get(feature, feature)}}{% else %}{{ featureview.field_mapping.get(feature, feature) }}{% endif %}{% if loop.last %}{% else %}, {% endif %} - {% endfor %} - FROM {{ featureview.table_subquery }} - WHERE {{ featureview.timestamp_field }} <= '{{ featureview.max_event_timestamp }}' - {% if featureview.ttl == 0 %}{% else %} - AND {{ featureview.timestamp_field }} >= '{{ featureview.min_event_timestamp }}' - {% endif %} -), - -{{ featureview.name }}__base AS ( - SELECT - subquery.*, - entity_dataframe.{{entity_df_event_timestamp_col}} AS entity_timestamp, - entity_dataframe.{{featureview.name}}__entity_row_unique_id - FROM {{ featureview.name }}__subquery AS subquery - INNER JOIN entity_dataframe - ON 1=1 - AND subquery.event_timestamp <= entity_dataframe.{{entity_df_event_timestamp_col}} - - {% if featureview.ttl == 0 %}{% else %} - AND {{ featureview.ttl }} > = DATEDIFF(SECOND, subquery.event_timestamp, entity_dataframe.{{entity_df_event_timestamp_col}}) - {% endif %} - - {% for entity in featureview.entities %} - AND subquery.{{ entity }} = entity_dataframe.{{ entity }} - {% endfor %} -), - -/* - 2. If the `created_timestamp_column` has been set, we need to - deduplicate the data first. This is done by calculating the - `MAX(created_at_timestamp)` for each event_timestamp. - We then join the data on the next CTE -*/ -{% if featureview.created_timestamp_column %} -{{ featureview.name }}__dedup AS ( - SELECT - {{featureview.name}}__entity_row_unique_id, - event_timestamp, - MAX(created_timestamp) as created_timestamp - FROM {{ featureview.name }}__base - GROUP BY {{featureview.name}}__entity_row_unique_id, event_timestamp -), -{% endif %} - -/* - 3. The data has been filtered during the first CTE "*__base" - Thus we only need to compute the latest timestamp of each feature. -*/ -{{ featureview.name }}__latest AS ( - SELECT - {{ featureview.name }}__base.{{ featureview.name }}__entity_row_unique_id, - MAX({{ featureview.name }}__base.event_timestamp) AS event_timestamp - {% if featureview.created_timestamp_column %} - ,MAX({{ featureview.name }}__base.created_timestamp) AS created_timestamp - {% endif %} - - FROM {{ featureview.name }}__base - {% if featureview.created_timestamp_column %} - INNER JOIN {{ featureview.name }}__dedup - ON {{ featureview.name }}__dedup.{{ featureview.name }}__entity_row_unique_id = {{ featureview.name }}__base.{{ featureview.name }}__entity_row_unique_id - AND {{ featureview.name }}__dedup.event_timestamp = {{ featureview.name }}__base.event_timestamp - AND {{ featureview.name }}__dedup.created_timestamp = {{ featureview.name }}__base.created_timestamp - {% endif %} - - GROUP BY {{ featureview.name }}__base.{{ featureview.name }}__entity_row_unique_id -), - -/* - 4. Once we know the latest value of each feature for a given timestamp, - we can join again the data back to the original "base" dataset -*/ -{{ featureview.name }}__cleaned AS ( - SELECT base.* - FROM {{ featureview.name }}__base as base - INNER JOIN {{ featureview.name }}__latest - ON base.{{ featureview.name }}__entity_row_unique_id = {{ featureview.name }}__latest.{{ featureview.name }}__entity_row_unique_id - AND base.event_timestamp = {{ featureview.name }}__latest.event_timestamp - {% if featureview.created_timestamp_column %} - AND base.created_timestamp = {{ featureview.name }}__latest.created_timestamp - {% endif %} -){% if loop.last %}{% else %}, {% endif %} - -{% endfor %} - -/* - Joins the outputs of multiple time travel joins to a single table. - The entity_dataframe dataset being our source of truth here. - */ - -SELECT {{ final_output_feature_names | join(', ')}} -FROM entity_dataframe -{% for featureview in featureviews %} -LEFT JOIN ( - SELECT - {{featureview.name}}__entity_row_unique_id - {% for feature in featureview.features %} - ,{% if full_feature_names %}{{ featureview.name }}__{{featureview.field_mapping.get(feature, feature)}}{% else %}{{ featureview.field_mapping.get(feature, feature) }}{% endif %} - {% endfor %} - FROM "{{ featureview.name }}__cleaned" -) {{ featureview.name }}__cleaned -ON -{{ featureview.name }}__cleaned.{{ featureview.name }}__entity_row_unique_id = entity_dataframe.{{ featureview.name }}__entity_row_unique_id -{% endfor %} -""" diff --git a/sdk/python/feast/infra/offline_stores/contrib/mssql_offline_store/mssqlserver_source.py b/sdk/python/feast/infra/offline_stores/contrib/mssql_offline_store/mssqlserver_source.py index 6b126fa40c..39abd1c9e7 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/mssql_offline_store/mssqlserver_source.py +++ b/sdk/python/feast/infra/offline_stores/contrib/mssql_offline_store/mssqlserver_source.py @@ -3,15 +3,10 @@ import json import warnings from typing import Callable, Dict, Iterable, Optional, Tuple - -import pandas -from sqlalchemy import create_engine +from urllib import parse from feast import type_map from feast.data_source import DataSource -from feast.infra.offline_stores.contrib.mssql_offline_store.mssql import ( - MsSqlServerOfflineStoreConfig, -) from feast.protos.feast.core.DataSource_pb2 import DataSource as DataSourceProto from feast.repo_config import RepoConfig from feast.value_type import ValueType @@ -20,6 +15,21 @@ warnings.simplefilter("once", RuntimeWarning) +def get_ibis_connection(config: RepoConfig): + import ibis + + connection_params = parse.urlparse(config.offline_store.connection_string) + additional_kwargs = dict(parse.parse_qsl(connection_params.query)) + return ibis.mssql.connect( + user=connection_params.username, + password=connection_params.password, + host=connection_params.hostname, + port=connection_params.port, + database=connection_params.path.strip("/"), + **additional_kwargs, + ) + + class MsSqlServerOptions: """ DataSource MsSQLServer options used to source features from MsSQLServer query @@ -114,11 +124,11 @@ def __init__( tags: Optional[Dict[str, str]] = None, owner: Optional[str] = None, ): - warnings.warn( - "The Azure Synapse + Azure SQL data source is an experimental feature in alpha development. " - "Some functionality may still be unstable so functionality can change in the future.", - RuntimeWarning, - ) + # warnings.warn( + # "The Azure Synapse + Azure SQL data source is an experimental feature in alpha development. " + # "Some functionality may still be unstable so functionality can change in the future.", + # RuntimeWarning, + # ) self._mssqlserver_options = MsSqlServerOptions( connection_str=connection_str, table_ref=table_ref ) @@ -222,11 +232,8 @@ def source_datatype_to_feast_value_type() -> Callable[[str], ValueType]: def get_table_column_names_and_types( self, config: RepoConfig ) -> Iterable[Tuple[str, str]]: - assert isinstance(config.offline_store, MsSqlServerOfflineStoreConfig) - conn = create_engine(config.offline_store.connection_string) - self._mssqlserver_options.connection_str = ( - config.offline_store.connection_string - ) + con = get_ibis_connection(config) + name_type_pairs = [] if len(self.table_ref.split(".")) == 2: database, table_name = self.table_ref.split(".") @@ -240,7 +247,8 @@ def get_table_column_names_and_types( WHERE TABLE_NAME = '{self.table_ref}' """ - table_schema = pandas.read_sql(columns_query, conn) + table_schema = con.sql(columns_query).execute() + name_type_pairs.extend( list( zip( diff --git a/sdk/python/feast/infra/offline_stores/contrib/mssql_offline_store/tests/data_source.py b/sdk/python/feast/infra/offline_stores/contrib/mssql_offline_store/tests/data_source.py index ccf826c068..9c87b8d752 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/mssql_offline_store/tests/data_source.py +++ b/sdk/python/feast/infra/offline_stores/contrib/mssql_offline_store/tests/data_source.py @@ -1,15 +1,15 @@ from typing import Dict, List, Optional +import ibis import pandas as pd import pytest -from sqlalchemy import create_engine from testcontainers.core.waiting_utils import wait_for_logs from testcontainers.mssql import SqlServerContainer from feast.data_source import DataSource +from feast.feature_logging import LoggingDestination from feast.infra.offline_stores.contrib.mssql_offline_store.mssql import ( MsSqlServerOfflineStoreConfig, - _df_to_create_table_sql, ) from feast.infra.offline_stores.contrib.mssql_offline_store.mssqlserver_source import ( MsSqlServerSource, @@ -26,7 +26,7 @@ @pytest.fixture(scope="session") def mssql_container(): container = SqlServerContainer( - user=MSSQL_USER, + username=MSSQL_USER, password=MSSQL_PASSWORD, image="mcr.microsoft.com/azure-sql-edge:1.0.6", ) @@ -56,8 +56,10 @@ def __init__( ) def create_offline_store_config(self) -> MsSqlServerOfflineStoreConfig: + connection_string = self.container.get_connection_url() + connection_string += "?driver=FreeTDS" return MsSqlServerOfflineStoreConfig( - connection_string=self.container.get_connection_url(), + connection_string=connection_string, ) def create_data_source( @@ -66,7 +68,7 @@ def create_data_source( destination_name: str, created_timestamp_column="created_ts", field_mapping: Optional[Dict[str, str]] = None, - timestamp_field: Optional[str] = "ts", + timestamp_field: Optional[str] = None, ) -> DataSource: # Make sure the field mapping is correct and convert the datetime datasources. if timestamp_field in df: @@ -79,13 +81,23 @@ def create_data_source( ).fillna(pd.Timestamp.now()) connection_string = self.create_offline_store_config().connection_string - engine = create_engine(connection_string) + + con = ibis.mssql.connect( + user=self.container.username, + password=self.container.password, + host=self.container.get_container_host_ip(), + port=self.container.get_exposed_port(self.container.port), + database=self.container.dbname, + driver="FreeTDS", + ) + destination_name = self.get_prefixed_table_name(destination_name) - # Create table - engine.execute(_df_to_create_table_sql(df, destination_name)) # type: ignore - # Upload dataframe to azure table - df.to_sql(destination_name, engine, index=False, if_exists="append") + con.create_table( + name=destination_name, + schema=ibis.Schema.from_pandas(df.dtypes.to_dict().items()), + ) + con.insert(table_name=destination_name, obj=df) self.tables.append(destination_name) return MsSqlServerSource( @@ -100,8 +112,12 @@ def create_data_source( def create_saved_dataset_destination(self) -> SavedDatasetStorage: raise NotImplementedError + def create_logged_features_destination(self) -> LoggingDestination: + raise NotImplementedError + def get_prefixed_table_name(self, destination_name: str) -> str: return f"{self.project_name}_{destination_name}" def teardown(self): - raise NotImplementedError + pass + # raise NotImplementedError diff --git a/sdk/python/feast/infra/offline_stores/ibis.py b/sdk/python/feast/infra/offline_stores/ibis.py index dd81fd1d4e..4de16cbda3 100644 --- a/sdk/python/feast/infra/offline_stores/ibis.py +++ b/sdk/python/feast/infra/offline_stores/ibis.py @@ -1,3 +1,5 @@ +import random +import string import uuid from datetime import datetime, timedelta from pathlib import Path @@ -95,7 +97,9 @@ def _get_entity_df_event_timestamp_range( entity_df_event_timestamp = entity_df.loc[ :, entity_df_event_timestamp_col ].infer_objects() - if pd.api.types.is_string_dtype(entity_df_event_timestamp): + if pd.api.types.is_string_dtype( + entity_df_event_timestamp + ) or pd.api.types.is_object_dtype(entity_df_event_timestamp): entity_df_event_timestamp = pd.to_datetime(entity_df_event_timestamp, utc=True) entity_df_event_timestamp_range = ( entity_df_event_timestamp.min().to_pydatetime(), @@ -107,7 +111,10 @@ def _get_entity_df_event_timestamp_range( def _to_utc(entity_df: pd.DataFrame, event_timestamp_col): entity_df_event_timestamp = entity_df.loc[:, event_timestamp_col].infer_objects() - if pd.api.types.is_string_dtype(entity_df_event_timestamp): + + if pd.api.types.is_string_dtype( + entity_df_event_timestamp + ) or pd.api.types.is_object_dtype(entity_df_event_timestamp): entity_df_event_timestamp = pd.to_datetime(entity_df_event_timestamp, utc=True) entity_df[event_timestamp_col] = entity_df_event_timestamp @@ -146,6 +153,7 @@ def get_historical_features_ibis( full_feature_names: bool = False, staging_location: Optional[str] = None, staging_location_endpoint_override: Optional[str] = None, + event_expire_timestamp_fn=None, ) -> RetrievalJob: entity_schema = _get_entity_schema( entity_df=entity_df, @@ -218,6 +226,7 @@ def read_fv( for feature_view in feature_views ], event_timestamp_col=event_timestamp_col, + event_expire_timestamp_fn=event_expire_timestamp_fn, ) odfvs = OnDemandFeatureView.get_requested_odfvs(feature_refs, project, registry) @@ -345,6 +354,7 @@ def point_in_time_join( entity_table: Table, feature_tables: List[Tuple[Table, str, str, Dict[str, str], List[str], timedelta]], event_timestamp_col="event_timestamp", + event_expire_timestamp_fn=None, ): # TODO handle ttl all_entities = [event_timestamp_col] @@ -375,6 +385,19 @@ def point_in_time_join( feature_refs, ttl, ) in feature_tables: + if ttl: + if not event_expire_timestamp_fn: + feature_table = feature_table.mutate( + event_expire_timestamp=feature_table[timestamp_field] + + ibis.literal(ttl) + ) + else: + alias = "".join(random.choices(string.ascii_uppercase, k=10)) + + feature_table = feature_table.alias(alias=alias).sql( + f"SELECT *, {event_expire_timestamp_fn(timestamp_field, ttl)} AS event_expire_timestamp FROM {alias}" + ) + predicates = [ feature_table[k] == entity_table[v] for k, v in join_key_map.items() ] @@ -385,8 +408,8 @@ def point_in_time_join( if ttl: predicates.append( - feature_table[timestamp_field] - >= entity_table[event_timestamp_col] - ibis.literal(ttl) + feature_table["event_expire_timestamp"] + >= entity_table[event_timestamp_col] ) feature_table = feature_table.inner_join( diff --git a/sdk/python/feast/type_map.py b/sdk/python/feast/type_map.py index 703c1dc7c5..4e9b54c631 100644 --- a/sdk/python/feast/type_map.py +++ b/sdk/python/feast/type_map.py @@ -600,7 +600,9 @@ def mssql_to_feast_value_type(mssql_type_as_str: str) -> ValueType: "char": ValueType.STRING, "date": ValueType.UNIX_TIMESTAMP, "datetime": ValueType.UNIX_TIMESTAMP, + "datetimeoffset": ValueType.UNIX_TIMESTAMP, "float": ValueType.FLOAT, + "int": ValueType.INT32, "nchar": ValueType.STRING, "nvarchar": ValueType.STRING, "nvarchar(max)": ValueType.STRING, 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 50f048928d..6d5eeb90c7 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 @@ -9,9 +9,6 @@ AthenaOfflineStoreConfig, AthenaRetrievalJob, ) -from feast.infra.offline_stores.contrib.mssql_offline_store.mssql import ( - MsSqlServerRetrievalJob, -) from feast.infra.offline_stores.contrib.postgres_offline_store.postgres import ( PostgreSQLOfflineStoreConfig, PostgreSQLRetrievalJob, @@ -104,7 +101,6 @@ def metadata(self) -> Optional[RetrievalMetadata]: RedshiftRetrievalJob, SnowflakeRetrievalJob, AthenaRetrievalJob, - MsSqlServerRetrievalJob, PostgreSQLRetrievalJob, SparkRetrievalJob, TrinoRetrievalJob, @@ -173,13 +169,6 @@ def retrieval_job(request, environment): config=environment.config, full_feature_names=False, ) - elif request.param is MsSqlServerRetrievalJob: - return MsSqlServerRetrievalJob( - query="query", - engine=MagicMock(), - config=environment.config, - full_feature_names=False, - ) elif request.param is PostgreSQLRetrievalJob: offline_store_config = PostgreSQLOfflineStoreConfig( host="str", diff --git a/setup.py b/setup.py index 2043bf1b3f..400555f0e1 100644 --- a/setup.py +++ b/setup.py @@ -150,6 +150,8 @@ SINGLESTORE_REQUIRED = ["singlestoredb"] +MSSQL_REQUIRED = ["ibis-framework[mssql]>=9.0.0,<10"] + CI_REQUIRED = ( [ "build", @@ -369,6 +371,7 @@ def run(self): "postgres": POSTGRES_REQUIRED, "azure": AZURE_REQUIRED, "mysql": MYSQL_REQUIRED, + "mssql": MSSQL_REQUIRED, "ge": GE_REQUIRED, "hbase": HBASE_REQUIRED, "docs": DOCS_REQUIRED, From b54c1cdbb8555682bcfd3aa9f63cc59c638957ab Mon Sep 17 00:00:00 2001 From: Tornike Gurgenidze Date: Fri, 19 Jul 2024 06:26:59 +0400 Subject: [PATCH 39/59] chore: Remove firebase-admin from ci dependencies (#4359) remove firebase-admin from ci dependencies Signed-off-by: tokoko --- .../requirements/py3.10-ci-requirements.txt | 180 ++---------------- .../requirements/py3.10-requirements.txt | 48 +---- .../requirements/py3.11-ci-requirements.txt | 180 ++---------------- .../requirements/py3.11-requirements.txt | 48 +---- .../requirements/py3.9-ci-requirements.txt | 180 ++---------------- .../requirements/py3.9-requirements.txt | 48 +---- setup.py | 1 - 7 files changed, 78 insertions(+), 607 deletions(-) diff --git a/sdk/python/requirements/py3.10-ci-requirements.txt b/sdk/python/requirements/py3.10-ci-requirements.txt index a7f300a0ed..a9f1d625a8 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,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 @@ -52,9 +48,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,9 +60,7 @@ bidict==0.23.1 bleach==6.1.0 # via nbconvert boto3==1.34.131 - # via - # feast (setup.py) - # moto + # via moto botocore==1.34.131 # via # aiobotocore @@ -77,15 +69,11 @@ botocore==1.34.131 # s3transfer build==1.2.1 # via - # feast (setup.py) # pip-tools # singlestoredb -cachecontrol==0.14.0 - # via firebase-admin cachetools==5.3.3 # via google-auth cassandra-driver==3.29.1 - # via feast (setup.py) certifi==2024.7.4 # via # elastic-transport @@ -108,7 +96,6 @@ charset-normalizer==3.3.2 # snowflake-connector-python click==8.1.7 # via - # feast (setup.py) # dask # geomet # great-expectations @@ -118,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 @@ -129,7 +114,6 @@ coverage[toml]==7.5.4 # via pytest-cov cryptography==42.0.8 # via - # feast (setup.py) # azure-identity # azure-storage-blob # great-expectations @@ -141,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 @@ -155,9 +137,7 @@ 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 @@ -171,7 +151,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 @@ -186,7 +165,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 @@ -195,8 +173,6 @@ filelock==3.15.4 # via # snowflake-connector-python # virtualenv -firebase-admin==5.4.0 - # via feast (setup.py) fqdn==1.5.1 # via jsonschema frozenlist==1.4.1 @@ -204,60 +180,37 @@ frozenlist==1.4.1 # aiohttp # aiosignal fsspec==2023.12.2 - # via - # feast (setup.py) - # dask + # via 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) - # firebase-admin - # google-api-python-client # google-cloud-bigquery # google-cloud-bigquery-storage # google-cloud-bigtable # google-cloud-core # google-cloud-datastore - # google-cloud-firestore # google-cloud-storage -google-api-python-client==2.134.0 - # via firebase-admin google-auth==2.30.0 # via # google-api-core - # google-api-python-client - # google-auth-httplib2 # google-cloud-bigquery-storage # google-cloud-core - # google-cloud-firestore # google-cloud-storage # kubernetes -google-auth-httplib2==0.2.0 - # via google-api-python-client 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 # google-cloud-bigtable # google-cloud-datastore - # google-cloud-firestore # google-cloud-storage google-cloud-datastore==2.19.0 - # via feast (setup.py) -google-cloud-firestore==2.16.0 - # via firebase-admin google-cloud-storage==2.17.0 - # via - # feast (setup.py) - # firebase-admin google-crc32c==1.5.0 # via # google-cloud-storage @@ -268,17 +221,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 @@ -289,46 +241,30 @@ 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 -httplib2==0.22.0 - # via - # google-api-python-client - # google-auth-httplib2 httptools==0.6.1 # via uvicorn httpx==0.27.0 # via - # feast (setup.py) # fastapi # jupyterlab 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 @@ -363,7 +299,6 @@ jedi==0.19.1 # via ipython jinja2==3.1.4 # via - # feast (setup.py) # altair # fastapi # great-expectations @@ -387,7 +322,6 @@ jsonpointer==3.0.0 # jsonschema jsonschema[format-nongpl]==4.22.0 # via - # feast (setup.py) # altair # great-expectations # jupyter-events @@ -433,7 +367,6 @@ 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 @@ -454,37 +387,28 @@ 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 # msal-extensions msal-extensions==1.2.0 # via azure-identity -msgpack==1.0.8 - # via cachecontrol multidict==6.0.5 # via # 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 @@ -507,7 +431,6 @@ notebook-shim==0.2.4 # notebook numpy==1.26.4 # via - # feast (setup.py) # altair # dask # db-dtypes @@ -542,7 +465,6 @@ packaging==24.1 # sphinx pandas==2.2.2 # via - # feast (setup.py) # altair # dask # dask-expr @@ -568,7 +490,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 @@ -581,7 +502,6 @@ 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 prompt-toolkit==3.0.47 @@ -593,16 +513,13 @@ proto-plus==1.24.0 # google-cloud-bigquery-storage # google-cloud-bigtable # google-cloud-datastore - # google-cloud-firestore protobuf==4.25.3 # via - # feast (setup.py) # google-api-core # google-cloud-bigquery # google-cloud-bigquery-storage # google-cloud-bigtable # google-cloud-datastore - # google-cloud-firestore # googleapis-common-protos # grpc-google-iam-v1 # grpcio-health-checking @@ -614,11 +531,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 @@ -630,14 +544,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 @@ -655,19 +567,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 @@ -678,26 +587,19 @@ 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 - # via - # great-expectations - # httplib2 + # via great-expectations pyproject-hooks==1.1.0 # via # 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 @@ -707,21 +609,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 @@ -750,7 +644,6 @@ pytz==2024.1 # trino pyyaml==6.0.1 # via - # feast (setup.py) # dask # ibis-substrait # jupyter-events @@ -764,21 +657,16 @@ 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 - # cachecontrol # docker # google-api-core # google-cloud-bigquery @@ -811,7 +699,6 @@ rich==13.7.1 # ibis-framework # typer rockset==2.1.2 - # via feast (setup.py) rpds-py==0.18.1 # via # jsonschema @@ -821,7 +708,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 @@ -838,7 +724,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 +744,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 +762,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.0.1a10 - # via feast (setup.py) sqlparams==6.0.1 # via singlestoredb stack-data==0.6.3 @@ -893,21 +774,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 +812,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 +829,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 @@ -1021,11 +882,8 @@ ujson==5.10.0 # via fastapi uri-template==1.3.0 # via jsonschema -uritemplate==4.1.1 - # via google-api-python-client urllib3==1.26.19 # via - # feast (setup.py) # botocore # docker # elastic-transport @@ -1037,15 +895,11 @@ urllib3==1.26.19 # 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.10-requirements.txt b/sdk/python/requirements/py3.10-requirements.txt index 39a278818f..0cca106863 100644 --- a/sdk/python/requirements/py3.10-requirements.txt +++ b/sdk/python/requirements/py3.10-requirements.txt @@ -20,22 +20,17 @@ 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 +38,14 @@ 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 +greenlet==3.0.3 + # via sqlalchemy gunicorn==22.0.0 - # via feast (setup.py) h11==0.14.0 # via # httpcore @@ -71,11 +65,8 @@ 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 locket==1.0.0 @@ -87,16 +78,13 @@ 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 @@ -108,29 +96,20 @@ packaging==24.0 # gunicorn pandas==2.2.2 # via - # feast (setup.py) # dask # dask-expr partd==1.4.2 # via dask protobuf==4.25.3 - # via - # feast (setup.py) - # mypy-protobuf + # via mypy-protobuf pyarrow==16.0.0 - # via - # feast (setup.py) - # dask-expr + # via dask-expr 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 python-dateutil==2.9.0.post0 # via pandas python-dotenv==1.0.1 @@ -141,7 +120,6 @@ pytz==2024.1 # via pandas pyyaml==6.0.1 # via - # feast (setup.py) # dask # uvicorn referencing==0.35.1 @@ -149,7 +127,6 @@ 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 @@ -165,15 +142,11 @@ 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 @@ -181,9 +154,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 @@ -207,7 +178,6 @@ 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 4c1be0a5b4..ce2e50b8a7 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 @@ -48,9 +44,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,9 +56,7 @@ bidict==0.23.1 bleach==6.1.0 # via nbconvert boto3==1.34.131 - # via - # feast (setup.py) - # moto + # via moto botocore==1.34.131 # via # aiobotocore @@ -73,15 +65,11 @@ botocore==1.34.131 # s3transfer build==1.2.1 # via - # feast (setup.py) # pip-tools # singlestoredb -cachecontrol==0.14.0 - # via firebase-admin cachetools==5.3.3 # via google-auth cassandra-driver==3.29.1 - # via feast (setup.py) certifi==2024.7.4 # via # elastic-transport @@ -104,7 +92,6 @@ charset-normalizer==3.3.2 # snowflake-connector-python click==8.1.7 # via - # feast (setup.py) # dask # geomet # great-expectations @@ -114,9 +101,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 @@ -125,7 +110,6 @@ coverage[toml]==7.5.4 # via pytest-cov cryptography==42.0.8 # via - # feast (setup.py) # azure-identity # azure-storage-blob # great-expectations @@ -137,9 +121,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 @@ -151,9 +133,7 @@ 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 @@ -167,7 +147,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 @@ -177,7 +156,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 @@ -186,8 +164,6 @@ filelock==3.15.4 # via # snowflake-connector-python # virtualenv -firebase-admin==5.4.0 - # via feast (setup.py) fqdn==1.5.1 # via jsonschema frozenlist==1.4.1 @@ -195,60 +171,37 @@ frozenlist==1.4.1 # aiohttp # aiosignal fsspec==2023.12.2 - # via - # feast (setup.py) - # dask + # via 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) - # firebase-admin - # google-api-python-client # google-cloud-bigquery # google-cloud-bigquery-storage # google-cloud-bigtable # google-cloud-core # google-cloud-datastore - # google-cloud-firestore # google-cloud-storage -google-api-python-client==2.134.0 - # via firebase-admin google-auth==2.30.0 # via # google-api-core - # google-api-python-client - # google-auth-httplib2 # google-cloud-bigquery-storage # google-cloud-core - # google-cloud-firestore # google-cloud-storage # kubernetes -google-auth-httplib2==0.2.0 - # via google-api-python-client 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 # google-cloud-bigtable # google-cloud-datastore - # google-cloud-firestore # google-cloud-storage google-cloud-datastore==2.19.0 - # via feast (setup.py) -google-cloud-firestore==2.16.0 - # via firebase-admin google-cloud-storage==2.17.0 - # via - # feast (setup.py) - # firebase-admin google-crc32c==1.5.0 # via # google-cloud-storage @@ -259,17 +212,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 @@ -280,46 +232,30 @@ 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 -httplib2==0.22.0 - # via - # google-api-python-client - # google-auth-httplib2 httptools==0.6.1 # via uvicorn httpx==0.27.0 # via - # feast (setup.py) # fastapi # jupyterlab 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 @@ -354,7 +290,6 @@ jedi==0.19.1 # via ipython jinja2==3.1.4 # via - # feast (setup.py) # altair # fastapi # great-expectations @@ -378,7 +313,6 @@ jsonpointer==3.0.0 # jsonschema jsonschema[format-nongpl]==4.22.0 # via - # feast (setup.py) # altair # great-expectations # jupyter-events @@ -424,7 +358,6 @@ 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 @@ -445,37 +378,28 @@ 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 # msal-extensions msal-extensions==1.2.0 # via azure-identity -msgpack==1.0.8 - # via cachecontrol multidict==6.0.5 # via # 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 @@ -498,7 +422,6 @@ notebook-shim==0.2.4 # notebook numpy==1.26.4 # via - # feast (setup.py) # altair # dask # db-dtypes @@ -533,7 +456,6 @@ packaging==24.1 # sphinx pandas==2.2.2 # via - # feast (setup.py) # altair # dask # dask-expr @@ -559,7 +481,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 @@ -572,7 +493,6 @@ 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 prompt-toolkit==3.0.47 @@ -584,16 +504,13 @@ proto-plus==1.24.0 # google-cloud-bigquery-storage # google-cloud-bigtable # google-cloud-datastore - # google-cloud-firestore protobuf==4.25.3 # via - # feast (setup.py) # google-api-core # google-cloud-bigquery # google-cloud-bigquery-storage # google-cloud-bigtable # google-cloud-datastore - # google-cloud-firestore # googleapis-common-protos # grpc-google-iam-v1 # grpcio-health-checking @@ -605,11 +522,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 @@ -621,14 +535,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 @@ -646,19 +558,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 @@ -669,26 +578,19 @@ 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 - # via - # great-expectations - # httplib2 + # via great-expectations pyproject-hooks==1.1.0 # via # 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 @@ -698,21 +600,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 @@ -741,7 +635,6 @@ pytz==2024.1 # trino pyyaml==6.0.1 # via - # feast (setup.py) # dask # ibis-substrait # jupyter-events @@ -755,21 +648,16 @@ 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 - # cachecontrol # docker # google-api-core # google-cloud-bigquery @@ -802,7 +690,6 @@ rich==13.7.1 # ibis-framework # typer rockset==2.1.2 - # via feast (setup.py) rpds-py==0.18.1 # via # jsonschema @@ -812,7 +699,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 @@ -829,7 +715,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 @@ -850,13 +735,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 @@ -870,11 +753,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.0.1a10 - # via feast (setup.py) sqlparams==6.0.1 # via singlestoredb stack-data==0.6.3 @@ -884,21 +765,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 @@ -916,9 +793,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 @@ -935,39 +810,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 @@ -999,11 +860,8 @@ ujson==5.10.0 # via fastapi uri-template==1.3.0 # via jsonschema -uritemplate==4.1.1 - # via google-api-python-client urllib3==1.26.19 # via - # feast (setup.py) # botocore # docker # elastic-transport @@ -1015,15 +873,11 @@ urllib3==1.26.19 # 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 44e658113a..687e4bfe52 100644 --- a/sdk/python/requirements/py3.11-requirements.txt +++ b/sdk/python/requirements/py3.11-requirements.txt @@ -20,36 +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 - # 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 +greenlet==3.0.3 + # via sqlalchemy gunicorn==22.0.0 - # via feast (setup.py) h11==0.14.0 # via # httpcore @@ -69,11 +63,8 @@ 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 locket==1.0.0 @@ -85,16 +76,13 @@ 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 @@ -106,29 +94,20 @@ packaging==24.0 # gunicorn pandas==2.2.2 # via - # feast (setup.py) # dask # dask-expr partd==1.4.2 # via dask protobuf==4.25.3 - # via - # feast (setup.py) - # mypy-protobuf + # via mypy-protobuf pyarrow==16.0.0 - # via - # feast (setup.py) - # dask-expr + # via dask-expr 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 python-dateutil==2.9.0.post0 # via pandas python-dotenv==1.0.1 @@ -139,7 +118,6 @@ pytz==2024.1 # via pandas pyyaml==6.0.1 # via - # feast (setup.py) # dask # uvicorn referencing==0.35.1 @@ -147,7 +125,6 @@ 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 @@ -163,23 +140,17 @@ 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 @@ -201,7 +172,6 @@ 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 25cdea7a68..017c1c8920 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,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 @@ -52,9 +48,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,9 +60,7 @@ bidict==0.23.1 bleach==6.1.0 # via nbconvert boto3==1.34.131 - # via - # feast (setup.py) - # moto + # via moto botocore==1.34.131 # via # aiobotocore @@ -77,15 +69,11 @@ botocore==1.34.131 # s3transfer build==1.2.1 # via - # feast (setup.py) # pip-tools # singlestoredb -cachecontrol==0.14.0 - # via firebase-admin cachetools==5.3.3 # via google-auth cassandra-driver==3.29.1 - # via feast (setup.py) certifi==2024.7.4 # via # elastic-transport @@ -108,7 +96,6 @@ charset-normalizer==3.3.2 # snowflake-connector-python click==8.1.7 # via - # feast (setup.py) # dask # geomet # great-expectations @@ -118,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 @@ -129,7 +114,6 @@ coverage[toml]==7.5.4 # via pytest-cov cryptography==42.0.8 # via - # feast (setup.py) # azure-identity # azure-storage-blob # great-expectations @@ -141,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 @@ -155,9 +137,7 @@ 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 @@ -171,7 +151,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 @@ -186,7 +165,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 @@ -195,8 +173,6 @@ filelock==3.15.4 # via # snowflake-connector-python # virtualenv -firebase-admin==5.4.0 - # via feast (setup.py) fqdn==1.5.1 # via jsonschema frozenlist==1.4.1 @@ -204,60 +180,37 @@ frozenlist==1.4.1 # aiohttp # aiosignal fsspec==2023.12.2 - # via - # feast (setup.py) - # dask + # via 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) - # firebase-admin - # google-api-python-client # google-cloud-bigquery # google-cloud-bigquery-storage # google-cloud-bigtable # google-cloud-core # google-cloud-datastore - # google-cloud-firestore # google-cloud-storage -google-api-python-client==2.134.0 - # via firebase-admin google-auth==2.30.0 # via # google-api-core - # google-api-python-client - # google-auth-httplib2 # google-cloud-bigquery-storage # google-cloud-core - # google-cloud-firestore # google-cloud-storage # kubernetes -google-auth-httplib2==0.2.0 - # via google-api-python-client 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 # google-cloud-bigtable # google-cloud-datastore - # google-cloud-firestore # google-cloud-storage google-cloud-datastore==2.19.0 - # via feast (setup.py) -google-cloud-firestore==2.16.0 - # via firebase-admin google-cloud-storage==2.17.0 - # via - # feast (setup.py) - # firebase-admin google-crc32c==1.5.0 # via # google-cloud-storage @@ -268,17 +221,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 @@ -289,46 +241,30 @@ 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 -httplib2==0.22.0 - # via - # google-api-python-client - # google-auth-httplib2 httptools==0.6.1 # via uvicorn httpx==0.27.0 # via - # feast (setup.py) # fastapi # jupyterlab 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 @@ -372,7 +308,6 @@ jedi==0.19.1 # via ipython jinja2==3.1.4 # via - # feast (setup.py) # altair # fastapi # great-expectations @@ -396,7 +331,6 @@ jsonpointer==3.0.0 # jsonschema jsonschema[format-nongpl]==4.22.0 # via - # feast (setup.py) # altair # great-expectations # jupyter-events @@ -442,7 +376,6 @@ 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 @@ -463,37 +396,28 @@ 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 # msal-extensions msal-extensions==1.2.0 # via azure-identity -msgpack==1.0.8 - # via cachecontrol multidict==6.0.5 # via # 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 @@ -516,7 +440,6 @@ notebook-shim==0.2.4 # notebook numpy==1.26.4 # via - # feast (setup.py) # altair # dask # db-dtypes @@ -551,7 +474,6 @@ packaging==24.1 # sphinx pandas==2.2.2 # via - # feast (setup.py) # altair # dask # dask-expr @@ -577,7 +499,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 @@ -590,7 +511,6 @@ 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 prompt-toolkit==3.0.47 @@ -602,16 +522,13 @@ proto-plus==1.24.0 # google-cloud-bigquery-storage # google-cloud-bigtable # google-cloud-datastore - # google-cloud-firestore protobuf==4.25.3 # via - # feast (setup.py) # google-api-core # google-cloud-bigquery # google-cloud-bigquery-storage # google-cloud-bigtable # google-cloud-datastore - # google-cloud-firestore # googleapis-common-protos # grpc-google-iam-v1 # grpcio-health-checking @@ -623,11 +540,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 @@ -639,14 +553,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 @@ -664,19 +576,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 @@ -687,26 +596,19 @@ 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 - # via - # great-expectations - # httplib2 + # via great-expectations pyproject-hooks==1.1.0 # via # 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 @@ -716,21 +618,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 @@ -759,7 +653,6 @@ pytz==2024.1 # trino pyyaml==6.0.1 # via - # feast (setup.py) # dask # ibis-substrait # jupyter-events @@ -773,21 +666,16 @@ 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 - # cachecontrol # docker # google-api-core # google-cloud-bigquery @@ -820,7 +708,6 @@ rich==13.7.1 # ibis-framework # typer rockset==2.1.2 - # via feast (setup.py) rpds-py==0.18.1 # via # jsonschema @@ -832,7 +719,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 @@ -849,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 @@ -870,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 @@ -890,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==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 @@ -904,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 @@ -946,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 @@ -965,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 @@ -1034,11 +895,8 @@ ujson==5.10.0 # via fastapi uri-template==1.3.0 # via jsonschema -uritemplate==4.1.1 - # via google-api-python-client urllib3==1.26.19 # via - # feast (setup.py) # botocore # docker # elastic-transport @@ -1051,15 +909,11 @@ urllib3==1.26.19 # 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 ea553bcae2..096f54ab1f 100644 --- a/sdk/python/requirements/py3.9-requirements.txt +++ b/sdk/python/requirements/py3.9-requirements.txt @@ -20,22 +20,17 @@ 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 +38,14 @@ 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 +greenlet==3.0.3 + # via sqlalchemy gunicorn==22.0.0 - # via feast (setup.py) h11==0.14.0 # via # httpcore @@ -73,11 +67,8 @@ importlib-metadata==7.1.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 locket==1.0.0 @@ -89,16 +80,13 @@ 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 @@ -110,29 +98,20 @@ packaging==24.0 # gunicorn pandas==2.2.2 # via - # feast (setup.py) # dask # dask-expr partd==1.4.2 # via dask protobuf==4.25.3 - # via - # feast (setup.py) - # mypy-protobuf + # via mypy-protobuf pyarrow==16.0.0 - # via - # feast (setup.py) - # dask-expr + # via dask-expr 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 python-dateutil==2.9.0.post0 # via pandas python-dotenv==1.0.1 @@ -143,7 +122,6 @@ pytz==2024.1 # via pandas pyyaml==6.0.1 # via - # feast (setup.py) # dask # uvicorn referencing==0.35.1 @@ -151,7 +129,6 @@ 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 @@ -167,15 +144,11 @@ 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 @@ -183,9 +156,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 @@ -210,7 +181,6 @@ 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/setup.py b/setup.py index 400555f0e1..b983617712 100644 --- a/setup.py +++ b/setup.py @@ -179,7 +179,6 @@ "pytest-env", "Sphinx>4.0.0,<7", "testcontainers==4.4.0", - "firebase-admin>=5.2.0,<6", "pre-commit<3.3.2", "assertpy==1.1", "pip-tools", From 56b411786af5616e2d5155df3a0076bea35dc657 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Jul 2024 02:37:56 +0000 Subject: [PATCH 40/59] chore: Bump ws from 7.5.7 to 7.5.10 in /sdk/python/feast/ui (#4362) Bumps [ws](https://github.com/websockets/ws) from 7.5.7 to 7.5.10. - [Release notes](https://github.com/websockets/ws/releases) - [Commits](https://github.com/websockets/ws/compare/7.5.7...7.5.10) --- updated-dependencies: - dependency-name: ws 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 | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sdk/python/feast/ui/yarn.lock b/sdk/python/feast/ui/yarn.lock index 7c01b6c1e8..aa17626979 100644 --- a/sdk/python/feast/ui/yarn.lock +++ b/sdk/python/feast/ui/yarn.lock @@ -10977,14 +10977,14 @@ write-file-atomic@^3.0.0: typedarray-to-buffer "^3.1.5" ws@^7.4.6: - version "7.5.7" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.7.tgz#9e0ac77ee50af70d58326ecff7e85eb3fa375e67" - integrity sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A== + version "7.5.10" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" + integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== ws@^8.4.2: - version "8.6.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.6.0.tgz#e5e9f1d9e7ff88083d0c0dd8281ea662a42c9c23" - integrity sha512-AzmM3aH3gk0aX7/rZLYvjdvZooofDu3fFOzGqcSnQ1tOcTWwhM/o+q++E8mAyVVIyUdajrkzWUGftaVSDLn1bw== + 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" From 9708d84a1479615d7387757f7e0edbbf33dbc31d Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Thu, 25 Jul 2024 14:51:51 -0400 Subject: [PATCH 41/59] chore: Update README.md (#4367) Update README.md --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a1e06774da..13c5db443c 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,10 @@ [![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) +## Join us on Slack! + +👋👋👋 [Come say hi on Slack!](https://join.slack.com/t/feastopensource/signup) + ## Overview Feast (**Fea**ture **St**ore) is an open source feature store for machine learning. Feast is the fastest path to manage existing infrastructure to productionize analytic data for model training and online inference. @@ -227,4 +231,4 @@ Thanks goes to these incredible people: - \ No newline at end of file + From 3ddb4fb90d845bb3113cc51c484938579668d2c5 Mon Sep 17 00:00:00 2001 From: Tommy Hughes IV Date: Fri, 26 Jul 2024 11:04:55 -0500 Subject: [PATCH 42/59] fix: Increment operator to v0.39.0 (#4368) increment operator to v0.39.0 Signed-off-by: Tommy Hughes --- infra/feast-operator/Makefile | 2 +- infra/feast-operator/config/manager/kustomization.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/infra/feast-operator/Makefile b/infra/feast-operator/Makefile index 1388778f9f..f446437751 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.37.0 +VERSION ?= 0.39.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 226b87118d..1c111ac7ad 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.37.0 + newTag: 0.39.0 From 4a135686430fc1d820e83a3dd854775ae3d85ff4 Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Mon, 29 Jul 2024 11:36:38 -0400 Subject: [PATCH 43/59] docs: Update faq.md (#4371) Update faq.md --- docs/getting-started/faq.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/getting-started/faq.md b/docs/getting-started/faq.md index 02f9db7d0c..d603e12ab6 100644 --- a/docs/getting-started/faq.md +++ b/docs/getting-started/faq.md @@ -70,10 +70,6 @@ Yes. See [documentation](../reference/alpha-web-ui.md). A feature view can be defined with multiple entities. Since each entity has a unique join\_key, using multiple entities will achieve the effect of a composite key. -### How does Feast compare with Tecton? - -Please see a detailed comparison of Feast vs. Tecton [here](https://www.tecton.ai/feast/). For another comparison, please see [here](https://mlops.community/learn/feature-store/). - ### What are the performance/latency characteristics of Feast? Feast is designed to work at scale and support low latency online serving. See our [benchmark blog post](https://feast.dev/blog/feast-benchmarks/) for details. From 32ec0e061a4eaf3fb968ccc10a465eec2d7d1cc1 Mon Sep 17 00:00:00 2001 From: feast-ci-bot Date: Wed, 31 Jul 2024 19:21:25 +0000 Subject: [PATCH 44/59] chore(release): release 0.40.0 # [0.40.0](https://github.com/feast-dev/feast/compare/v0.39.0...v0.40.0) (2024-07-31) ### Bug Fixes * Added missing type ([#4315](https://github.com/feast-dev/feast/issues/4315)) ([86af60a](https://github.com/feast-dev/feast/commit/86af60ad87d537b17e4ce6ec7a5eac0d637fb32d)) * Avoid XSS attack from Jinjin2's Environment(). ([#4355](https://github.com/feast-dev/feast/issues/4355)) ([40270e7](https://github.com/feast-dev/feast/commit/40270e754660d0a8f57cc8a3bbfb1e1e346c3d86)) * CGO Memory leak issue in GO Feature server ([#4291](https://github.com/feast-dev/feast/issues/4291)) ([43e198f](https://github.com/feast-dev/feast/commit/43e198f6945c5e868ade341309f2c5ca39ac563e)) * Deprecated the datetime.utcfromtimestamp(). ([#4306](https://github.com/feast-dev/feast/issues/4306)) ([21deec8](https://github.com/feast-dev/feast/commit/21deec8495a101442e78cabc9a30cb5fbee5382f)) * Fix SQLite import issue ([#4294](https://github.com/feast-dev/feast/issues/4294)) ([398ea3b](https://github.com/feast-dev/feast/commit/398ea3b86c83605963124404ff4baa95162dc1f4)) * Increment operator to v0.39.0 ([#4368](https://github.com/feast-dev/feast/issues/4368)) ([3ddb4fb](https://github.com/feast-dev/feast/commit/3ddb4fb90d845bb3113cc51c484938579668d2c5)) * Minor typo in the unit test. ([#4296](https://github.com/feast-dev/feast/issues/4296)) ([6c75e84](https://github.com/feast-dev/feast/commit/6c75e84b036f84910dcbd7f1733ebd0d8839ab6c)) * OnDemandFeatureView type inference for array types ([#4310](https://github.com/feast-dev/feast/issues/4310)) ([c45ff72](https://github.com/feast-dev/feast/commit/c45ff72f821404c595477e696ab4be1b888090cc)) * Remove redundant batching in PostgreSQLOnlineStore.online_write_batch and fix progress bar ([#4331](https://github.com/feast-dev/feast/issues/4331)) ([0d89d15](https://github.com/feast-dev/feast/commit/0d89d1519fc6b8ddd05a2588138e2e85f5a921b1)) * Remove typo. ([#4351](https://github.com/feast-dev/feast/issues/4351)) ([92d17de](https://github.com/feast-dev/feast/commit/92d17def8cdff2bebfa622a4b3846d5bdc3e58d8)) * Retire the datetime.utcnow(). ([#4352](https://github.com/feast-dev/feast/issues/4352)) ([a8bc696](https://github.com/feast-dev/feast/commit/a8bc696010fa94fa0be44fba2570bee0eab83ba2)) * Update dask version to support pandas 1.x ([#4326](https://github.com/feast-dev/feast/issues/4326)) ([a639d61](https://github.com/feast-dev/feast/commit/a639d617c047030f75c6950e9bfa6e5cfe63daaa)) * Update Feast object metadata in the registry ([#4257](https://github.com/feast-dev/feast/issues/4257)) ([8028ae0](https://github.com/feast-dev/feast/commit/8028ae0f39e706637bc2781850a3b7d8925a87f7)) * Using one single function call for utcnow(). ([#4307](https://github.com/feast-dev/feast/issues/4307)) ([98ff63c](https://github.com/feast-dev/feast/commit/98ff63cd389207998b3452ec46e5a2f0fc70485c)) ### Features * Add async feature retrieval for Postgres Online Store ([#4327](https://github.com/feast-dev/feast/issues/4327)) ([cea52e9](https://github.com/feast-dev/feast/commit/cea52e9fb02cb9e0b8f48206278474f5a5fa167e)) * Add Async refresh to Sql Registry ([#4251](https://github.com/feast-dev/feast/issues/4251)) ([f569786](https://github.com/feast-dev/feast/commit/f5697863669a6bb9dbd491f79192e8ddd0073388)) * Add SingleStore as an OnlineStore ([#4285](https://github.com/feast-dev/feast/issues/4285)) ([2c38946](https://github.com/feast-dev/feast/commit/2c3894693e9079b8ad7873b139b30440c919e913)) * Add Tornike to maintainers.md ([#4339](https://github.com/feast-dev/feast/issues/4339)) ([8e8c1f2](https://github.com/feast-dev/feast/commit/8e8c1f2ff9a77738e71542cbaab9531f321842a4)) * Bump psycopg2 to psycopg3 for all Postgres components ([#4303](https://github.com/feast-dev/feast/issues/4303)) ([9451d9c](https://github.com/feast-dev/feast/commit/9451d9ca15f234e8e16e81351294fd63b33c1af2)) * Entity key deserialization ([#4284](https://github.com/feast-dev/feast/issues/4284)) ([83fad15](https://github.com/feast-dev/feast/commit/83fad152ffe01a3b2691095a45b90eb30044c859)) * Ignore paths feast apply ([#4276](https://github.com/feast-dev/feast/issues/4276)) ([b4d54af](https://github.com/feast-dev/feast/commit/b4d54afaa83cb3e1391d62f4243e7d63a698064c)) * Move get_online_features to OnlineStore interface ([#4319](https://github.com/feast-dev/feast/issues/4319)) ([7072fd0](https://github.com/feast-dev/feast/commit/7072fd0e2e1d2f4d9a3e8f02d04ae042b3d9c0d4)) * Port mssql contrib offline store to ibis ([#4360](https://github.com/feast-dev/feast/issues/4360)) ([7914cbd](https://github.com/feast-dev/feast/commit/7914cbdaffeade727cf3cee538cf128cbfd86e06)) ### Reverts * Revert "fix: Avoid XSS attack from Jinjin2's Environment()." ([#4357](https://github.com/feast-dev/feast/issues/4357)) ([cdeab48](https://github.com/feast-dev/feast/commit/cdeab486970ccb8c716499610f927a6e8eb14457)), closes [#4355](https://github.com/feast-dev/feast/issues/4355) --- CHANGELOG.md | 38 +++++++++++++++++++ infra/charts/feast-feature-server/Chart.yaml | 2 +- infra/charts/feast-feature-server/README.md | 4 +- 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 | 6 +-- .../feast/charts/feature-server/values.yaml | 2 +- .../charts/transformation-service/Chart.yaml | 4 +- .../charts/transformation-service/README.md | 6 +-- .../charts/transformation-service/values.yaml | 2 +- infra/charts/feast/requirements.yaml | 4 +- java/pom.xml | 2 +- sdk/python/feast/ui/package.json | 2 +- sdk/python/feast/ui/yarn.lock | 8 ++-- ui/package.json | 2 +- 17 files changed, 67 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 798df5d024..6b7c8be4b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,43 @@ # Changelog +# [0.40.0](https://github.com/feast-dev/feast/compare/v0.39.0...v0.40.0) (2024-07-31) + + +### Bug Fixes + +* Added missing type ([#4315](https://github.com/feast-dev/feast/issues/4315)) ([86af60a](https://github.com/feast-dev/feast/commit/86af60ad87d537b17e4ce6ec7a5eac0d637fb32d)) +* Avoid XSS attack from Jinjin2's Environment(). ([#4355](https://github.com/feast-dev/feast/issues/4355)) ([40270e7](https://github.com/feast-dev/feast/commit/40270e754660d0a8f57cc8a3bbfb1e1e346c3d86)) +* CGO Memory leak issue in GO Feature server ([#4291](https://github.com/feast-dev/feast/issues/4291)) ([43e198f](https://github.com/feast-dev/feast/commit/43e198f6945c5e868ade341309f2c5ca39ac563e)) +* Deprecated the datetime.utcfromtimestamp(). ([#4306](https://github.com/feast-dev/feast/issues/4306)) ([21deec8](https://github.com/feast-dev/feast/commit/21deec8495a101442e78cabc9a30cb5fbee5382f)) +* Fix SQLite import issue ([#4294](https://github.com/feast-dev/feast/issues/4294)) ([398ea3b](https://github.com/feast-dev/feast/commit/398ea3b86c83605963124404ff4baa95162dc1f4)) +* Increment operator to v0.39.0 ([#4368](https://github.com/feast-dev/feast/issues/4368)) ([3ddb4fb](https://github.com/feast-dev/feast/commit/3ddb4fb90d845bb3113cc51c484938579668d2c5)) +* Minor typo in the unit test. ([#4296](https://github.com/feast-dev/feast/issues/4296)) ([6c75e84](https://github.com/feast-dev/feast/commit/6c75e84b036f84910dcbd7f1733ebd0d8839ab6c)) +* OnDemandFeatureView type inference for array types ([#4310](https://github.com/feast-dev/feast/issues/4310)) ([c45ff72](https://github.com/feast-dev/feast/commit/c45ff72f821404c595477e696ab4be1b888090cc)) +* Remove redundant batching in PostgreSQLOnlineStore.online_write_batch and fix progress bar ([#4331](https://github.com/feast-dev/feast/issues/4331)) ([0d89d15](https://github.com/feast-dev/feast/commit/0d89d1519fc6b8ddd05a2588138e2e85f5a921b1)) +* Remove typo. ([#4351](https://github.com/feast-dev/feast/issues/4351)) ([92d17de](https://github.com/feast-dev/feast/commit/92d17def8cdff2bebfa622a4b3846d5bdc3e58d8)) +* Retire the datetime.utcnow(). ([#4352](https://github.com/feast-dev/feast/issues/4352)) ([a8bc696](https://github.com/feast-dev/feast/commit/a8bc696010fa94fa0be44fba2570bee0eab83ba2)) +* Update dask version to support pandas 1.x ([#4326](https://github.com/feast-dev/feast/issues/4326)) ([a639d61](https://github.com/feast-dev/feast/commit/a639d617c047030f75c6950e9bfa6e5cfe63daaa)) +* Update Feast object metadata in the registry ([#4257](https://github.com/feast-dev/feast/issues/4257)) ([8028ae0](https://github.com/feast-dev/feast/commit/8028ae0f39e706637bc2781850a3b7d8925a87f7)) +* Using one single function call for utcnow(). ([#4307](https://github.com/feast-dev/feast/issues/4307)) ([98ff63c](https://github.com/feast-dev/feast/commit/98ff63cd389207998b3452ec46e5a2f0fc70485c)) + + +### Features + +* Add async feature retrieval for Postgres Online Store ([#4327](https://github.com/feast-dev/feast/issues/4327)) ([cea52e9](https://github.com/feast-dev/feast/commit/cea52e9fb02cb9e0b8f48206278474f5a5fa167e)) +* Add Async refresh to Sql Registry ([#4251](https://github.com/feast-dev/feast/issues/4251)) ([f569786](https://github.com/feast-dev/feast/commit/f5697863669a6bb9dbd491f79192e8ddd0073388)) +* Add SingleStore as an OnlineStore ([#4285](https://github.com/feast-dev/feast/issues/4285)) ([2c38946](https://github.com/feast-dev/feast/commit/2c3894693e9079b8ad7873b139b30440c919e913)) +* Add Tornike to maintainers.md ([#4339](https://github.com/feast-dev/feast/issues/4339)) ([8e8c1f2](https://github.com/feast-dev/feast/commit/8e8c1f2ff9a77738e71542cbaab9531f321842a4)) +* Bump psycopg2 to psycopg3 for all Postgres components ([#4303](https://github.com/feast-dev/feast/issues/4303)) ([9451d9c](https://github.com/feast-dev/feast/commit/9451d9ca15f234e8e16e81351294fd63b33c1af2)) +* Entity key deserialization ([#4284](https://github.com/feast-dev/feast/issues/4284)) ([83fad15](https://github.com/feast-dev/feast/commit/83fad152ffe01a3b2691095a45b90eb30044c859)) +* Ignore paths feast apply ([#4276](https://github.com/feast-dev/feast/issues/4276)) ([b4d54af](https://github.com/feast-dev/feast/commit/b4d54afaa83cb3e1391d62f4243e7d63a698064c)) +* Move get_online_features to OnlineStore interface ([#4319](https://github.com/feast-dev/feast/issues/4319)) ([7072fd0](https://github.com/feast-dev/feast/commit/7072fd0e2e1d2f4d9a3e8f02d04ae042b3d9c0d4)) +* Port mssql contrib offline store to ibis ([#4360](https://github.com/feast-dev/feast/issues/4360)) ([7914cbd](https://github.com/feast-dev/feast/commit/7914cbdaffeade727cf3cee538cf128cbfd86e06)) + + +### Reverts + +* Revert "fix: Avoid XSS attack from Jinjin2's Environment()." ([#4357](https://github.com/feast-dev/feast/issues/4357)) ([cdeab48](https://github.com/feast-dev/feast/commit/cdeab486970ccb8c716499610f927a6e8eb14457)), closes [#4355](https://github.com/feast-dev/feast/issues/4355) + # [0.39.0](https://github.com/feast-dev/feast/compare/v0.38.0...v0.39.0) (2024-06-18) diff --git a/infra/charts/feast-feature-server/Chart.yaml b/infra/charts/feast-feature-server/Chart.yaml index aa39b158dd..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.39.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 121b0cc0cd..63ff7cf61b 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.39.0` +Current chart version is `0.40.0` ## Installation @@ -40,7 +40,7 @@ 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.39.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` | | diff --git a/infra/charts/feast-feature-server/values.yaml b/infra/charts/feast-feature-server/values.yaml index 33430749d8..0c46bfff85 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.39.0 + tag: 0.40.0 imagePullSecrets: [] nameOverride: "" diff --git a/infra/charts/feast/Chart.yaml b/infra/charts/feast/Chart.yaml index c724a748a6..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.39.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 d611b69d84..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.39.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.39.0 | -| https://feast-helm-charts.storage.googleapis.com | transformation-service(transformation-service) | 0.39.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 5616d46301..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.39.0 -appVersion: v0.39.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 f4a8ea8cda..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.39.0](https://img.shields.io/badge/Version-0.39.0-informational?style=flat-square) ![AppVersion: v0.39.0](https://img.shields.io/badge/AppVersion-v0.39.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.39.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 | @@ -64,4 +64,4 @@ Feast Feature Server: Online feature serving service for Feast | transformationService.port | int | `6566` | | ---------------------------------------------- -Autogenerated from chart metadata using [helm-docs v1.13.1](https://github.com/norwoodj/helm-docs/releases/v1.13.1) +Autogenerated from chart metadata using [helm-docs v1.14.2](https://github.com/norwoodj/helm-docs/releases/v1.14.2) diff --git a/infra/charts/feast/charts/feature-server/values.yaml b/infra/charts/feast/charts/feature-server/values.yaml index cd60eaf93f..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.39.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 2e3211697f..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.39.0 -appVersion: v0.39.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 dec106617e..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.39.0](https://img.shields.io/badge/Version-0.39.0-informational?style=flat-square) ![AppVersion: v0.39.0](https://img.shields.io/badge/AppVersion-v0.39.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.39.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 | @@ -25,4 +25,4 @@ Transformation service: to compute on-demand features | service.type | string | `"ClusterIP"` | Kubernetes service type | ---------------------------------------------- -Autogenerated from chart metadata using [helm-docs v1.13.1](https://github.com/norwoodj/helm-docs/releases/v1.13.1) +Autogenerated from chart metadata using [helm-docs v1.14.2](https://github.com/norwoodj/helm-docs/releases/v1.14.2) diff --git a/infra/charts/feast/charts/transformation-service/values.yaml b/infra/charts/feast/charts/transformation-service/values.yaml index a6935a9993..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.39.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 1f579a5f3c..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.39.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.39.0 + version: 0.40.0 condition: transformation-service.enabled repository: https://feast-helm-charts.storage.googleapis.com - name: redis diff --git a/java/pom.xml b/java/pom.xml index 492e756ba5..2c1c32792c 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -35,7 +35,7 @@ - 0.39.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 66daf7b993..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.39.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 aa17626979..cd1913bbb1 100644 --- a/sdk/python/feast/ui/yarn.lock +++ b/sdk/python/feast/ui/yarn.lock @@ -1451,10 +1451,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@feast-dev/feast-ui@0.39.0": - version "0.39.0" - resolved "https://registry.yarnpkg.com/@feast-dev/feast-ui/-/feast-ui-0.39.0.tgz#9ab9bdcfd866399383b489f192e3d907590ac841" - integrity sha512-ggTyiv+D/i6sF5WZRxEFmVKMVgWmrdP3bnUzbDYnMpJ6A1UKFOdj29Ukh4F8DXDvrAskV1LjF+DZVkaD5lF4TQ== +"@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" "^55.0.1" diff --git a/ui/package.json b/ui/package.json index a380c65cfc..cd80859aa1 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "@feast-dev/feast-ui", - "version": "0.39.0", + "version": "0.40.0", "private": false, "files": [ "dist" From 67a63d9ef51ccd21dc56c3354aa800121577469c Mon Sep 17 00:00:00 2001 From: Francisco Arceo Date: Wed, 31 Jul 2024 20:10:51 -0400 Subject: [PATCH 45/59] 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 46/59] 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 47/59] 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 48/59] 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 49/59] 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 53/59] 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 54/59] 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 55/59] 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 56/59] 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 57/59] 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 58/59] 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 59/59] 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 %}

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 50/59] 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 51/59] 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 52/59] 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 zcmeFac|4T+`#+A*SVARK(r6iEO|piGRCcYF5|tt?BvE9U7L>wiTI^IxMG}%FOnWLz zRFbt)woumWziTjKn|n@kKJWMUd_Ujc{l|HnyK&F!zFybzyq?$fdfl%%ZDO=$8s{8N zCMKq7x@%XhXJQf#Wn$uz!mz_9YQEP_!~d8MtzVO@i^TFk~V*f=Eu=r{Yq0n~B zzoi1Q-U)u*S8~@*-|_DlVZ`#>hCTnDBY+I}Rir|j7|hat$rXpS;DhV`SA7w?z}ilJ z{YjyJ(;Jz!iJt!^C}8=f%PGv3(Bl5rU5S3@lqL)}5Kx+c(gc(yP`avUHv#P?4EqAwP56JZoAC1YstuIA z0RLc73+*hRorR%A5Oe_oU4R%~lt(E8N*PefKwTL|I}2!M0qrbM_G_Uu0i_8jO`sN9 z=mG?~0D&$*47FRylo9PFpxp$tn=rgKjc#Wg+E0si6VPq~+D$;a2^3I7X~Kvvp!*ci zeF{Uw8QsnZ5rNVKlqR4w;eSXIHos}`r0fM4n{TQ0`9HouUzCJm27OTyI=|?Pk`P(w zc?y8l=y?jz`9;rDK+jVc>T{vzDFC_nf0QO{?Q9MvAHuXf*sCP@-Zh@JpM8h9>hQjR zA^X!i!NL2Q?%Y4Tm7#I}_+|rEVMC*T`K*mgKq_;Ky_CZVmxc&5^<&V9ZS}ienAvfJoLFL~*g9wELJ}e3eP*9QAON`5FLppY;`l~G6lHJ~g3WeLO8Gs+TBmN2yW9EAiF5>QB>bdpe(fU<<) zhT53IVNbF}24ye6f4!qds|$&?8o?{kRwE=TboBuO5M6!1d!vwmLc(yOLLmW#1c(I` z5{AlIlqH}nVYr+{Spo_PC?pKW0$Ml>mDMODppbw<0tyL~ZZBFmpoIfv`y^U8poIfk zI8cxgZ7-tjMYO$0eH#TW9MHl6EgaCofte-x0x_Sm7XXEaF`E@o#(**glrf-?fI`9$ zQJ|^;?JJ;t1+=e#_7%pwSdBsg3JEAAppbw#a#Kf8r#RU`>{`cd;l$mBNpXRG>+waxa(j3@W@xUsw!F2Cbmt##c{=83O%V%cb zZ`G_C)z|TQaIn9pyQbv6tzGdYvV$NsTax(r6hfW_&pSUcu8_VkJQm>!$Fcdi(o@== z>}xpB2!C4Cp-(Xa{VzgCu626dV=>JW5}GH(PnmpQ<|Vcm`73qC_(B`6OD+qLZ=pFg zd&YFl#JgLpc!Cw@ml!o@CZ6zB=NEw$5~<~t`pJu#?YU_*F|$(`oXfe zB-7#e;)1$;d;fd@>)$N?d!~OU=4kLNIO6oX&6x%HyM(WxHxY+e23?JrWl*e;P2VLa zqm=8Cv*vh{(Vvzr~ly3eP!sui^L00>2qE(aFnxP zz(Hm7qL;jwUwiA-Tb+p4+v&}xPYP>S6hYI$=}esT7Yhq6@Z^TolN<>-h?KzvBKVb$ zF1YGM$AC6qK%Cl~bJH0UDd?)VV=jN}RWanDDUEu{w6LZG8ONkvj)*rM?8-_L|4N-$ zwLD0)?fcetaZ`k?Z!;*$4P@4J)hRz;qX0 zI-o{Nd)62h%d{H%GM|i6-Yb^s^t^S_*H7|gkaC};VA^yaouH!!5*C!jiLp|LQc@A9_X4D(no5Vid;%b=RTN9P|eAkMk4 zn;sgBPvV3^woRV%RZH2{-4M_!Wi#Rob2jG~f6G|0eXv+NC(MSTg=0VbgQwZftkbY} zZc9@s8}Qv%erhczamA`vbN)PRto>LteN8^(4Frm4SJ%w{*5!!&UBVPlriix<2ZJ(R zmT>G%;fUMc`^~rU_~J~fuD`B!*R*(bEo8vYYZm>St&v{o(Yv$dklW>Dv2$zV5sgaN z&VdOP{_xQG&pFSKrt!QihuZ`ic2_+y^4$V06BnMT;0zs0SwAwP_6r;HdLng+o`}6>AT)o~I-IPzr44|!Yx|Y%80E5M`wVD<98EWTX=$CcR}g|Oz&mY1+@HPB9$o@iP+LseU`=646&Ow=u*!*Q#PxiF!9 z#WQ~%L)I?MXXfJ^19&kUmsgafF{Cg*fx1e;CeOpkfeHPVE4%XR+MGT#Mk>;S2*;bi z3bn7t)4PX;YgwDHJTMMR{FIv2A!VJUq&Jt5oSDA>{I7GKa<`KbSDWs^OP>4n=&3r2 zoPFlf^bq81yY&Let;8qng`tD(47t*|*U$Yq{xGvxEhu!Yw%HV*y(u6&#+6r;h5{%8 z7XnH;^D;6xOe9T+kgbjL;IG?};Bx{wolP1wgaZ@yW?yvxG9;(TL9hxcn!KPV^W^CS z#o?poPq8rYFFa+CFQ$_ao^ME*;N+VpeUgSDXjCn2s+7G0Y(Gtzm8MS*O$Pr7YPaIZ zz~)I)qizXT zY@a2c9xU+rsEU49v?@cvq^ui#x6245Q(%8aM+;Lin9vnWxigT3r>XWLe(1S40<}q3 zncu4tQpir{5flGJSc$=e2|o>1&_#5Hm)oFq(P_$rkZBNnoA8;lTw^~u{Wvp;62*Q= zFhbV3jUX#L$@}`fCfllc_jU~lqskX_XXPotr%-Q%APP;Tfp^NSC1_~jJj!i@u9YYt z-83l@u=r>}f=1;U53jW5>w;nQp7|Rz7t>2)1Vmkv(^WNV!*}U-c>{IXg2f9qep7xU z_Ux^@_##Gz5z5BTZ@I1^Yx~64kzOMs24EMroF|XoBxvC8PX`u_?`;lR14i^FGSOf7 z7FPGa6O!Ibhv#K$;R|a_#ogH%um)L#EKGRM^r+jyzPE)tiu?^3LLQklB=_5wm+6Zk z!|B%JkU;f=uP$RuPcQK!UR+1y3|SFsB+9_7uw2Y7al4o9_C>ZkH(6QO=P|kr!bL)} zRp4qq`J)OES`2&>zj>JRd$orQhh#v4=~_DGkDdt;DZ-e-ut5(=67zqU8~Fcx7{1tR z^)!a~XpCre#e0SC(OihL!skP3Iux0mjLb1+YYS^$bdsIfH~1bg~ys7dkJHmf@wIk#jlIJlOcmwZ>Tt z>8(c6QSW0+mswkuk`Z!1*J(o*n17~|BIqAlwpwb$ZQfOGQsLR8qI!GtbeeNOQVO9X zVVY84*fp#`@q!hywrk!mq1T~=Hsd@7CB*6yHfX$AXd|EdmpvohH#ueImK2@?Hgk{@ z+t!nS8~#GqwEs|9BjJ>`V#H_aPH=>r#ciPHUy~y6=UhU@Q7)*-Ue$d&An={|<)($s z#H2sQ0NtFJ&^NA~bPP{Romg|iPR-}Z!(^y)>l}v2kkAe9t}VVQN^}}&K1-n9zey}c zZkSG*Hzn+U#UkI=ygO;_7m4 zt;i91U()-eW8Go8a5Y(e;U&X8Ecd$B!36j8uv6VJehKtkI4Bl^WJPWBy9wI*83cXiBfM}=2vX@q3+H)`AfYK%POYLA#%XbP*#DctsU zo+wc_VEqOLCP(m}HO%KvYgE0ika{Dg;~YVc#PtD*`>u3I3>k+eTM0I4#^5U+p2{d) z)73fPIM4DLW7;E0jPwT9Z2^Y5@tgSV=R1&sF6x67h1lG z0r_5P4TlM?As*KBA}=T3oa0Z2bhh$CTdB4@B9x{Pkqn1-VlnNy`&KU>RXpqfl9a$luerNbNgN}V~$cq?oJ#KQCe3fb3^CIw*3E+l)^I=hMR4Fm76Ot z5-!-FC^8hbU#Xg(aBVNWlvJ}295KpVO@fx5kR5Rm+YZh65wg%K6-tl4O)~dOA(qI1j>-M;N%Sq6%u@FjiL`!YE}HW)-zP=tlM~wUbl;e!{kdRR^I>o z{ou@wU|0q&cx_I9grm?ed$Tj&2)TDf3zxAfgq_E6;Os;-Umv8hgSLk|k7T{6JE2h* z{0g~##=C$5ljo*Sp#$qQGQ(Q!>V|>kxBvQQfL$Fog7^&w|0GtnW&`nh9d&OegGOEt z&U=36TYlOqqA@El0&L|N0m1jjao5u5z_blHg2UFSi8C@h6Tz91?5=eAHs0)@mriL& z_}hLYlbo2JIaJiB9mzUH!#^qB@(&$Pg77g~cYdL+{jpHwW)m&(rZTtWX3U`@kOxG< z5AC-ibZ(*br&SzeM7eX9+?ilJTC*)EJnP;c`CBU4-y-i8GqR3^0Z<{mICOsQX~sg; zjUQSf=RO%0+VEdS1|fnJaA*2mHX7!@1_g|8=jCK9zF$d)`qJ`ZFTLTM)>-{xI9u>+ zbD;JHNnRf_wZm1*q&Z1d+&^bw&pf}(>cNg*v(j?*)WgpF&P;Wwf+`uFp2`=K<)VI40Rm>W9iB%^Rn(g%UJ#TCyjET!Da5O} zC(@2FL~MV)Vi`1kTN5a2TNWF*=I+DvUlRu(BZ&kEx8Z!^)G;58+V0%7}d>nO|?tmC|OQ287*SRjeYB+frYbz3}S&ku%WWUG{8cRWBXx9&L($ zYvYhA!%#B|%P02pb7R2?m-Pxq`7Q@CWC$(RfvBsw^R4nzjpqky$t2QQ^&su#6c0US z=#k9O127Mlk;6DD$MWGa(OQPmd{;4@3zjL6{cTDskKav`1=UI9BdrKQDa$U02j8q@V(E0XVm`Llz>T`_kviw=e z+GmpQu3w%-XXuH4B6LZQsJ!`{qo-R+g=Iuje-V9aySg+O^wdR;c5iDm zGc~<3*Z!A`Ne27ET+Qt8{ZSes^V+=_OK5nkE~%;zcYV7cqM=A-(@hk*U6PU*EZvo) z9qR&XR(NF|Dekg}q>&4dZA+?c2`sx?CF`!;_uDDeqvuEpea+YLn<$iq#4)gmko@db z94l62?YsOSaB5SGT`N{*K0^f;E1)Vj_cOhTp+cP%K?w`-Iqjs44x(ylD#gLF`ujkO z%b`KrmSumGA!&?4SVhnt4Cr)oO|onWm}z`(CRM6Q7o=zayOsN{{ZUL%0qmhEvu04+ zHliyd%PiF%Tq7EHUl<-GvftIz*4?W`8(BRBBM9w?^bAJU*6`l=3oxUb%8ccFIRH~9 z%qJm4Vela^Y7#zzF(!dsSKZe2qN$|ia+Yc4FdLyycMy!}j~h(?++C*gMS|XIKtd1) zDt`MJDzBLcT}#LyHNt6IF21fnbm>XX1JdfS%+DTJQ8!12IQ3vRGDR|xh_*|T-@Jyg z0`BFEpI?>DkAC!_p%z-g3`uTnMAiA!fj`5KnSdXcZ}~G815Y``L(lp@)nZ6p+K}OV zqt7AN@cb~U6lbvnL!+{;hje=bw^J6;eQ79=@E&9_<#Lar@wZ{+7K(w!rx?%CBmbyP1Cajh8$7qMS{CNG2-kt&)cDf3lvHIEe*$lb% zc1Rpgs-GjPniED9I9bQa$f(RLxezhL;(K!uJ%a`ZCE0^4ai_A7`Dxuhf}d4n_&M`L zo`%YhW8?`Lejc4eu2cCj06&)*P#9!~um%8A;HrkDN0lmXyAdzSAmnnThs*HD=GfYb4un2X#P z66b@I!FP2gL!)9AaY4#3dU1fE2>5TMjKz>LmhdpvL*<4E2(v2%Af|N_C`^*DtHk~& zQ&1ZxYoUq`2@j}XNSm8MmaWo1JP?5a9|g2J+rW~cRcAa_4-)W3;Y-LgipJc8U;--K z2ZRy9I~ot8uJkjaPWg{6f~3GkD)Tata}5oR#e*#A79V>Nos6IXS;HeF!w=Dlw8G+_ zl|C>5C!um>WE&B{M#uRgXgv{GJghlr(@E6@IoHszk(o5eazC*PxwGT}8LU_SFoQcm z#X*~l=LG!Q8o9KC$TnhuQQ1UQ3CK#I!3Xe3pv@ZRAB?=3j!m=xS?ssBAh#%bfY5aA zYFL11MVdnXqlQ}|Oklg?R~l|TVo^4*5to)T3u2_vPzw{m2DF)5|M476K@p5#eII0b z|F{zQ{YI2@()I|HbkcUI!hcrtq6CH}9Ppt8hDMgdq6CJfxBS!I?f+^5bA(TdvKN3> zTAZ9i2`HJNttgbt(56mk2W@f?kv~B@Xtb%JA4+CurYDrl&{PXbW@xMBA1*}yuO>6J z6VL?xpace?lZFx)+WvzQ7?i*ugbd9l4oYBXss)P@mN4?`P`Q38V!7}`jJfW~MC zhBoY=1O_EA2q8l=zUX&gf?o2-Q1${Kq0AoZH8{{rNx0r z=;mHpYGERvZI7pH)j)f&I=1Hzq6S+ueo~V$qlhnR!lB!l1G@=njJ5yzp-|?O!OKhHa(rW zqAD|#F#Rd=Vq(jP>dPuS2a-R0D!3GItVO-d{cgvx%LNyUj*ZtjB?_?;%vW+_o^M}D znTpbK#e;Kp^oKG8XhV-`GPvzht@+DI`*Gvh=gtYN;QYdh{l zh5|N?S;5B!^M%6BmOSfTihT0^@RMGeL2dH&v$-mAD2jn-k=0q37!r+S0;Q6#{GQ%1 zM~GSr65<+rL2KH5E6q=ykCy&O+zMYhhd7tPR~NF57rApJ>#MEf&M@fr-wZR zvwAbtu6vBDDlAAewr>3@$YrD`&q?=(ZsFD4TJWU^qC4_wzgTy8!jCm}Q^q_2nI};# zTqFR~ao5;9W;*!Z5pSF8{#Uil&I=;+s~Un4=uB;UC-SeO;@5%jI_HWR`0;tN{$j+i z3L%vS@ncSDAsk>0z`LZ$o zG(-M|UkOuUn^wG;@*vRqN8Rd%9*s98Wqg`t**hFtzx1Uwl^j7XsE0)t#$pwYGDY4+ zJ|2#J306U+@KKrKM5m%VcR~V~CG&iz$QXd?5vY@w>;AQkO{_OEy9>FTB~Pll7-nch z<9v0B6nIN;xQrdme*=@0+bQaez67Pcw2ac2rw834^|i`?;wnvLl}mduQgkQA%u)a| zo5wk3USo{#p9Nm*GBSiXram;Zf4_sMd+y1O36GjEJ3bJM<^PCaGYLY$;ddX1dfss3 zFuI{SyOHj3iPm;tOzp6=eq=A>0bE;AUdZ5iUVOjLdKbog9rMfYcwH71R-9(1K)6&r z>nE-W3L2lBRX8z*-<6l|9h2%Nn7&;SS}%8V9*psp@k7u=JMnFh0Dn&}!&0A4c!YT) z+w%BWR-RK%tk|05{R_n?djUSTH_kul-}Y6i#NjQ?(Woi#CAqOyTW_vL9d_bH^n8d3 z&+@E`6mOp}9om3QvId8qy3Bs==wag~@GE#7{o2;@p5Hw!A9{)^hV=5?K|V{A%r<_& zAjxw>*pOA_1}6UD!7A_v8l=t{er{|Z%qgjMuj{CC^aR8u07t@E#q>4OJL#UagtrIJ zyJh&)4LNY?$r!D5% z@SGr+gu$aZ$buc^0QX!j8#ii}Kha!J1PJD`Fxh&HU;@Ebm8ZJh)WlExY2wNQr6@Wy zoE}yB1UU&sKtJ`P68n*<%7kwWIcpvGa8fPv5%n>ytH!a_vPzQ`_qEpKu2A?xWlfQW z&Ze-*DjWOB9nekc>c&UN^6dcm{O8>{$C!L;wIRmNshF{ik?%bV3$ehGRjl8F){I77 zwJ;tupBx#9?2tmop<>{>u`du&>@klWy~m9o0%v+v+!)sVJAOE4ZP9Dt2#*v#B@}u# zN@mpJqu(7PvfXOX{p>Zy$V*q;x_CRMwZ*BXe>2kPoQFJ89fL9K^lrX5=BdXvP5{S%_xS`$d#H5|d;Bo670Pvl+TE-rF{ZJN)!1>=B z_xnX9h!LO^L3A}8T237!a&VLhEz~Z%tO>^C+ zP{){f5v>5gUWBvPL==DGH-=z&2CQ`$?89CfUC^<{vY3{1ecpFeEvTY7JI}vF_VKgs zU(HsIVl)RB^*~`M;J$KKrXp*H9cloTw7<&a{$`}{BM=S%_GET+DvpMcwxAy&x7%&* zm)J(#>h>D(r!Q^#{#Jz#K!p`gcSIGaj5Tm>ZB)KIPvj?zS*w#VJ$ zAN7M7v-pDJMc&lsuJijV_jf&AdJa}~CQBA_>f$7UsFreqw$+pw8~qlz0qGJ~nM zrDG)-BFdd=^ykuFm%8O>NzK^}JP4d&?O10gHgHgxgHt z;!N*qeBAtS%}x0(-7T7kqk7|;0m;&h3wuV#XkUnxGj9@^LKM7fb@K21a|I}%Ett3o zxqw_xUg+O5rz+uFe{jvtRqGYxAFVVW&0u8VY{Y&Fg&#Y+?u^N9B`q%Gs*msGMo?7o zBlFRg2SJ4G`(@Yd*_EAX$J@6_%S~tu(VBWDkdNji-!#U+#TdsP!cypG=0@fp54JJ> zxlMn3#A}HDJb2PcH6n>_8Z=BukcUo7bV?8#uqQiambjad`F@lo4aF--lK{t+kM6;{Y3lMnmz z4n)kgF%d($%$ktU^5j_7v|q&?iyYmY_;=XIduB{f*F+^_tzh!UY;C_GMm`}M+-A## zz@@pr3(6iioLaoaxzldEJqj<9j;!FhyDiJ@NW@PY(}v1XUNPPvAdCsncN>lkDN{#> zXu@C&HfM4gq#mkVft+uKrs@GDOXuPiSc0wF?PK#g|2WaF*i-o;=k6n5s&0%FNNJPWT4i z6k>X+-ix%D#2SKMw$b<@IHw2a?dE-?`!-jL-G1(buZ~TP=RhlptLs;e-g1Tyaw@Ow z>#_x%MKw5?Z8;dh+82fz^1Zgtl4sogor~)|_-X72qu(5!F<~Xtt~_piipUEeAN7H2 z$fokK;<`?@EgwHcIVOTo_}LO?)p?`?`~RZzAkt+pSI)i~p3(1X^ZDH)b-VTQO@rh% zRF;LDjY@E3iBgr^5pIN;=)mJtmQHR)sj4(0!%{ntQS&Dn&li8ybg0Vq_&u3FW-sEq zMm$cmTHiVzt;aoxRS)_wtpDTqPF4H>$%)<`f0R@EmOD9Q`rR>Ijm| zTau_nR!BDYQiXcA8Gb8^lE3Yjp7ChH!qxbBlieel47nn|vO*CJ8*~e5bX#^Yxsdu> zsr$yu6_kcQYs6`U(^&KxxmM+~gY1#C&WLyPmY`Gkv+Vig&O9XR$9@IIPp;UWx@9Fq z*CAA(y8AA-crS8aDrPY_#L~cTPO}C4bu5VXcFVv1O1mO6yQ3s{CI`}D7|jS=`_TOG%uT)L?Vy9+dZ zA$I5fi3X)knxFvwQ5B}oL&)vY{)-I3X8o~W{l! zy0))#TBf?;J|`tHS30g$1I`*!@-TjMVU?7?VyQF5)zWxL*Rz+O!|GpeyhMDe+5=b_ zrF*t&!RV|v@rbjjwJUg!P(ZcSr46cpPu_mL!l`RJU-awY<+X2o2rpeMjeh z38`Q+!r~rDLMKMR6WtSC?uzLwS-7LDmq5NDISn=k;+`ch zP!9P~@bd8lU5yixUl#DE&u@RwL>Hn`yTUY5v=(tGX8JoM<-X9j6d_16saq+O?K<=W1P?q z;$2$wJLJnOTY<09zvc^$6MK>=GN5$$q4&uhd9L3u+&G0&9h=srauPD;`Mgh!#@|bl zVX^G1Sh>&XvTm9$qBii@O#l)b#deNiU)fSYZVEqNtJ&2C2uU2{7D#RL>5B|4GoQ|p z*K*dJ-VZPn!T~-HMMmBq1?y3T3|pXQy3i^+C;5<*dAp0xB&OL&#*2(veaQSt{NOZ4 zqy_vpe`smpyOZc9j;t#DRB+gNSohK1IKhX_$)8>EVZX745wv@E4r!Lnf&p%Ubvn5BQfNmbZ0vA!8N*8v{Of!6* zR%0W7tMON9@3X~ozqQMaK9T%QISdfYV-T@^UU7D zlmkVt8(A9O-yX7fg@O0aZvL<4A>G)grDC~Iodh0b|tL8M#$va zSapm8Bv%&6qx{uYlhQvF3oxvUrk@>ckio3+S!=9Qqhj6$h%%F4tmee((P#={6f zhOMvZs{Z~KI=Wt~1GWD}U9-`x2LxU=pBdBQ)m*_(?hUty4yz<*TRF-%{xV;I94}0S z7$`r9b9|poLL92oC-I3dQBVmnc)UA#53A=25nmyw7$z;`;EP!d*~PwA$!?C@RIJ~= zV!T69(RS!!azQm`rqB)rN5O|9m7g)o(zR}>59dkguHcQe8pOhOGu{uR=i?E1f~l3a z;_X@;wT&kN40RqDZo}x{LL1^=xk9`7f`vaBKk^(uAij&m@Hy^lL%-p*-q~&ok+nuT z04S5Gl0NEfctMt>bCk*ZsGzFa3ainJe!nF+Zq{HuxVle}?h*+>8w|kvw#^bzVI(=W zGf;re3L(RcjpHQ0%HX$KqFnDV*U~62b9Azk?AZWZ{li-)S?aV1F08Aj#90~}2WW9) zpgXL_Z2PTEY^7t7?8NT6e$-cO%3gqR>Lqx9@LlWL<{=B8$d<$l!ta2!1)-gjh410} zxKDAre28$L{k_~19O~4BKlvh>4KkB}iTZh};qlW5$={l^R}^+Ef=->soZJl{! z#;Do5MgUPN@(9_m1mG-0Php%k^FsG6ke~gPE4kFbCaF31;Kcr6;Til{!{>E1qNaUY zUO20&*G$iz&?6f+e)Kw+!D#AD-fv_f6j<)?Q((uj>ADUaRWBDRD0))c zeK1QlgYFtL;SpdY&w(1nQOupFHf1wxS)t8_zjEK(AC|j1c^N5`-|kC`$PS{R(bME{ zvW_2!XBKRI*;qK{cd_2hPhimQ{n*}&6%y~se0 z0U(cL&A;?OG;+4-A(rFBJaf14E!gpqLns0l*AKhdKmKFN&}2(~5_~DIpfcwi&-v*g zEBp#Bk4F_ILI7*1pCP1wIgvm$i+d%=17Y|kw#8Cp<#HZ!wRNQS3+`%XC$E30NH)#i z@hG?RT-LZvrW3V9MyPbdH*OPT86$Q-myKX(xB+f^a_J9gG5K_*2-)$Oh?zwl^EBtg z(;Q!6r;74&;nu@d^oWOJA~Yo)ofH{``fB3HqNje{Lgp(uwQCkcUKCn*E-Imu?aS=5 z_lkc9jshq(BF0`o?J@SB6X$42h}R;%{w6Oa;Gf~%*F5mve%AA^zbdP`Cbg6C^I4WE z&K|_g(CkY8QlK+uP1{ zHTG{9gl)`aJTA$*Q4d; zi18oJ1XLG&j_bjz`8mCTQFS$%itB1#30tqNTI2UDpeGK`(#u$dkzd~UUSGMD%d!&?6dqv4OU_&&#( z#%&S`e8JKD<~;I1tl$u#Qb#jJH+B_{ZJ!@T<#>c-cK&dUYglOIY-O*GVOBu=xUj zF?NXI-jpRXTxH3lk*tq$SJduC_5jdzC#Ear!v5teJyP zD3atg*jx3pz-!>pk+~0cg0CNw{P|@5fL!127nuTO{46@U+dNr-K*vksLIin>=>{bd2{j5y)Zl=!8AIhmq@pyZm~WnUuict~SRU zlgHckWy7X+f)v|8U#+HZRe!VV=jLXa*Bx6Fz#yUgzbm`41wA|BtWH(+eOLM%cQ@O< z^=;(P9wiR0A9KTm17B>vZ?X8{GJQI+FAjUTl4ACZZh^&bA=9BGSZ~F&c&z#wpVA>| z@HIXE*qlR&bT%_Hvlx=M0!lX=9W~no$0W3}ou6hHu=-k7`%bK=&x|4XH_0Y}wOW3@ zwi=M{YjW_C^h$PtO$~&3Y@V&esFRv~pLsvmY7TY>R5=dqZ>f< zoYJ}dpIz-7*qv-1Y!rJxrZb~+Z(+bkDNZo4ZUR;N;H>>9FdZgDiIyQWjN$~dxpaagzw zR`s6fBp#wSfe_HeZVmkF(fl>h8``lZ26pTDulN>!C2v2hGq2uoCD*F&i=gOgm+YQ~ zxJc8w4*1;8v|84yTi@(%$!GAnf~m4ILid$a6axndN+Q`Q+|GIk`0izTa3c1TZA^Kj z-3ACKL(2kcLVl@>S4ecTAa4#0zZmXWT5;#S!*#X24)oBcFa`KT@zqG=y}{yE*jp5L zOYPaG$W>gQ(%E%vh>*4l{N8BS8s?DAk<;Jg@FdW){n|q%JMsj7A16I~rt|yj{6TSr z`WUj%Bv+QOyzinJ+qzmGT%`Y=#7)@cZf^RfpE}#?9Aw0AC+|~p+%|^fXaVayPUIPF zjZDrTgg$`7wy=;{ql}5F!I6s|ftMNzDk6bT?C&>}z3n9v!RV>rHX?;t=g0L-u(JuU zuk}%`>gAlf@5WZwlO9XbfLzmivzv$V7rX?3U4fVcRIV8j7ws><;M>EUnxVnZ7w$}V zVWDOrtWp%j$0NU)7IX%`{z~p!yNzYL6vIx{J6rU+XAikZH#qBeO>H%JcSR0n^Q7+> zIS8(E!dAbth99z^b>sJlK7&vM)s>la7*Ilj@Q!TLEgp+?RQT zG<zdJ{o>hFZu{u$enDLDABp+m}HQFos+_x z_ad+I@(i0=eh(efaRF>=o-fsOl8Mj{n(jK(CQY7+4$S2K(?T0=Bf2x0)Ov(Bw_qk0 zg@XIGD#$%Ece#hV_q6ua;R4Pl4a6nNrO#wPs*OPps`!+Pcd!Z{@9-UYZv^2A*P`hZ zdxSn4MuHEn$NumK$e^;-j+xQ9PDa!MLO#*3NEs{j&zoKTdAeQeX`3gLa}8 z(a_&bgc>dOEXQ37#MUd#3I1eIW?{^IK2qAD%9=x-L`oPHlr^^|b;MHhGMj z9(FtU$w}Xxd#ETf@q)05?L5ZN2uFlWZ$BNX-gg;QdE}c+$VJxCW%q&2HNd<+v^gn( zWIp0@bI#d2lvQoDCw31RkcKmmXUzk%QNyNF8*5m7g3z>~my@6w!P*=d#cq4ON3dvP zkV22foL=c3X;r!}DXs@EH#oTD1M+K!YqvyqQ6TzK(RjmrBO69(gW3Z8n9|YOkDNjG zDTY~WvAVN)`wEego`5iF0X$BP!`$*1khfNStpVz568HmT@-}ntk~zU;M}L>QMO-{k z@T#x1S#+SG#{}Q^{xrp{Xz8dh2lP9*ED`zDFc^NcrBSU0ui5$%n7{#n8}+k(BzNYr zrC_7{QbqIHGtlA5SV_NJ_uA2L{l^vsKDWGgu{d!3lw!;$DkNrYn`H8e7Q5UkT*K;oo6r&EwG zq^~fc#jbjQxUVbB_VpIgCxLz&)FeG$7kEt>?DiTgZsvVLW_)9Sa9m>wNR$!y+IBiO zfUj+_b4uUp^K8euy;;enQUO=p=172tcWhn$AG1t${=s^-jlpxcJb6kxn!{b zSkU3k*-x)2!OYS`E+-s*AX{&257rtFYMnAC_09~e32Y0kw}uT3ee^2;4zMYXU!B@iiMrLjZzq0%=yYb*RO19;u`QPjD>{+%gu&$$kz zv5Gx;5)aGS%6I5(eW$SjbAngmAFyPd!mYWst|B;A~S66QKY+Q&p5KjEO0hTv$4lNdB#NRhxi! zX|s7y_W?v36GTtp^iQ)ZdTjHVBieWV>$66|&tLj}>p$4l(c-JP_eySjKVTPyeqa1wHzft`Wq|e?OlkE<^qvqE@Y7%ew3C? z8e{}^Y8JuMic{jLn}w@)60X1^$XRv~x6r$zo1jhHf)rrAbe64E&te7ho|8?16ToVW z_OXjWf(tjcOH(a7@q(~|)s2JPqOnlbBHm9Ub4Yk48M6ECcZz!6W8Q1IDLLGR`H+6# zz=4MHW@IM?aV|ns@vu|e7oX2^-50l0LGJnYt;0L)uC$YCUStM5$}QSS)FKYNMlO0G z6#(hTZH~`E-qL4s248z6*W)-l>_#~I;_Lc1O+yefhn&(dMLRKkFE>N?qU^1?GDze$ zO9*mJWlM|v@N6kUPE_JgKr7ufRII|<64q-Iwf)(f-PnEk1k2xl>F@OsUL@6RzESH6raLNN~GcF z5Yok5d0HIc84{J9Lw)9H05Xbx^{{o_269Nqw~tcfCUF+TOQ!}4 z*BG~*@MLbFjZQ&3AM6>yMf_5h#iwvOQ=;bU!`fWbZS0;7^V5Us;`K8^rS^n&PTWgO zO`Oov$pD0Hn3;t9>IL47YzRmb(rlE&Zb;Dpt5cUB2wRMg)&djU8!>@mn4#ajT-6f$ z`<$@5Hs7=yxl;(hI0f8k>T!xn!JTGk5up)b91V83TV#IAC1kjT*XH$VNr{YtdHLG? zL+6VOXu5Za?0v;*CQjK4P-^~xw=cqly^j0(H(u5Saw#T~)v3qTm4&X~^xKYG$MJI2 zD*qR2KRJcXiPzJmSo^ms1oxg#Coy{UAVu+u7Oxsj>yA$tLYY8t_5NAG#-;$rQ z>p;%KSdn~#f=HFUce6??3Nk;~F3$VlY#G_rwxVU5JLbvdVbPU!s;vgTS!E9nR=AYy z-%z^Mv-?|BjwfKkZTIwG%JhZ|=p%`>b4ts4;DnscED4)W;2K$HmmjT-I9&2wm3(Ji zx_5MLq|(P$44;`PzOW`^mC=yGmtpo*WE@^FcO`M=L6wkkPa4OXtj~H!>+Ym(hcrpT z=dL7bO?hFw%}RSpY=teCT58Oxrm)y7LjYBG?5*hBZRg^~Y{U_Y!SfK`h@Ox2(TnWy zQaYV9Lpi-Qea31Dza-tmUDO^Q$b>ItIgWeX9MyoFSuHUsa}16m5+c-GZ)p4SY|1`XmbzcU*HCrb+$IFZyIFkD zp36T>1@-BydecJru@79_dPL@!x#nPkg`4AlN8ZfbyLAl2!nPS+o{Re=O_G`|4prCa z)z!Q=wX*C|Oc+;t5JWgIp+D9wlctu>6P^y~?>s(kC6VYOwAH=o{+^NqOX+n-hPIer zz+vr@QUY#WW+Iq#VM4?2-|U43sbH#`77d-X6E)$HM5_DA>2k8|)H2?9S|IOL~?*?8uuUT|qHS9$x*y{p+!#j%L$ zzch-H=l~c*)jok*`}!>~(W?tDB>kG1oHn@DqWl5>u5hh)+c&>?bq4O_+p>Q8EHx#UFh-zg8`6OZa>YzeliX-=^!vs17iykdLwDpP_v0Ts}vzRn*U2{g&IH#;2}*S)0=AH?w&9=IIrAP@VI}bUp=@bCG+IBjE|ZowQw& zk@uqr*GiQ)31-X5$z|gVaNyIqu)y4IhA*@U4BJa?64|Y^Zv1d(rn*jH&8n?NVkjXp%J^76UqP*Nh<&(pZq_6}vu+~gAYR#MM(a~YpCHsWI!fMU9kktkE zk%k2)AaVkzYMfl@ z8?hfE78~$u4Ghz~FLQ*(m+-G7c9bvJwzyh+hv}!g60MP)>Jz#8W4roG-Kqt5B>yhWq4*|AL^$HL9j$m-^nx{}s;G=C}p6(w^ zUPJ7{o+@9Sd3tKD&GCKj_Lrqz&vNhDtWkBE&b(QT0P**TQ1z`R;3uFEqCG`0)j=l2 z`TKpYXYS4Cv2UlXpxrLzHlX_2g59#bpB?Y{BoB3BKG^ApK<%OTYq` z>;0kMYil=nKG0EbPWu{g^ZKngYr=}QZV!Q@o%+?YRL!&W2e{W&*o)it%u~Ezw1skP z0yY(<|FM4^op-OV;`gp3s-$733T;$R$m;x*vbQARcC6Y^t->x2#K7C|fej0*(M*^R zmOLNpIiN6r@X@zGCIma&vYrfDXH=;581_6Ka>66WxZ;Bl-xqqnDqG@(>OY-+nVgj7 zAo0!k#T1tBPZvQlT67~NAR>JmSldkkT7Fxe#<2?P{Lsr>elans>qjZEwd?)~Qar0( zaIg8D`*(UV;ajafMTve;n)7M4r|+~oUq4W&ou35OPJXx5M&y-FKdC~C!kQ0vwum`> z%yf15l)ArcRf+NtU-4NC^ET^Gg^2k84y-VXY-zc3q?nb&ZgbblqqPcw&eDjS+Z*=N zLAa!KU!t@S>InE4^7MJRF97z>Z9Jo|DD6A-yRomiN*Q;}Y?Z`Dt({uY;>@B;b?!ID zKjQ?4*JrjIb@)5CPWJ!E%%}*dYb$PbNCU$czdh?s%3YS(vuxF}Wu~o`{XCNC?)H>L z*7h}ka$atG79)BnA(izp#G#DcUe*=lg6JDeQ|f{15;45BA~?S7*E$*au22$*tRIVk z$}IO{&<`%-IhhhsQ~kt?BuJEpsJH?s9}}G`%&k&z6g;?T)B>{X7E%e>%p!^M`J?TG`6z1aDVU z%XGJrqdD=1+id;ZlHb+F{UB<%mVBZ}^^J_2v8GFqx8kjuZnxO3weX7ep4$9~nRm=7 zxr>A)TP>+cz|fl{0XjH0bnc0PWYAz+XkS#>Ms-%h%@>`h3oetK-kWp+1_-4d=SesZ z|9oB-2^RMda{FDUd{KC9XxE*u&dkh2+*N0o^mSLq`<_Ft9v??_b@$~LQ!aa0Jx~(# za9=-AamiNQOZAiB&q(v-UABImwX!`kn*!_Rt)D;Qe%3-Ds_$FdIONx!-mJ&k6xM+G z*7L|X4jGS-42)4>KCGNs0^Svhk;rfL%Vhyp)K9CRvu`pw6mGFM%BL=mlcOxDEW4(V z1lTR%1u5(#dqMF4QjeQpH@5g0QY>j@Xu2o#d7!B@v!b$?1FOqW87W z?QZ3IVBmF3Lk4yS1K%d~^*!{?y=^uvs@8*e$#!W$%n8yNZ;$N)6a%v^01KZjWLu8x zhOcIjE&M&q!APmXdBd&{D3lbNKHZON?KUE7iLf1cH>t|G{ zfn=Q8VirIM}P8j{_MC6dWIB7LxAE?0y#S+$R0)7Hj}Hy=TkA^jb(= zwvuCTOl|qav_KEr2g~Mq9MK!NbIEpvm+B78$DHH1od!GNm-O-YzvrbME#k8TQ0QjF zg9C^X;XOe7j;5_c?JW2)h|nKt%p%6*qdt%0)Eo#bpfi()q$}FKMfnkGl6QAPrLw1` zGV5Z2EAsgfpr2VV>TaqVMBBhWn_OkjAMK5sKe!4a*;1fuzOH}orK>3t2J=R$v?uSb z@v$WH0lDOQCe+2}w(&0SV)l3Mx9l0bJHzqaph(o7Q`g?!eEF=UG+FFh%YmtcWgNdF z1P&=abbufUe{&!%Nj9US%b7A&!c<@!q4c(7>9SdHguWrLsiZ|W9BjH@a>FvyWs`cv z&>kl_6A*dI{)f9}Vt$~j*0u9&x@&B0emHJJfs{&2LgkrWcZlhM&dz<3 zbI+~Tk?jjmYZlM&e5SOpy5im0#`tFYDk!SoOLf$104Ki{_JPv=5r^i1O?uV_6~v4h zElCJkNo3*5m@}39au?nl{QGsoWqS_3U@~ZxhGmf7M)46(Jly!<^rbAwcGX9=z_cwFG(G$7^329nQIdF6C37F#yQ{0^T?3ophsNu2 zcij$52e0P8Cz$hQgkUmf&Xs_Y%K{X1w`}i0u(h3`HKeV2NM9GD z#O#vfFV3Xn&PGt|?GdITCy-G71FqI3rzEDn?)Yq0?zKp|c}v5S^}AEqVa^hHuc)Qi zvM^1h1z_d1218aZv>{uWziyO|mWK8udU?1isV%%-QPGn0dtT<(0JSfIrCO(U-)F0N zEb`gaZ2Pngls0N~EEvrFli6!(;+V)BFbd*@M+$9tmb@geZ3-lFY+KXE4Md7`vYEuIF2hj*vU^pj7gRz6_WG02TMGKg_@AbaA_ym#>UW|u;dZZ z$Piu~syQ!?g+0-QV6MpP!=J_C^`Qy$+1(l_6u7v`rSg`H=q+2wFeOJqb57nk=iT^73?b zT_dBSjJsV!7AAO`e0*Q|d)caTm8RSBIZraqcWtFUYXh0=j5$VSA}=K9!VN}Cz8@oh z0|T8ic>_s)EpPl9kWuY;ml=HAZUBB=FvnhH;&-X~B1FU8ClptkQ}4J4(6o7n>Q`OB z;^LY_JQ#BSNIJSPHzU$=|7JHR+HOe$p=yVKB*jrjda*cq&%gn@B`3Od!=JJ(uMoG zL@Wh4L718ofJfOSd3FKAx<7M{vS;KQh*#m&8HpZ;`VH&!Sk!0m%_xrAJ(PhyZ=)6u zp|@@Y_I_|ZvMhQ1W6NIUg2idZ7S~+g)bAMJURuxEF?h^9W>7tqvhxhj0SeczJ3j{* zRqlZWo;Uc~lVQc01tvbzTIC`WVH>2*AG@|llTUrP15{*RMEMltK~C^PE`FIhv|E}y zP_z;}SIpttqbF-NxEuRW;`3h7H}eU{Bam+)4#=?<0;0h4-CV{U!;TEMsAS4ZoPoF{Ij zoBb&u#4iT=OuHm8z8OwMOSmW`cewMnL^F|n7`9l#8DQa!(D?R;kF%YN$SvQ(J8*rV zU!Bge666Mhc74iWN(;4zOq$)SPO%3(m@y~q!?XC{iw#v$`HOyfdj|!38Xo71$?cV} zb86$Oibo8EcbyX(#cP3?^lk)OkDqqt*BHtUUK z5kK$}c_-I*qbCDW(+lMe34q#|#N)x|Ov#C+2MglxTBfkGu#RmGTI-O!Ty~Dq?y-6q zF7ZA=f-3b%vZXqx0=aDVFelRnR)YXn^Wg+z;g!hl@|<8Pcw@yfXtF#9Zd_kt18t#) zQCitJC4aY5-q&tMgzUGx_SP;{iQ7e~>g%I9Mb;{nn$y1I|NhYV&B6p4yT5`us$GCL zM4!GyDKP5@pn4I7w=*V-M)^dV?1;0=zmorL?E)@lYg=!`ih%>O?2_JO-y*MSy#;q( z-RS2=`3eT3PW#?ftfGdM#w)FeVLDk_q;>D|-y|f6{xChXBJ=2J&)96=8>_qST12UDyj>}1*7WRE6rf>e@N6?pnKawTxs zT{?gqiX6hK&f-nw>B+5aPy$}+Y_8Zc($#~%Qs+4K;w21}M?KU6p9}wd;Y!Y$g5<2( z8ZXtnMX$T~-V#SjXCGAJ0p)Dt+>6dW(Yd*q%THEk5f;;M<%en2H==<~ zNG;FyOSRv>KUWkR?3Akid0=*xw3mY1 zb0vI{g5&;JHiLjD&t&-v4s4C#_aQ*d{axHOKid#{c}0Khb-E~jm(OwI?ke0ZH-$Nd zPHy*Vo1wsokcheaQAeUrfly?7beBe3?YGNT*$-1QmSxUZakOmz1CJ`ZuI7sExaSUW zzwZ3B^^;pT=LTQSlgBAqLz^kc)`SzVLWP}sr6Un!hssVLvZu77AgtFw0~b^3WOxC8_HVl$1tQ=p-LAGluMSM06#QHw`mQSabeJzov2j-ADbs zBaQkKTOP2@3_e9xhWPU|U&qUC9za#ey~3xB-)1UIjrU;N;uHiLwd zdLO1;h%e6TQ#-uaTYj)g^2&w7&6{fHv|LP%yC@QDVT;>(Xg723Gv`sk09BIBMZ6` zVzj>fHD0azLm|{Ug1~{$FJ44rR@6RwZxW4an92(KiMnG_eLdV4FyleD2sE4}lNbY* zC*MNJXhtS@v7^JJ_@F{ZL>14MTz!F`X1*acw>g#tI*h-m$i!gKequNMkD}p5B}H#? z>LlW#<;pbcuk_wZ97l@^QV}z$M(kx^Z~*;Z9GDV{!k5dx{^oQZN7gw zT?eDSua4On>;Qclyjnea`CpE*LD&ORcZqmSO@PKw-827=@NEI z91oTg4}~wDh1gYIoL>H3uv|R<-6!om1o-> zUE>1qt7M5;DQH%DMU09=J+j>65)K@70djiUa=-_-@qcW|EA)H zrxpU?ZJOGqhH{ds$nxz?EB@oNIN9C?Hr4d|R^0GXr;h}Ood*tnv$$5e#d43wAp$7N z(1@7)R`@+dC$H@Ko!dEAAGsgT9(j8gcGXW@kP>tzw^`2o`cofY#0q8`y&nr*}IGgNSY74NeQ_|P*0mgnSQz9Xy6MV&tP z=AP_c!F_L-6uikzLY8Rt13IP_=Ejb032iS{*lBad@517)mrZsZlw5+B*8Dl?4nUBn z3=POPhh~3!$0mU<1|Gd7b@u3{YgQl!O0heAs$q>7G~j|B)P4kmeNcPK6MPOFJt_cO zkoJl~lRraXszv`P{KL*W3Ewf_(7n2e+_Mse?3c?^ z$ckA6vivaCc%>OcdPg=OTl1-p7*6iYQIyiX&3f|z1Yx4EssyK{v84n6y#0y|;uDax!Z~JSy8ez<_CrustAqPh6dRe^%`H-MF>&3s%cuO6puE&x%9-4jP#*Mk z7h#Yh*uX7RS)!584mp{g3%xMx_y#@YO{<5V7O^7Pdg4L-;gtXTS3+M?lGvXC27No;aiJ&GK{hGn7M`Toe@mqTZ#wqe(UF3uze^Kz3;6Mb0m7c zPNhTQ=Pw^5JZ~-1E95`?W!2mU3dW2T6%`gL_w}kowH+Mtrd0SXsM`6M+?RG~=T~Td zBhD0y?6~JIRjqUALBx9hQZ@YaxeRfXdirw{&?`>Q&^z9V0a*-GN(?ME#j$?4Aw76Z z`>~{SuvuXma6M{^q5k_8Zumavu7E1WHw%L`4gsoQ^~`I|G9eS(Oxu62WUat}{~}-; zUK`>09h3YPGav1<0-eK!`Q*Pmt45;_2ZT0oq81&V)#Z4a?;z%-v{A*I^tlPMMQT>riXw>F#HmLp-EMhe!u` zwD(bR@jcBf zOM1r!g*W{UcsKZ=Tc>ft@*7Yc+m4(|M_u8vcx_i+)Qb#*Bk6R=^Kf>9c&It)e-OW) zmKE%BNLUCS^Lz3knJ{j&l^n8do>Nm(O!*wyf~NBt4&wZ!R|p&|D#XBKQ!;=4e*x!C=`vObq~29TfN%k$e#LZ`9&WqGKFqSJ50 zECDI7!(jX+j(FGjBMm*{73}V`eLERYT!WR(Z?9zk zQylVt8Qw^=M8g2E>Ia87fUwA=vf;SL|FnzrCy=e-yl;-}c-KdCm!-ZCiF=~xvEMF+ zQ^Qwb6G=T|v76^CTweaODvp+iB@9jD0lZTQ!T&J$Lt*6evj zVJ5Svu84g!`w%XJcxKXTvK}pN*xlpI7VPWmlXsBiFC}|?qLJlxJ9lq|r)%Jz$7vi> zL-4N}UUEO8t6ZCDf$aVU3-Pz-{8eWXo5R{48Bi$8k7MEZJMcmFs9sZDLX?e8 zxL;!9u*uE8gaB2F!!BOJ6;630;D;x;qVN$vr9}h^U3$$4)_X`Fv%$_;I$mka!P-`> zin$iDE@H?cR=Uwp&QkQ+Gd;_kns}{v&q^AFWBOO_~d9N4^_C)==dQ=1_+ZA!|B*+WkX5t&X-$ZJG0jRF;AO*b1_Gn0R=5OhfD zrvz!n;<{h8QG#khVUG|N%;vF;iM-f1$JR7NviEnsq8u^?1$dY5rz$lyZ2iuDe|TQ2^fjH{a^MO=d1=_+ zb%}$tSHNDZ*$T6&S4o7EqyPlFbKWxkx%g|ZETY`K@xgRjSdpC)u&eh1?!@Bnc#1ed zC;mH2y5|9d^R}c>pL-<}Q-Kd8rgC;1Wm{-b89j#}Q<{g`_ya)oI|&20LeK4cJSoht zR;}z!jhHi_fR6;(3loLpt0S$XO}Zn#Kf*t}yscc4oU2JL0)nRB&RJUy32MEO8%`nr z6x8$ZNK*UT9-_}=A*bkUYg3i8G?wk>CcOG{s_xOQ1Ep#OtBQ72LUWO3RYE*>+H*3$ zaUZvvh40og9`XRe*dGUS8E)C0gr;SGypRU{N6mzk?MLvp z^*t;3nP={Og!Gz`Qd(TO4POfLF*z;zkC=XJd2c+5W&MT{KuWo=QzHJrpT8DmBfX9E z0_f>WC3DR$C0>kv2Jk!J`mbE2`sB-swAtTY*}pxNgol&Se&7}N zPDmK^Jj*s7RY9=h2L~sgj#G{P*M-UWhjwB%#zeYfcIphw^dL%MwNF(0duWNzL%ic3 zpYy=Bb@zsmCMU>{GrDHJ`3!pC>_e~;*R~IcTTTwJ;Umf-j4BmR$QWxitlH9P4*RdV z4Q$^{BcZi2=~pK-*P2ShZnL^P7egcLUsbj$*tN$~V%)YdyVfjZD(E6DSzjw=JeNmk zSfxr;vxToeXlmJ!cJi4EtBY)3L5U44Ux*3_DuatFF2zTat4Vm`*oV$1kTESbwnMUymfQESPhukU(Nd{aAR# z`zYzL69$D+ca<*I)p@EOL7F;JETvM|gSTh4^XL-CS^cm#1tt9nWh{rw%VUSZkpPF0Vh7gcY1vhC+aZO}EE@hylkTFlYo8ujBwL+bFVWMJqWkO_ zd%ecV{8N{zN#H)02O<>L3@DcRV%Hx5u6U1B*@qW+42_kyLfipO$)ot3bxb5hkYjBs1^XeAn@#HZ$Cb!o)usBya`@E>1|mJ02Kji=hN79VpmRO>t^s*SEJf znC2FUwdT(*j_8R5?xq!dxiaUt%G2h!x=hCAAaTYk=A$4|a9K=qU9DN@QtR|nJ;5Uy zwu4b+;a(mq$REmbBqEclfztO`TmPkfZy+D!bY<=v&L`Pp^Oo6&MzkCo?sSrQD31v6 z3+9B8((aKgIa0m3Bk}iL@h*Q%Q7WYfP<(456Zjj3avH=Rpg2l;jijLov*+VUX98c& zfjxacKd3sG`EZIb-?>2FoCh94Dzps;Ikm6CQ@hj~|I4X{c>V)Pw5;P|UD+8gt2PT|ew#_1#C{TPeN%$rYb2Pfwjaqtp0Q_- zq)fwbB@qpiLW*16MegaW8ssSFo#5~q9hPzomWg!(>2XbV);G)h5V+x|!m+rCnA$lA z3$fp4$Zk6cAYIz#^uZWn){$>7_PsMMGIbRV@xex!!t>y#ul!xI_^(+>zkJW=beiBW z{%Ke%EFwCWE&Pzcvd!ssmFIMlO6*m+Tj}k5@%eZ)vr(`=l>zH1y);y76f%(AJfks< zLT4@hTL+OH3WNJ}IK8~GG%lObZfjsW71$A!H{os^`E#{#*Gu(1)V9?mWH4alV63p3zBUgiFBtUX_66Jsclj25@Az>g9wfcIND@acfLf&?gM& zQ!0;z_!@fr!V|A%8XvY$=@%0Ym!@%kg(HavFo* zw(J0iy0(BY^n;79U8Y)R*+0(grcZSN{~X&gH7J|F`(G^`X+{_V=_JXXhoX=c_XMWC zm+8Rs2Qaz|n?ih7Z&CH+FzK~b>C|y7rQ6feeh1@l*4AMWk>>G41(toMGaXigYGj>u zjE5=QwD+4ct6V=x;s2dU2_r!!sanhc?zcb`EnLh*vByD4t@)3XwRAr}&_${l=W-tu zvNex(1MNd?K8yaL3;zD?8Hs^y;5*A42j@dDWB!9j9KUaOpwInIz#+kbX)3F|LAP}E znipsNoqk;$^JXWs{`8P$8M<%2fJSHmhsOtT@2Eo|>^P&oX;x-H&rn>rlUHjhNk7p= z^_D^)$mXZU(e4C}CC7cywC_its1<$vD))IJssM%`Mprdbn}v6rZ^G2!W5jG+7RQd? z5C=P7?HmvR34Zmhc$eGHu}B8?*?;}Z6Pu*@g=vpNI$!7c9R)O*ZQVHFHWV(x6hH=p zCo!n)6xtfVp;PDbHiih=oU)&A(~>fFm-CK!>MY&yhL+YPH?8 zbML2zl=JzQe~_%+4RV&1ySTgEu9rp2KW{cY89-l@d839^l(FMd3+G$@@tmnzj+HKs z=tlS}({_l6kpol8zk=`&s=OYt62G&_Z6bt({De4N;#z+qOujnl^Vg1_uJ^(z90`RET!2yqjApjm?+)g_%#``erXRW`twyKwo%ZeMK z&Ma?7D=`t>w;ZpMFWcj%h(4UFjtvH*$i9w^r4SakJ!iG17aP~}mpR!9F+$0( zf#=AgadEmZ^pO@K%Re|j(XGJ7>~d+PwQ5=2wvRK^KV;H7CbC#~_AQf(OrIk%Y-s=} zV#hjq3t2wAzzXp~sj_^?>QAkYt4sN-OVbs`4rXz+2;W8O0puI>Tp956-O7J(0|{Kz z92TfXAo5=tNf%(%%f`cTD*otKE*a7i87xDOww+eK{8T^-v6;_7Albde>6V^Yb>u~6 zXbRG~*d}j4hOLKSxNEM>J!K|66lMaeS!(q-tnal~o~p99N-Q(C+w$cZmC7S91T#$t z+8W%Leyk^0^AP4$6HnZ{Ex8(btv+`lEQu9*rMa3&4c#(?OuEiz_4=ySy7RPOWzjMH z^k^58%TeKpC$ds7LUO9kM_JRK{r&g*=3|-t@7}u_(OIBi3~XT(iR1jIK{u_R*RvpE zQ?N`l%R#{i$Y}7J5`OE08b|a~j##6KccLW7e!PkG-F%MD4v2J5T|MWGn;m}wZS;gX zka?&hN*ToiGD^jG>|Q~%TJXEDq%a6QcCJ;0Yt)R7grMDfhbW8$$-h1m^|t46lS@86 zUXz)b=?w*B6M|RerIBYB+PpnshN5k)WUwrKny9x`4N)y?`^E4N6zkoRF#r0D7aKhW z+ne;KG$J+EgPs1}(*uK+AP?Iy+G3n9q+wC7I2f~e{l zQGOsV6Ln67B_nIxtTMd0-~Wu*e=(HIYsE*>m+JKLKeQs{c%g-+su48W#vgb>4%H+9c6L3$+siz7%4V$qoy$zltgeL;&S7$NR_b+ zq|O~N8g%X98dl#&syt@{<2`(~gqUn7*YO*Kps3s4Riy}!msV^Z6lGgYa!+b?Ibl%?*bp9TZ@#Ohz3bRr>n zAZt;`b`HXZN=gVJKEtPI)y;=c z)4zmwYwx;@oAXm}9#*~Bj4&Wb|7oT5BL}h0^wK>J%ct-A!HV;!DGMGa#p9HIU8(_!UX6eU0aKW-dz9%f9Uo+U_)wiMBIb>H<9eC$z2;6cOp0_k|p#0bbXO*z3Ppp?mvHZ3z+xP! z>Ykod^C1f9hudVp2F549*HI`vc@=NuRcQY1RepuDO-TCs&r6S_AK!An zJtEk%QHA*NY_@@~>U~DN*g?%ZJ5au$5n3bMlQ8QEiVZ+-6JNcN@4X`p=sGFK2DZ^o z!Jt{8+)1BgsRa)IAVHgyNwP|a{ip9!=FsPk-Z7RWu^%|cqQ&!-Ol0!8XXJ2lzA%D1 zWP5t@f6hkp0mE{g4n<@^oPvsD;cO z)aasew$tD*GLzoS=blJ=I+E(xBOn<9=3DR+Me#>jRyH8dD2ki_io!zL}x z2~P-gEXXWQiL6dd$2cy=41Rfj#E0p$QR~a3?6Iur6u(?np`Jq|v39{^E&r_r1mIxJ ziR_$ge`_2=gs6DnGhwLqmG z;hQD~CmWj1)GN;!{MeQiuwyB)guV!*@nh+A<2O5&!tR6{BxUL@JipJ_;yagy~$*LA7qKON@zU5NU<@EFQMGpDp%24tO^FbliWsYnu z^BS?iws8k5+ak`8%Y7dOUN82Pcnv~mVrq+wp72bpY*cr#r*cwPzvD`O-GPqzqUi@B z^ACKIO|CD2C2QfCI}f&E8VcAtS^@?do8J&Gyt{k)?0Vau7eq@m#tix?i>&Ds)K_y~ z?bjoEsovu94oTnB+%Q*|8psgOhmBd!K3ZfEYAbzMEz?igIdmMuw!NL3LY-ekBy-8WLM0JFMGAik@=NSO=z2F79>91_*rw@^`>AZfzJ=+yx9wGaouAXni6W?Bl7j@#XgMD8Jm1o44v1RzL0XsZkZaT&+{~ofa2l zo0zd}p0Oi^5`fOL3&(L|cfx!{DLakzn7QPS2ZfcSde(KV9JyuEA)n|!JS+w^ZL4=> zDo>_^Ya25&l9~w56BeqfJU@}yG}90qM|hr?Do$zUN9AOjoI@4#1NOVU?+Y-fg;^JE z`UfxL;g9-XXc3&Lh*(|;usy?>=k&dV>Oof9RVV%YEd8uB`Fm7j?Co~foKjjrUXou^R6?Bd&!-kQT{2HqooA|3zyo>hIgDN=HL5N zF~%*){{p@0mufD9!Kw7T1Epg|-u4X_9xHp#-QGP^wuI!bH19D>nySj~Cl%s#XWkyW z`r5=25>(m;#$PuLW|FPjyO*FUH)h2YH#aXE!_&J;Ahf-^zGOgo!@S#_Cz{0W`&J(} z$MPGrUZyYGJq`*mYjuB4$IIw&z}ObDXg<#a_k5*qp#+w&SG!tVc=9>2Z`tspEi0U-`wsX<@q;!?Go#~(EXzOyNY!bV?rkub~l`g96;%4@k) zAMyt6MjF_FB_rJ1aouAGkMBJ9s@X!X9|v@^ex-OkdqA#Qr%fgc@vDEWRsj^VF4nE> z?SBY~Ps9Q;xL6njlOwj^U4y%RbW5!F?8k%$E)PSou=*E~%B)76( zXu5G{oAd=jlJY}#HsicqMCX?d?o%Er_vy_I_I!6=N0Li^%G&Apc#O{w7|Q=nJri_M z7G}9lWLd#>p-d|zZ>eouiP-^y-9bOc1;09|V9ZWey9lS%a?4P2-2FK}FTCHMY`;jES}> ztaEn-zI2kslYNJI7t&-Moguh=gtEyC*+-iX>7P;+|7v#HNq>ttA34h^8gyVSZ$`EU zWG>Uk*Io~lX4aUC+WB~1#b3!1<4PFD&-20Qk0qw|Y7LK25Cya5J{WDF8|Bds9FMh( z5?Z&wq8sv-xGntKg{T(D)ohek1K1R&Er=-)Kgg#)sSvvO)wH_j?Tun)ihP@?zUh|y z<(9$vWCx4p_a--xZPRr^X#wTjAw|f%Cv9l(o_<1C>+THphz;bi?GA z{~+S9dQOPs@PO+q70!mFBL&sND(Ju&uitz%gVEGHP4BG?*lZQm1jN5@S#I-t-Mbrc zO83ZiM#4bDXk{dEDmNFbD14Q5Ng_s)+4IizqX`(rmi9eA`f?m;-V(eT@%eViOOess z^i=Ypkb`$R2m@%k$W^gJ3emz8B!CsJRQvgK_ammv~z5 zZfyudi}0&w?Kn_6beyCWHt$0XI{^aa%q^9PyQdh0Dn&?vn!6y_ke5)u}pVDmEScMT(iJ-2gyF47}eIvVY7zoU4_6)zP1< zzrTkvAQkgyuijODMcn3u6- zL|Wx9wfQ_?phGwehG#KB2;-CfEv(*QWhzvb!>*HIct}`DI{P_d#I|xU_U76S7|%gqSMsdH znX8}blJl;g@v2W`5aI;C7&$}IYX>@K!C%@wG`Yli>8`flwq9wY3<`07e{y)hCMxSjZaw+2msgcXfH$JXJ#?}U9XqjpE7e&iLg369>6X5VY* zRyygT%h%*E1osTXcIz!X?ZUJeqZUh=PL3$#xLm09Q>=F?_*)YEq-8GFFrunw(}q!Be=ZaKBY5|I zdpTSyuBZ^Q-)+c#Q=P5u`RKuR>)KMEZD4GSvd{h#nYBdVMGuySuuE3!P`QtuI-a;Q zd{OZN;M4pU4qA}9%~^p7oRw}Nfq{}XmRW*_d3$DB&S}Ve=&71D3TRCjqj0!U@58d1 zrN0EJ_65$H1UO-818h2yx1ISvTTY|_>$BM4e|abqk#OxjgyOXZno|Mh5TKVjYuQZ{ z4x$RZEu6rj4xlqc<9jJfvJ@&3-i+Qbn&^KgNW$BtH8&)`Uurlh6kb-hj>+q}5 zlc1E9FG_JXs0+z*9kI{fJQ~gnpo>e8C{?)>^A#Ir8h}~ww2k^mq*i%pAE*#N{c;=A}R~IA}ie@A;Qz)B#Qr2RTG9mIxlHNR_zDV z!%A$&36w1m2k*e##Zc-)taoYyB>6;CzsNBe-Dza{E8)(>U*_f(L6J@c&lbs`Jt6kqtFDlefpQo)PZpbQmUT8iPS$YoH~)~ z(5y;C9j&=&xs+JOA#=?IF}zXs=(|hKp=OnQx8MNw!9-(wgRJlOiJ3L(8ZNFbgs%F7 zjaoSb3=jKsTALa%$|%a5?*!>d!LzTFbGj%7;Zzdq-WGMsrkjJP4){M+#6ijV@1>>t zEDbLbx9~K!@CxM)0mB^ykENmZyZ~v!-9D+Di~K90^R;ZA{0DKbK*ij{u%h)@E|wwp za!mPYZ_0`-wwF7S2<97S3nQj!yQIe?J_zDu4?{9A1)$6nsZr=Q4!+9 z%bs{pmrR33iRp)og5fhavAIddIV(ryg-kRo&cgInSc%LxA;i+mMG5Y*cdF!@dO>Lf&4m+_Ci)Md5k7 zTCe%#4FeNp04Ck~w?kJL5QQ`LU z83d^o0kC_cq@g(&dR0%m=?J=D;U7*Uk!~D$?;B?7)Q_S9taT$%z|%%D3e-S=s+>`l zGHw5qc-)dXnIeNw$Kr#y`PgktK)2+;j4VBq&;}S4kM}%pt8N>FJgNIE@)>|Fsl}aH zSv$@)@;Y-jSo+&WQ8znOcr~e(-BLmiJLp{?{2qj?y$D|^R0OI@DH1eoK99c`suwU5 za?;0KtnRTrI5m#5V>=R1UDW=_dRO$H77g0l^=r?g*G1oAT_P-0y#o{i3@nc*v#fP* zJEfpUs^_T3J<=cFbO}U9<$B}S^(iukp@hMXO~IsNBEKTh=*}O%G}?c(qy2fF0z+&| z<|p)-3mpUpxf^5j51p$>OdAm(4q%ON#9D0SEMfG_Z%cCDhGvCS4~CAnapzvE7ucbf z$gJ3IQ#1QKtYlJGq7b(c6L6D7!%ti{$JjL>j2FhAEuwdPe~O-$dN;(I%X_(08b&T8 zDdD(be`Mm;wi5T;-Y}0Y<#SRjuEk3mNHab2Dv+ydi2AJk@pqE=^iL){6p+D)BKeh( zr9=8D;|IqY7hyGPsvd{my7?th>n5ng3=3@^)%}UCIiXrj*ZmC?gZ9J0dmTj-6Hkn1 z+l{p6W)B^!j6Xzg2*s@}e&=nCZzeq+Pr_?{jgqmyZwX;_E>hfo|GfAQ1ko$Z=c{{| zy~0W^9qEq5HEn|=GovKm=bp_QyZ&-wg4LFB>c>qpbgG2K^)Plqc=#B>Z%4fiBKUKV z=2b}#yC@KL)Jful^0CZxjd-~?eV-$ocXDUcri`=dpK<_C9rdNd54-PHM`-XW4x3-I zV?Nicv?|9oc*9Cw5O?hNy-v`?mC-&-?q8zjd2bBR11E zlLPO;Jt|%~hv*zmZb~3WdcVpwdAY8md_HDT7%`Cx2JZ$ zaLDy3%~y)SU;|m_{f!q~e`p#!v{;D#2)%HlU6nPnmwM)uDVHa3h59Xc7VwP?m(6zJ zjLttVw7-FLyRGa(F*KYSaWwNFWPHrTnVa7@eF{>n$IUTCo`9dAz}ak9~{3R~#!xA*si zsDo$q%@&~#?SrzZ5jojP=Lw`|_1-1m(j72990;UMn16|1H=5>Wpkmzk_ZgIzjsbXW za#v5p4LsUFbNT18{f6zwkyJg=P*{)O(M;TlRCw-0$*j?UaUY)_}Bf5t19md6b@bU`0w!6d6`aSVq)vP4*J3oUn@JXnPjTqv}% zP_OQ}osSkH`*}RY+7O2gW2MEC;gD&QpaZX7 zYtg9T2dQ7B_;>6N7_?^Ml%udQ-f-SXN*M!x{-k>_??y&As(y2A!`;N67t#e#;K1BYW-58d$lv(lDj+viwyx?Cl#>L>6qFqo@aad!|C{LJ;E7!r> zI8MfKRC|jJRzQbZtu5K#FaD+}QZQe{^iqK@M=}}LLKkH|Z!VnmXPjA^Dv~rERsw_S zupAs8X##1b=%JqTz&(3|K~2%NXfBE0o%G|I{mMPj;~-z*&Efmk-r+=uJivC8VLK-O ztn+t3OBj&$JJ{;aus%~7MSPo)lZFl$itK#4czvDi0#spLA>TQ>pK@^XM-e` zVj}M)NsPB6+RdCF9G39~!%vrOHNH*$OuPiSak(S86e=ch!7wRgDvO;cm9l z{Z+P3=lrL3e1-N9m8})nk|gOlSjDHpR}#Chm1Oshd3W=r(G2CRo>sFe>HJxPeBbuz zXdXrW{6w!yBmqBzI~IbcGaQ#Pd@_Mfun5QCYQpG}|5cp@3aQc!EjOe!e12yl2Y>Yx z14o}}B<^U&I|FEI3bF=EN~78fi@1ZM?@CWT%Yn}069T5FG+=f2;kjo957|E_ocYnS zX#du+fydSMLR!bY`wS5>uIqe@H6roagdTcn0e<;4F2Fz6i}Y`WA#UVyM2CO8py678 zTEXyO$I562vm&%OX)V_JMHdzpUh2=VkRLqbg(^{t=q;t{ScW+iNm)Y2KX2&78b}3m zx^YzM-vt<1;gj%lMFS6*aUzcDjx^&8V1eBrw8a=UC=o&bou_fc9dc4fa-KsK93`{;@+X}!6^PZGM zHFH^E?D%z!y=6&SsxRt*E3 zJNSpeh8a8{;8YPLakxLc#x{fmB1i_~%17U-Xi~&(PgPe|nA^+MUk>5|&qJk}> zHAQ`CqPv)-jj?eNm1729xv|HY=XtF+FaEbH`26f{%HN5B1B-ac&{^{vVEq|uu%x%Sn#$LYXXCF;xmR@ei9XiVF76vm;I;Bg~8VRVmwg z8q|ythf-s)xiL>E_6KkQqZ{*AW|&rcBlL@*_)-gHX1~*h-{j)c?AqWI4dtapRg&d= zH{rR=swm3*`NF}0n%k?N2>XM~WNgPV9f2WBt^G1nR~t_w%YEo-1`req{sLpa`^}&) zF5vsaeCVSp>aQ~XulqiMxXs;sKuNdQ!^KyZs642P2SWmbCxG`~w~(3jt2e*VUKQ5Q zqOrBX0=s^RNtntsC=*wt4G#`2{@m@cmy79may-+(xS7j7L8gTC92Jjg_A*<5U4e(z zd)Yrj0-7qBk$H2v<9Vt zUeS|bVj4cvqlliRwYxOIHfJh$Tq=K(C0~BVygwa9gJOoB@MEG?1nBq`JVRy04w9c% z=V*^@!Jnv?KPV?p@xC^nlwM3sjgD>b`_)?td!nQh1I}{qadV|UwvY7A6IIEmOR9+! zr^M2j_1PYOV;;B{#z{|Zah_Q$vXo_usqu@|g!l~nv5RBE{UgQAdXBSFeS1?vLOWFT zI*K&&?lXwz6uevKp#C!h{RMiibhe4T!N<94C)mN+XfoIdI5eT4(J;(|udTxA^s_UJ zXS}G$KkOv}#KS68uYYdK!}^JS@>7wO_4Q3(dWoKTdin{3)ufP;di#k{U9j9!_rZ&g zAi?(uTSB^2g>6ykAf_Pg227s)+Qy1A>=DqpY??M~V1;|-pBGNd2>6BH#)dhsQ40!N zd6ZNN%u(e9LF_`Q=FW-PZ#O*Oce%23)p$CXcM4NivY+7NqjI4!m1qUPm&&su<~DsU zlrhxKv^-cK<1XE|&|;gSIE21@<;#a7s ztig@x)=Jw?N9Q?JB_d+kVL$iLo+|;>m4zyGQKvd3?0{u>=`0+r{)eUc^@_d{ygPM8 z7l*R;%fJi9BCtFfrbMFZsfL#m_-s4xx`?EnG2m$5KNuf_J@|x|I*JzVe}+Mh5lG7hoqpq?NZrb zIP9d|*KG|%2px{yoPJCWQ z`2}V@71WpWC#z@5|4?2icD(&L2u=J^x}x*Rm*H8RJ<}b9ZMH8(ZMVmS`puH+U!u3K zj06p-ymgy!YZZ&Wsdm-{{=QimGOngCCr_+R{JmH37y+oa_nS8ltiraz_Hfe&VmmtG zkm9%-G(|+;EdgYbt040E>h#6V7XWG;={T%E5W8B|Km?D9s?QysrRx9a&#{^6@=3P4 z8Du*Yd(vvGRamz+?KXU)BJlw<_qGkbuqjxz1$Re($XFN?yj!{?@b|G%kZeWd?pz82 z=}PlGTq;HGnECyD{=WIyed5puXtygN?7gVb8L#`Zu8lHfrue2rRHiFsv9Xuq$Uw%E zjqV=NJ3`OO5h!o&aELEA!j1f#oWY(byXGFtJbdXsv(p=L3%N*@)C5>?VvU1PYZqvs zWD1kb6tW!!XcCPJL&D8_aCkbO3~%Sdc`!#;tn<(*S02DorJsfB1?Uzl`&}q5etyCj zvcF?>WscZ_`oICV2fO)?BQopfE!%12UVC1cfpSwqaT79?188Ij`;{hIoc+PU-?fVh zRF-Y%7{LkLC)n1JQRrccmXry9Y~ai0$}P@8Gxme$n*d|kHY(;&m=o9gnc?_l`i!uP zol3{{HOe`orGlL2YaMFtqtEIzZaog;tRpHLmWowNt$Am+@?Id3PhNqzVpN?FO27+m zai_I734+V$Q=yFC+^P1UnVq z&=RT#cbCxqZ6}<$9Kc#tD=|v`1>3?PP$j3V6kMk<=xXD&L~h*>nU{$nOzY*8(6_}@ z8XM`A_8Va;J=U}RJrLxc>S(@7g|h+wyr5SGvCdQ~!p3FVh`pA&m%IIYmm7yiovetE z0~8@BG|O5u&AN}!El3EC+f=cD*;&Dd4v)Ql>ZTWiOiP*G-l~f}8(dsebUoC5_K8*A zRPY2)%){j9+~WY&sN}w-0*R?c{e`)RBm(QeoRvoK)aHm6yI=eUPY@Q3NLe z9ncH0(N#CA;C}9))#J~pC&Q|d#f4gUi=gjv;IKv-mE3n%A$gcS4qsF$Y>8Z5};NWv8IAi%AzE-X$HA^gT~hK1KTFGJSv5 z(eX(dS*daEUP}*XkLVw{inEHMdGHfciSFwSyT9{d>nmjnD8=94W&-w9C0$fpV!3=| z7Xu?>%vlELtZI!V{WQJws>&;5I+C~hDQDN0Bc4ym`3GZV=<9n^LzfM6fVt)Y-A1`T z&O>9fq#(e4cP|-!rVwEM?Hh}^#tnB6Z^Dh5cC!$$byLjHTXSFkl;wM($aW6ToW_lk zhvRW!jWAIE8+@Ul-4$@t?RrHGaar!pW1g0U6=cuH{9^b(EGwDf~BLincSUj~3n`fSW5r zuLa+;&`?ymW+c~_YVDOwFq`G+r+)E8QsF;AKCfrDkqG_}MTO*va1#6DM(;Zx<(*}q zWW7q7Y+c9_jkj~I*&w;OvAvMc7%J9Rvn}r+>d;d*wRhwU64$)8(^_jMsArynCcysU zp9@pObMQIjZBQ5e1mUVoSaC(V7 zA6f`Y1DbjN+&$bY2N8yNezpZ>fni_MK96D;_Jpt!f5>oHtsJ7)K9x^$5u~C1v#TYz zqT0cMEK!+5ATikv)Kjgi^Yhjr#Hn$r`0~{{M3tb7irR&)6wmWQD|+{E`AZ(t)e@KK z*cx3A(wnQRx~_RIQ3I*r*%jOnr9ZvR_vaw@!Pql*^wFRrT}i*QL_Bwz$zd3z9-t(x z$DWQqyG@1@GiDE-@W&%oc;7RuwYtc=I1q7TtTnU3DB(t`td!Je#N{iBjR~R7XtPx7 z$nJoawdkQaamQ)#3B*34oqAdP87kJzbG-aJq2ivqOmoBA=hYq1^-h zkjgO*B7A&Jb9PKRlVJ~byEf*ISxlgj41n0{=JxaNquYEse(!IEJ#y6XCMD^hdtwN z$joq~>@tmyi>L_vB|45TB{V$wz5igfXC?RlP6D{`YwKWAWma)9sziTEzU5^_n zvCx>wgPn=8Xs(I}mi&gDmdtvwdtJNMKsN4`5tvjJnTl&js75To-1uK0*U1(9W|N@8#dbS~8O(uFtij0X9X;fyj_RlEVvH6rC?7O37Z<7uCaNq; zWmlScR^Up(a@bxr@KbkQkDT*$gD;~(kavUAzt&Oh8&3Ga%I$wRJDhxo=ELePQPg1v z6Q`h1=?>Fr<8~Sr<)o*myw!QdRzUDImql2PSAyzN@hE?zWIOh<=iK^%6Lu??UE z7a{`4r3+1rumcX=Q)q!Uek~T2RT;>`v5#Gym*ObDUS-fcs!i|?C!|K7Wfb}Z7?q~I zdI!?qA7*fmGS~r3xOMy6g=jt8WBk6;*pofbUqpT216(t)d9&)6eCyZGpbnTq7KH? zXWkMIPFT+Xi(%?cx}ELS*);hbboAZ5Gx&YIcRxR@JGScC@Jr= z+mYXmJ*n_TXEEUH4xt4?Ll|_xp4s!_8}OZ5A)NqYD|rQ^C*Sfp@ind4y!R@Iy3q&V;DWb-q{9K;2KQj-0#V~ zZ%;x75I$?KWSVuZ-tN*g`1!)A!vuW7lO-kRs&#@HKdOHs>e+o1iipNqM>d(lm!G#IG#mFHL~#3Q=Q=;;$$xx9XCN)$0J+CoY}=FkXsP zjeO7`nURs?jF&~w4m0m#&lFC`HUmp1h1nqJ)h`Fas>(eYw-2Q`Y>B!v{n3A|L`}hodWvRJ+zpSJ&qHUj(a5i*xlhQZ#FC#$+5|GJdxLYaDT3$Q%On5d*}y0 z9PG~FxKL)YI2cn5gGm+xEx!H zv&cqY99XU=s*X=8YU#+HX6K!*K6k_h<4V{wo!P6QnV`#;Ryx>v6jq!shzQNA`DN(! zvq9+JDOa!eC#>BH?w)3l9qDL&iWNBQv;umo6XhUqb)VR-zMGRyzx} zl00=LrYs8LGmF!BUn5C@te9cz`;!h}|3523U)nrs8Jd|vDMx);C}_VOeFGc+zt+<8 z2`cZ7H_+t1ss4fNKI@fJ?x#RP{VyJ3jX3!$wBWYfDU}mkFh9>rf+s+JHQyokug^r4 z;>no)U^yCFEM|_W=)9ak6DMc5i_f0qr4sA^!iPTE70~Kn0pXUKU^i9O*GIYilni=;K^ue>p8ezn_rJaZIpSVH<*q2eA%sE+F2?(Su8Y25T^Pk23YdJ2g}2W z8mc3aEBF{2AV}CJ13-B@MTvrT$%|vpKQajQ5|&$z=N!<@sAQs7-akAtQyNuhXq7w$ z2fHVzwc6oCDFR|`Q}X!Rvbb0NdEs;$;hSLtbELbkkp?-x-(vzH|M|s30T7Rdc|U;= z?D|}rl_$zFYQmtUqVbin@uC}`s1mE~UY-ST@XBH5rw$N`xdO$Nx0XaZro(S$1bQPU z$jI!I*0=zJvX1<-K!r$oG&0Q#r(|h31VV096cKMEd%5=iVKdf&(K^mUS}Phz@bKeI z5_HL+01XrB6Er5L5-RopUrMGb7&XnO$UHO|Lc|Q06h6~^2y(o9lT);Fz7mwWXqWqh)VaO@=dYQ*)`ZLPpj8`hL~U7Hc> z;Iwj8Gsr=;2^flc+Is+dd8f)byIn+nUi%5G4i7Yv3LI(ZE}>gQzO6vZ>O!4XIZ(ZS zOfEQ3a*p0v$^jx1H`HS~fwJ9jbTS+~evV`8t*5==&(h?dmm-thC^e9yDSN7Q5%_Lx)hyL6=N;!sIBD*r7~&)UghJb3Q2 z)YPjp!Jm}lzwNls;KtWm9oMm>JQENc9OvwgZ3;zV_kSpsl|ts}w+j>vy8!DsbIllR z85-OmfR2WK|8ugeJEe@>O<+8Slh*FXhnGP?Ps!zYzj%GzUIjBTc;~-AxKSCz_h4tZ z>_xoTM}sm^KdQia7=;Bylh(jlB1Mo@iMfBwaf-*0Gh`+~uaGR{$HQ*``C?XbWB%(@ zOZeTjR7=xRLXWx2qXqB%r?pLO$B=ICHak3>s0O>$*40-AIOW-)E!Q)HJ&Y0=W3sE+ z7A(h36G%rFT&%i__u$dd=()mh7ux0p>YmxmULoOD``6i4M*ZUOVa9_0R=owN_zCtJ zwPkUm#(8iK6Yi)Xy=!qe*%LFCsTX$lqy20Xqm0P%^wli$TR&?#mgPAEhvpMvLppx; z@pLDoR=Q4aZCu{w0?8yeQkw+}7h%K|6++xJre6-dg%!6{f44YFjLm+--VY$MW73so z;mz%31;8xB%Jy=cfr7xqdHBxp4fc}j3*$$ymtsWzQd(u!rjsBm7sT8z`@wf-MOJvl zBYTM#XDW0oB0s6MogjjvedXX{4t3t6n>mj12WwMJdrLbhzozUd{lS7M8xw1KRqx@~ zf~ppGa@}Rt!iwr$Aa-r9Q#CeEaF-&Id35w!Wb?yNu07p!F31p4117)$l6EF@fPtZ| z=vdyA>_{4Z=6TIHIul_93T5@Bi)#sc6UmM~yNduGv&i>0e2DO!@h#g8zB~2)JIjQO z>xHggnC{jJV19UM_GUBYO70e#u71ADFtOI?hwF+UJx?2nmi2Fi?x2Otbn zUW>a5-H7Mv-q?ZY_Xbiw2T!emSPu22E&oCx6-MS;d&(^YVSio2Lb6kx5B zqv#{_Xv%Ptch@!ICP3D$*4^$Veg1kyXu#pimoJa!RFmH|Z&1-I>~x`{g@}-;Rrb>g zf&3SpxGE~WpgpLI>hN;j7fvW%W796y!)hJWh{Nb=UK4#5zM7(o8XeSk5Eaed-fvm& z-ufpX3fltj-W3*zOO3!ZBv?SEjSFZw{o&bKxK}TW9C>`wyNNRLFKlVImltOg!X;?& zEU@l8GA=1DP!HoYF)?8%+UnFK`4%KG;r?t+hF)^h5jC!v_KR~YLXdkJaje-jr(c>7 zdWV>BM4ZL*U1l(g-f>T}YwL3=Mqxdh#uinC{&~x5HdQo?5bj<2bs#FT{%Un3x^e75 zWc5d6CEJ(BtctlC)ydS}88UIn5}g$6dwHWPtxm=bO12SDe%wX|u3zAKTb6}Mr!!>{ zAadeeFFuNp2#mau=zO%D=C0^dcXx`Rn9CL@%1q@(eEuvnq~3rcLu>PxbQ#_~%)@hJ z?XE>_5HM2Pn9C9}b+*{HhASuADK5kzLf%Y~?iV$1;Nj#c0cTqT@rNLn^)p6@xG5la zGBY>B-l#$|Rs7wT&i3>}=BI*ZrjI5MI*w^&htQri9X4Ud_|lAZ&0;2Zvu1xR!Z9xg z&=EK6N^K$QVu};T-xsJXPg5ytjGjmzA@v9I1koV?fS~!BwH*Yl2(YD6SaDS#t7_Wu z|Btr!j;Ff+|HsQki3Vjvlu|}!Wkl$tBAJ=l%FfDOp;KuOrC~c=6*3Ev%xIBuB4n2> ztIQ(&9?#d|5b65fZtu_U`s-Xe=k1_lLM+XM7Xv}kG+?`vVi$h1?lI!E4TW8}8i+od3U9e-?9 z0_8JCd84Pt4{E>k?$iHX`{=GjLuP8kkWcl{sL_RuURDp9EpsFRh&Jq2lc(r8#*yvN1mn@$N&f{>IfU(<+63nF@f z`mXFMlS(=1y=r{j^ZuOiW~7Y5APMsnRFrJE#*)rBC@R?f{na|tmuIN_$E-Bb;O?*2 zO2A>jCxRK>bNEp&M*9nOH8PCD;M?szCFHtC;VcvgJ3luNX6-L85W&oAw$sP184H7< zAnI8#1lTlHa*xoz1CRtwgV4qc#H1R&fVvvyC!*dZQTnagnGEVdzU>XWGI93L>S`{> zttN!6zLNzhwJD+9u47Flg26hpk;a~t4?0gcW!@+~@}bCjw&2eMjb;jmv8I1qHrkvo zXvY!0D2(&X5MW%WdXLLy0E*cE>-(lD$9h1!@@LL!@*RH-a81pipie;o851)EseS2C zIc_sRU1{G}F*P;)J`}xIx>T||-U9)N{ht)EfV++2`=sFOy*(dY@>)|>1UFhsL#d7T z-cCbQIpCFDayJrZzj^=970;Xx!igv8t5_OmTO?I;>O)oHXw&2Tjl5Pnsn&V92?ppl zEK8_%Nw`w=>*ms1cB^jpw6zV|5H&uY$_;Bgs{nS?pk+P{`K)SDllz2d>OYH~AFct3 zASUrmh`F!^d6A;S%R_*rca`quECnYge-Nxiag3K?0zd~t--Bb0cp;xI#hbqCTfwRn z`e@yl<>?59(d)X#k+h(_e!ll*SSt}>Z?sGS!9k#fS7fT0doSLACW5q`u3ri{*0_?J zgCQ{;n!BbQY_>i%75rrh4<()L-1ol7JRa_wDJpX33ebNSaN-Up#}Av1>lxwR!(XTB zla4;!;zZM}h(8Ot;}lbTeP6g9e1q)?+gmQV!d?|gMddIGXR>*H6^$i05*ITuU*649 z-JJRp9AOJmnlH75dlJ$cB^g;!axeb@YV!6BJ2gB3LW(^gpnnG&m7s#ye?C(N)S>Q=u7tj4! z@)UG2Vp3_u*p6bf$Pz&vptM?Pe}RBcfp3Qni<9d*@=hXcI!@N6j|W$`u?b5gXhgrY zjr0W}wQit80<3}J$E;Q5=aBSLUVpVYj3Up?pt8O+%Ov5YgNc3rHwK1qh!yUla-wn9 zigcnp<#wm#Q!7v71?8`yvuSCZD7<{SG>0s@e*0i@8-)CIC)Zw7gTe{j-N}YjjgU7( zHDmtF*(bl`|9~*pMpQA4RR=@PZOyAB@H5^{j5h5>^}#>lAfx+Z0{poAUeS1xe`f)y z(YzRxd>p2!!vx?T`Jdnk1YJ^{H!D_`F(c+vA^Wn1Wac|-JQCtn)po>e4+NojtOKvW z$!PHswo_?g{;d8xHN)|Z!=9ANeb4hHs2U$vd=uXh?G@GFaic8$@o4e44ykndQ*LeA zH(yo-)-^B~Ngib%o|C`@ovzSfrHYETq6iou_009sIZRb+m) z{(u0eUd#k4X$|i`Wr^y8{cCMBl|G}u3u=+rv17-Pn1WBAjv~wAyMFGlKKl1*C52LI zM^N^nsPdWk;tFLWT;^cR*;&vWsOJD#4*fdQvOKn5O`n1*+BJ5hvm+32{`czDilGv` zE}EcRYkWOGos6u%^V9$IaH5%ghha?BH*tFunQv^%kmBW+^jG%Uxb|IdW~uTqLc|Ap zx@Eg1pB49d<1B4lO7Dh~S2Fhm7OWAYs9Kl-o}6$e#U5RD5Dh=7me?TP=+ROj0yyxK zJdNMZi@$;zD4xSCD^Q~XW&;dY8yj0`fxS|>ld7w2t)a!$>>>3$kGQQfzDcYNZmD}%a5HaXR%7Z7g3tyR$) zT_J1?0&Khb_0x0(!C*tUif-?Sk3OOvaLg;RW8h&x;gHPefmXNvk7m1HXsd8VTF>J4!gv+!2)7yMh*{CYIOh0bG@i}JoeRGEk6eeTylNh&I+u&GWI z_?VL@hZG}HmySie(XZe?T!l=pwdxf#@(WdCIt`C|OSfpNM& zis@XZ$nam;X7%xn=!zX-y3guMU*Hl3?6vLt2U2(8Pwh?WhYB@Me=+q36peh>26T32 zj2_<>zl>Z2#6gS!15ECxGC2e~pIzy!5bU@1-I9N?fCyS}wZx%)fvE7#0Lp;;nIg_` zp$S%RGaH8!WqYAPAB?i+fPB@g?BJ!Lq@uD~q!%!DegI%UZ8HGP58x&)bh$vd34uNy z^|&~6b{~uk>Y(}mf%fX}KVT}m!f<&vjVjAVK#>pOS1jM{O5Mt&vMng~dTgkvHxhc9 z&`yFkS>s;Z8y2T3(Q#`0h!d@dL9$t)<|ngzk@bCs_iT3bH2PSq;tPUt!F@4H+NH`a zX_3(=Q&AEo^Q-x@`9w!{n(=IiOVp^aL@*u?|Fim4)zN4`+25lbf0SOw*vIw0D3q>q z-xV_6*zY@|~kf#6Y?Y(=JunXVl+WpbXRBE2a9 z!Scly9os=(oDV9~Sp{vtBu~*iV2{Y~P~EtcfKdWxmg$(}n;KEK^C^4^ zk7-)6{s(+K z5T%9I>MP_-Vy6a@)sT25&6tXxxm0<$B!!H(E1nO!YS!CmLE?3DJirza*7B+^HraA@*t zwCD^qEWsmk2&Y|?T>cn(V92Eg=hh4uz7^#z@ z$+C++8&?NZl$L#_Y9@8rnTFyq!!UFh`Wk^>)Q48HKl3m`y2Z%m&Hn*NsZ-`x9=ZH)mU1-CMn%r#kC{1_-1(BPSHVj zzVz|ViJ*Sn*-A|O2+F>yCd`(klkLoz4*^$zk`1;WV@Fp>Oo#u;DP-;Cuo~Dr`F8K* zcgL4h%&!8WzNpp)O7q+X4s$bz=<+7L1VSRBuBhUJB~o;1pIWp&ED z0-+1n$sV$5#H!gVpR>*kYg0Rb;+kJ;Y~`nN-UZdptewR7_$aFDyPGdYD1Q2o>)@5$ zzDWsmzKOjTlG?asKwZ0dWH%|&iTtFy3&;VhuSe|xq1tsCJ%og1a!X|KzP8hO*MVIm zH;}o=jtPz@6FP>cYuDV|F1%Rp(z)wVi36pL1DeqkIM!ae5BaogN^-|mQLOSadJRG& zzKHig+?Z4Ak6^9?FWeiP`Y1CvOm91efEsnv~j#*x=WS-V}B9hc$KRsL8&Y0600ypfSnB-q_29)ovw zgR#xYhHv*1TlWt`|9)ah=->&6qZ+UyNB~?WY%wj)Xco(Tfo8EnLxsh(OJ))1qfmBC zhL(H|UJWzGi`&52H9;_U_MYTn~3Rw-w#Fnd9|hTR>d{^dKe-TBegX2xT=P@oy?)NPo!dr0L;NQV(eD ztg0{lP5u*f@eI3p&n4He4uu#J&3r|lZb1To2X&)xXmSDQv5Y3zT)}Kb^heJV3h8MAXX1rK9C1ER)WUe&~@iPyd$4qn~Qn(&p9ychvps=xl?31k$7NS7xjePimzL z@`?KC_9yn`Zyk)eGV2;*?1Hz(Ls^OHgelDfrCg~$)H?EQ1Xp{19z?NZY}r|jh-Mcq z+r`0FV-Q~Q>%8-eYc4uC&ecn2Bs!GsK&Uv;Nbs1agueqc0{Cc`vmaKi{)a1YpNi$y z1gLEYZBl8_&mzJDZjfMI6{1U6D= zAC8Ac0Oq6R$Q!+tTLVg4ith0FH?4jsE*b9RtwR&S7M^0xJ$)-nDSj_7!1+>~*)|w= ziIHr?k&eTcGU_Zh7+saB;&#iSh)7~dh#-1Izy+Xmsjq4-jAytu-GZ8dp^sMLq42kw zbH60e9vP5Hrg?bJqFQAMejl8`>h00wU?DhS5)pqPa5Ne&pj>UulIQ+USWAAiSW4#=Q zzamB}7rS6e`I`!I+w3&Ozmvy(Gjn|gJB7Yt%py4s{K5FoJ*g~0;RRgew{zUDes)A| z0uw(_Q9HutIZ8RnjMtQvHhyCkSS|D8^A8xvs_{Jg{4k_0;;ZXyVIAvHvFif^LvM02 zjOz1m#F(ngtJWOIeMq&I_z zgygf-z?aA(VYOxUl2#{lzdYa4(xS^w^F?>~9%uTf_u9^}Y_@iWhwfUj*7jfS7xX+X z&7lCd%D&7X!)HyWVNFDe=_4`?{e)E)r;BM%+uE9(0r^`ik^qcF{fU|AE4f*p3X=@F zG;EHq=%0e@J!D)V1cf~i=oqZPLT{{KS=Vq(F43S3IYr$IR=ouZH9mzz$tn&B11Qoc z_}NwkT{HHKPtlm|R21Xzwy|s@uVnrQd>XPB6d%Qt(@cbWxWs*d)(O0}0}#)M@~xbJ za(vzhjUbbXeGgqD-x_peHn(3o6uCJ=<_y;2F}(Jjz(tfMzL|d-OKO5}PV~7kCS|xf z^?k3Bdj^woQ%XIBeBcF+XJ1qgzG&%!iYw>yn`~dn(+Nv7Q*Cte&d3(>ky)*))<8BJ zs1KZjQRUO*(|H+3bI%~%KKx{1B18SWVxT;Jia4F5>soQzj;xT~Y(0JQ1*WP8;n+fx z_U+|c4;E%>2b!be_=d&u6*RM2(t&UJ2C6zXSm%O->s7$Olx4;~+(7lPW1IOO0Ysbn z`V^J}L^H8RPw3D|q6BWYqSZh14{%0KEy4Eyn_3uep(STbK3&3be_N=UI5&Bb z*axe7IGQk}8$__2!4BUmlz>j1gA=%kh4De2`ZDklP!&IFaP5IkS1OBl(S_h}uLmp} zKNcAHSmq`8l}gBCAIvM;WNr2`$+N_!8G&|x$yoD$%Bglhc#XQ@_&~4yWphq{o8}qk?X2^4sSjU z`6P{NIGwZH6&Z)iK-fo@23^3>ga2ykS=M?tzx$u5*}3haeUYhshtRU7_ps4b|HU}%H59+#8wE!N&)in)s#|6J|-cf`q&Z#`hFuFhHg2+ z43!as(H(Eu)}dNLjq!*y`v$BN5zWTrl^Vt3-vC&Z%VeK)EE}FW-OnnA&>eYnsv8$K zC_I6aiyy4kCif}{2jM~i1keDnQ>U^dKQ5JnI{77fg@!2F(B&)`{Wdcv;>1<2PP4JA zsxNPKbv1YRNJm?xS{8PB<2GkK@Tuw{&;KC&Ubz#ajeBnPEJtph;x}A-2pl^`J<&}s6sW#Cjo0EJF=&y{TWHKxgN!G7Xh|XSJ&Rp45qiWFxIx-f>{VrhS2S@p|diVH!*B z5D4~>ucO=|E7+}yOg^^vezjDvx6Fuheb*}^!JMJ>FnIW(^GTS-rK%h;DUt5HBeZC! zLxJYrP|eQJyL*MU2fUX)8FfWlX^wI58-dy}(+Ss%(CF>`oxk7da8M}=LOZ&4g5I6} zEgSTL35DlBD`VL$Qrp&(QhJyG67e{oAPMNsKUx_tkH~Qy)G(aT!yA{}Za)D?IAz8nwsO9iSHlYoQGlv)eA^J|-tgW$85lswvpg5F5*Dht$-Y zSY0w|<&5P(wtxwSK-+a%LbKztJ)YzA&jPg`(fUjPY83~MIbB9tJ+HO)in!cZZ50@L z`h1piz>dMJ*I&}+^4Vz23p(pn823{fr zt?O1;%7hf;G^e8vL~T4MiYbAnc1vg(rr=KJgQbW?TA*SO+*{<`cQ2`j=`-KY zi)U@l`SDta$>_&AKm_xc8gJS}N@OXE*F(!_)^LKuHDt}8N)MLz2p=E|CRK;gG{lc* zz}!q=KW=VrTGa`dFE5T1&;Jd(L*Osk-Oz0gyT{vQ%fZB-S_9~$Rf!y)b2v}3aWsT5 z-^84;uMxZJ(u3`VC;B(Ctj?I|kN#(C^f>h`myx;0Lqb3?=V6_$l!?*c7(HFs_3a%4 zQ-)7K>vh>}-Ko2n=Mmx&j)|tRx(jMigd&6eyc(a(ybwupnkU$RCCryzh+aTI$YSOv zwD(p;DTf|MC9RV`JD4MSP|*j%z4?wNvta3Yg$8a1XD{tN(D^=vOoI+vR|vU0Z!3AHgMDk+8<*7N$VDC#>Pn z-=aha8fSG!G`TJjmqPIX28bsd)g~P-G;OE2Kf7Mz9P7w34tP1||K0K1X^{z?^nn4& z^mL0A_(&wju?7$AB$o6do0WFn7@Q+s5FHASQDmG~i^?lZyr2xe$i%qi1^DxNT{xxQ z?Y*yF9WXfglIr_Ndag7yP-t6yAM7Z*(Q;$23@Xb)NK5yDf5*nEb7h{rg}Eb-JJi)Z z+8^CBVns>$1Vt!@vw^8_{a6S1&B=_;K^Q$Rp`B=;o0{P!iaLl`>Rr(Lj=}pl4Gi=b zH?GYf2(U9y_9z)BJ`YFnfJr2nu82AmOz6SgZ49zU<*qG%A2YdU+-yn&NLlnH4kXI8MerQt2z;QF8E1)o{>bTMG?2G%{;inVe z`^XY@cFsup>pF1;?nmdn5C=2?5T8$8el6JE|I#`iao~P)qCar0kLH z_0f;d^X7g3HCT3IG^<0}5s>SK4dli)#lZOG8?T{-;)G#4B*oVIeOvBFhhXi#cmnK6 zGXk=JnjdD!-~Z1VJCA3XvWnu$b#hR^-tGw$_lf;|Ms*D2!9r<6ihmC2E}o)MZBiTEj7iVNeM!+cm-EF#Ot1j@FV~rL(k3%T_D0%>dxM0uIxPEJm5cB-Fj0btWo$d(z=BaDxP_rC`2<+4SGIUPoAE`n11&|A?` zt|)ZP_iQwcD&Y$W(ntim`(LtzrqKz&zRM?@74tX?NpPGoF!y+KIhyc*?iu$?M!*Ur`74j$x-Mx^*rP<&j`VOH1l90a@l2`h)jEBCm21N#v8>M zcj~^~#m*(czVgW1u=W`Tb4Gu}uCvIDENx=rJs-ZrDwA5?ntKifGjn{5RP z*&tfYHf%F!3a}}(`=G9*5IWh39}07hsJOT%G>>~DbJiaDMC0vkB*vI$H>!Mz^@66yYr{SjJ_O(+M0_tR_JEIh9rUw zsdaS{(l{cGgBE9AwSK4~i5De2{kDY@1Jp&tsnHbQ*C_$j!BJ<# zP^@8S*N~cBx?P%M0 zu+Cecz9PaYGAxW=sz2YK7PO3qJR7K;o4=$zQ2+!W4?z9m#o)88X=OZlUcrLh5(Sa6 zi@jGlbClFl1_T>8%gjiD801QaLS`a7a--qOps>W-ghICn-MGc8M(C`f;qct5N@B6- zLLLvz!+;B*e|3_dG4wovD9h1jC$zS;Y#@X9Os~f(G7ktW=O`SrIjwQF*YKe!!*hhf zqU+c3SO|**VxqHa5+Rx%`r-ja7N*AQu)aG`9{rarrMq>(ZmH5~+RhqHe4`vH_8Q?v zMMwI2)w@(E?+LR2BjysS;Q4+xJ!=~_zwl{TglUvX834DL0G@E9NtwteXqt$o*tZ6{ zcWbL`{+)!|8*zxj@F4!PA3+mU=$ts#Laxx%+8|Kb;5umgv}=B>>T@Ei+pzy~!J55UNxtUd zIlXCh%}75?ftn4q>Cq98A zFBWEHgJMj06tS=K7ES~dQjhVvrbhQA$7{bMO@La)qGUIOdVqz_OpKZ~WxQzkjdP?DUwn-% zccE0eD`(B|yPzgos|%H6`rM1GzT9QfnH$pkdT;J4ZjN4}j?4 z$g`VIpFaI$8R^rkI@zk)yk_d#8XH=NFr!w04{QNM=Mb7q{^SuIFo!T_5UKB+5ecgn zv+*x${B{F)=%&$nXVr(qLf;6Gq_3X7-M9f^!J=NW375*WIZZj7P_gB7fYwHT(c{El z1#iScbEP8LYU|YJpek=$8~bUrc@J?PbyFwORP5sQgH2YcCno&2tRIDbNg(D6v>SpG)R%e{3{L5B0NCkXZXXx zr(55#`4aaVR98)ubAw*S9|)t~qcYBjQPs0Z<2Y2B|8uRvA$rTcK;D(Rs1(uA+pyXe z=vn;GFXEHl1padUslgJBrI^*&4E0c*-d9umCPwOn&~U_8&OpLBLA0iJ^iU=TAL?J zr8p%TLJ#ldSh;vD6FEX!fF@eN^W`bk-?a|3y;rR3<)IR}eg617dczS!@W};SI!Q+C zaoP#n?R=D1f${V^Zl~?3(+H!p6tMA-0$r~69^%hYvRuXkx9+_Hx1#5E6wy;|hF%Kw@|IZ1)wbi3~56@FoR-xsc7Vx)qHo)b=|SoxX`-=Obd*NzPh z%}Z(gM%u#9;UZu54owHj$C!C`1%0{5dHo{8&Mq>A&sf+WH>qTDZtNPpTw4|5+nN2a3Vyp#u@{kEFiU^D;PoZ+`9g>DUF-WhXOi16EG_ zR(?nMeSxTPrxK>0AP(g9Q1Xp8`0V>o{9 zQn%_ra|f}O9VqMGL{9g|P5EN5 zIAsRtJ8H3TDe4j%HlUcw6@>bKb&XHwevL1e3Zwu%3i&GB(p?G~O$3BH9!cX@4;lg? zezI6}PIcG8fUawPACJhthD1RAMB~*v!*H|O|c=M*j+PSQK~l{ZIgi5}N`X(k z#oGWl*Sv-wb+1w)n3@lg%OQ?@Sy3okq;&7pujddRh1S=2OW@Y4P!Iar0g&+Tikc;~ zppIgUn*mr|E1*&&MhQV+J2GIK>VYH#mLQ0mvH>~5asP2KM+aaSO z>Vz&I<}O-A<5=#~%5v4|de3)M9tf(vY4T1LwS_=MOr-rd#M|8DH!F z$Hr7>Z=v9hbA&bl5F%9%SVeeElHCCwy=pYrl)irmYGNoQs3Xi?DA>d`O?xZ>X+qBJ^F#4NmH2R#O#fd-78n&>vZ0aVUc zp4&azkGmfwa8wE4@4$J+E2$f@xt^fw-~pfMfYw6-^f&<}zCsfFPZz_U%MtpY|*_ zZFzI;d%>nj;hbM>LT}sd8ELMCkgsX+4s>i z-(4vw$bX&s1V2&x2>k*4l7na0r{My&ax5K-o9eFTdapdzSm*9}uS4_E&WBBHVI5(i z$zJLcp?8d+h}{xj*i{iN-rDvrOzl->vP>#QcC;rmT}bix>R@v5&MX07k^u~u9~82I zubcMI3Lq;(j%JTR2VedY<0o0e7oIy`gKrF57cqofEi0{8yf?NoA-U4L*3|Dc$~`Z5 z;PhW&&YUKiF}l^Tl5S&^Dr7>!iLdnJ$&2 zOA5al9o70zE%OA)G#z2rerv`>$Jq2U@6wlrBN5B)7mh_7T&+x_RMn(`FC3X{_+tGx zHhv+%AyQ-K*iTCRv@xyUxKU;Q{`~?%PA)E~ZQczF=;mEup4suhvuDp*o0xE@zSp%A zlnu;i+!%<7vS%x(l_fU~Q*M#{*|B#@0+=^f5o{TyHPS!tS#RcO6u zK}YGSD7ouWY=D{Auds#;W$;w{rgdoaVNYA^K}DN6M0BtFq>`E-0+!=%-n_}M)L;88 zG~}rp8ym-^XHKW7W_dAy51;Ipbiit-&f5|*jAB_}DHf>)il=cbK%(!T$5laq2j2*S zV&qMO{^A#^wU#aRJ>19+dbQtPsl(@uds;ln- zfX2PLXDgt>#zqNiMc{(fg&lfVy@HM>6NF8T`zJKdmM9II?bG$f0)2D&mMp3WvA{E0 z4W6^DepZ^elgUROS0=sJxG1DLe)Kl+VV>DU%BBv<5n*3(bcEy|&;P)40bVUJ@`2Qu zhfAV5f8jy}k*KCvK^JU$sbu~*R@^?yqA{O!_Y2Rk;VLlMRtYx}4%Zti2sb^v&SV0J z%PJBcI?gXmMxB}eR$%;`)5M2?1B#XE;8~xTNIfd@l>bZklrhop%to0c?~ah^Xw4G1ewN zk>E^$9h;FjgoCt7KEhi~&!MIb*O1&7?~1Y);}j*6XYYUb@S#t&e8b}8Mo9y#uiM$X z%abfx1!)g>2>F&|yI9|AGAOQIpG;6ZI93 zSbK37+c;zHY&kx*&UtrzTL+Fb%iG4Bw22KA=CoT~3X1p4xe`R4_^&iWqodIv9-@{m zTfV#^FArBS>N)_zSLLgh7!uMh=x!clZB#jpTMBGQ`$QoM#j?&IbHVI^61f6%-upo@ zn!r_3I7B=qn{Y!0xRXx3`mo0)Zio8{9Uj9zYlC`z8YlfF*bEmC2d8P+HbTPfR+GeA z_!$|j;<({>uH5cA)qG(2S?{lb%c@hiEybELIUJz=w_a1i4`Qd!w6R&`?DzBN9NyLr#pR{+Qe~6Bj zKkD@~SrM^CMAvWKAVWx)bk^=Y>2ilXYnjJqCB=gK1v+;JA(YwOYe%9nC1BV1=e4!+ z^78LrnF!23NoY|S8y~ND_wG=|Xj|9#cG*B>hsX`+SU7~>hCORm48mpWz{l)cqo_{e zwkn~w1m)0)KuU^ociKsngA%Nhg3}Z%-_G_nz~_Zl{_g?tU*+> z+WBaA^i%HdnSbArDC{UMqhFhpN#f!YmXZoPfBt-kZ}I2PCamk%TaC$xFF50(t#mgF zFDi@e85gjL=u6GK6o#e^FzoKhchd3l z2Pizp`-S$AuPU4FMN4;PpJ!Qc)?_w<4;&(0E2qCuLc<_^hS=j}IMc;Ort{9*xRkwD z_O|Z;r~6}db@gLj(O(Al3(r3`z$`+~b#R1r*Mp$?qyd)J1K`z2^<+_$_&O_s1hsrg zQg44a)TI>Rz;!*h<2z<0y2oJ-&~npWkUBpYZRvYdZx+-PT;X#S95ge2oV${r+~M^n zmMR4Tbk5lqMJoPt;vpWkaw0iDttUmJ<|WtwYcd?i*JODKi9B!Vb6nop|1@GJAOKc( znoPonzRvc)y<)J1ti<8_f_~G4CX9dXSA%_kHeOQ=xSGhsSv8U|fm5v+NuZxYdR+H7j%pGsvOl)e3l#`PS z(X%?pn@FY%wL<9h?=Im|rY^qQ%TZMLCU}9@ zLq1;u4rrFDmS2M$;ODu9(>fCf*K;EtW{%1fQ3N)mjQll3auBG4f1odzgj>>?GvjoA zeNA#92#>uD=GzB!F_&=%xH!dXiZP|+Ede+5xF4$qg~4+73l+X+1TVp_1>U@CvQ7Y1 zoJfFYS6nC;^q0N7g1u~OG*BctI_MCxaW!YLy%30I?lK{sPJN}9v&j$@nY@qam(c{1>bV2c_iLGW-4XUScAK<}Pa?Z8` zXW@S#L24;fd~W6mb551?@VO=|=^q)C30GehN>NP>wDO761glzxr9ZjW^mi+PS_I65 znyYthFj0zoHlsT=sKKyNHtfgNIQM9Aj8x6ug5R_6S;%1|7YEG4=h=@a9mC5_{ETd1 zptLSulaO$Yk*X@09Oo`-`jFHP!kf1Dy%5EC9)j0;m)KX5E~VYLeF5YejtUW6(^+c^ z@J7!BX0Hd6Ky6e7t-~VDg)}cnO>&z1!7QL<-VPL2 znfLVY%o}ES1{X>>Ee9~^+zZ>8jEY&jlje*FB9Xg1LT3^Vpt&m`G(kNd2a{R4?Tu~>RvA$q#ZJLr&! zhsW+!t5zwy)EqY6#Kxxd&p-btH?%Bdn#!3QO3GQo38GE#KSbL(F2xs5?_%}X^DdjT zop(23``5(^G-u_W5{FtUwwG0mA-25_eB1J_GFTSei;NHc94DEcFN9O>7{kT5ItgKP2I)5CG{tgfxEOF2%#OI1W}%DO zi?_(V*|v$H&0$T+;)ZpAJ5Wbff56XKC+n!L$>f<`EZz(^yq4SQf$fZJy=b>xUp&Lu4s&p0xlFF0vUd02>~ zx7{Q1QMRoO@~%m)f6E2FTx5on|2oSxLZ$hSJlPz5^SA#O4-pmMlxDnTI-VTdfmK`V ziN)ZL$-RF@e#P{MMB3|zW@-WOcc}D!`W8=z$84Ok|UW*|Zu?^OC4&34~&z65SUl(bIWF=&E?n1YWxikvZGs(EV1) zp|y<|o7geJN$^EqfY=TWc12+y!V6k}BO1i9M8o=u#jol1A-JXkq1Lm3&O1!+Oi!wV zM|YDU#9&ZM}_p`#WXGHP&g>(=71uDv-?_wnLg^t<{M4 zPT4?S^G7A8g|Q#y9e}m2Tk%tJDTO&CM)=29>m$4A1ve;YX&Z;dcc7^SJBZ!mfEj$) zYs?ugelok7aZj-1^#Ed?v&7b~f}T4IUtf5;byi}`UlY(yC*oe6jBFB^3g*8p=N8d@ zE+BSno?PX)K*7n$$$LDopD|qg+1ce{vBY5QkBrw-82@?{a6%>l6R_ZZyy^!1(?*dW}B<%BrHNxAb2af`}D$ z`U9o$_f!S%#uC{fw(B0gMJI_nF|y(4|KA#M!cNH8#8viEkM1 zE8q*8e+e94JT?Rii$xLlYd&cttx3F$OSxZ|las@+?;kQqFPUFW24*DNm*uYG-n$@J z=x~h|ks^l!keX*5{orOCp$vX8xqUDxaNn7TOWARDT0%D{cr9q+AUvG0vAzHr%t$Q*w>rIGqRmU-#q>QCI#~V!>OnZ*_vhxR+MNDw(^kbyoh9js!dx za6Z)`m!=UHTK&4J`8E_&K8>=dZhigq2hPgH#rgZS(n44C>uVjkDmx939hMc4F~P)nhO16TP?8$NF=%u)PXt zai6_Yl94g*d7hvmidzzcNU4j?Y%=R?@n!lW>nQPnBmTAD!sdvTQ+}^AhFZ}IvTA42 z9#bQT_@cm+et9GC3x0rBWW0oL>UqpB0Q+4aEXkw)MzC(V6kz2mhEOQLW5dvQ-fS=>7 z2Z5=Gs!yS+chmoBgQ{%>s@T$dHv{_?zHf!dxbx@fsQM^Z=lrA&Ah1#&Fx!bFH7HgK zL5H7u_Y3cK)njp~Y!r+dUhT1WDaD+Rltn05E&R1gJh6z=b{qNp7k-cpES-C1;g=yX zUbR=>^Rh6?@3adYZA@FUug5c9=)VNB8Ut+V-=5o^)0?6}n?gx72zI{Mqu{W>@4Ib4 zxC33VIjWo@x&sB4$b{udfzu6kudW3L?f?)M{kCdMGIN3r_z&m(LOD&tzX<0i|3a91 zaE_YBDH9s7o3V_$3c^{z@5Uc)be2if%+R&;N1((vkaQ_^>RRD%&YvYym?IVpJL>H3 zSN%Q_B6opk_CP%mO9dC9xW8MsK-d!e6u6l#42VRa6Bx4jw}#jj7pHhpkb-kSlPIa< zw-XaFPas7r3Q~m+bN9|t+aGqoa~4jG#q9$t_AQ+EjJH{^rWbLB@SiwCh-f<#FHH5@ z5+~(Y^a&dKxtq#+@-wwQ^|XdaV~89tP;i0W(qS`>BNsOEvth$7*emV>SdUlIOxwwj z!gCItL`^(MhJPO^o?|$p0Zt;EvDO-nIrR zB`kq0QGME~fjpr3&m#SW!2qc6^9HN2*|kXW3y&t73t&EggGvO&wUi6YmisBJr}IMK#=_@Dl33}jw(W*!fN@$3vRmY2coY9x z_Xg=r*}#MKqRu2!E}Ze=$$gY7S=mvhb`cT-(jh!{P#2|c!%}jGWHI0)EijGa$5uS? zh5e+e!twgtjaU$_p2L*Jkc}#w{eK(*>S(ZVjQN%IAz4l)a+fDW{>mO4YI5k%`(j10 zGsYK!xZU;n*UG6v&&*lG=V<~EzKKg|N@8JW5AVDgI5~!Cc7?s>0&5V%AE~i*`N5?% z&F~OMummv?3!xtE3Cg1&fGKOa1Kv95J#%pV+l~U3!*Zm0&PdF%GS6MyD=yRGY?m%Q zZb1><$Y}7vq)MJ0ja-{TjzrHn0y4D{4xvk_6>uhftAYO8uFo%8dKRYCJDs3cs;?W2Sm2faH%|DIIr?(XKR4|^~61&z4T zFNc(G&0P!WSx)%v)Cmdj47;`=;aNd{gxq2N0_$5wsh98$jG3Y_hFOLW_Al8yiDd{McrU9?4Id{c!XFC>7H1 zbkffA%&4jukhv@r+5y73)okyozFBwR50Yd)=d7EqI9ioUK$7Zt-oo8?hG8=9m%#up zR9=~e>pLAT5qz=1wL~ScIt3X=1aCMTQYK=qv(In@HhVGCqSTYI^NqOc;0w3G#k@`% zq@v~yc(uBh7mF#&WY3jQOqJHH@mXS#yPlSfGcUuwMR2AlInOn5Dz3EGpL@a8oB+XG zh6!f(r-2}t17Ad7nNxOf#d>&>E`VTO0$f31dy5wdW)9dLRSi!C|JBoeS9O9Cb{?6f zs<~@|`7lH?=BL@Sz@|7V$}M6NQ|)ide^I>;guvjcG7YPpBfN4wcmBiwk45U+wIhKf z5Vcx4dRH4RoCyT6ry`_zkW?7M^JXpr{e)GV$Y}@WTN)GI7ecVQ~SXqyPR+F zpg=s9b(;HO8Gvb}iJZ%?+<9ZbIz$-=X&$T05%*q;AD|f;9)9%0D0)%S_yxZ#SC~0g z#CSC5ocl# z*RZ=_Q)61?z{?WXKeh#G6i|3e{+kilTKKhaBCF!ou06UIf1Ub=yeIpyVYNR~!rbus z%w1a@h9~g~{y}igp*kEv9hQEl*8U1fr`Rw~n<2`)KZCWE9W%%<1T$KQn2bBs#FgV$ z(Fp`HO1pvTzqm=H*ar|^OoNf$jsxQLqG#opfb0D)5g3@Q-(QfA9Ra z|New`$m@x4e_M~O@%Lhx*;849w*aNZO(+(vPMV9;DLA^+p-hq>AjPKOrA(kRAz9%LM_^$z%l6=5I-_Z&(@q6U8(PLE=K z*(7LXh3{D-HL4>D-`Bjp+PC7BxO*sz3}1;ADIGMA$l$DtP}$`Odm4()%}}5$$P|g+ zj`eqXK>zfcQ-}kxSgyO^7p*!o5qBvFzG3;1Z^I=%u6iV`}EMe z=bU?I7Fjf=RTcaX$*C8ke`=3x&kLWcSz^^yhW_}hN-Sb=ZY zr&G-Yu7LET`0#~}5tQ#ZG-c9Jx91=4{5@{aJ(M?C$k9m5;X<2X_HRic*$MlVj?5vyIg6j8yJq$g^AN~?TY3XF>S2yZo_9IK0QlN|B|(y{a)<7nemr4q zC)0vz6e~3oGqbJ3qod*;u7{z_8BHr&W!%)`FB@2LIU@p{wG*dWDzO!&L%ekX__rvS zInJ>QbqH1fn*D7zX!@-W3OV$YauVjr?PvhJjhct)b@rJUDNc1L?l$22(~XQl3v306M<2mtx7R5YPm9)WNF0>!dF*YoU`UiRThj%}{P&(JG0tVZ|P3vIXe*(z#RF zJrw3El?4r+3;T>_HYvN;JIbf|gxV%6cmWJuq!`S4Np`SQ>|Pf#Q~s_dkCQ5U%0Ub| zU3S=11(V+M?gd-gFlesdpz~wF9n|$VOoQ6D*-g%&gXs;W0&&-bgihxLSx|kA1|=;k zyRVzyOdw$H1C)$#R!$WAo8m)$Z62gV2aeL&+xOUo+pOg=wHkoAMyfCoRiWC*+b*p~ zAnnr@Lx6R&4z){jr27gYPm&3(2GscXEC}hS)Bvd#GwQLw4CE` z1u(-oX7!x&T)36Ff8p?h3;tQRZWoDIvr&cOovhIXGKUL{jIg{zR!+_PTd^*YADK94 z%m8>S2N+A3QeOT@G394JYJv*bzgl6JMe&nIlzQ+m+e-B_o! zbMG-Dv5gBgkNxwR2ke2x*V|oMF(({GTnc|%P4Jo(<$t=~-|83P08yY#jKhm^{&xxc zVc+ZWXq4SLTRl-9=3yS-G_w`ydAFve;k%dJ{#?#aRzv{-;BQf1?6_mbDe$0$LWYl> zdjw2^KM@PhQ{end;??DI{&777x?KJa@&SNFNMDprSMjMKpPXsVBS@<1^?UD6GqnIe z!C?+FaX7~{=R|Klc1XmflH>5Utbw7f^N4aeRp1;1o zP3d^;9IX_53qT0(iq%>@5OMwb@or>B^EK4ib%{ITUBsp-ce_%!P!;_M<8v22UM+Ax zyGrb6oLx^hG&xp4Z`<8`o)Pil=Ei5&q|S4h=9;oUojj@-|KR4ck5!%%!`0Ce&g~D; z;M(yAbBWs8s;*@M$?bNv@vK*t+kBNl~|1`7dV2QXYg5jxNf7Kw>>d57(FDKzo(a`I@8%9&_ugRtts5c8# zR;N@k`gVr-A>4o{?Lo1f%`D%G_X47T^_P7A8fVUByM4xnQY*FfA0apq@5b78#{NsE zn*hKtllDF7cR?1c-UjXc_>c-QRZ%b>xEh>_Hu8iaE=l4ySoO-S!b z%H$wGR@ONlo4BiOv5Iv26;u!@xbVo1XvgryxWvX8piO@E9n=d1%7d;qyKN3+p zIWjzKKAsh|#L7rEklsBDwE+UYSU?89QPytS;5T|(zOC;#0o|(aNA{kH8h#_>`~6Ds zXBe`j+B8;`zB70IG1QCYW0Zfa$!D~rIWX=zwdi`wK{7QjgT1DkCB4gZ-j4{9 zNU-!gBbvF&tkgywjfFG}0wNhEFVDCDbc8v54u;Pp2_2FZX`rju9;6&=FN=&6g!xbW zVazWJy-cAivY5gbomqu<<6s2pF-^KNRK%C0gkZJC1|z4!sOUKJ)0;0YF+H$^lj3Jc zbru)9?~gCmDJ5cYoI+&V9yp>tFfAPkFGA6%eUk{Wo&nexrF zc!%xrT!iJ)lWQuZ3K&cLY^;sY-;(2o+;tys|kAXQ&lMucdqa0 zd(Q8+e)DU@IlL5C=;Pz54z+kX`n=j3`Lo#4LOaQ;GA`LYUUGmg`eJn5EJ-HHOU2?qTns{l|CP4%skdnd- z@(TvOvcvqJF?HxXk1I#Bx9;%kh+HXCe5KnoIg)Pad{iSZC9vaZLmAUd7Mk#aqS>>u z!JE%P1*DIJkj@-UI%Cmb-UA~@i16ie?mTu2$^%*VJyP^etG}^HO4{dF<%Z#&38MwY z-SZrdVf3VFp+!KJGyhz@j9Hfhd>kSYFB`!D8vPc+0^~WsHDeC)zxe_7t=Vn*Jy(x@ zS-q!6SY-n~=Za9y?h*hAl>YUtU{&BIkI(q7bL&(Nf%s@RF?yiq8z5Thjlqn8z>S`* zH_u0~(C1Q{$YnVIpX&EUz}-1m16&8o)L?aXpDXW33)m=|Ztx}c9`x>grZWcMv!VXX zd!&y~0Fd+^9O` zq%Yu+noqGC(bHd}GZzrdh;ltBH4tY!aYC}^NkKA4+X2oq|9R_%G(jy|H^oBql&X;sLr8IVX%LE50NY0B0Nb`SQvZk_TC^>dzy@60_DMc&&ZR-` zqtPoaL)0w;--ioQn^Xz9k2|NTC?X5jo=UllcAMT4rIorEjow- z@|viM{y*m4J1*q)4Ie+pk&ziGDYGLQL<8-NinNqUrE=Pvv`Zy~)0Aj0G-zsThlZq~ zG^tP|EotxX^%Oo1`Ha{1_xiqm|0S*W`?;TcT=#Wf_u2qAnO369XI{MCO3RS!k*k*C z>%Kuj$G9cKs3FN55RTN4y>>077kr4j9xA5# zfWhmsq-$CejaWBb4Uq{yqJ3r#Vb=&K33#>$Ar!I6j^3ut(TmVbH_$s<$R3~GY}-Ls zVJ&LgSRLX+=GZsb4?H*T%^8j)^5hY^=RO?V`1F0&)L5t0#9^#u=vFfN{66sX2FJPu z^hVcqW`BBgQ{!S{{Gyc((0ixLGx3((a|91^(iW~KRKs|;!ZOMD2j@Sv*mlMx4;C+~ zsIRzswU(ahx?3ls4xQS}%ygJr#)~4s@hXtt%AH~k>TIxI5@xiJSg{>R6*2!v2RO~biQ*T zKL;Hb!rCrS<CP zk#bTz+UNdQ!P9naz-h&~O=MHM(k2ek`BOwOa3JoqJ?mth<7C}PDpjlNIQ(sUpAh}} z^^&A2A^Q)Kgj{tC>KH!{C;8^EmHF*vbfqogWIQxVWGa>%6}^$LXA6hTdw<`ua;C9$ObvHYDqFrCW}10gWNCcBRD zQSf+ox;l+#5VxU1S^|}&o0&x!cU?GH8{QEaXtHka($GZ$u#?<|47W`kNP3g&+mIRi zO>SU(^>~X>YiyC-mM3&RwCd+fEps;_=yo-Javd(-P{8Basj&inDNifZ7va}#qVd5L zSs1|!$H-?hz&mAhtHfiq9icC$E8&sp&W^oSXKE8U|5FC_KTm+EBse5x|=N= zb-{b1khSgDNX*{oZ1&&&tsO0*CJmQGSy~>m_QD5yX)+z6`SF{C$&lXayNe~$&c?nX zJT3#u8#}n@b{C{)zuVD4l-NctJD~2C#(S}ATIR9n4L$s@d3S1^j=JYvwfU5&1AEzC z(W*~FXxpENboZUnI5fV+_5teK2rOW=N^LI^pi<%QPN}X!`Wb9*PqMpW1~Um;h&l>7 z*ymKX!5jEeFf*Q_n;v#y)NmyEay1{c@%n^3hSe=<J-fOc==hkF~V zR#rgX+2N+(XD`oK>c${Jds|FuK6!MN-sAyvcu6#*k;z6ZDW0B)O)eKF+XECc;}vg} zTs^P>J8N2hul9RE#Gwant+3zj`j_a+vxQX0o{-F1<@6;k%}cuTrCaNY=uh)I*o+i6 z8n)ZI?npcAd-80FLHnQcBLA{Ne&^O=H}d9+t}eF`VrSw61oC~IMGP6s%?rd_`9Z45 zwtEkiKBsewueQH46ne&#hqChv>*nj_{|$mkgjxpT1&kajY|71&2Av-^7I``h-052SLi7?pqrtk`)aQD799yEP!ehG?=R5Aq z{BU~!o6+svaUh@tac(5F_4 zo#arN@eltTRQS$fPMsy*Vq~$TRL}Vg(kGW+wOvK;GrZ(svGo?mGqIq$DBToB|3u?T zR(G0801-Jk^LQD($Jdx*jZ*WXl7eEAsdN$2ae+|Um#gB8*Kgd?~b>w z>>H-XAZ`=v0TJ!YI(CST1tY$7xN(-RogO0^zPkNNrUNWobcUl4^YbCL{H&dzUE6}q zWt*P7cjnG{C254Z?4FLZt^UPzbc?1dbffY|aY z55@7dyOf-goS7JHt+7Z`i>hGJ=eT`Chyeo-g=U|L{SG3pv~c)G5s_m`*H(;Sv+?!j z(Q+#fNBg0!1|_WoFRIPC!@0vbtzJ*9hlv!UmRJ7Tn9EGV^_re!oq5!H;M?w=j?f28 zI`IyRT=*O$3z$ijx0fR#O|`$xTRO78oR3{aI`&Spb(x`>H(4Z~(pN-ZH))$wsWoxh z4_m{cdIkwbNux0lIz1ZNmks=(q@gE8)l#NtO<3}05L zm}G1vCp80YY4dfb$nn0pv^vSE@B}y4DABB2!f66KFg#~;iK#Up0aajY-2mnS**!DP zp)iyp2$o{t=|h|q$o2^)7H7HxR={Yo&v9yk`|=)~QeD~*#8@^0o-!|>5(~fTRVzB5 zXK%~7{>xJ?BS1s4We*RQs!yG@#r6d+NN!zkYB{XOG0zuj#?#jxF&DYfaV814ZZfR7 z3@v}lSGF+UN*-kJB5)4#)^W!Ri-6H9yr?UY@fX#FSl z+IitVl=-fCR}^Ls<{J~}$8$;nzwzumxmzVe8+oO1lZBOLs=yqVYc1Fr7+Q{Pa^`?z z5-i3^J+?)V?pCoUR+A~MlX`kZ6p_-)I*0P> z*#Yl`3}=LgTJU;xD?);u=@ElFxdjEmCGyjXHO`wHQZddYD=X55Qr&HDl?Y9|S)mUP zZE-q=Hh|>RA9IB^Tx+jvLxFz-LP}MnFD2?xb*V0hILg--SD3JHW#~P>odQ8n4n)#@!-{$Z-;6)Cbb@x0Yh>R ztFN_ourcrQuMpt)^)GTjP|!DyiX)Ru3v+@>n~;BuO%u3C~@PB`DWH=BnN}_2~*5f5zK=V;}BRlkG-qjaF|U zqTcGkXEISLw!ubgtiD*VcBSq7ot_BSkdMCIHHP^q^QX*F;mXG&p4=)~WFdN~pXBeQ zzU&E|RBs*wLD^x9($^wS?OkOXiqval1+qD+Kdn9Vr?X9Iwh{=Ro>wJCjoRy}es!e# zv?+!S>eH`=7O55ap-kXKev)LCnMUPQ`(T)~OLnU023u6&q+6=8`_F=B_+=zS<{T{i zZSSo$=3;;o|H%gW{w=#%wS3SCl&8V>7)B9^?}n zjQepo!ow6#(aZR)`TEwQMVlF4r)!)~7;?s|{IdxS9s`OvULa;2=n=YPrzFrvZZdI_m~K zx5DZseXKcH796*)H8o$iUhGef3!bUZ;nU;oyA9O?aho#x#&bV8vxi??&Z`%p8^?PK zOWk$_Bi?shrZ;(lF}hk(wdsF+ac^0ffF~{F+)Ndp5l%!QHJbnLbNwxb(NWDj-SZ13 zqRa-E>*ubBe+jGq`;uw47~^8cRmk?|n{_x!IDpd3KYKwH4(IXibi&{? z7YhS6KysO2^Qt4Q)Jyf^9nx9t zRj`(<3#~@!;ad)0M=h9|h>9fA)4ofygCt9e{n2OjICc?;SLEnMlI=1b2q%EOtY4h* zp=bSsUqSq#Wdi_yzjFTmXDJ%7QO_&et{`wwPiH0WX&8$s4~iG*25G7fc@ zEK86Q`0UIbd7J7`l-ZxI)OngJz^JJ_n3(+ir1RDxtM`M76?(HM=;V)Y@9Q~yMBd6v z`>Q+h{U!VMHKbsT{oJE6E6H*KLv>Yu|I0&tiO6jbLPMrY74WS04L5O=ZIa}4LZ>I% zJFqsUe_#I$E=#;Olu#4Arb6s`jv&&Vm;oRRL#76zqDcFsLEz2mTLso8#)q_K)-5!i zy14A4pc$3}bu1Jb*-x`98-PJkDBbhYKbG%1H4!3pnQ5crL|=+l$@RXKTG{4z-1mq> zJ##AD>?;?@$SL z>xWAeT&QT6x#;ZH{Z*)Zf|F+B?jrcJ)UHw3ju4;93ZaQJLvD*CND0#sI@J-HRIi^y zHNMX2Dfy|b>Qla0$JE6GGD5>hXw4Ey(WPh6Ls0EVBx>Gc`3nViZOC%t+-OH{Cx7}7 zbw61RMo0ifdHk~qi$&ApSYP)nRwF)@lwWa!ob?Mn`!Eg z2$8~umR&6f9x;#2oK_QudwApM6S1lowZZ5V2zSS)^-?|No1W-TUa7IejrL&yE*G~e z2W}RULz}mRq#lDN#wGg4oI|mDnxRP=g}o>-+7;N=sNMeI zb1C~q$0DL+Zw|&l)b#N5REq&sDe>D&{6wh}_Yk+v#!shfS=f)!FF525^!JP{^$Uc7 z2eL=e=6n!gE!$gO40C=s095$QM8$A}Zm3bh5K#J?kHSc!QLbROG?dG`%&2C%nzwyT zrHv1{4FEk9g-m{6(Xdbu@%=t=y!Ll0wW6E_2*tdHYGp-CxtdBx8}dV@Z#te|%;DRO zy_<$~C3U#vz1!pz-Xw6%OIJoTOmbaOm(fw>0q7m zAb*jSj74jma!^ES@Nc0@EI?f2apjN-=H<34Adsg(znIY2^AXM{>Ch0hp(Njk$>A0u z;Gt6bxV}fx;Ze+lZB-%s`kRmoZP?0#<_*d&#l|T(hj#RcMV1DdJikX{eqO&ufo%F5Psp``+e#OV({boz|LIwgrXn zg3;5{L#LTF0#o~-S}cf$UxHx+r-@sI`uwAeTa@V0vehK;TNUxtZ@aKT;!cCjSVM(_ z57G1PyK2y-A5^^pJr-69e#44MBx62g|F5p6yPZ(4mW&F;f zxFTE9)R1}_?Y$RLdH+{^nE&iPWV8|fS$cC*XOQ<_aH!{Eiy*^V+CoZV*Lk9cllE*G zdhl=oeK;Ce+s3)~?D5$E56|`%lPZl@SH8U>IB@IcqrXkNYZTRkx4fdI!e`qL`EhJ< zpsIF#@{Y6~+T-_aIY~F|8TCM?aON)asP_ldX{Du7g%E-8&7J^^GL1B^WEk8`}RWtdMxNIo@-)$i_Vb;HCm& z<=R^{YbwxiYM%1hixFyoACagu_NL~Q&(GK^S z60#fqom<=Hz3242W4jNzzhz&r_6=9UtzXf%qpX2D5Ix6_+iuI zVYGn)8O10|#>=g8R})ed16uMCud|f|NOSV7uF$wrHkznj7iilDo?(|);*%||H{Kte z7K>66P+t*n(s6RI6%eOqFIu|E!Mt&Sk0|7Jy>@Gd>-OA#aX}f^dn`tDL;kQy_O+(1 z?3p(op5H;|a_C9UI`!I!-^#d#KE@-;b|G~&D%)YETk2!KIK3ef6_26 zRYz$WalQafUzJiOsE+|FiWAWeE<2w0<}$s{0ru)-l&3zAR$p00zelbNPEeJST~Z*h zF9-&;`)u127lAy%P0st8;pE6d+nX17@-mC+485Nd*qzdsfm26<4J#3nRi|N)o{5^p zKFv-E3l2s8PV$hB@@I>#O8n(bt7?9XdC>rvQ~}JDpF5X?4=Sn{r&cwo=||kmO`N*4 zc(u4yD{$S(qy&27lw)r6d*dG~3>ON_3xq8?@$Z!bFN{}14oPa?AVB&1@;xhHUHKABcLCE z9b#JkJ&Q5j(P`X^@6(GFW?dl3^pLuF7{^`=M}C5mO5%41=DWL`M2CQEr%!s#;eZ@Vb|BwI)lN8Y*? zyRgQueSBqwr?!t06L8IVrE>py408ERZrj{ZxJineJ?k08ve=nr1XniLeki?8y`cd? z%>Q!unTOQZhXIR-j;U4V*bSqaX4HUlj~}@Z5%z>6MOBTkugm^Zp8CXHwLhq%W*No) z#PZ|GNPc}Ttnz+`f?t;MfP^%qfW-cg_eEZQ|RC}If)vMIAxqB~# zNo3`7I%&$A682Lc&9K*7lu``eb&O%R#vwL1JI`+q5G$vN?h;vGTx@LfNA`~cO{ zzH=p4QwDN&!KsOMA>T;pUQx>MR{(MhG~_*xV1?1anjmtZ(!am4>29!!ia(j3C7a<- zHMfxopSp~-qE=tlihfIa{_O{0!PuygW;Pr=E5-DTZIfP=`%m9NP4)u%H!lic>v@+- zDjety%3oyC-4}KLA+;arf`>8XyrDkr<*jLjfz!)BanFL;>wnEm;s!TM0`b^ah#>!NTXqbJHhTFrlRtn zqijiyEDiyp1x$Bg#LDs#?`^(4od7IVG8LJ-c01oLhc{dk=l}lj=HQNy$gmPF#ms5u z-_9yXQwB-&mZG{Hc-JKLFt$Un@nB|q+z@tcrh4#_jljIp!q#f-4JsKK^RrpYYS^>p z9-Ma4z!qUP1)(lNO8&(Ge7)}ff@;GOaq;OpjfsFQ7^_;i!y-K%WZ~#PzP20tc#@ti z!)~0Ogsc6em7^ZkaDgclYv$`gmN~lLv4f6FPbLU$Ct}|d2h)$eBO8-6i#^=2+GGz( zyw!g!TyAuyx^Cz$^WELf7HaUiUXRvJqj7li0|Ml=wcMR*|6HAx^3qX5Z^6;v><#?~-wB1Sj!5UL?k5QG-k`j~w36y-f z&WYz(=tC37I7GM#@7Jshsmsnu?k}RWR$TY>bbfj;y&c5m)<{Rq-H`c(8Qhh?WzklP zo_ztVcpXOXdU$`7Hmd9*FTh*|qEPCl<4AghTjvLC*82>ATnx36vJKJWJS zP{$2383ao9L_}2@vtL@btaDOpMm!l_}=q6#zaMQw|%6+Z_Z|IsAB9ITTP%-)>+ zKO|cJG@M2fk@&1y8fIQW{xdvj(@*n+!lv*%J{|61C2!T#_z={f4dSw4W<|>3jS~bX z56>_8d-XO4Iu0HX(~9pNyR*f)Ai{BYA~YY{p{b&REtO-@bdT6oj%-*U)3V#;uDBEK zITxV~7Opdlr9$!Ksqyz#pSx$79)ll+0e98JZMcYBQMdr_y8>1Bs^w=@S(~x`G`leK zxWb*pc!Sk=7ysg<|7FhKnU3%~JTB{A&RHGo6tbvIp*z;>$JZOT34f0SErn#-BW^1K z>2ZmM(OqDIzbqf=^K_h8c8z*yuVq*L2{0+?FV-B2IR}>PZkx|-ADVOQI((xv=V8a3 z(m7BTccW}71eCTKYs@?9FNxBM`PnXtb zHC)fx9Tva9Q|5e7hzsFLDV&d(T}hKdwinWzzTf)elS-0pyAtDA+%!0uMiwY=9iMqp zR1H|(`-k{WC4maJWo$>d-zt*ZicjMRhW$?Gj%vWsxSKR(rqa4p|HFoa5h~0U&zH14N`B-f#jp?9(i|o2 zi4vES_^%HbewoMvfTGb?Nb8wvKVCF%3EMS02{K{t7l$bBJEO>Duoc;*!u9w17Qgq3 z$5_>m&V3ET$i~~wdxo=R&VT#RxAD+y-@HGU6s)E-Htx`-#o0rRhJDDVZ%mD%7%0NZ z$5kD@dqeqc&eF(?@fB7l2-=Sw8XNymC8VgNy~eS^+9zbo_ph5P6qdjtuo~O&d>cLB z11go~iL40dVx#R`g(-UctuLeMqzKO)v7#adu|h=z_mCyn{LAp*Mx#U8pDiS;Moo>!E<4kwm;7k`Kc*qgA;YAq}6)`55pVr`v<7Z~FUx*_ii~F;z`s2Ljn+m=6 z`+xK(2L5fJ}&<%vhM;g<_ z`l;XMEh)$-OaOh95xC6>C=%BRx)JA97NO+ zYABIADU|P;wcqXkYF9pNQ|7}gjATawX4y^rwciax!0tQnv2 zo~3O=puHUIDCgpkx-`CN@CEnY*&ki`5%yJTegY4WH?JJ?hfa;iA!36g{1K3($j@)s zn?>4-R1}xm*CetI9X|y3MwO)@J@-jgvJrcYkQ@5N%+B23C9{J3Q8L`K6Z_wO?@o_= ze%5@kE zM-w$ygLqw&5?-c^I{6iY+~^J!60@4v(6AY=AV386n{576kWU3}tzrHBZ=1Hh9o5VS zT*>%(Iz1sd|03(o9cce?V%U~bsU=3?K8usZ5%*$Y+wrG6EwDUV4YC|`@oFY6hrZ?U z)*!bPU|@)zBnGb~E?2C??(v-~{c;dr(!VM`%HUrm9F`ZDV)9Ok!mh%aV3cVp#lgP6 zIVHf?m~SIi6Q%$YG}x*iqCop8KE?J*@GnY+k7PMvNap+5&i>p7^HY4gV1*`5VDIf1u9J^7rWX84MxeCj0tzTmootn?r zo4uNBNNdn1V=2YpCb4&A#NL?c(D-e}pY==?kFQNqd8d;&w+z{jFh_ZO6xXQKj=V^? z73Rp@{K}<_`L(Kkd2R#uKi>OSAGV14AP!*n*s1@mlNd}6%IsC#tRCRkY&7cjl8Ldp zJP$IRBGRH$JEfp%QS;!#ZC8z3&NNS>6}jy2_n9noY9J@U*SP;R7Cn00I(S8A{U9EY zmdk%@#EiK1jAafBSBBHhEM+(gNAOXj}N4DO+%oG#ut! zS;Yk=zN5{VfFp>vJjVOiMm&+812V<3mV?{$-0+yY%N6D>-kfTw@v{T$Em&rq@sByj z+&9sQAMeISDtgvZ`AB_mWhb9z-25Vs_4FWiD8xm&h6vadk+A?$d$FNm9Wg!1*D@+) z-dSrL$qg#jI_82;367J7WCXGg%*8^0%QOkupT*0Du0LWz|8}fuEaW5JfI_>@aXa!W zJO$IonUO(^FFQk2HeN{MY3Wqpda};x5I-6N_xc6RxJe?&afulzpl4FT)*)H@H%aGb zU2hNnagP4$13p8!5N`KN9izF# zss?StD=<9Uy8+(#!@8M~QVERryts*!isfrX_VWWhoArxdU&zm2@bhOfEpJiFVIn)DzOao~u=_Wk$4H|{MM~8 zaSML~lTpA9NT89uw%qTy$iQEtGNO-4ddnNum7n5|f341beuQ(9ICs|n56;~$IY0b} zxA>cD9icV+B!PT>M~qbKA2$T-!KB!O?zES0(S&wIEw7N=ROLsgY>5pzf${>mstarR z^gg56_jgo?Z8Tf&g(W-JPL2(-szjX^W~3mm11Z+Q^O}r&T5-y_39-(%NkSxiN`btK zS4;MLtIf!xK3TJp%bzvE<2NS-eAEyfY_XHPAT_*+*0Q+zAd)tr+@Cwn3h%~Uqq(!f z@_&05Jopp%ikoKT2%ywA8;Dr0=Yt|aMa)}XrBTCm$6FBnY{FmXi3)}~jOhmZ}c9Ou>FI}1K28$89NIX;s>!ZafU61N$vTo+K4RXlH3C0m$-vZ zlt`wLB8ZyU#qR;9AmV9G00yad>5wF!uS^o>qp8(@ecb+9^&VsVy&3$4T+tbS_4R|W zcYnq_y783@Kmx=$*Y)4cf&BROKfA_L%kfvLu9*F~r)E*-{4NSI4i15l_0e0%+SXP~ zdFh*jb06K8y4IXPa(%@c)oYFu!Y}x>kEd*=1!lepM3 z&q-;Cgg?jD%gfiVOUM~gE<8kfTt=uI)_BGgpO1;pWFho~=tu%}-$9)q?`~YYAMP@- zvpk3u{TxMcRZdd(z3*uttRj~gbi@&lHa_nq(VhGb!QdsChVESz;>_ZRGvglrcsvyu z*o)>DZvQ;fuV?gqdHtWy@#}YelEkB#d|B_d3%@lyOZZgnJTkEso_9LEsvY%^8xoGZ zs=qAa@-XeF3GnHjwY$UQSRvX=Ynw7HVyun$mg5zBs9L@Ml3vX;rZ}xCnqh`AM)(cd zV~l!9^|bXpc%M-40u=u7at=Hj9l1&en*zvjN|NJjP>hPs!Ep{n>U=k%@b#}H8Qc)b z3o zo%}iMCiWs-qqpJp|v>7#QI7^>TjqkT}0aG5oIa zfyN_q-{gOO@wZE!i!8hM;5al-ZuUdJOl5F^A>5^vZ_B8UKlX%dsB}P`{6C~H(V)0n* zpSV;?BrxULt{Ax*V9K&~a-6@Y(Z@+b7plhhV?*TMnsEMJm{sPQ*|+f35BXCN4koBl zt64NMzBrpT?j5mjQ3-JpLY0e8i+)tF#xuX&Yuxn@^`+=nza?rf;lGt(Xjw=QY_t=o z8IR`CC~0|JUg$^}z}7NIy=Mdjfg83yQ}!6sVH=DF?LWe?7jWh15lZFV5Puo@^ms)p z$zLS`xR~FA%1CI=10QQxX)wS~P?G&t&3?v<17hRM_+F)(7Xk829x$5%vuNtG;#iAg zr%DUUr1q*d0t zH3A+*|2v`T_Q)&0&3JR445|xEIUM9wUYm8taJq3UIBW2ZkAw9R>>38QhQ zXm%AYVb7MKbY>YI=xO33sWowAv7qo-EORv&3M(<6mdlsV*yeot9Y5(F@T1A*b>RdO zlMZXN&Yvznx?h=)(cISV-cqaXIOzu{v(%#__5$^GE8LwTnA>XRJDVsj;#WIqgA6uJ zg9MK`xEu7{zX%&8ICb)J0;^tyTUa(WWE9SEDm7eJ7t7%VFIhxd$da?V4j<%)@Qv@+Ki?^FqgY zIM5&w9qwr=mE2p}66!te_B&61)R__#{y76VZ&7vybgL2m!U;_O#xxpc?C;GTHzD$8 zrWi&lM5jS<$WC%BH|2%w(ZcITchXHy^-gafBoAi4L_O)ug{{S&>^YA+##>RO$n?=v z>+0~T+j>?HKX}!)q7g-?H^J z(3d!FIYp+J71Zqqm1-l-+yk_pLcP=fLe^6XcOMm_kxy?T?Dm>}sQiGdoAEhw+Tr6v zNCi=WqH|QlDJ8k_M5ZV6H4_LTd4k1g1-K)&gq6CO&-yfL~~ znleD*VJfqcwZFE{y^?`IlT3Z)Ka;2`f&@2KkKMfFk%MUviF{9=_Jvy-^P`>2ai*J^Fg zrIibJa|qb?C$zQ-Db!Hay&(R?4C#Tf!|x2Czd4~!UPZ#6a2$-IM!bqY4jp)XARe=5 z^iMwIg&d4Sz@lt+SMa~FhwuMO#++nUz1JpZU~{LU#Uokc^t#3oZZTtr>oZXD*&FEK-*RxI?qt{Oj%thWTE)T+|1$q znyC)5+Z%8m>pF~r`)1nQtEKkcqF$=txy|`aH?-gqQ7zUENY!d4)RZ%0M|!+!kII-5 zp?Zl>3Yipm&l??TahyJZB!w^KB&C7Li;z+ZNkSG##Kfg`a1_;kZ|0S@$i`W-LcwZM z9I)?piy+0k47*fKI1OVHMJFI~UT-mlGqVqw_m}D^h zg29^g*%O~jcf+^poQ`h9WiJ{4^^1cDOm(G3GoOSrZN*%a%T}Kgw*%g^tgVhmI}@$Z z8=V}s10MqRm783r=HM}T^+4(J$y&oVgr!lAETqcbs2~|L^~Jbqny@?Lg0st14_0c~ z^jKz`W)9yyX+K2#t*7i27cWM!B9w<4`zwST<&&3SL%2$hm23H;fOELC8t~ojh>XWy z40|k)lnNCTFaZ@@cGoCy2Nvb5fVTrm{_{?PGN@4Y$)z|O^@%A+LSmHphHc6CNAd1r zhU!l;WuhU^mFMTqFl~Zy6C?hA1_hU|eDI~RDpAX1+Dr#F}htHW=fsm2fs zQYo{6-WH*(+<>qWbTRk|>DFcFX3i}oY9zUgglFwi&Yd;bxLc2-DU^EGAbM4n;mg*z zOa88mTaHwhyKuT+XnHjocaD#H-YO1{kP#{}hV2KcQA~sF=Hf*GL1rqA?{dlTP$H%> zEyvb(zf3=U5~-rslu`$y-q|4yk!1lAZz4fj)(+p)DIV%EUosa%n-RS}9a)lhLa7)< zZ#>#Ti>xwphdr2Z#UxIokCsZlN>I-OV}tdQq|e3A<2z#sI+=xNGwGd)6wzQAf?@frg8W?;j3CUA% zCM`p>%ot@qXo-bbE(wtMT^ZC;c?rK=w-}A&-6bcL;kshh1#;d^ewIy(-IrjYI1x(?}7lf7I+nDzt`;9)p zRDC1{Fd0akTPAs8t_%0N?9>o79aG~3b!4sJIcrf1?X6_E1cF)1Z!U{II7k#O@a^~R zm+;mHBw6F>O9u{N;m*#u_3%Jpckv0=M!IpV_Be(Q>9-oM$H}DRlH#T}adoXIiW_>d zzF%}s=KijHTKu9WB6y1bKXrMA!idk|jVkejd)>g0kUM4o^n6EF z?p0^@yos|SfBAQ`7cxf$UAMm>4w4*Z_+(e`cG7Vh91p@Y+5yREDy|;5!D9rH?ynVibu&M;`>A(j#?^2ed9QArlu12L3UR;jkyf^3BC6wY(A%2ptpKykYq@ zE3&W`v5#2(X%i80G|{kp!r!VpEwgul<-q`U$uiYTc=RFj> z0AZRgEx1P|B9v48<~k0o!%Nq3ZKqK`*H=|BPC;`bjC%Pbz=Q$>8}+e4#2Aq=BU{LS zMd#*_>^v4D+)Aw!lI0=y4)6DlNyxPgLJU$82$nmxb-y?IV7APc5*ek(Z1Pak++}ftSH%}b&rO6cbo$($x5LZsor6C zlCp!K?OU9y75Y!2$KvInV@=K$&`$6gGz=75GT?n=NVhe0FE^G&lx;=UmNBNCj>FlW z1RL05Rov|IC-}0md%J$I_&xv~6|H28XAW}*s9%6}2Bc-~6YzePnTL4-{9eoxq*6VA z6y;lhEfpcL*M+0Ij_}?4v^Q^5l5l_fHA4C&b;pd8> zAV(J?liCPoyG4g0x3t&+m-&+AY#|r;?t&@A3%;g9P zNS;Ek8%!2cTxt30kN!pBD-#Il8@=lZ0F+wia>)$ zk7TVIpbF<`-`Z?7@@ZLsaAn){{`6{lqF1E}%Z>Tq6R|6Ez(451#&&<*kgi1H#3y>J z@>rc;M^s!C4?)5}CPIR_+sG(eH($`u8Pu9uW!|6fzaIz>-4+v@^pwGnrOzH=E@`O7 z>-KGcC~ZsCHwMyk@n}E20sGhyP07E0kI@O{PyCZX>Uy`Fb+RU@i~J54R9giN_{(;9 zKMDa?c1X5TlUth7ya{+p@|DPnNn8jXOU$^8VDQOTV5hSI(L5y$8ePvL&K-7Qw%c;2 zChy-qLcJPG)L|@TzyV*sG&Ceu;fGIL;4ZL4iASi9m%UX;4`l}dJhfFiPjS-#@Lc=C zeD-$#n4wpa4%W-)rse+=;Q5Ig1{(iGnfeP(zexj^+Y~NVM-DR(tI0r+Rvm4|nTe6U zMC6mYr*~7xnWQ`yRT@N?eAy%8Jit7}#C+}b1+;UfPzc>1)WdV#7-Ig6yTVpIOMM%D;xGo4I z1Sbf^YO*Si9h~Th@nO^t#f0@u60X{u5eT-)k1@owDUs@h3{HhRDvW{IgIe~_5abLl zmrWpkyBmp;OVP9Kf*~w1>66HVQzkOf32Hek%fnfd`UrQC zplw4ma{G=^GC#H9UB~5rU_Lt}#a!kw$dat~=SF zsY6W};06mIq#Eb{H(aH3L1kW`@NB9s4-ydcg?~bU|Sro;FPBPdQ(Y2YfgJW-fMVIo8Py1 z<)Z!GJ7?B~z7P+)KE3T?%6sU%9E*u4I~aBk<3R*P`Neq91!RD1Ck`52`O($SQFVUv z0AaUCs`=D-_l#$d(fi%e%JJ4qO|xaI?zZ(>yhN_uio(sXvV2{#1m)(EWrt@((bhN0 z8&_(zx{C{zJuTwz2=~@_araomASdNZ9-Ibyd}^)on!qxH{2R=2 zgpcEOVDxMVL0jl_gS_O%+yf}I|J?MI3jnXUO;%X3y!wuq=L6LK2A1r)vHss7UUo^e!8RMJb?@nI=omTe|0^Ij&SOe{^KM^Bg~;! zpSv=t51b=PcXd&(pO8_ftf_%&=?Y3^b#N>g{a=jDG!5$S|D{w-#PS+al~%KC!LC+x zo7Z{Ca#wFOn%z4YMwq^54>$mYRUpUxalIwkti&Znj@dPZ6&%=3_nR(Tk>UM%#w*|` zoPgP2smcGA;i1oz^o^PDt+|-Ga-=SAYW6xEXAb*7hK()lIjNT;`zP@!Hje0VnIdh~ zsZm(X<^O1+CvSRc1bI_ns;J3xzulT|(UyP4B0VW-A3va9hk}$+`N%b0k4|JYbV#Lj zrSu0n>)83&KF6$<^4qm}!HNw*siG{=x|U^xEJ-e#LfBo7&Z@FEY92^~%L0g| zH>0z5-DiJZ3j<~@j+_>rip^ITRuRBSO23esIc1--mghzxK|`alrAI=yW>gSxXv{&O zXS74waa~7KLNO;G#RotQOQmu!8C(dtOXW1F-@kg~!HJ}1Bj3WA9QzdEp)~;Q3Jo=|9r-CT3oJyo-^4Wsz0AP-iNe3F$}+{u-Uchz2_mKp;puUq4g zdiVNx!`1T*h|2o-XjH8kO) zd!g)|^cVuW-LWgPcqISSOY+MWBC4QAHjw-VBC=5j?xxOQnLR&e#*)iE!t-eOWr?DGoVAf^7Hf7Sc`QT-()kG?sOc83;?LGJgLbNOQHd|}f zD#*!HiO{OvkXJc!Upe3m+F>q43(T-b+!azRt0u>XJ*iKf=;^3SP=5uG>D|-ZzVU7v zu7>o}ErjnjGbK{bqWg^tCZ#_C^L9Z|yeVeS8u?YW^)ZSG)~Ju}faurIuPEo&^MOpP zA*i$P@l#xy#B@pQc*s+e@Cz5jpRxL-y@_DY9nmPX%DtYPUS(gpII~LuztvbJ9kAcf_NR>Q*ddkz~ zlDCe}3~iuim7-GFhPX(@DCQAc&uo>C!NTj-JMx zCLKY+6^zBu1L{;4Q)X``RE(oc+Rpge|+(R5V0Wy^fG_l2BvoM9<} zRZaldEsfu-m&A6g;lh>$k|ccXHmbj$WS5Y#iGslsO?bl-j6I`n?#EXhvO+;zfFw7> z;_|D!KWy^P|87d8w9AwvpX0XD6XBfpeO%7BqwX85a8g)(w{22NSK6t9*)CB{+83%* z(g}5K`fAgO(X-@yVY?;IcIpv{rXBWPT_9}Q{lNpy@DBDf5MnEWSR-|Z&};j_3_uVX zajCTiqy2oFh#P$&wP{$0q2fdzSYLOW-s)_rJcRBxlYvj%FX%3B-~?#mx9<6$bPp