ResponseFactory

Mapping a dataclass instance to an OpenAPI model class

Source code in src/alpha/factories/response_factory.py
class ResponseFactory:
    """Mapping a dataclass instance to an OpenAPI model class"""

    def process(
        self,
        response: DataclassInstance
        | Sequence[DataclassInstance]
        | AttrsInstance
        | Sequence[AttrsInstance]
        | PydanticInstance
        | Sequence[PydanticInstance],
        cls: OpenAPIModel | Sequence[OpenAPIModel],
    ) -> object:
        """Mapping a dataclass instance or a collection of instances to an
        OpenAPI model

        Parameters
        ----------
        response
            DomainModel instance
        cls
            OpenAPI model class or an Iterable

        Returns
        -------
        object
            An OpenAPI model instance or a list of instances

        Raises
        ------
        exceptions.ClassMismatchException
            The targeted object type is a list but the source object is not
            iterable
        Exception
            Encountered unexpected exception
        ValueError
            Unable to instantiate a class of type OpenAPIModel without values.
            Probably because there are required values which cannot be None
        TypeError
            cls value is not a valid OpenAPIModel type
        """

        cls_origin = get_origin(cls)

        # When the source instance and target class are of an iterable type
        if cls_origin in [list, tuple, set]:
            if isinstance(response, Sequence):
                arg = get_args(cls)[0]
                return [
                    self.process(response=obj, cls=arg) for obj in response
                ]
            if cls_origin != get_origin(response):
                raise exceptions.ClassMismatchException(
                    "The targeted object type is a list but the source object "
                    "is not iterable"
                )
            raise Exception(
                f"Encountered unexpected exception; cls_origin: {cls_origin}, "
                + f"response: {response}"
            )

        attrs: dict[str, type]
        # Check if the openapi_types variable is set as a class variable
        if len(cls.openapi_types) > 0:  # type: ignore
            attrs = getattr(cls, "openapi_types")
        # Else try to instantiate the class and read the openapi_types value
        else:
            try:
                cls_instance: Any = cls()  # type: ignore
            except ValueError as e:
                raise ValueError(
                    "Unable to instantiate a class of type OpenAPIModel "
                    "without values. Probably because there are required "
                    f"values which cannot be None: '{e}'. A solution to "
                    "this problem is to set the 'openapi_types' attribute "
                    "as a class variable instead of setting it in the "
                    "__init__ method"
                )
            error_msg = (
                f"'{cls.__name__}' is not a "  # type: ignore
                "valid OpenAPIModel type"
            )
            if not hasattr(cls_instance, "openapi_types"):
                raise TypeError(error_msg)
            attrs = getattr(cls_instance, "openapi_types")

        # Process attrs through _attr_factory
        params = {
            attr_name: self._attr_factory(
                attr_name=attr_name, attr_type=attr_type, response=response
            )
            for attr_name, attr_type in attrs.items()
        }

        return cls(**params)  # type: ignore

    def _attr_factory(
        self, attr_name: str, attr_type: type, response: object
    ) -> Any:
        """Handles the attributes of an object by returning the response value
        in the correct form

        Parameters
        ----------
        attr_name
            The name of the attribute
        attr_type
            The type of the attribute. This can be
        response
            A dataclass instance or an list of instances

        Returns
        -------
        Any
            The value from the response object which corresponds to the
            attr_name
        """
        response_value = self._attr_lookup(obj=response, attr_name=attr_name)

        if isinstance(response_value, Enum):
            return response_value.name

        # These if statements are needed to achieve a certain behavior
        if isinstance(attr_type, OpenAPIModel) or (
            get_origin(attr_type) is list
        ):
            if get_origin(attr_type) is list:
                first_item = get_args(attr_type)[0]
                if not isinstance(first_item, OpenAPIModel):
                    return response_value
            return self.process(response=response_value, cls=attr_type)  # type: ignore
        return response_value

    def _attr_lookup(self, obj: object, attr_name: str) -> Any:
        """Lookup the attributes value from an object. The object can be of a
        random type, including a dataclass or a dictionary.

        Parameters
        ----------
        obj
            The source object
        attr_name
            Attribute name

        Returns
        -------
        Any
            Any value which corresponds to the attributes name

        Raises
        ------
        exceptions.MissingAttributeError
            Raises in case the attribute is not found
        """
        if hasattr(obj, attr_name):
            return getattr(obj, attr_name, None)

        if is_dataclass(obj) or is_attrs(obj) or is_pydantic(obj):
            attrs = getattr(obj, "__match_args__", [])

            for attr in attrs:
                obj_attr = getattr(obj, attr, MISSING)

                if attr == attr_name:
                    return obj_attr

                # Lookup nested attribute
                if hasattr(obj_attr, attr_name):
                    return getattr(obj_attr, attr_name)

        if isinstance(obj, dict):
            if attr_name in obj.keys():
                value: Any = obj[attr_name]
                return value

        raise exceptions.MissingAttributeError(
            f"'{attr_name}' can not be found in the response object:"
            + f"{json.dumps(obj, cls=JSONEncoder)}"
        )

