Repository: feast-dev/feast

Issue:
"feast apply" does not register a new BatchFeatureView
## Expected Behavior 

Same behavior as regular FeatureViews

## Current Behavior

Take following sample .py file and run cmd "feast apply" -- works just fine for regular FeatureViews

## Steps to reproduce

```
from datetime import timedelta

import yaml

from feast import BatchFeatureView, Entity, FeatureService, SnowflakeSource

driver = Entity(
    # Name of the entity. Must be unique within a project
    name="driver",
    # The join keys of an entity describe the storage level field/column on which
    # features can be looked up. The join keys are also used to join feature
    # tables/views when building feature vectors
    join_keys=["driver_id"],
)

project_name = yaml.safe_load(open("feature_store.yaml"))["project"]

driver_stats_source = SnowflakeSource(
    database=yaml.safe_load(open("feature_store.yaml"))["offline_store"]["database"],
    table=f"{project_name}_feast_driver_hourly_stats",
    timestamp_field="event_timestamp",
    created_timestamp_column="created",
)

driver_stats_fv = BatchFeatureView(
    name="driver_hourly_stats",
    entities=[driver],
    ttl=timedelta(weeks=52),
    source=driver_stats_source,
)

driver_stats_fs = FeatureService(name="driver_activity", features=[driver_stats_fv])

```

### Specifications

- Version:
- Platform:
- Subsystem:

## Possible Solution



Discussion:
@achals when register the feature view through feast.apply() I am still getting errors:

`Traceback (most recent call last):
  File "test.py", line 66, in <module>
    main()
  File "test.py", line 41, in main
    training_df = fs.get_historical_features(
  File "/Users/madkins/Documents/feast/sdk/python/feast/usage.py", line 294, in wrapper
    raise exc.with_traceback(traceback)
  File "/Users/madkins/Documents/feast/sdk/python/feast/usage.py", line 283, in wrapper
    return func(*args, **kwargs)
  File "/Users/madkins/Documents/feast/sdk/python/feast/feature_store.py", line 1092, in get_historical_features
    fvs, odfvs, request_fvs, request_fv_refs = _group_feature_refs(
  File "/Users/madkins/Documents/feast/sdk/python/feast/feature_store.py", line 2553, in _group_feature_refs
    raise FeatureViewNotFoundException(view_name)
feast.errors.FeatureViewNotFoundException: Feature view driver_hourly_stats does not exist`

`driver_stats_fv = BatchFeatureView(
    # The unique name of this feature view. Two feature views in a single
    # project cannot have the same name
    name="driver_hourly_stats",
    # The list of entities specifies the keys required for joining or looking
    # up features from this feature view. The reference provided in this field
    # correspond to the name of a defined entity (or entities)
    entities=[driver],
    # The timedelta is the maximum age that each feature value may have
    # relative to its lookup time. For historical features (used in training),
    # TTL is relative to each timestamp provided in the entity dataframe.
    # TTL also allows for eviction of keys from online stores and limits the
    # amount of historical scanning required for historical feature values
    # during retrieval
    ttl=timedelta(weeks=52),
    # Batch sources are used to find feature values. In the case of this feature
    # view we will query a source table on Redshift for driver statistics
    # features
    source=driver_stats_source,
)`

`feast.apply(fs.apply([driver, driver_stats_fv])`

Resolving patch:
diff --git a/sdk/python/feast/feast_object.py b/sdk/python/feast/feast_object.py
--- a/sdk/python/feast/feast_object.py
+++ b/sdk/python/feast/feast_object.py
@@ -1,5 +1,6 @@
 from typing import Union
 
+from .batch_feature_view import BatchFeatureView
 from .data_source import DataSource
 from .entity import Entity
 from .feature_service import FeatureService
@@ -16,12 +17,15 @@
 )
 from .request_feature_view import RequestFeatureView
 from .saved_dataset import ValidationReference
