Hone logo
Hone
Problems

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:

  1. ServiceWrapper Class:

    • 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 the execute_service method.
      • error_types: An optional tuple of exception types that this service might raise, which should be considered "service errors" and handled gracefully. If None, 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_func with *args and **kwargs.
      • It should catch any exceptions defined in error_types for 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.
    • 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.
  2. Return Structure: The execute_service method 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 the result field.
      • "service_error": The service raised an exception listed in error_types. The error_message, error_type, and original_exception fields will be populated.
      • "system_error": The service raised an exception not listed in error_types (e.g., network issues, programming errors). The error_message, error_type, and original_exception fields will be populated.
      • "no_result": The service executed successfully, but returned None or an equivalent "empty" value that you define as no_result (e.g., an empty list or dictionary). The result field will be None.
  3. Result Transformation: If a transformer_func is registered for a service, it should be applied only if the service execution is successful (status is "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_service with a service name that has not been registered.
  • A registered service_func raising an unexpected exception.
  • A registered transformer_func raising an exception.
  • A service returning None or 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_order function returns data, which is then passed to order_ref_formatter. The formatter returns a string, which becomes the result.
  • The second execution uses a bad_transformer. The create_order function succeeds, but the transformer raises a RuntimeError. This is caught as a "system_error" because RuntimeError was not registered as a service error for order_service.
  • The third execution triggers a ValueError within create_order itself, which was registered as a service_error.

Constraints

  • The ServiceWrapper class must be implemented in Python 3.x.
  • Service names registered must be unique strings.
  • The service_func and transformer_func will be valid Python callable objects.
  • The error_types argument to register_service will be a tuple of exception classes or None.
  • 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 None as 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 direct None return as "no_result" status.
  • The error_type in the output should be the string name of the exception class.
  • You will need to implement the ServiceWrapper class from scratch.
Loading editor...
python