Methods:

process

process(response, cls)

Mapping a dataclass instance or a collection of instances to an OpenAPI model

Parameters:
Returns:
  • object

    An OpenAPI model instance or a list of instances

Raises:
  • ClassMismatchException

    The targeted object type is a list but the source object is not iterable

  • Exception

    Encountered unexpected exception

  • ValueError

    Unable to instantiate a class of type OpenAPIModel without values. Probably because there are required values which cannot be None

  • TypeError

    cls value is not a valid OpenAPIModel type

Source code in src/alpha/factories/response_factory.py
def process(
    self,
    response: DataclassInstance
    | Sequence[DataclassInstance]
    | AttrsInstance
    | Sequence[AttrsInstance]
    | PydanticInstance
    | Sequence[PydanticInstance],
    cls: OpenAPIModel | Sequence[OpenAPIModel],
) -> object:
    """Mapping a dataclass instance or a collection of instances to an
    OpenAPI model

    Parameters
    ----------
    response
        DomainModel instance
    cls
        OpenAPI model class or an Iterable

    Returns
    -------
    object
        An OpenAPI model instance or a list of instances

    Raises
    ------
    exceptions.ClassMismatchException
        The targeted object type is a list but the source object is not
        iterable
    Exception
        Encountered unexpected exception
    ValueError
        Unable to instantiate a class of type OpenAPIModel without values.
        Probably because there are required values which cannot be None
    TypeError
        cls value is not a valid OpenAPIModel type
    """

    cls_origin = get_origin(cls)

    # When the source instance and target class are of an iterable type
    if cls_origin in [list, tuple, set]:
        if isinstance(response, Sequence):
            arg = get_args(cls)[0]
            return [
                self.process(response=obj, cls=arg) for obj in response
            ]
        if cls_origin != get_origin(response):
            raise exceptions.ClassMismatchException(
                "The targeted object type is a list but the source object "
                "is not iterable"
            )
        raise Exception(
            f"Encountered unexpected exception; cls_origin: {cls_origin}, "
            + f"response: {response}"
        )

    attrs: dict[str, type]
    # Check if the openapi_types variable is set as a class variable
    if len(cls.openapi_types) > 0:  # type: ignore
        attrs = getattr(cls, "openapi_types")
    # Else try to instantiate the class and read the openapi_types value
    else:
        try:
            cls_instance: Any = cls()  # type: ignore
        except ValueError as e:
            raise ValueError(
                "Unable to instantiate a class of type OpenAPIModel "
                "without values. Probably because there are required "
                f"values which cannot be None: '{e}'. A solution to "
                "this problem is to set the 'openapi_types' attribute "
                "as a class variable instead of setting it in the "
                "__init__ method"
            )
        error_msg = (
            f"'{cls.__name__}' is not a "  # type: ignore
            "valid OpenAPIModel type"
        )
        if not hasattr(cls_instance, "openapi_types"):
            raise TypeError(error_msg)
        attrs = getattr(cls_instance, "openapi_types")

    # Process attrs through _attr_factory
    params = {
        attr_name: self._attr_factory(
            attr_name=attr_name, attr_type=attr_type, response=response
        )
        for attr_name, attr_type in attrs.items()
    }

    return cls(**params)  # type: ignore