Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RESTClient: middleware / interceptors #2128

Open
joscha opened this issue Dec 9, 2024 · 1 comment
Open

RESTClient: middleware / interceptors #2128

joscha opened this issue Dec 9, 2024 · 1 comment

Comments

@joscha
Copy link
Contributor

joscha commented Dec 9, 2024

Feature description

I would like to be able to use a middleware on the rest client that uses a Pydantic model on the response.
This has some similarity to using hooks, bit hooks currently don't allow changing the return value, they are only evaluated alongside.

Currently, I am doing something like this:

response = rest_client.get("companies")
companies = CompanyPaged.model_validate_json(json_data=response.text)

for pagination it is a bit more tricky even, as the value returned from .paginate is only the data part of the response, e.g. for something like:

class CompanyPaged(BaseModel):
    data: List[Company]
    """
    A page of Company results
    """
    pagination: Pagination

only List[Company] is returned from .paginate, as the pagination property is evaluated and used internally.

In my ideal world I'd be able to do something like this:

from dlt.sources.helpers.requests import Response

def my_transformer(response: Response):
    return CompanyPaged.model_validate_json(json_data=response.text)

for companies in rest_client.paginate("companies", middleware=[my_transformer]):
    for company in companies:
        print(company)

or even better:

from dlt.sources.helpers.requests import Response

def my_transformer(response: Response):
    return CompanyPaged.model_validate_json(json_data=response.text)

def transform_errors(response: Response):
    error_model = ...   # match response against a list of error models
    raise CustomApiException("Oops", error_model)

try:
    # yields `List[Company]`s
    yield from rest_client.paginate("companies", middleware={
        "success": [my_transformer],
        "error": [transform_errors]
    })
except CustomException as e:
     print(f"Payload: {e.error_model}")
     # handle error (gracefully?) here

Other rest clients have this notion - for example Axios interceptors.

Possibly this can be done via the low-level access to request's Session added in #1844 and mounting a connection adapter, however it doesn't necessarily need to be that low-level, and the involvement is quite substantial.

My current workaround

def raise_if_error(response: Response, *args: Any, **kwargs: Any) -> None:
    if response.status_code < 200 or response.status_code >= 300:
        error_adapter = TypeAdapter(AuthenticationErrors | NotFoundErrors | AuthorizationErrors | ValidationErrors)
        error = error_adapter.validate_json(response.text)
        response.reason = "\n".join([e.message for e in error.errors])
        response.raise_for_status()

hooks = {
    "response": [raise_if_error]
}

AuthenticationErrors | NotFoundErrors | AuthorizationErrors | ValidationErrors are pydantic models looking roughly like this:

class AuthenticationError(BaseModel):
    code: Literal['authentication']
    """
    Error code
    """
    message: str
    """
    Error message
    """


class AuthenticationErrors(BaseModel):
    errors: List[AuthenticationError]
    """
    AuthenticationError errors
    """

Are you a dlt user?

Yes, I'm already a dlt user.

Use case

postprocessing rest client responses.

Proposed solution

Expose/add a middleware / interceptor / connection adapter interface to RESTClient.

If we want to specifically support Pydantic, support for the TypeAdapter might be a good way to do it.

Related issues

#1593, #1844

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Todo
Development

No branches or pull requests

1 participant