Repository: feast-dev/feast

Issue:
Add support for feature tables to the apply method
We need to deprecate [client.apply_entity()](https://github.com/feast-dev/feast/blob/master/sdk/python/feast/client.py#L443) and [client.apply_feature_table()](https://github.com/feast-dev/feast/blob/master/sdk/python/feast/client.py#L565). Both of these methods were added because `client.apply()` was previously used for applying feature sets.

In Feast 0.9 we should
- Add support for applying feature tables and entities through `client.apply()`. This method doesn't currently exist. Please have a look at [Feast 0.7](https://github.com/feast-dev/feast/blob/v0.7.1/sdk/python/feast/client.py#L359) for a reference implementation. We can follow a similar approach.
- Add deprecation warnings on `apply_feature_table()` and `apply_entity()`
- In Feast 0.10 we will remove `apply_feature_table()` and `apply_entity()`, and `apply()` will only work on feature tables and entities.


Discussion:
Hi @woop, I would like to help with this one. Since this will be my first contribution in Feat hopefully you'll help me out.
Let me raise an MR for this based on the reference you pointed by this weekend.
Hi @priyankvex, feel free to go ahead. Please ask if you have any questions! If you get stuck at any point, please reach out on Slack (#Feast)

Resolving patch:
diff --git a/sdk/python/feast/cli.py b/sdk/python/feast/cli.py
--- a/sdk/python/feast/cli.py
+++ b/sdk/python/feast/cli.py
@@ -145,7 +145,7 @@ def entity_create(filename, project):
 
     entities = [Entity.from_dict(entity_dict) for entity_dict in yaml_loader(filename)]
     feast_client = Client()  # type: Client
-    feast_client.apply_entity(entities, project)
+    feast_client.apply(entities, project)
 
 
 @entity.command("describe")
@@ -252,7 +252,7 @@ def feature_table_create(filename):
         FeatureTable.from_dict(ft_dict) for ft_dict in yaml_loader(filename)
     ]
     feast_client = Client()  # type: Client
-    feast_client.apply_feature_table(feature_tables)
+    feast_client.apply(feature_tables)
 
 
 @feature_table.command("describe")
diff --git a/sdk/python/feast/client.py b/sdk/python/feast/client.py
--- a/sdk/python/feast/client.py
+++ b/sdk/python/feast/client.py
@@ -16,6 +16,7 @@
 import os
 import shutil
 import uuid
+import warnings
 from datetime import datetime
 from itertools import groupby
 from typing import Any, Dict, List, Optional, Union
@@ -105,6 +106,8 @@
 
 CPU_COUNT: int = multiprocessing.cpu_count()
 
+warnings.simplefilter("once", DeprecationWarning)
+
 
 class Client:
     """
@@ -442,13 +445,17 @@ def archive_project(self, project):
         if self._project == project:
             self._project = opt().PROJECT
 
-    def apply_entity(self, entities: Union[List[Entity], Entity], project: str = None):
+    def apply(
+        self,
+        objects: Union[List[Union[Entity, FeatureTable]], Entity, FeatureTable],
+        project: str = None,
+    ):
         """
-        Idempotently registers entities with Feast Core. Either a single
-        entity or a list can be provided.
+        Idempotently registers entities and feature tables with Feast Core. Either a single
+        entity or feature table or a list can be provided.
 
         Args:
-            entities: List of entities that will be registered
+            objects: List of entities and/or feature tables that will be registered
 
         Examples:
             >>> from feast import Client
@@ -464,9 +471,33 @@ def apply_entity(self, entities: Union[List[Entity], Entity], project: str = Non
             >>>         "key": "val"
             >>>     }
             >>> )
-            >>> feast_client.apply_entity(entity)
+            >>> feast_client.apply(entity)
         """
 
+        if project is None:
+            project = self.project
+
+        if not isinstance(objects, list):
+            objects = [objects]
+        for obj in objects:
+            if isinstance(obj, Entity):
+                self._apply_entity(project, obj)  # type: ignore
+            elif isinstance(obj, FeatureTable):
+                self._apply_feature_table(project, obj)  # type: ignore
+            else:
+                raise ValueError(
+                    f"Could not determine object type to apply {obj} with type {type(obj)}. Type must be Entity or FeatureTable."
+                )
+
+    def apply_entity(self, entities: Union[List[Entity], Entity], project: str = None):
+        """
+        Deprecated. Please see apply().
+        """
+        warnings.warn(
+            "The method apply_entity() is being deprecated. Please use apply() instead. Feast 0.10 and onwards will not support apply_entity().",
+            DeprecationWarning,
+        )
+
         if project is None:
             project = self.project
 
@@ -570,12 +601,12 @@ def apply_feature_table(
         project: str = None,
     ):
         """