+from .stream_feature_view import StreamFeatureView
 
 # Convenience type representing all Feast objects
 FeastObject = Union[
     FeatureView,
     OnDemandFeatureView,
     RequestFeatureView,
+    BatchFeatureView,
+    StreamFeatureView,
     Entity,
     FeatureService,
     DataSource,
diff --git a/sdk/python/feast/feature_store.py b/sdk/python/feast/feature_store.py
--- a/sdk/python/feast/feature_store.py
+++ b/sdk/python/feast/feature_store.py
@@ -775,6 +775,7 @@ def apply(
             FeatureView,
             OnDemandFeatureView,
             RequestFeatureView,
+            BatchFeatureView,
             StreamFeatureView,
             FeatureService,
             ValidationReference,
@@ -834,9 +835,9 @@ def apply(
             ob
             for ob in objects
             if (
-                isinstance(ob, FeatureView)
+                # BFVs are not handled separately from FVs right now.
+                (isinstance(ob, FeatureView) or isinstance(ob, BatchFeatureView))
                 and not isinstance(ob, StreamFeatureView)
-                and not isinstance(ob, BatchFeatureView)
             )
         ]
         sfvs_to_update = [ob for ob in objects if isinstance(ob, StreamFeatureView)]
@@ -919,13 +920,18 @@ def apply(
                 validation_references, project=self.project, commit=False
             )
 
+        entities_to_delete = []
+        views_to_delete = []
+        sfvs_to_delete = []
         if not partial:
             # Delete all registry objects that should not exist.
             entities_to_delete = [
                 ob for ob in objects_to_delete if isinstance(ob, Entity)
             ]
             views_to_delete = [
-                ob for ob in objects_to_delete if isinstance(ob, FeatureView)
+                ob
+                for ob in objects_to_delete
+                if isinstance(ob, FeatureView) or isinstance(ob, BatchFeatureView)
             ]
             request_views_to_delete = [
                 ob for ob in objects_to_delete if isinstance(ob, RequestFeatureView)
@@ -979,10 +985,13 @@ def apply(
                     validation_references.name, project=self.project, commit=False
                 )
 
+        tables_to_delete: List[FeatureView] = views_to_delete + sfvs_to_delete if not partial else []  # type: ignore
+        tables_to_keep: List[FeatureView] = views_to_update + sfvs_to_update  # type: ignore
+
         self._get_provider().update_infra(
             project=self.project,
-            tables_to_delete=views_to_delete + sfvs_to_delete if not partial else [],
-            tables_to_keep=views_to_update + sfvs_to_update,
+            tables_to_delete=tables_to_delete,
+            tables_to_keep=tables_to_keep,
             entities_to_delete=entities_to_delete if not partial else [],
             entities_to_keep=entities_to_update,
             partial=partial,


Test patch:
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
--- 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
@@ -4,6 +4,7 @@
 import pytest
 from pytest_lazyfixture import lazy_fixture
 
+from feast import BatchFeatureView
 from feast.aggregation import Aggregation
 from feast.data_format import AvroFormat, ParquetFormat
 from feast.data_source import KafkaSource
@@ -78,14 +79,29 @@ def test_apply_feature_view(test_feature_store):
         ttl=timedelta(minutes=5),
     )
 
+    bfv = BatchFeatureView(
+        name="batch_feature_view",
+        schema=[
+            Field(name="fs1_my_feature_1", dtype=Int64),
+            Field(name="fs1_my_feature_2", dtype=String),
+            Field(name="fs1_my_feature_3", dtype=Array(String)),
+            Field(name="fs1_my_feature_4", dtype=Array(Bytes)),
+            Field(name="entity_id", dtype=Int64),
+        ],
+        entities=[entity],
+        tags={"team": "matchmaking"},
+        source=batch_source,
+        ttl=timedelta(minutes=5),
+    )
+
     # Register Feature View
-    test_feature_store.apply([entity, fv1])
+    test_feature_store.apply([entity, fv1, bfv])
 
     feature_views = test_feature_store.list_feature_views()
 
     # List Feature Views
     assert (
-        len(feature_views) == 1
+        len(feature_views) == 2
         and feature_views[0].name == "my_feature_view_1"
         and feature_views[0].features[0].name == "fs1_my_feature_1"
         and feature_views[0].features[0].dtype == Int64