prkumar / uplink

A Declarative HTTP Client for Python
https://uplink.readthedocs.io/
MIT License
1.07k stars 61 forks source link

Support Union type as response model #233

Open TimoGlastra opened 3 years ago

TimoGlastra commented 3 years ago

Is your feature request related to a problem? Please describe.

It would be nice if Uplink could support Union types as response model. We're using Uplink (with Pydantic) to automatically generate a client. An example method (simplified) that it will generate is as follows:

    @returns.json()
    @json
    @post("/schemas")
    def __publish_schema(
        self,
        *,
        conn_id: Query = None,
        create_transaction_for_endorser: Query = None,
        body: Body(type=SchemaSendRequest) = {}
    ) -> Union[TxnOrSchemaSendResult, SchemaSendResult]:
        """Internal uplink method for publish_schema"""

This will throw an error: TypeError: issubclass() arg 1 must be a class because Union is not a class (see stack trace below for more info)

Describe the solution you'd like

It would be nice if uplink could support Union types as response models. Pydantic has this feature, where you need to make sure to put the in the right order, as the first schema that matches will be taken. Something similar (maybe leaning on the implementation in Pydantic) would be a great addition I think. Pydantic feature docs: https://pydantic-docs.helpmanual.io/usage/types/#unions

Additional context

Error log when using Union type:

Traceback (most recent call last):
  File "test.py", line 20, in <module>
    event_loop.run_until_complete(run())
  File "/Users/timoglastra/.pyenv/versions/3.8.10/lib/python3.8/asyncio/base_events.py", line 616, in run_until_complete
    return future.result()
  File "test.py", line 9, in run
    schema = await client.schema.publish_schema(
  File "/Users/timoglastra/Developer/Work/Animo/Projects/Yoma/aries-cloudcontroller-python/aries_cloudcontroller/api/schema.py", line 57, in publish_schema
    return await self.__publish_schema(
  File "/Users/timoglastra/.pyenv/versions/3.8.10/lib/python3.8/site-packages/uplink/builder.py", line 95, in __call__
    self._request_definition.define_request(request_builder, args, kwargs)
  File "/Users/timoglastra/.pyenv/versions/3.8.10/lib/python3.8/site-packages/uplink/commands.py", line 287, in define_request
    self._method_handler.handle_builder(request_builder)
  File "/Users/timoglastra/.pyenv/versions/3.8.10/lib/python3.8/site-packages/uplink/decorators.py", line 62, in handle_builder
    annotation.modify_request(request_builder)
  File "/Users/timoglastra/.pyenv/versions/3.8.10/lib/python3.8/site-packages/uplink/returns.py", line 64, in modify_request
    converter = request_builder.get_converter(
  File "/Users/timoglastra/.pyenv/versions/3.8.10/lib/python3.8/site-packages/uplink/helpers.py", line 96, in get_converter
    return self._converter_registry[converter_key](*args, **kwargs)
  File "/Users/timoglastra/.pyenv/versions/3.8.10/lib/python3.8/site-packages/uplink/converters/__init__.py", line 54, in __call__
    converter = self._converter_factory(*args, **kwargs)
  File "/Users/timoglastra/.pyenv/versions/3.8.10/lib/python3.8/site-packages/uplink/converters/__init__.py", line 114, in chain
    converter = func(factory)(*args, **kwargs)
  File "/Users/timoglastra/.pyenv/versions/3.8.10/lib/python3.8/site-packages/uplink/converters/typing_.py", line 128, in create_response_body_converter
    return self._base_converter(type_)
  File "/Users/timoglastra/.pyenv/versions/3.8.10/lib/python3.8/site-packages/uplink/converters/typing_.py", line 122, in _base_converter
    if issubclass(type_.__origin__, self.typing.Sequence):
  File "/Users/timoglastra/.pyenv/versions/3.8.10/lib/python3.8/typing.py", line 774, in __subclasscheck__
    return issubclass(cls, self.__origin__)
  File "/Users/timoglastra/.pyenv/versions/3.8.10/lib/python3.8/abc.py", line 102, in __subclasscheck__
    return _abc_subclasscheck(cls, subclass)
TypeError: issubclass() arg 1 must be a class
TimoGlastra commented 2 years ago

For anyone else running into this issue, I've managed to fix it using a custom pydantic converter with a wrapper model class. This leverages the union support of pydantic:

        if typing.get_origin(self._model) is Union:

             class Container(BaseModel):
                 v: self._model

             data = {"v": data}
             return Container.parse_obj(data).v

https://github.com/didx-xyz/aries-cloudcontroller-python/pull/71/files#diff-26a4b54a23153cafa5f939c580250b6a143fdc57767837a3b35b535a8199ce8aR70-R77

EvaSDK commented 1 year ago

FTR, I shared the hopefully generic implementation I came up with combining various solutions found here and in the discussion #255.