-        Idempotently registers feature tables with Feast Core. Either a single
-        feature table or a list can be provided.
-
-        Args:
-            feature_tables: List of feature tables that will be registered
+        Deprecated. Please see apply().
         """
+        warnings.warn(
+            "The method apply_feature_table() is being deprecated. Please use apply() instead. Feast 0.10 and onwards will not support apply_feature_table().",
+            DeprecationWarning,
+        )
 
         if project is None:
             project = self.project


Test patch:
diff --git a/sdk/python/tests/test_client.py b/sdk/python/tests/test_client.py
--- a/sdk/python/tests/test_client.py
+++ b/sdk/python/tests/test_client.py
@@ -377,7 +377,7 @@ def test_apply_entity_success(self, test_client):
         )
 
         # Register Entity with Core
-        test_client.apply_entity(entity)
+        test_client.apply(entity)
 
         entities = test_client.list_entities()
 
@@ -429,7 +429,7 @@ def test_apply_feature_table_success(self, test_client):
         )
 
         # Register Feature Table with Core
-        test_client.apply_feature_table(ft1)
+        test_client.apply(ft1)
 
         feature_tables = test_client.list_feature_tables()
 
diff --git a/sdk/python/tests/test_historical_feature_retrieval.py b/sdk/python/tests/test_historical_feature_retrieval.py
--- a/sdk/python/tests/test_historical_feature_retrieval.py
+++ b/sdk/python/tests/test_historical_feature_retrieval.py
@@ -109,12 +109,12 @@ def client_with_local_spark(tmpdir):
 
 @pytest.fixture()
 def driver_entity(client):
-    return client.apply_entity(Entity("driver_id", "description", ValueType.INT32))
+    return client.apply(Entity("driver_id", "description", ValueType.INT32))
 
 
 @pytest.fixture()
 def customer_entity(client):
-    return client.apply_entity(Entity("customer_id", "description", ValueType.INT32))
+    return client.apply(Entity("customer_id", "description", ValueType.INT32))
 
 
 def create_temp_parquet_file(
@@ -191,7 +191,7 @@ def transactions_feature_table(spark, client):
     feature_table = FeatureTable(
         "transactions", ["customer_id"], features, batch_source=file_source
     )
-    yield client.apply_feature_table(feature_table)
+    yield client.apply(feature_table)
     shutil.rmtree(temp_dir)
 
 
@@ -239,7 +239,7 @@ def bookings_feature_table(spark, client):
     feature_table = FeatureTable(
         "bookings", ["driver_id"], features, batch_source=file_source, max_age=max_age
     )
-    yield client.apply_feature_table(feature_table)
+    yield client.apply(feature_table)
     shutil.rmtree(temp_dir)
 
 
@@ -288,7 +288,7 @@ def bookings_feature_table_with_mapping(spark, client):
     feature_table = FeatureTable(
         "bookings", ["driver_id"], features, batch_source=file_source, max_age=max_age
     )
-    yield client.apply_feature_table(feature_table)
+    yield client.apply(feature_table)
     shutil.rmtree(temp_dir)
 
 
diff --git a/sdk/python/tests/test_streaming_control_loop.py b/sdk/python/tests/test_streaming_control_loop.py
--- a/sdk/python/tests/test_streaming_control_loop.py
+++ b/sdk/python/tests/test_streaming_control_loop.py
@@ -66,7 +66,7 @@ def _create_ft(self, client: Client, features) -> None:
         )
 
         # Register Entity with Core
-        client.apply_entity(entity)
+        client.apply(entity)
 
         # Create Feature Tables
         batch_source = FileSource(
@@ -95,7 +95,7 @@ def _create_ft(self, client: Client, features) -> None:
         )
 
         # Register Feature Table with Core
-        client.apply_feature_table(ft1)
+        client.apply(ft1)
 
     def _delete_ft(self, client: Client):
         client.delete_feature_table(self.table_name)
diff --git a/tests/e2e/test_historical_features.py b/tests/e2e/test_historical_features.py
--- a/tests/e2e/test_historical_features.py
+++ b/tests/e2e/test_historical_features.py
@@ -70,7 +70,7 @@ def test_historical_features(
     customer_entity = Entity(
         name="user_id", description="Customer", value_type=ValueType.INT64
     )
-    feast_client.apply_entity(customer_entity)
+    feast_client.apply(customer_entity)
 
     max_age = Duration()
     max_age.FromSeconds(2 * 86400)
@@ -86,7 +86,7 @@ def test_historical_features(
         max_age=max_age,
     )
 
-    feast_client.apply_feature_table(transactions_feature_table)
+    feast_client.apply(transactions_feature_table)
 
     transactions_df, customers_df = generate_data()
     feast_client.ingest(transactions_feature_table, transactions_df)
diff --git a/tests/e2e/test_online_features.py b/tests/e2e/test_online_features.py
--- a/tests/e2e/test_online_features.py
+++ b/tests/e2e/test_online_features.py
@@ -55,8 +55,8 @@ def test_offline_ingestion(
         batch_source=batch_source,
     )
 
-    feast_client.apply_entity(entity)
-    feast_client.apply_feature_table(feature_table)
+    feast_client.apply(entity)
+    feast_client.apply(feature_table)
 
     original = generate_data()
     feast_client.ingest(feature_table, original)  # write to batch (offline) storage
@@ -95,8 +95,8 @@ def test_offline_ingestion_from_bq_view(pytestconfig, bq_dataset, feast_client:
         ),
     )
 
-    feast_client.apply_entity(entity)
-    feast_client.apply_feature_table(feature_table)
+    feast_client.apply(entity)
+    feast_client.apply(feature_table)
 
     ingest_and_verify(feast_client, feature_table, original)
 
@@ -126,8 +126,8 @@ def test_streaming_ingestion(
         ),
     )
 
-    feast_client.apply_entity(entity)
-    feast_client.apply_feature_table(feature_table)
+    feast_client.apply(entity)
+    feast_client.apply(feature_table)
 
     job = feast_client.start_stream_to_online_ingestion(feature_table)
 
diff --git a/tests/e2e/test_register.py b/tests/e2e/test_register.py
--- a/tests/e2e/test_register.py
+++ b/tests/e2e/test_register.py
@@ -143,8 +143,8 @@ def test_get_list_basic(
 ):
 
     # ApplyEntity
-    feast_client.apply_entity(customer_entity)
-    feast_client.apply_entity(driver_entity)
+    feast_client.apply(customer_entity)
+    feast_client.apply(driver_entity)
 
     # GetEntity Check
     assert feast_client.get_entity(name="customer_id") == customer_entity
@@ -162,7 +162,7 @@ def test_get_list_basic(
     assert len(actual_matchmaking_entities) == 1
 
     # ApplyFeatureTable
-    feast_client.apply_feature_table(basic_featuretable)
+    feast_client.apply(basic_featuretable)
 
     # GetFeatureTable Check
     actual_get_feature_table = feast_client.get_feature_table(name="basic_featuretable")
@@ -181,7 +181,7 @@ def test_get_list_alltypes(
     feast_client: Client, alltypes_entity: Entity, alltypes_featuretable: FeatureTable
 ):
     # ApplyEntity
-    feast_client.apply_entity(alltypes_entity)
+    feast_client.apply(alltypes_entity)
 
     # GetEntity Check
     assert feast_client.get_entity(name="alltypes_id") == alltypes_entity
@@ -194,7 +194,7 @@ def test_get_list_alltypes(
     assert len(actual_alltypes_entities) == 1
 
     # ApplyFeatureTable
-    feast_client.apply_feature_table(alltypes_featuretable)
+    feast_client.apply(alltypes_featuretable)
 
     # GetFeatureTable Check
     actual_get_feature_table = feast_client.get_feature_table(name="alltypes")
@@ -234,11 +234,11 @@ def test_ingest_into_bq(
     )
 
     # ApplyEntity
-    feast_client.apply_entity(customer_entity)
-    feast_client.apply_entity(driver_entity)
+    feast_client.apply(customer_entity)
+    feast_client.apply(driver_entity)
 
     # ApplyFeatureTable
-    feast_client.apply_feature_table(ft)
+    feast_client.apply(ft)
     feast_client.ingest(ft, bq_dataframe, timeout=120)
 
     bq_client = bigquery.Client(project=bq_project)