Python Service Wrapper
Imagine you are building a system that interacts with various external services, each with its own unique API, error handling, and data formats. To manage these interactions efficiently and consistently, you need a way to abstract away the complexities of each service. This challenge asks you to implement a generic service wrapper in Python that can handle calls to different services, including basic error handling and result transformation.
Problem Description
Your task is to create a ServiceWrapper class in Python. This class should be designed to wrap around different service-calling functions. The wrapper should allow you to:
- Register Services: Be able to register different service-calling functions with unique names.
- Execute Services: Call a registered service by its name, passing any necessary arguments.
- Handle Errors: Catch specific exceptions raised by the service-calling functions and provide a consistent way to report them.
- Transform Results: Optionally apply a transformation function to the successful result of a service call before returning it.
- Provide Status: Indicate whether a service call was successful, failed with an error, or returned a specific "empty" or "no result" state.
Key Requirements:
-
ServiceWrapperClass:- An
__init__method that initializes the wrapper. - A
register_service(name, service_func, error_types=None)method.name: A unique string identifier for the service.service_func: The actual function that calls the external service. This function will be called with arguments passed to theexecute_servicemethod.error_types: An optional tuple of exception types that this service might raise, which should be considered "service errors" and handled gracefully. IfNone, all exceptions are considered system errors.
- An
execute_service(name, *args, **kwargs)method.- This method will look up the registered service by
name. - It will call the associated
service_funcwith*argsand**kwargs. - It should catch any exceptions defined in
error_typesfor that service and categorize them as "service errors". - It should catch any other exceptions and categorize them as "system errors".
- It should return a dictionary representing the outcome of the execution.
- This method will look up the registered service by
- An
add_result_transformer(name, transformer_func)method.name: The name of the service to apply the transformer to.transformer_func: A function that takes the successful return value of the service and transforms it.
- An
-
Return Structure: The
execute_servicemethod should return a dictionary with the following structure:{ "status": "success" | "service_error" | "system_error" | "no_result", "result": <the transformed or original result if status is "success"> | None, "error_message": <a string description of the error if status is "service_error" or "system_error"> | None, "error_type": <the type of exception if status is "service_error" or "system_error"> | None, "original_exception": <the actual exception object if status is "service_error" or "system_error"> | None }status: Indicates the outcome."success": The service executed successfully, and its result (possibly transformed) is in theresultfield."service_error": The service raised an exception listed inerror_types. Theerror_message,error_type, andoriginal_exceptionfields will be populated."system_error": The service raised an exception not listed inerror_types(e.g., network issues, programming errors). Theerror_message,error_type, andoriginal_exceptionfields will be populated."no_result": The service executed successfully, but returnedNoneor an equivalent "empty" value that you define asno_result(e.g., an empty list or dictionary). Theresultfield will beNone.
-
Result Transformation: If a
transformer_funcis registered for a service, it should be applied only if the service execution is successful (statusis"success") and before the result is placed in the return dictionary. If the transformer itself raises an exception, it should be treated as a"system_error".
Edge Cases to Consider:
- Calling
execute_servicewith a service name that has not been registered. - A registered
service_funcraising an unexpected exception. - A registered
transformer_funcraising an exception. - A service returning
Noneor an empty collection as a valid successful result. - Registering multiple transformers for the same service (the last one registered should take precedence).
Examples
Example 1: Successful Service Call
class MockService:
def get_user_data(self, user_id):
print(f"MockService: Fetching data for user {user_id}")
return {"id": user_id, "name": "Alice", "email": "alice@example.com"}
mock_service_instance = MockService()
wrapper = ServiceWrapper()
wrapper.register_service("user_service", mock_service_instance.get_user_data)
# Execute the service
response = wrapper.execute_service("user_service", user_id=123)
print(response)
MockService: Fetching data for user 123
{
'status': 'success',
'result': {'id': 123, 'name': 'Alice', 'email': 'alice@example.com'},
'error_message': None,
'error_type': None,
'original_exception': None
}
Explanation: The get_user_data function was called successfully. The status is "success", and the result contains the dictionary returned by the mock service.
Example 2: Service Error
class ProductService:
class ProductNotFoundError(Exception):
pass
def get_product_details(self, product_id):
print(f"ProductService: Looking up product {product_id}")
if product_id == "XYZ":
raise self.ProductNotFoundError(f"Product with ID {product_id} not found.")
return {"id": product_id, "name": "Widget", "price": 10.99}
product_service_instance = ProductService()
wrapper = ServiceWrapper()
wrapper.register_service(
"product_service",
product_service_instance.get_product_details,
error_types=(ProductService.ProductNotFoundError,)
)
# Execute with a non-existent product ID
response = wrapper.execute_service("product_service", product_id="XYZ")
print(response)
ProductService: Looking up product XYZ
{
'status': 'service_error',
'result': None,
'error_message': 'Product with ID XYZ not found.',
'error_type': 'ProductNotFoundError',
'original_exception': <__main__.ProductService.ProductNotFoundError object at ...>
}
Explanation: The get_product_details function raised a ProductNotFoundError, which was registered as a service_error type. The status is "service_error", and the error details are populated.
Example 3: Result Transformation and System Error
class OrderService:
def create_order(self, item_id, quantity):
print(f"OrderService: Creating order for item {item_id}, quantity {quantity}")
if quantity <= 0:
raise ValueError("Quantity must be positive.")
# Simulate order creation success
return {"order_ref": "ORD12345", "status": "pending"}
def order_ref_formatter(order_data):
print(f"Transforming order data: {order_data}")
if not order_data or "order_ref" not in order_data:
raise TypeError("Invalid order data for formatting.")
return f"Formatted: {order_data['order_ref']}"
order_service_instance = OrderService()
wrapper = ServiceWrapper()
wrapper.register_service(
"order_service",
order_service_instance.create_order,
error_types=(ValueError,)
)
wrapper.add_result_transformer("order_service", order_ref_formatter)
# Successful execution with transformation
response_success = wrapper.execute_service("order_service", item_id="ABC", quantity=2)
print("\nSuccessful Execution:")
print(response_success)
# Execution that causes a system error (e.g., internal exception in transformer)
def bad_transformer(order_data):
raise RuntimeError("Something went wrong in the transformer!")
wrapper.add_result_transformer("order_service", bad_transformer) # Overwrite previous transformer
response_system_error = wrapper.execute_service("order_service", item_id="DEF", quantity=1)
print("\nSystem Error Execution:")
print(response_system_error)
# Execution that causes a registered service error
response_service_error = wrapper.execute_service("order_service", item_id="GHI", quantity=0)
print("\nService Error Execution:")
print(response_service_error)
OrderService: Creating order for item ABC, quantity 2
Transforming order data: {'order_ref': 'ORD12345', 'status': 'pending'}
Successful Execution:
{
'status': 'success',
'result': 'Formatted: ORD12345',
'error_message': None,
'error_type': None,
'original_exception': None
}
OrderService: Creating order for item DEF, quantity 1
{'status': 'system_error', 'result': None, 'error_message': 'Something went wrong in the transformer!', 'error_type': 'RuntimeError', 'original_exception': <RuntimeError object at ...>}
System Error Execution:
OrderService: Creating order for item GHI, quantity 0
Service Error Execution:
{
'status': 'service_error',
'result': None,
'error_message': 'Quantity must be positive.',
'error_type': 'ValueError',
'original_exception': <ValueError object at ...>
}
Explanation:
- The first execution is successful. The
create_orderfunction returns data, which is then passed toorder_ref_formatter. The formatter returns a string, which becomes theresult. - The second execution uses a
bad_transformer. Thecreate_orderfunction succeeds, but the transformer raises aRuntimeError. This is caught as a"system_error"becauseRuntimeErrorwas not registered as a service error fororder_service. - The third execution triggers a
ValueErrorwithincreate_orderitself, which was registered as aservice_error.
Constraints
- The
ServiceWrapperclass must be implemented in Python 3.x. - Service names registered must be unique strings.
- The
service_funcandtransformer_funcwill be valid Python callable objects. - The
error_typesargument toregister_servicewill be a tuple of exception classes orNone. - The number of registered services is expected to be reasonable (e.g., less than 100).
- The complexity of service functions and transformers is not a primary concern for this challenge; focus on the wrapper's logic.
Notes
- Think about how to store the registered services and their associated error types. A dictionary would be a good candidate.
- Consider how to handle the case where a service might return
Noneas a valid successful result versus an indication of no data. You might need to define a convention or allow configuration for this. For this challenge, treat a directNonereturn as"no_result"status. - The
error_typein the output should be the string name of the exception class. - You will need to implement the
ServiceWrapperclass from scratch.