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

Allow Optional type annotation #307

Open
ScarletBlizzard opened this issue Apr 25, 2024 · 2 comments
Open

Allow Optional type annotation #307

ScarletBlizzard opened this issue Apr 25, 2024 · 2 comments

Comments

@ScarletBlizzard
Copy link

Error when calling Consumer method with return type annotation Optional[...] if response is None:

...
 File "/home/max/.cache/pypoetry/virtualenvs/autom-tnujRd_K-py3.10/lib/python3.10/site-packages/uplink/builder.py", line 100, in __call__
    self._request_definition.define_request(request_builder, args, kwargs)
  File "/home/max/.cache/pypoetry/virtualenvs/autom-tnujRd_K-py3.10/lib/python3.10/site-packages/uplink/commands.py", line 287, in define_request
    self._method_handler.handle_builder(request_builder)
  File "/home/max/.cache/pypoetry/virtualenvs/autom-tnujRd_K-py3.10/lib/python3.10/site-packages/uplink/decorators.py", line 62, in handle_builder
    annotation.modify_request(request_builder)
  File "/home/max/.cache/pypoetry/virtualenvs/autom-tnujRd_K-py3.10/lib/python3.10/site-packages/uplink/returns.py", line 66, in modify_request
    converter = self._get_converter(request_builder, return_type)
  File "/home/max/.cache/pypoetry/virtualenvs/autom-tnujRd_K-py3.10/lib/python3.10/site-packages/uplink/returns.py", line 57, in _get_converter
    return request_builder.get_converter(
  File "/home/max/.cache/pypoetry/virtualenvs/autom-tnujRd_K-py3.10/lib/python3.10/site-packages/uplink/helpers.py", line 96, in get_converter
    return self._converter_registry[converter_key](*args, **kwargs)
  File "/home/max/.cache/pypoetry/virtualenvs/autom-tnujRd_K-py3.10/lib/python3.10/site-packages/uplink/converters/__init__.py", line 52, in __call__
    converter = self._converter_factory(*args, **kwargs)
  File "/home/max/.cache/pypoetry/virtualenvs/autom-tnujRd_K-py3.10/lib/python3.10/site-packages/uplink/converters/__init__.py", line 112, in chain
    converter = func(factory)(*args, **kwargs)
  File "/home/max/.cache/pypoetry/virtualenvs/autom-tnujRd_K-py3.10/lib/python3.10/site-packages/uplink/converters/typing_.py", line 127, in create_response_body_converter
    return self._base_converter(type_)
  File "/home/max/.cache/pypoetry/virtualenvs/autom-tnujRd_K-py3.10/lib/python3.10/site-packages/uplink/converters/typing_.py", line 121, in _base_converter
    if issubclass(type_.__origin__, self.typing.Sequence):
  File "/home/max/.pyenv/versions/3.10.13/lib/python3.10/typing.py", line 1158, in __subclasscheck__
    return issubclass(cls, self.__origin__)
  File "/home/max/.pyenv/versions/3.10.13/lib/python3.10/abc.py", line 123, in __subclasscheck__
    return _abc_subclasscheck(cls, subclass)
TypeError: issubclass() arg 1 must be a class

To Reproduce
Create a Consumer with method that consumes API endpoint that returns None (it is shown as "null" if using curl). Add Optional[SomeType] return type annotation to this method. Call this method to get the error

Expected behavior
Absence of this error. Instead, simply return None

@EvaSDK
Copy link

EvaSDK commented Aug 16, 2024

This might not be exactly what you are expecting nor the best solution but for a similar case, in our Harbor API integration, we use the following code to deal with that:

class Success(BaseModel):
    """Placeholder model for successful operations.

    Some operations do not return any data yet `uplink` requires model objects
    to map the response to.
    """


class OptionalJsonStrategy(returns.JsonStrategy):
    """Converts response to object if there is data, else return Success."""

    # pylint: disable=too-few-public-methods

    def __call__(self, response: _ClientResponse) -> BaseModel:  # noqa: D102
        if response.content:
            content = super().__call__(response)
        else:
            content = Success()
        return content


class optional_json(returns.json):  # pylint: disable=invalid-name
    """Specifies that the decorated consumer method should return a JSON object.

    See :class:`uplink.returns.json`.
    """  # noqa: E501

    def _make_strategy(self, converter: Converter) -> OptionalJsonStrategy:
        return OptionalJsonStrategy(converter, self._key)


@optional_json
class HarborAPI(Consumer):
    """These APIs provide services for manipulating Harbor project."""

    @delete(
        "projects/{project_name}/repositories/{repository_name}/"
        "artifacts/{reference}"
    )
    def delete_artifact(
        self,
        project_name: Path,
        repository_name: Path,
        reference: Path,
    ) -> Union[Errors, Success]:
        """Delete the specific artifact."""

Hope it helps.

@EvaSDK
Copy link

EvaSDK commented Aug 16, 2024

For reference, here is the excerpt from Harbor spec:
$ jq '.paths["/projects/{project_name_or_id}"].delete' ~/harbor.json

{
  "responses": {
    "200": {
      "$ref": "#/responses/200"
    },
    "404": {
      "$ref": "#/responses/404"
    },
    "403": {
      "$ref": "#/responses/403"
    },
    "412": {
      "$ref": "#/responses/412"
    },
    "400": {
      "$ref": "#/responses/400"
    },
    "500": {
      "$ref": "#/responses/500"
    }
  },
  "parameters": [
    {
      "$ref": "#/parameters/requestId"
    },
    {
      "$ref": "#/parameters/isResourceName"
    },
    {
      "$ref": "#/parameters/projectNameOrId"
    }
  ],
  "tags": [
    "project"
  ],
  "operationId": "deleteProject",
  "summary": "Delete project by projectID",
  "description": "This endpoint is aimed to delete project by project ID."
}

$ jq '.responses["200"]' ~/harbor.json

{
  "headers": {
    "X-Request-Id": {
      "type": "string",
      "description": "The ID of the corresponding request for the response"
    }
  },
  "description": "Success"
}

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

No branches or pull requests

2 participants