diff --git a/go/README.md b/go/README.md index 43125bfad3..b2c34d37ca 100644 --- a/go/README.md +++ b/go/README.md @@ -5,5 +5,5 @@ To build and run the Go Feature Server locally, create a feature_store.yaml file ```bash go build -o feast ./go/main.go - ./feast --type=http --port + ./feast --type=http --port=8080 ``` \ No newline at end of file diff --git a/go/internal/feast/registry/repoconfig_test.go b/go/internal/feast/registry/repoconfig_test.go index 90a20b204a..4d30bf7bca 100644 --- a/go/internal/feast/registry/repoconfig_test.go +++ b/go/internal/feast/registry/repoconfig_test.go @@ -44,6 +44,40 @@ online_store: assert.Empty(t, config.Flags) } +func TestNewRepoConfigWithEnvironmentVariables(t *testing.T) { + dir, err := os.MkdirTemp("", "feature_repo_*") + assert.Nil(t, err) + defer func() { + assert.Nil(t, os.RemoveAll(dir)) + }() + filePath := filepath.Join(dir, "feature_store.yaml") + data := []byte(` +project: feature_repo +registry: "data/registry.db" +provider: local +online_store: + type: redis + connection_string: ${REDIS_CONNECTION_STRING} +`) + err = os.WriteFile(filePath, data, 0666) + assert.Nil(t, err) + os.Setenv("REDIS_CONNECTION_STRING", "localhost:6380") + config, err := NewRepoConfigFromFile(dir) + registryConfig, err := config.GetRegistryConfig() + assert.Nil(t, err) + assert.Equal(t, "feature_repo", config.Project) + assert.Equal(t, dir, config.RepoPath) + assert.Equal(t, "data/registry.db", registryConfig.Path) + assert.Equal(t, "local", config.Provider) + assert.Equal(t, map[string]interface{}{ + "type": "redis", + "connection_string": "localhost:6380", + }, config.OnlineStore) + assert.Empty(t, config.OfflineStore) + assert.Empty(t, config.FeatureServer) + assert.Empty(t, config.Flags) +} + func TestNewRepoConfigRegistryMap(t *testing.T) { dir, err := os.MkdirTemp("", "feature_repo_*") assert.Nil(t, err) diff --git a/sdk/python/feast/on_demand_feature_view.py b/sdk/python/feast/on_demand_feature_view.py index 586f5d1bac..0c50b25009 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 @@ -227,15 +227,19 @@ def to_proto(self) -> OnDemandFeatureViewProto: ) feature_transformation = FeatureTransformationProto( - user_defined_function=self.feature_transformation.to_proto() - if isinstance( - self.feature_transformation, - (PandasTransformation, PythonTransformation), - ) - else None, - substrait_transformation=self.feature_transformation.to_proto() - if isinstance(self.feature_transformation, SubstraitTransformation) - else None, + user_defined_function=( + self.feature_transformation.to_proto() + if isinstance( + self.feature_transformation, + (PandasTransformation, PythonTransformation), + ) + else None + ), + substrait_transformation=( + self.feature_transformation.to_proto() + if isinstance(self.feature_transformation, SubstraitTransformation) + else None + ), ) spec = OnDemandFeatureViewSpec( name=self.name, @@ -631,7 +635,15 @@ def mainify(obj) -> None: obj.__module__ = "__main__" def decorator(user_function): - return_annotation = inspect.signature(user_function).return_annotation + # get_type_hints will resolve the forward references based on the + # current global and local namespace, giving you the actual type + # objects instead of strings. + + # signature function to get the return annotation, can sometimes + # return it as a string if the annotation itself was defined using + # forward references + + 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/feast/transformation_server.py b/sdk/python/feast/transformation_server.py index 6a661382ed..b2ab33c35d 100644 --- a/sdk/python/feast/transformation_server.py +++ b/sdk/python/feast/transformation_server.py @@ -4,7 +4,7 @@ import grpc import pyarrow as pa -from grpc_health.v1 import health, health_pb2_grpc +from grpc_health.v1 import health, health_pb2, health_pb2_grpc from grpc_reflection.v1alpha import reflection from feast.errors import OnDemandFeatureViewNotFoundException @@ -73,10 +73,13 @@ def start_server(store: FeatureStore, port: int): add_TransformationServiceServicer_to_server(TransformationServer(store), server) # Add health check service to server - health_pb2_grpc.add_HealthServicer_to_server(health.HealthServicer(), server) + health_servicer = health.HealthServicer() + health_pb2_grpc.add_HealthServicer_to_server(health_servicer, server) + health_servicer.set("", health_pb2.HealthCheckResponse.SERVING) service_names_available_for_reflection = ( DESCRIPTOR.services_by_name["TransformationService"].full_name, + health_pb2.DESCRIPTOR.services_by_name["Health"].full_name, reflection.SERVICE_NAME, ) reflection.enable_server_reflection(service_names_available_for_reflection, server) 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 diff --git a/setup.py b/setup.py index 9168d5e9b4..6ea0f76d37 100644 --- a/setup.py +++ b/setup.py @@ -11,23 +11,19 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import copy import glob -import json import os import pathlib import re import shutil import subprocess import sys -from distutils.dir_util import copy_tree from pathlib import Path from subprocess import CalledProcessError -from setuptools import Command, Extension, find_packages, setup +from setuptools import Command, find_packages, setup from setuptools.command.build_py import build_py from setuptools.command.develop import develop -from setuptools.command.install import install NAME = "eg-feast" DESCRIPTION = "EG-specific Python SDK for Feast"