From 3e07339fd8506d3cf778eb546b723361a4514361 Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Wed, 28 Aug 2024 17:19:08 +0200 Subject: [PATCH 1/8] initial commit targetting grpc registry server Signed-off-by: Daniele Martinoli --- sdk/python/feast/errors.py | 29 +++++++++- .../client/grpc_client_auth_interceptor.py | 20 ++++--- sdk/python/feast/permissions/server/grpc.py | 55 ++++++++++++++++++- 3 files changed, 95 insertions(+), 9 deletions(-) diff --git a/sdk/python/feast/errors.py b/sdk/python/feast/errors.py index ffafe31125..e9897bb2f4 100644 --- a/sdk/python/feast/errors.py +++ b/sdk/python/feast/errors.py @@ -1,4 +1,4 @@ -from typing import Any, List, Set +from typing import Any, List, Optional, Set from colorama import Fore, Style @@ -419,3 +419,30 @@ def __init__(self, query: str): class ZeroColumnQueryResult(Exception): def __init__(self, query: str): super().__init__(f"This query returned zero columns:\n{query}") + + +def to_error_detail(error: Exception) -> str: + import json + + m = { + "module": f"{type(error).__module__}", + "class": f"{type(error).__name__}", + "message": f"{str(error)}", + } + return json.dumps(m) + + +def from_error_detail(detail: str) -> Optional[Exception]: + import importlib + import json + + m = json.loads(detail) + if all(f in m for f in ["module", "class", "message"]): + module_name = m["module"] + class_name = m["class"] + message = m["message"] + module = importlib.import_module(module_name) + ClassReference = getattr(module, class_name) + instance = ClassReference(message) + return instance + return None diff --git a/sdk/python/feast/permissions/client/grpc_client_auth_interceptor.py b/sdk/python/feast/permissions/client/grpc_client_auth_interceptor.py index 98cc445c7b..37cdb55555 100644 --- a/sdk/python/feast/permissions/client/grpc_client_auth_interceptor.py +++ b/sdk/python/feast/permissions/client/grpc_client_auth_interceptor.py @@ -2,6 +2,7 @@ import grpc +from feast.errors import from_error_detail from feast.permissions.auth_model import AuthConfig from feast.permissions.client.auth_client_manager_factory import get_auth_token @@ -20,26 +21,31 @@ def __init__(self, auth_type: AuthConfig): def intercept_unary_unary( self, continuation, client_call_details, request_iterator ): - client_call_details = self._append_auth_header_metadata(client_call_details) - return continuation(client_call_details, request_iterator) + return self._handle_call(continuation, client_call_details, request_iterator) def intercept_unary_stream( self, continuation, client_call_details, request_iterator ): - client_call_details = self._append_auth_header_metadata(client_call_details) - return continuation(client_call_details, request_iterator) + return self._handle_call(continuation, client_call_details, request_iterator) def intercept_stream_unary( self, continuation, client_call_details, request_iterator ): - client_call_details = self._append_auth_header_metadata(client_call_details) - return continuation(client_call_details, request_iterator) + return self._handle_call(continuation, client_call_details, request_iterator) def intercept_stream_stream( self, continuation, client_call_details, request_iterator ): + return self._handle_call(continuation, client_call_details, request_iterator) + + def _handle_call(self, continuation, client_call_details, request_iterator): client_call_details = self._append_auth_header_metadata(client_call_details) - return continuation(client_call_details, request_iterator) + result = continuation(client_call_details, request_iterator) + if result.exception() is not None: + mapped_error = from_error_detail(result.exception().details()) + if mapped_error is not None: + raise mapped_error + return result def _append_auth_header_metadata(self, client_call_details): logger.debug( diff --git a/sdk/python/feast/permissions/server/grpc.py b/sdk/python/feast/permissions/server/grpc.py index 3c94240869..de273f1d67 100644 --- a/sdk/python/feast/permissions/server/grpc.py +++ b/sdk/python/feast/permissions/server/grpc.py @@ -4,6 +4,7 @@ import grpc +from feast.errors import FeastObjectNotFoundException, to_error_detail from feast.permissions.auth.auth_manager import ( get_auth_manager, ) @@ -31,7 +32,7 @@ def grpc_interceptors( if auth_type == AuthManagerType.NONE: return None - return [AuthInterceptor()] + return [AuthInterceptor(), ErrorInterceptor()] class AuthInterceptor(grpc.ServerInterceptor): @@ -52,3 +53,55 @@ def intercept_service(self, continuation, handler_call_details): sm.set_current_user(current_user) return continuation(handler_call_details) + + +class ErrorInterceptor(grpc.ServerInterceptor): + def intercept_service(self, continuation, handler_call_details): + def exception_wrapper(behavior, request, context): + try: + return behavior(request, context) + except grpc.RpcError as e: + context.abort(e.code(), e.details()) + except Exception as e: + context.abort( + _error_to_status_code(e), + to_error_detail(e), + ) + + handler = continuation(handler_call_details) + if handler is None: + return None + + if handler.unary_unary: + return grpc.unary_unary_rpc_method_handler( + lambda req, ctx: exception_wrapper(handler.unary_unary, req, ctx), + request_deserializer=handler.request_deserializer, + response_serializer=handler.response_serializer, + ) + elif handler.unary_stream: + return grpc.unary_stream_rpc_method_handler( + lambda req, ctx: exception_wrapper(handler.unary_stream, req, ctx), + request_deserializer=handler.request_deserializer, + response_serializer=handler.response_serializer, + ) + elif handler.stream_unary: + return grpc.stream_unary_rpc_method_handler( + lambda req, ctx: exception_wrapper(handler.stream_unary, req, ctx), + request_deserializer=handler.request_deserializer, + response_serializer=handler.response_serializer, + ) + elif handler.stream_stream: + return grpc.stream_stream_rpc_method_handler( + lambda req, ctx: exception_wrapper(handler.stream_stream, req, ctx), + request_deserializer=handler.request_deserializer, + response_serializer=handler.response_serializer, + ) + return handler + + +def _error_to_status_code(error: Exception) -> grpc.StatusCode: + if isinstance(error, FeastObjectNotFoundException): + return grpc.StatusCode.NOT_FOUND + if isinstance(error, FeastObjectNotFoundException): + return grpc.StatusCode.PERMISSION_DENIED + return grpc.StatusCode.INTERNAL From e833ff12d1139cbccae3553b00bfb2e3f5ded6e1 Mon Sep 17 00:00:00 2001 From: Daniele Martinoli <86618610+dmartinol@users.noreply.github.com> Date: Thu, 29 Aug 2024 09:39:17 +0200 Subject: [PATCH 2/8] refactor: Introduced base class FeastError for all Feast exceptions (#4465) introduced base class FeastError for all Feast exceptions, with initial methods to map the grpc and HTTP status code Signed-off-by: Daniele Martinoli --- sdk/python/feast/cli_utils.py | 2 +- sdk/python/feast/errors.py | 144 +++++++++--------- sdk/python/feast/permissions/enforcer.py | 9 +- .../feast/permissions/security_manager.py | 8 +- .../tests/unit/permissions/test_decorator.py | 6 +- .../unit/permissions/test_security_manager.py | 10 +- 6 files changed, 92 insertions(+), 87 deletions(-) diff --git a/sdk/python/feast/cli_utils.py b/sdk/python/feast/cli_utils.py index edfdab93e3..264a633c31 100644 --- a/sdk/python/feast/cli_utils.py +++ b/sdk/python/feast/cli_utils.py @@ -279,7 +279,7 @@ def handler_list_all_permissions_roles_verbose( for o in objects: permitted_actions = ALL_ACTIONS.copy() for action in ALL_ACTIONS: - # Following code is derived from enforcer.enforce_policy but has a different return type and does not raise PermissionError + # Following code is derived from enforcer.enforce_policy but has a different return type and does not raise FeastPermissionError matching_permissions = [ p for p in permissions diff --git a/sdk/python/feast/errors.py b/sdk/python/feast/errors.py index e9897bb2f4..6833f9c4fb 100644 --- a/sdk/python/feast/errors.py +++ b/sdk/python/feast/errors.py @@ -1,34 +1,52 @@ from typing import Any, List, Optional, Set from colorama import Fore, Style +from fastapi import status as HttpStatusCode +from grpc import StatusCode as GrpcStatusCode from feast.field import Field -class DataSourceNotFoundException(Exception): +class FeastError(Exception): + pass + + def rpc_status_code(self) -> GrpcStatusCode: + return GrpcStatusCode.INTERNAL + + def http_status_code(self) -> int: + return HttpStatusCode.HTTP_500_INTERNAL_SERVER_ERROR + + +class DataSourceNotFoundException(FeastError): def __init__(self, path): super().__init__( f"Unable to find table at '{path}'. Please check that table exists." ) -class DataSourceNoNameException(Exception): +class DataSourceNoNameException(FeastError): def __init__(self): super().__init__( "Unable to infer a name for this data source. Either table or name must be specified." ) -class DataSourceRepeatNamesException(Exception): +class DataSourceRepeatNamesException(FeastError): def __init__(self, ds_name: str): super().__init__( f"Multiple data sources share the same case-insensitive name {ds_name}." ) -class FeastObjectNotFoundException(Exception): +class FeastObjectNotFoundException(FeastError): pass + def rpc_status_code(self) -> GrpcStatusCode: + return GrpcStatusCode.NOT_FOUND + + def http_status_code(self) -> int: + return HttpStatusCode.HTTP_404_NOT_FOUND + class EntityNotFoundException(FeastObjectNotFoundException): def __init__(self, name, project=None): @@ -110,49 +128,49 @@ def __init__(self, name: str, project: str): ) -class FeastProviderLoginError(Exception): +class FeastProviderLoginError(FeastError): """Error class that indicates a user has not authenticated with their provider.""" -class FeastProviderNotImplementedError(Exception): +class FeastProviderNotImplementedError(FeastError): def __init__(self, provider_name): super().__init__(f"Provider '{provider_name}' is not implemented") -class FeastRegistryNotSetError(Exception): +class FeastRegistryNotSetError(FeastError): def __init__(self): super().__init__("Registry is not set, but is required") -class FeastFeatureServerTypeInvalidError(Exception): +class FeastFeatureServerTypeInvalidError(FeastError): def __init__(self, feature_server_type: str): super().__init__( f"Feature server type was set to {feature_server_type}, but this type is invalid" ) -class FeastRegistryTypeInvalidError(Exception): +class FeastRegistryTypeInvalidError(FeastError): def __init__(self, registry_type: str): super().__init__( f"Feature server type was set to {registry_type}, but this type is invalid" ) -class FeastModuleImportError(Exception): +class FeastModuleImportError(FeastError): def __init__(self, module_name: str, class_name: str): super().__init__( f"Could not import module '{module_name}' while attempting to load class '{class_name}'" ) -class FeastClassImportError(Exception): +class FeastClassImportError(FeastError): def __init__(self, module_name: str, class_name: str): super().__init__( f"Could not import class '{class_name}' from module '{module_name}'" ) -class FeastExtrasDependencyImportError(Exception): +class FeastExtrasDependencyImportError(FeastError): def __init__(self, extras_type: str, nested_error: str): message = ( nested_error @@ -162,14 +180,14 @@ def __init__(self, extras_type: str, nested_error: str): super().__init__(message) -class FeastOfflineStoreUnsupportedDataSource(Exception): +class FeastOfflineStoreUnsupportedDataSource(FeastError): def __init__(self, offline_store_name: str, data_source_name: str): super().__init__( f"Offline Store '{offline_store_name}' does not support data source '{data_source_name}'" ) -class FeatureNameCollisionError(Exception): +class FeatureNameCollisionError(FeastError): def __init__(self, feature_refs_collisions: List[str], full_feature_names: bool): if full_feature_names: collisions = [ref.replace(":", "__") for ref in feature_refs_collisions] @@ -191,7 +209,7 @@ def __init__(self, feature_refs_collisions: List[str], full_feature_names: bool) ) -class SpecifiedFeaturesNotPresentError(Exception): +class SpecifiedFeaturesNotPresentError(FeastError): def __init__( self, specified_features: List[Field], @@ -204,47 +222,47 @@ def __init__( ) -class SavedDatasetLocationAlreadyExists(Exception): +class SavedDatasetLocationAlreadyExists(FeastError): def __init__(self, location: str): super().__init__(f"Saved dataset location {location} already exists.") -class FeastOfflineStoreInvalidName(Exception): +class FeastOfflineStoreInvalidName(FeastError): def __init__(self, offline_store_class_name: str): super().__init__( f"Offline Store Class '{offline_store_class_name}' should end with the string `OfflineStore`.'" ) -class FeastOnlineStoreInvalidName(Exception): +class FeastOnlineStoreInvalidName(FeastError): def __init__(self, online_store_class_name: str): super().__init__( f"Online Store Class '{online_store_class_name}' should end with the string `OnlineStore`.'" ) -class FeastInvalidAuthConfigClass(Exception): +class FeastInvalidAuthConfigClass(FeastError): def __init__(self, auth_config_class_name: str): super().__init__( f"Auth Config Class '{auth_config_class_name}' should end with the string `AuthConfig`.'" ) -class FeastInvalidBaseClass(Exception): +class FeastInvalidBaseClass(FeastError): def __init__(self, class_name: str, class_type: str): super().__init__( f"Class '{class_name}' should have `{class_type}` as a base class." ) -class FeastOnlineStoreUnsupportedDataSource(Exception): +class FeastOnlineStoreUnsupportedDataSource(FeastError): def __init__(self, online_store_name: str, data_source_name: str): super().__init__( f"Online Store '{online_store_name}' does not support data source '{data_source_name}'" ) -class FeastEntityDFMissingColumnsError(Exception): +class FeastEntityDFMissingColumnsError(FeastError): def __init__(self, expected, missing): super().__init__( f"The entity dataframe you have provided must contain columns {expected}, " @@ -252,7 +270,7 @@ def __init__(self, expected, missing): ) -class FeastJoinKeysDuringMaterialization(Exception): +class FeastJoinKeysDuringMaterialization(FeastError): def __init__( self, source: str, join_key_columns: Set[str], source_columns: Set[str] ): @@ -262,7 +280,7 @@ def __init__( ) -class DockerDaemonNotRunning(Exception): +class DockerDaemonNotRunning(FeastError): def __init__(self): super().__init__( "The Docker Python sdk cannot connect to the Docker daemon. Please make sure you have" @@ -270,7 +288,7 @@ def __init__(self): ) -class RegistryInferenceFailure(Exception): +class RegistryInferenceFailure(FeastError): def __init__(self, repo_obj_type: str, specific_issue: str): super().__init__( f"Inference to fill in missing information for {repo_obj_type} failed. {specific_issue}. " @@ -278,58 +296,58 @@ def __init__(self, repo_obj_type: str, specific_issue: str): ) -class BigQueryJobStillRunning(Exception): +class BigQueryJobStillRunning(FeastError): def __init__(self, job_id): super().__init__(f"The BigQuery job with ID '{job_id}' is still running.") -class BigQueryJobCancelled(Exception): +class BigQueryJobCancelled(FeastError): def __init__(self, job_id): super().__init__(f"The BigQuery job with ID '{job_id}' was cancelled") -class RedshiftCredentialsError(Exception): +class RedshiftCredentialsError(FeastError): def __init__(self): super().__init__("Redshift API failed due to incorrect credentials") -class RedshiftQueryError(Exception): +class RedshiftQueryError(FeastError): def __init__(self, details): super().__init__(f"Redshift SQL Query failed to finish. Details: {details}") -class RedshiftTableNameTooLong(Exception): +class RedshiftTableNameTooLong(FeastError): def __init__(self, table_name: str): super().__init__( f"Redshift table names have a maximum length of 127 characters, but the table name {table_name} has length {len(table_name)} characters." ) -class SnowflakeCredentialsError(Exception): +class SnowflakeCredentialsError(FeastError): def __init__(self): super().__init__("Snowflake Connector failed due to incorrect credentials") -class SnowflakeQueryError(Exception): +class SnowflakeQueryError(FeastError): def __init__(self, details): super().__init__(f"Snowflake SQL Query failed to finish. Details: {details}") -class EntityTimestampInferenceException(Exception): +class EntityTimestampInferenceException(FeastError): def __init__(self, expected_column_name: str): super().__init__( f"Please provide an entity_df with a column named {expected_column_name} representing the time of events." ) -class FeatureViewMissingDuringFeatureServiceInference(Exception): +class FeatureViewMissingDuringFeatureServiceInference(FeastError): def __init__(self, feature_view_name: str, feature_service_name: str): super().__init__( f"Missing {feature_view_name} feature view during inference for {feature_service_name} feature service." ) -class InvalidEntityType(Exception): +class InvalidEntityType(FeastError): def __init__(self, entity_type: type): super().__init__( f"The entity dataframe you have provided must be a Pandas DataFrame or a SQL query, " @@ -337,7 +355,7 @@ def __init__(self, entity_type: type): ) -class ConflictingFeatureViewNames(Exception): +class ConflictingFeatureViewNames(FeastError): # TODO: print file location of conflicting feature views def __init__(self, feature_view_name: str): super().__init__( @@ -345,60 +363,60 @@ def __init__(self, feature_view_name: str): ) -class FeastInvalidInfraObjectType(Exception): +class FeastInvalidInfraObjectType(FeastError): def __init__(self): super().__init__("Could not identify the type of the InfraObject.") -class SnowflakeIncompleteConfig(Exception): +class SnowflakeIncompleteConfig(FeastError): def __init__(self, e: KeyError): super().__init__(f"{e} not defined in a config file or feature_store.yaml file") -class SnowflakeQueryUnknownError(Exception): +class SnowflakeQueryUnknownError(FeastError): def __init__(self, query: str): super().__init__(f"Snowflake query failed: {query}") -class InvalidFeaturesParameterType(Exception): +class InvalidFeaturesParameterType(FeastError): def __init__(self, features: Any): super().__init__( f"Invalid `features` parameter type {type(features)}. Expected one of List[str] and FeatureService." ) -class EntitySQLEmptyResults(Exception): +class EntitySQLEmptyResults(FeastError): def __init__(self, entity_sql: str): super().__init__( f"No entity values found from the specified SQL query to generate the entity dataframe: {entity_sql}." ) -class EntityDFNotDateTime(Exception): +class EntityDFNotDateTime(FeastError): def __init__(self): super().__init__( "The entity dataframe specified does not have the timestamp field as a datetime." ) -class PushSourceNotFoundException(Exception): +class PushSourceNotFoundException(FeastError): def __init__(self, push_source_name: str): super().__init__(f"Unable to find push source '{push_source_name}'.") -class ReadOnlyRegistryException(Exception): +class ReadOnlyRegistryException(FeastError): def __init__(self): super().__init__("Registry implementation is read-only.") -class DataFrameSerializationError(Exception): +class DataFrameSerializationError(FeastError): def __init__(self, input_dict: dict): super().__init__( f"Failed to serialize the provided dictionary into a pandas DataFrame: {input_dict.keys()}" ) -class PermissionNotFoundException(Exception): +class PermissionNotFoundException(FeastError): def __init__(self, name, project): super().__init__(f"Permission {name} does not exist in project {project}") @@ -411,38 +429,22 @@ def __init__(self, name, project=None): super().__init__(f"Permission {name} does not exist") -class ZeroRowsQueryResult(Exception): +class ZeroRowsQueryResult(FeastError): def __init__(self, query: str): super().__init__(f"This query returned zero rows:\n{query}") -class ZeroColumnQueryResult(Exception): +class ZeroColumnQueryResult(FeastError): def __init__(self, query: str): super().__init__(f"This query returned zero columns:\n{query}") -def to_error_detail(error: Exception) -> str: - import json - - m = { - "module": f"{type(error).__module__}", - "class": f"{type(error).__name__}", - "message": f"{str(error)}", - } - return json.dumps(m) - +class FeastPermissionError(FeastError, PermissionError): + def __init__(self, details: str): + super().__init__(f"Permission error:\n{details}") -def from_error_detail(detail: str) -> Optional[Exception]: - import importlib - import json + def rpc_status_code(self) -> GrpcStatusCode: + return GrpcStatusCode.PERMISSION_DENIED - m = json.loads(detail) - if all(f in m for f in ["module", "class", "message"]): - module_name = m["module"] - class_name = m["class"] - message = m["message"] - module = importlib.import_module(module_name) - ClassReference = getattr(module, class_name) - instance = ClassReference(message) - return instance - return None + def http_status_code(self) -> int: + return HttpStatusCode.HTTP_403_FORBIDDEN diff --git a/sdk/python/feast/permissions/enforcer.py b/sdk/python/feast/permissions/enforcer.py index ae45b8a78b..d94a81ba04 100644 --- a/sdk/python/feast/permissions/enforcer.py +++ b/sdk/python/feast/permissions/enforcer.py @@ -1,5 +1,6 @@ import logging +from feast.errors import FeastPermissionError from feast.feast_object import FeastObject from feast.permissions.decision import DecisionEvaluator from feast.permissions.permission import ( @@ -29,14 +30,14 @@ def enforce_policy( user: The current user. resources: The resources for which we need to enforce authorized permission. actions: The requested actions to be authorized. - filter_only: If `True`, it removes unauthorized resources from the returned value, otherwise it raises a `PermissionError` the + filter_only: If `True`, it removes unauthorized resources from the returned value, otherwise it raises a `FeastPermissionError` the first unauthorized resource. Defaults to `False`. Returns: list[FeastObject]: A filtered list of the permitted resources. Raises: - PermissionError: If the current user is not authorized to eecute the requested actions on the given resources (and `filter_only` is `False`). + FeastPermissionError: If the current user is not authorized to eecute the requested actions on the given resources (and `filter_only` is `False`). """ if not permissions: return resources @@ -66,12 +67,12 @@ def enforce_policy( if evaluator.is_decided(): grant, explanations = evaluator.grant() if not grant and not filter_only: - raise PermissionError(",".join(explanations)) + raise FeastPermissionError(",".join(explanations)) if grant: _permitted_resources.append(resource) break else: message = f"No permissions defined to manage {actions} on {type(resource)}/{resource.name}." logger.exception(f"**PERMISSION NOT GRANTED**: {message}") - raise PermissionError(message) + raise FeastPermissionError(message) return _permitted_resources diff --git a/sdk/python/feast/permissions/security_manager.py b/sdk/python/feast/permissions/security_manager.py index 2322602388..29c0e06753 100644 --- a/sdk/python/feast/permissions/security_manager.py +++ b/sdk/python/feast/permissions/security_manager.py @@ -67,14 +67,14 @@ def assert_permissions( Args: resources: The resources for which we need to enforce authorized permission. actions: The requested actions to be authorized. - filter_only: If `True`, it removes unauthorized resources from the returned value, otherwise it raises a `PermissionError` the + filter_only: If `True`, it removes unauthorized resources from the returned value, otherwise it raises a `FeastPermissionError` the first unauthorized resource. Defaults to `False`. Returns: list[FeastObject]: A filtered list of the permitted resources, possibly empty. Raises: - PermissionError: If the current user is not authorized to execute all the requested actions on the given resources. + FeastPermissionError: If the current user is not authorized to execute all the requested actions on the given resources. """ return enforce_policy( permissions=self.permissions, @@ -108,7 +108,7 @@ def assert_permissions_to_update( FeastObject: The original `resource`, if permitted. Raises: - PermissionError: If the current user is not authorized to execute all the requested actions on the given resource or on the existing one. + FeastPermissionError: If the current user is not authorized to execute all the requested actions on the given resource or on the existing one. """ actions = [AuthzedAction.DESCRIBE, AuthzedAction.UPDATE] try: @@ -140,7 +140,7 @@ def assert_permissions( FeastObject: The original `resource`, if permitted. Raises: - PermissionError: If the current user is not authorized to execute the requested actions on the given resources. + FeastPermissionError: If the current user is not authorized to execute the requested actions on the given resources. """ sm = get_security_manager() if sm is None: diff --git a/sdk/python/tests/unit/permissions/test_decorator.py b/sdk/python/tests/unit/permissions/test_decorator.py index 8f6c2c420b..92db72c93d 100644 --- a/sdk/python/tests/unit/permissions/test_decorator.py +++ b/sdk/python/tests/unit/permissions/test_decorator.py @@ -1,6 +1,8 @@ import assertpy import pytest +from feast.errors import FeastPermissionError + @pytest.mark.parametrize( "username, can_read, can_write", @@ -22,11 +24,11 @@ def test_access_SecuredFeatureView( if can_read: fv.read_protected() else: - with pytest.raises(PermissionError): + with pytest.raises(FeastPermissionError): fv.read_protected() if can_write: fv.write_protected() else: - with pytest.raises(PermissionError): + with pytest.raises(FeastPermissionError): fv.write_protected() assertpy.assert_that(fv.unprotected()).is_true() diff --git a/sdk/python/tests/unit/permissions/test_security_manager.py b/sdk/python/tests/unit/permissions/test_security_manager.py index 228dddb01f..d403c8123b 100644 --- a/sdk/python/tests/unit/permissions/test_security_manager.py +++ b/sdk/python/tests/unit/permissions/test_security_manager.py @@ -2,7 +2,7 @@ import pytest from feast.entity import Entity -from feast.errors import FeastObjectNotFoundException +from feast.errors import FeastObjectNotFoundException, FeastPermissionError from feast.permissions.action import READ, AuthzedAction from feast.permissions.security_manager import ( assert_permissions, @@ -66,7 +66,7 @@ def test_access_SecuredFeatureView( result = [] if raise_error_in_permit: - with pytest.raises(PermissionError): + with pytest.raises(FeastPermissionError): result = permitted_resources(resources=resources, actions=requested_actions) else: result = permitted_resources(resources=resources, actions=requested_actions) @@ -82,7 +82,7 @@ def test_access_SecuredFeatureView( result = assert_permissions(resource=r, actions=requested_actions) assertpy.assert_that(result).is_equal_to(r) elif raise_error_in_assert[i]: - with pytest.raises(PermissionError): + with pytest.raises(FeastPermissionError): assert_permissions(resource=r, actions=requested_actions) else: result = assert_permissions(resource=r, actions=requested_actions) @@ -125,7 +125,7 @@ def getter(name: str, project: str, allow_cache: bool): ) assertpy.assert_that(result).is_equal_to(entity) else: - with pytest.raises(PermissionError): + with pytest.raises(FeastPermissionError): assert_permissions_to_update(resource=entity, getter=getter, project="") @@ -165,5 +165,5 @@ def getter(name: str, project: str, allow_cache: bool): ) assertpy.assert_that(result).is_equal_to(entity) else: - with pytest.raises(PermissionError): + with pytest.raises(FeastPermissionError): assert_permissions_to_update(resource=entity, getter=getter, project="") From d94c9ac9522b7148e258559cc44292652c94c2c3 Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Wed, 28 Aug 2024 17:19:08 +0200 Subject: [PATCH 3/8] initial commit targetting grpc registry server Signed-off-by: Daniele Martinoli --- sdk/python/feast/errors.py | 56 ++++++++++++++++++- sdk/python/feast/grpc_error_interceptor.py | 48 ++++++++++++++++ .../client/grpc_client_auth_interceptor.py | 4 +- sdk/python/feast/permissions/server/grpc.py | 22 -------- sdk/python/feast/registry_server.py | 24 +++++++- .../auth/server/test_auth_registry_server.py | 35 ++++++++++++ sdk/python/tests/unit/test_errors.py | 26 +++++++++ 7 files changed, 186 insertions(+), 29 deletions(-) create mode 100644 sdk/python/feast/grpc_error_interceptor.py create mode 100644 sdk/python/tests/unit/test_errors.py diff --git a/sdk/python/feast/errors.py b/sdk/python/feast/errors.py index 6833f9c4fb..ff7d46df02 100644 --- a/sdk/python/feast/errors.py +++ b/sdk/python/feast/errors.py @@ -1,3 +1,4 @@ +import logging from typing import Any, List, Optional, Set from colorama import Fore, Style @@ -6,16 +7,65 @@ from feast.field import Field +logger = logging.getLogger(__name__) + class FeastError(Exception): pass - def rpc_status_code(self) -> GrpcStatusCode: + def grpc_status_code(self) -> GrpcStatusCode: return GrpcStatusCode.INTERNAL def http_status_code(self) -> int: return HttpStatusCode.HTTP_500_INTERNAL_SERVER_ERROR + def __str__(self) -> str: + if hasattr(self, "__overridden_message__"): + return str(getattr(self, "__overridden_message__")) + return super().__str__() + + def __repr__(self) -> str: + if hasattr(self, "__overridden_message__"): + return f"{type(self).__name__}('{getattr(self,'__overridden_message__')}')" + return super().__repr__() + + def to_error_detail(self) -> str: + """ + Returns a JSON representation of the error for serialization purposes. + + Returns: + str: a string representation of a JSON document including `module`, `class` and `message` fields. + """ + import json + + m = { + "module": f"{type(self).__module__}", + "class": f"{type(self).__name__}", + "message": f"{str(self)}", + } + return json.dumps(m) + + @staticmethod + def from_error_detail(detail: str) -> Optional["FeastError"]: + import importlib + import json + + try: + m = json.loads(detail) + if all(f in m for f in ["module", "class", "message"]): + module_name = m["module"] + class_name = m["class"] + message = m["message"] + module = importlib.import_module(module_name) + class_reference = getattr(module, class_name) + + instance = class_reference(message) + setattr(instance, "__overridden_message__", message) + return instance + except Exception as e: + logger.warning(f"Invalid error detail: {detail}: {e}") + return None + class DataSourceNotFoundException(FeastError): def __init__(self, path): @@ -41,7 +91,7 @@ def __init__(self, ds_name: str): class FeastObjectNotFoundException(FeastError): pass - def rpc_status_code(self) -> GrpcStatusCode: + def grpc_status_code(self) -> GrpcStatusCode: return GrpcStatusCode.NOT_FOUND def http_status_code(self) -> int: @@ -443,7 +493,7 @@ class FeastPermissionError(FeastError, PermissionError): def __init__(self, details: str): super().__init__(f"Permission error:\n{details}") - def rpc_status_code(self) -> GrpcStatusCode: + def grpc_status_code(self) -> GrpcStatusCode: return GrpcStatusCode.PERMISSION_DENIED def http_status_code(self) -> int: diff --git a/sdk/python/feast/grpc_error_interceptor.py b/sdk/python/feast/grpc_error_interceptor.py new file mode 100644 index 0000000000..c638d461ed --- /dev/null +++ b/sdk/python/feast/grpc_error_interceptor.py @@ -0,0 +1,48 @@ +import grpc + +from feast.errors import FeastError + + +def exception_wrapper(behavior, request, context): + try: + return behavior(request, context) + except grpc.RpcError as e: + context.abort(e.code(), e.details()) + except FeastError as e: + context.abort( + e.grpc_status_code(), + e.to_error_detail(), + ) + + +class ErrorInterceptor(grpc.ServerInterceptor): + def intercept_service(self, continuation, handler_call_details): + handler = continuation(handler_call_details) + if handler is None: + return None + + if handler.unary_unary: + return grpc.unary_unary_rpc_method_handler( + lambda req, ctx: exception_wrapper(handler.unary_unary, req, ctx), + request_deserializer=handler.request_deserializer, + response_serializer=handler.response_serializer, + ) + elif handler.unary_stream: + return grpc.unary_stream_rpc_method_handler( + lambda req, ctx: exception_wrapper(handler.unary_stream, req, ctx), + request_deserializer=handler.request_deserializer, + response_serializer=handler.response_serializer, + ) + elif handler.stream_unary: + return grpc.stream_unary_rpc_method_handler( + lambda req, ctx: exception_wrapper(handler.stream_unary, req, ctx), + request_deserializer=handler.request_deserializer, + response_serializer=handler.response_serializer, + ) + elif handler.stream_stream: + return grpc.stream_stream_rpc_method_handler( + lambda req, ctx: exception_wrapper(handler.stream_stream, req, ctx), + request_deserializer=handler.request_deserializer, + response_serializer=handler.response_serializer, + ) + return handler diff --git a/sdk/python/feast/permissions/client/grpc_client_auth_interceptor.py b/sdk/python/feast/permissions/client/grpc_client_auth_interceptor.py index 37cdb55555..5155b80cb5 100644 --- a/sdk/python/feast/permissions/client/grpc_client_auth_interceptor.py +++ b/sdk/python/feast/permissions/client/grpc_client_auth_interceptor.py @@ -2,7 +2,7 @@ import grpc -from feast.errors import from_error_detail +from feast.errors import FeastError from feast.permissions.auth_model import AuthConfig from feast.permissions.client.auth_client_manager_factory import get_auth_token @@ -42,7 +42,7 @@ def _handle_call(self, continuation, client_call_details, request_iterator): client_call_details = self._append_auth_header_metadata(client_call_details) result = continuation(client_call_details, request_iterator) if result.exception() is not None: - mapped_error = from_error_detail(result.exception().details()) + mapped_error = FeastError.from_error_detail(result.exception().details()) if mapped_error is not None: raise mapped_error return result diff --git a/sdk/python/feast/permissions/server/grpc.py b/sdk/python/feast/permissions/server/grpc.py index de273f1d67..8c4da13b83 100644 --- a/sdk/python/feast/permissions/server/grpc.py +++ b/sdk/python/feast/permissions/server/grpc.py @@ -1,6 +1,5 @@ import asyncio import logging -from typing import Optional import grpc @@ -9,32 +8,11 @@ get_auth_manager, ) from feast.permissions.security_manager import get_security_manager -from feast.permissions.server.utils import ( - AuthManagerType, -) logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) -def grpc_interceptors( - auth_type: AuthManagerType, -) -> Optional[list[grpc.ServerInterceptor]]: - """ - A list of the authorization interceptors. - - Args: - auth_type: The type of authorization manager, from the feature store configuration. - - Returns: - list[grpc.ServerInterceptor]: Optional list of interceptors. If the authorization type is set to `NONE`, it returns `None`. - """ - if auth_type == AuthManagerType.NONE: - return None - - return [AuthInterceptor(), ErrorInterceptor()] - - class AuthInterceptor(grpc.ServerInterceptor): def intercept_service(self, continuation, handler_call_details): sm = get_security_manager() diff --git a/sdk/python/feast/registry_server.py b/sdk/python/feast/registry_server.py index 7b779e9f9e..7e12710a3a 100644 --- a/sdk/python/feast/registry_server.py +++ b/sdk/python/feast/registry_server.py @@ -1,6 +1,6 @@ from concurrent import futures from datetime import datetime, timezone -from typing import Union, cast +from typing import Optional, Union, cast import grpc from google.protobuf.empty_pb2 import Empty @@ -13,6 +13,7 @@ from feast.errors import FeatureViewNotFoundException from feast.feast_object import FeastObject from feast.feature_view import FeatureView +from feast.grpc_error_interceptor import ErrorInterceptor from feast.infra.infra_object import Infra from feast.infra.registry.base_registry import BaseRegistry from feast.on_demand_feature_view import OnDemandFeatureView @@ -23,8 +24,9 @@ assert_permissions_to_update, permitted_resources, ) -from feast.permissions.server.grpc import grpc_interceptors +from feast.permissions.server.grpc import AuthInterceptor from feast.permissions.server.utils import ( + AuthManagerType, ServerType, init_auth_manager, init_security_manager, @@ -668,3 +670,21 @@ def start_server(store: FeatureStore, port: int, wait_for_termination: bool = Tr server.wait_for_termination() else: return server + + +def grpc_interceptors( + auth_type: AuthManagerType, +) -> Optional[list[grpc.ServerInterceptor]]: + """ + A list of the authorization interceptors. + + Args: + auth_type: The type of authorization manager, from the feature store configuration. + + Returns: + list[grpc.ServerInterceptor]: Optional list of interceptors. If the authorization type is set to `NONE`, it returns `None`. + """ + if auth_type == AuthManagerType.NONE: + return [ErrorInterceptor()] + + return [AuthInterceptor(), ErrorInterceptor()] diff --git a/sdk/python/tests/unit/permissions/auth/server/test_auth_registry_server.py b/sdk/python/tests/unit/permissions/auth/server/test_auth_registry_server.py index bc16bdac3b..9e9bc1473e 100644 --- a/sdk/python/tests/unit/permissions/auth/server/test_auth_registry_server.py +++ b/sdk/python/tests/unit/permissions/auth/server/test_auth_registry_server.py @@ -8,6 +8,11 @@ from feast import ( FeatureStore, ) +from feast.errors import ( + EntityNotFoundException, + FeastPermissionError, + FeatureViewNotFoundException, +) from feast.permissions.permission import Permission from feast.registry_server import start_server from feast.wait import wait_retry_backoff # noqa: E402 @@ -70,7 +75,9 @@ def test_registry_apis( print(f"Running for\n:{auth_config}") remote_feature_store = get_remote_registry_store(server_port, feature_store) permissions = _test_list_permissions(remote_feature_store, applied_permissions) + _test_get_entity(remote_feature_store, applied_permissions) _test_list_entities(remote_feature_store, applied_permissions) + _test_get_fv(remote_feature_store, applied_permissions) _test_list_fvs(remote_feature_store, applied_permissions) if _permissions_exist_in_permission_list( @@ -118,6 +125,20 @@ def _test_get_historical_features(client_fs: FeatureStore): assertpy.assert_that(training_df).is_not_none() +def _test_get_entity(client_fs: FeatureStore, permissions: list[Permission]): + if not _is_auth_enabled(client_fs) or _is_permission_enabled( + client_fs, permissions, read_entities_perm + ): + entity = client_fs.get_entity("driver") + assertpy.assert_that(entity).is_not_none() + assertpy.assert_that(entity.name).is_equal_to("driver") + else: + with pytest.raises(FeastPermissionError): + client_fs.get_entity("driver") + with pytest.raises(EntityNotFoundException): + client_fs.get_entity("invalid-name") + + def _test_list_entities(client_fs: FeatureStore, permissions: list[Permission]): entities = client_fs.list_entities() @@ -188,6 +209,20 @@ def _is_auth_enabled(client_fs: FeatureStore) -> bool: return client_fs.config.auth_config.type != "no_auth" +def _test_get_fv(client_fs: FeatureStore, permissions: list[Permission]): + if not _is_auth_enabled(client_fs) or _is_permission_enabled( + client_fs, permissions, read_fv_perm + ): + fv = client_fs.get_feature_view("driver_hourly_stats") + assertpy.assert_that(fv).is_not_none() + assertpy.assert_that(fv.name).is_equal_to("driver_hourly_stats") + else: + with pytest.raises(FeastPermissionError): + client_fs.get_feature_view("driver_hourly_stats") + with pytest.raises(FeatureViewNotFoundException): + client_fs.get_feature_view("invalid-name") + + def _test_list_fvs(client_fs: FeatureStore, permissions: list[Permission]): if _is_auth_enabled(client_fs) and _permissions_exist_in_permission_list( [invalid_list_entities_perm], permissions diff --git a/sdk/python/tests/unit/test_errors.py b/sdk/python/tests/unit/test_errors.py new file mode 100644 index 0000000000..b3f33690da --- /dev/null +++ b/sdk/python/tests/unit/test_errors.py @@ -0,0 +1,26 @@ +import re + +import assertpy + +import feast.errors as errors + + +def test_error_error_detail(): + e = errors.FeatureViewNotFoundException("abc") + + d = e.to_error_detail() + + assertpy.assert_that(d).is_not_none() + assertpy.assert_that(d).contains('"module": "feast.errors"') + assertpy.assert_that(d).contains('"class": "FeatureViewNotFoundException"') + assertpy.assert_that(re.search(r"abc", d)).is_true() + + converted_e = errors.FeastError.from_error_detail(d) + assertpy.assert_that(converted_e).is_not_none() + assertpy.assert_that(str(converted_e)).is_equal_to(str(e)) + assertpy.assert_that(repr(converted_e)).is_equal_to(repr(e)) + + +def test_invalid_error_error_detail(): + e = errors.FeastError.from_error_detail("invalid") + assertpy.assert_that(e).is_none() From acd9763594184009d6e454828863ca190a934fc9 Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Thu, 29 Aug 2024 12:15:35 +0200 Subject: [PATCH 4/8] fixed merge error Signed-off-by: Daniele Martinoli --- sdk/python/feast/permissions/server/grpc.py | 53 --------------------- 1 file changed, 53 deletions(-) diff --git a/sdk/python/feast/permissions/server/grpc.py b/sdk/python/feast/permissions/server/grpc.py index 8c4da13b83..96f2690b88 100644 --- a/sdk/python/feast/permissions/server/grpc.py +++ b/sdk/python/feast/permissions/server/grpc.py @@ -3,7 +3,6 @@ import grpc -from feast.errors import FeastObjectNotFoundException, to_error_detail from feast.permissions.auth.auth_manager import ( get_auth_manager, ) @@ -31,55 +30,3 @@ def intercept_service(self, continuation, handler_call_details): sm.set_current_user(current_user) return continuation(handler_call_details) - - -class ErrorInterceptor(grpc.ServerInterceptor): - def intercept_service(self, continuation, handler_call_details): - def exception_wrapper(behavior, request, context): - try: - return behavior(request, context) - except grpc.RpcError as e: - context.abort(e.code(), e.details()) - except Exception as e: - context.abort( - _error_to_status_code(e), - to_error_detail(e), - ) - - handler = continuation(handler_call_details) - if handler is None: - return None - - if handler.unary_unary: - return grpc.unary_unary_rpc_method_handler( - lambda req, ctx: exception_wrapper(handler.unary_unary, req, ctx), - request_deserializer=handler.request_deserializer, - response_serializer=handler.response_serializer, - ) - elif handler.unary_stream: - return grpc.unary_stream_rpc_method_handler( - lambda req, ctx: exception_wrapper(handler.unary_stream, req, ctx), - request_deserializer=handler.request_deserializer, - response_serializer=handler.response_serializer, - ) - elif handler.stream_unary: - return grpc.stream_unary_rpc_method_handler( - lambda req, ctx: exception_wrapper(handler.stream_unary, req, ctx), - request_deserializer=handler.request_deserializer, - response_serializer=handler.response_serializer, - ) - elif handler.stream_stream: - return grpc.stream_stream_rpc_method_handler( - lambda req, ctx: exception_wrapper(handler.stream_stream, req, ctx), - request_deserializer=handler.request_deserializer, - response_serializer=handler.response_serializer, - ) - return handler - - -def _error_to_status_code(error: Exception) -> grpc.StatusCode: - if isinstance(error, FeastObjectNotFoundException): - return grpc.StatusCode.NOT_FOUND - if isinstance(error, FeastObjectNotFoundException): - return grpc.StatusCode.PERMISSION_DENIED - return grpc.StatusCode.INTERNAL From 592c1ce8bcfcbb3ec744fc175bf7e9eac8583346 Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Wed, 28 Aug 2024 17:19:08 +0200 Subject: [PATCH 5/8] initial commit targetting grpc registry server Signed-off-by: Daniele Martinoli --- sdk/python/feast/permissions/server/grpc.py | 53 +++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/sdk/python/feast/permissions/server/grpc.py b/sdk/python/feast/permissions/server/grpc.py index 96f2690b88..8c4da13b83 100644 --- a/sdk/python/feast/permissions/server/grpc.py +++ b/sdk/python/feast/permissions/server/grpc.py @@ -3,6 +3,7 @@ import grpc +from feast.errors import FeastObjectNotFoundException, to_error_detail from feast.permissions.auth.auth_manager import ( get_auth_manager, ) @@ -30,3 +31,55 @@ def intercept_service(self, continuation, handler_call_details): sm.set_current_user(current_user) return continuation(handler_call_details) + + +class ErrorInterceptor(grpc.ServerInterceptor): + def intercept_service(self, continuation, handler_call_details): + def exception_wrapper(behavior, request, context): + try: + return behavior(request, context) + except grpc.RpcError as e: + context.abort(e.code(), e.details()) + except Exception as e: + context.abort( + _error_to_status_code(e), + to_error_detail(e), + ) + + handler = continuation(handler_call_details) + if handler is None: + return None + + if handler.unary_unary: + return grpc.unary_unary_rpc_method_handler( + lambda req, ctx: exception_wrapper(handler.unary_unary, req, ctx), + request_deserializer=handler.request_deserializer, + response_serializer=handler.response_serializer, + ) + elif handler.unary_stream: + return grpc.unary_stream_rpc_method_handler( + lambda req, ctx: exception_wrapper(handler.unary_stream, req, ctx), + request_deserializer=handler.request_deserializer, + response_serializer=handler.response_serializer, + ) + elif handler.stream_unary: + return grpc.stream_unary_rpc_method_handler( + lambda req, ctx: exception_wrapper(handler.stream_unary, req, ctx), + request_deserializer=handler.request_deserializer, + response_serializer=handler.response_serializer, + ) + elif handler.stream_stream: + return grpc.stream_stream_rpc_method_handler( + lambda req, ctx: exception_wrapper(handler.stream_stream, req, ctx), + request_deserializer=handler.request_deserializer, + response_serializer=handler.response_serializer, + ) + return handler + + +def _error_to_status_code(error: Exception) -> grpc.StatusCode: + if isinstance(error, FeastObjectNotFoundException): + return grpc.StatusCode.NOT_FOUND + if isinstance(error, FeastObjectNotFoundException): + return grpc.StatusCode.PERMISSION_DENIED + return grpc.StatusCode.INTERNAL From 73070ca3ef5c6fabad2891fff3edecedb8cfa315 Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Thu, 29 Aug 2024 14:38:27 +0200 Subject: [PATCH 6/8] fixed merge error Signed-off-by: Daniele Martinoli --- sdk/python/feast/permissions/server/grpc.py | 53 --------------------- 1 file changed, 53 deletions(-) diff --git a/sdk/python/feast/permissions/server/grpc.py b/sdk/python/feast/permissions/server/grpc.py index 8c4da13b83..96f2690b88 100644 --- a/sdk/python/feast/permissions/server/grpc.py +++ b/sdk/python/feast/permissions/server/grpc.py @@ -3,7 +3,6 @@ import grpc -from feast.errors import FeastObjectNotFoundException, to_error_detail from feast.permissions.auth.auth_manager import ( get_auth_manager, ) @@ -31,55 +30,3 @@ def intercept_service(self, continuation, handler_call_details): sm.set_current_user(current_user) return continuation(handler_call_details) - - -class ErrorInterceptor(grpc.ServerInterceptor): - def intercept_service(self, continuation, handler_call_details): - def exception_wrapper(behavior, request, context): - try: - return behavior(request, context) - except grpc.RpcError as e: - context.abort(e.code(), e.details()) - except Exception as e: - context.abort( - _error_to_status_code(e), - to_error_detail(e), - ) - - handler = continuation(handler_call_details) - if handler is None: - return None - - if handler.unary_unary: - return grpc.unary_unary_rpc_method_handler( - lambda req, ctx: exception_wrapper(handler.unary_unary, req, ctx), - request_deserializer=handler.request_deserializer, - response_serializer=handler.response_serializer, - ) - elif handler.unary_stream: - return grpc.unary_stream_rpc_method_handler( - lambda req, ctx: exception_wrapper(handler.unary_stream, req, ctx), - request_deserializer=handler.request_deserializer, - response_serializer=handler.response_serializer, - ) - elif handler.stream_unary: - return grpc.stream_unary_rpc_method_handler( - lambda req, ctx: exception_wrapper(handler.stream_unary, req, ctx), - request_deserializer=handler.request_deserializer, - response_serializer=handler.response_serializer, - ) - elif handler.stream_stream: - return grpc.stream_stream_rpc_method_handler( - lambda req, ctx: exception_wrapper(handler.stream_stream, req, ctx), - request_deserializer=handler.request_deserializer, - response_serializer=handler.response_serializer, - ) - return handler - - -def _error_to_status_code(error: Exception) -> grpc.StatusCode: - if isinstance(error, FeastObjectNotFoundException): - return grpc.StatusCode.NOT_FOUND - if isinstance(error, FeastObjectNotFoundException): - return grpc.StatusCode.PERMISSION_DENIED - return grpc.StatusCode.INTERNAL From 24661dfdbe677d99d123507b1a3e541535b7fae9 Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Thu, 29 Aug 2024 14:39:16 +0200 Subject: [PATCH 7/8] integrated comment Signed-off-by: Daniele Martinoli --- sdk/python/feast/registry_server.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sdk/python/feast/registry_server.py b/sdk/python/feast/registry_server.py index 7e12710a3a..40475aa580 100644 --- a/sdk/python/feast/registry_server.py +++ b/sdk/python/feast/registry_server.py @@ -647,7 +647,7 @@ def start_server(store: FeatureStore, port: int, wait_for_termination: bool = Tr server = grpc.server( futures.ThreadPoolExecutor(max_workers=10), - interceptors=grpc_interceptors(auth_manager_type), + interceptors=_grpc_interceptors(auth_manager_type), ) RegistryServer_pb2_grpc.add_RegistryServerServicer_to_server( RegistryServer(store.registry), server @@ -672,11 +672,11 @@ def start_server(store: FeatureStore, port: int, wait_for_termination: bool = Tr return server -def grpc_interceptors( +def _grpc_interceptors( auth_type: AuthManagerType, ) -> Optional[list[grpc.ServerInterceptor]]: """ - A list of the authorization interceptors. + A list of the interceptors for the registry server. Args: auth_type: The type of authorization manager, from the feature store configuration. From 50306c1041cad50029f85f1ea9370a3c87ab8ba6 Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Fri, 30 Aug 2024 08:30:23 +0200 Subject: [PATCH 8/8] moved imports as per comment Signed-off-by: Daniele Martinoli --- sdk/python/feast/errors.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/sdk/python/feast/errors.py b/sdk/python/feast/errors.py index ff7d46df02..d39009ae7a 100644 --- a/sdk/python/feast/errors.py +++ b/sdk/python/feast/errors.py @@ -1,3 +1,5 @@ +import importlib +import json import logging from typing import Any, List, Optional, Set @@ -36,7 +38,6 @@ def to_error_detail(self) -> str: Returns: str: a string representation of a JSON document including `module`, `class` and `message` fields. """ - import json m = { "module": f"{type(self).__module__}", @@ -47,9 +48,6 @@ def to_error_detail(self) -> str: @staticmethod def from_error_detail(detail: str) -> Optional["FeastError"]: - import importlib - import json - try: m = json.loads(detail) if all(f in m for f in ["module", "class", "message"]):