Repository: feast-dev/feast

Issue:
Push api call to a push source that doesn't exist returns 200
## Expected Behavior 
When we `POST` to a push source that doesn't exist like so
```
curl -X POST "http://localhost:6566/push" -d '{
    "push_source_name": "push_source_name_that_does_not_exist",
    "df": {
            "some_entity": [1001],
            "event_timestamp": ["2022-05-13 10:59:42"],
            "created": ["2022-05-13 10:59:42"],
            ...
    },
    "to": "online_and_offline",
  }' | jq
```
I'd expect an error. It is a valid format to the correct endpoint, but is an entity that doesn't exist. A [422 unprocessable entity](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422) error would probably make sense here.

## Current Behavior
Method `FeatureStore().push` receives a [`push_source_name`](https://github.com/feast-dev/feast/blob/7c50ab510633c11646b6ff04853f3f26942ad646/sdk/python/feast/feature_store.py#L1418) indicating which feature views are impacted by the new data being pushed. The set of feature views is filtered down [here](https://github.com/feast-dev/feast/blob/7c50ab510633c11646b6ff04853f3f26942ad646/sdk/python/feast/feature_store.py#L1437-L1445). If a push source name is provided that doesn't actually exist then the resulting set is empty, the loop is skipped and the function is a no-op. This successful return means that the [`/push` call in feature_server.py](https://github.com/feast-dev/feast/blob/master/sdk/python/feast/feature_server.py#L95-L100) never throws to return an error code.

## Steps to reproduce
* start a local feature server
* make a push call to a push source that does not exist

### Specifications

- Version:
- Platform:
- Subsystem:

## Possible Solution
* Raise a custom exception when no push source is found with the provided name
* Catch that exception in the feature server
* Return 422 in that case

If this is considered reasonable, I'm happy to make the contribution myself.


Discussion:
That generally seems fine. Ideally you'd give a detailed error message as well explaining why it failed too, but sgtm!

Resolving patch:
diff --git a/sdk/python/feast/errors.py b/sdk/python/feast/errors.py
--- a/sdk/python/feast/errors.py
+++ b/sdk/python/feast/errors.py
@@ -398,3 +398,8 @@ def __init__(self):
         super().__init__(
             "The entity dataframe specified does not have the timestamp field as a datetime."
         )
+
+
+class PushSourceNotFoundException(Exception):
+    def __init__(self, push_source_name: str):
+        super().__init__(f"Unable to find push source '{push_source_name}'.")
diff --git a/sdk/python/feast/feature_server.py b/sdk/python/feast/feature_server.py
--- a/sdk/python/feast/feature_server.py
+++ b/sdk/python/feast/feature_server.py
@@ -13,6 +13,7 @@
 import feast
 from feast import proto_json
 from feast.data_source import PushMode
+from feast.errors import PushSourceNotFoundException
 from feast.protos.feast.serving.ServingService_pb2 import GetOnlineFeaturesRequest
 
 
@@ -98,6 +99,11 @@ def push(body=Depends(get_body)):
                 allow_registry_cache=request.allow_registry_cache,
                 to=to,
             )
+        except PushSourceNotFoundException as e:
+            # Print the original exception on the server side
+            logger.exception(traceback.format_exc())
+            # Raise HTTPException to return the error message to the client
+            raise HTTPException(status_code=422, detail=str(e))
         except Exception as e:
             # Print the original exception on the server side
             logger.exception(traceback.format_exc())
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
@@ -59,6 +59,7 @@
     EntityNotFoundException,
     FeatureNameCollisionError,
     FeatureViewNotFoundException,
+    PushSourceNotFoundException,
     RequestDataNotFoundInEntityDfException,
     RequestDataNotFoundInEntityRowsException,
 )
@@ -1444,6 +1445,9 @@ def push(
             )
         }
 
+        if not fvs_with_push_sources:
+            raise PushSourceNotFoundException(push_source_name)
+
         for fv in fvs_with_push_sources:
             if to == PushMode.ONLINE or to == PushMode.ONLINE_AND_OFFLINE:
                 self.write_to_online_store(


Test patch:
diff --git a/sdk/python/tests/integration/e2e/test_python_feature_server.py b/sdk/python/tests/integration/e2e/test_python_feature_server.py
--- a/sdk/python/tests/integration/e2e/test_python_feature_server.py
+++ b/sdk/python/tests/integration/e2e/test_python_feature_server.py
@@ -84,6 +84,29 @@ def test_push(python_fs_client):
     ) == [initial_temp * 100]
 
 
+@pytest.mark.integration
+@pytest.mark.universal_online_stores
+def test_push_source_does_not_exist(python_fs_client):
+    initial_temp = _get_temperatures_from_feature_server(
+        python_fs_client, location_ids=[1]
+    )[0]
+    response = python_fs_client.post(
+        "/push",
+        data=json.dumps(
+            {
+                "push_source_name": "push_source_does_not_exist",
+                "df": {
+                    "location_id": [1],
+                    "temperature": [initial_temp * 100],
+                    "event_timestamp": [str(datetime.utcnow())],
+                    "created": [str(datetime.utcnow())],
+                },
+            }
+        ),
+    )
+    assert response.status_code == 422
+
+
 def _get_temperatures_from_feature_server(client, location_ids: List[int]):
     get_request_data = {
         "features": ["pushable_location_stats:temperature"],