Source code for uplink.builder

# Standard library imports
import functools
import warnings

# Local imports
from uplink import (
    arguments,
    auth as auth_,
    clients,
    compat,
    converters as converters_,
    exceptions,
    helpers,
    hooks as hooks_,
    interfaces,
    session,
    utils,
)
from uplink.clients import io

__all__ = ["build", "Consumer"]


class RequestPreparer(object):
    def __init__(self, builder, consumer=None):
        self._client = builder.client
        self._base_url = str(builder.base_url)
        self._converters = list(builder.converters)
        self._auth = builder.auth
        self._consumer = consumer

        if builder.hooks:
            self._session_chain = hooks_.TransactionHookChain(*builder.hooks)
        else:
            self._session_chain = None

    @staticmethod
    def _get_request_hooks(contract):
        chain = list(contract.transaction_hooks)
        if callable(contract.return_type):
            chain.append(hooks_.ResponseHandler(contract.return_type))
        return chain

    def _wrap_hook(self, func):
        @compat.wraps(func)
        def wrapper(*args, **kwargs):
            return func(self._consumer, *args, **kwargs)

        return wrapper

    def apply_hooks(self, execution_builder, chain):
        # TODO:
        #   Instead of creating a TransactionChain, we could simply
        #   add each response and error handler in the chain to the
        #   execution builder. This would allow heterogenous response
        #   and error handlers. Right now, the TransactionChain
        #   enforces that all response/error handlers are blocking if
        #   any response/error handler is blocking, which is
        #   unnecessary now that we delegate execution to an IO layer.
        if chain.handle_response is not None:
            execution_builder.with_callbacks(
                self._wrap_hook(chain.handle_response)
            )
        execution_builder.with_errbacks(self._wrap_hook(chain.handle_exception))

    def prepare_request(self, request_builder, execution_builder):
        self._auth(request_builder)
        request_hooks = self._get_request_hooks(request_builder)
        if request_hooks:
            chain = hooks_.TransactionHookChain(*request_hooks)
            chain.audit_request(self._consumer, request_builder)
            self.apply_hooks(execution_builder, chain)
        if self._session_chain:
            self.apply_hooks(execution_builder, self._session_chain)

        execution_builder.with_client(self._client)
        execution_builder.with_io(self._client.io())
        execution_builder.with_template(request_builder.request_template)

    def create_request_builder(self, definition):
        registry = definition.make_converter_registry(self._converters)
        req = helpers.RequestBuilder(self._client, registry, self._base_url)
        if self._session_chain:
            self._session_chain.audit_request(self._consumer, req)
        return req


class CallFactory(object):
    def __init__(
        self, request_preparer, request_definition, execution_builder_factory
    ):
        self._request_preparer = request_preparer
        self._request_definition = request_definition
        self._execution_builder_factory = execution_builder_factory

    def __call__(self, *args, **kwargs):
        request_builder = self._request_preparer.create_request_builder(
            self._request_definition
        )
        self._request_definition.define_request(request_builder, args, kwargs)
        execution_builder = self._execution_builder_factory()
        self._request_preparer.prepare_request(
            request_builder, execution_builder
        )
        execution = execution_builder.build()
        return execution.start(
            # TODO: Create request value object
            (request_builder.method, request_builder.url, request_builder.info)
        )


class Builder(interfaces.CallBuilder):
    """The default callable builder."""

    def __init__(self):
        self._base_url = ""
        self._hooks = []
        self._client = clients.get_client()
        self._converters = converters_.get_default_converter_factories()
        self._auth = auth_.get_auth()

    @property
    def client(self):
        return self._client

    @client.setter
    def client(self, client):
        if client is not None:
            self._client = clients.get_client(client)

    @property
    def hooks(self):
        return iter(self._hooks)

    def add_hook(self, *hooks):
        self._hooks.extend(hooks)

    @property
    def base_url(self):
        return self._base_url

    @base_url.setter
    def base_url(self, base_url):
        self._base_url = base_url

    @property
    def converters(self):
        return self._converters

    @converters.setter
    def converters(self, converters):
        if isinstance(converters, converters_.interfaces.Factory):
            converters = (converters,)
        self._converters = tuple(converters)
        self._converters += converters_.get_default_converter_factories()

    @property
    def auth(self):
        return self._auth

    @auth.setter
    def auth(self, auth):
        if auth is not None:
            self._auth = auth_.get_auth(auth)

    def build(self, definition, consumer=None):
        """
        Creates a callable that uses the provided definition to execute
        HTTP requests when invoked.
        """
        return CallFactory(
            RequestPreparer(self, consumer),
            definition,
            io.RequestExecutionBuilder,
        )


class ConsumerMethod(object):
    """
    A wrapper around a :py:class`interfaces.RequestDefinitionBuilder`
    instance bound to a :py:class:`Consumer` subclass, mainly responsible
    for controlling access to the instance.
    """

    def __init__(self, owner_name, attr_name, request_definition_builder):
        self._request_definition_builder = request_definition_builder
        self._owner_name = owner_name
        self._attr_name = attr_name
        self._request_definition = self._build_definition()

    def _build_definition(self):
        try:
            return self._request_definition_builder.build()
        except exceptions.InvalidRequestDefinition as error:
            # TODO: Find a Python 2.7 compatible way to reraise
            raise exceptions.UplinkBuilderError(
                self._owner_name, self._attr_name, error
            )

    def __get__(self, instance, owner):
        # TODO:
        #   Consider caching by instance/owner using WeakKeyDictionary.
        #   This will avoid the extra copy/create per attribute reference.
        #   However, we should do this after investigating for any latent cases
        #   of unnecessary overhead in the codebase as a whole.
        if instance is None:
            # This code path is traditionally called when applying a class
            # decorator to a Consumer. We should return a copy of the definition
            # builder to avoid class decorators on a subclass from polluting
            # other siblings (#152).
            value = self._request_definition_builder.copy()
        else:
            value = instance.session.create(instance, self._request_definition)

        # Make the return value look like the original method (e.g., inherit
        # docstrings and other function attributes).
        # TODO: Ideally, we should wrap once instead of on each reference.
        self._request_definition_builder.update_wrapper(value)
        return value


class ConsumerMeta(type):
    @staticmethod
    def _wrap_if_definition(cls_name, key, value):
        wrapped_value = value
        if isinstance(value, interfaces.RequestDefinitionBuilder):
            wrapped_value = ConsumerMethod(cls_name, key, value)
            value.update_wrapper(wrapped_value)
        return wrapped_value

    @staticmethod
    def _set_init_handler(namespace):
        try:
            init = namespace["__init__"]
        except KeyError:
            pass
        else:
            builder = arguments.ArgumentAnnotationHandlerBuilder.from_func(init)
            handler = builder.build()

            @functools.wraps(init)
            def new_init(self, *args, **kwargs):
                init(self, *args, **kwargs)
                call_args = utils.get_call_args(init, self, *args, **kwargs)
                f = functools.partial(
                    handler.handle_call_args, call_args=call_args
                )
                hook = hooks_.RequestAuditor(f)
                self.session.inject(hook)

            namespace["__init__"] = new_init

    def __new__(mcs, name, bases, namespace):
        mcs._set_init_handler(namespace)

        # Wrap all definition builders with a special descriptor that
        # handles attribute access behavior.
        for key, value in namespace.items():
            namespace[key] = mcs._wrap_if_definition(name, key, value)
        return super(ConsumerMeta, mcs).__new__(mcs, name, bases, namespace)

    def __setattr__(cls, key, value):
        value = cls._wrap_if_definition(cls.__name__, key, value)
        super(ConsumerMeta, cls).__setattr__(key, value)


_Consumer = ConsumerMeta("_Consumer", (), {})


[docs]class Consumer(interfaces.Consumer, _Consumer): """ Base consumer class with which to define custom consumers. Example usage: .. code-block:: python from uplink import Consumer, get class GitHub(Consumer): @get("/users/{user}") def get_user(self, user): pass client = GitHub("https://api.github.com/") client.get_user("prkumar").json() # {'login': 'prkumar', ... } Args: base_url (:obj:`str`, optional): The base URL for any request sent from this consumer instance. client (optional): A supported HTTP client instance (e.g., a :class:`requests.Session`) or an adapter (e.g., :class:`~uplink.RequestsClient`). converters (:class:`ConverterFactory`, optional): One or more objects that encapsulate custom (de)serialization strategies for request properties and/or the response body. (E.g., :class:`~uplink.converters.MarshmallowConverter`) auth (:obj:`tuple` or :obj:`callable`, optional): The authentication object for this consumer instance. hooks (:class:`~uplink.hooks.TransactionHook`, optional): One or more hooks to modify behavior of request execution and response handling (see :class:`~uplink.response_handler` or :class:`~uplink.error_handler`). """ def __init__( self, base_url="", client=None, converters=(), auth=None, hooks=(), **kwargs ): builder = Builder() builder.base_url = base_url builder.converters = kwargs.pop("converter", converters) hooks = kwargs.pop("hook", hooks) if isinstance(hooks, hooks_.TransactionHook): hooks = (hooks,) builder.add_hook(*hooks) builder.auth = auth builder.client = client self.__session = session.Session(builder) self.__client = builder.client def _inject(self, hook, *more_hooks): self.session.inject(hook, *more_hooks) @property def session(self): """ The :class:`~uplink.session.Session` object for this consumer instance. Exposes the configuration of this :class:`~uplink.Consumer` instance and allows the persistence of certain properties across all requests sent from that instance. Example usage: .. code-block:: python import uplink class MyConsumer(uplink.Consumer): def __init__(self, language): # Set this header for all requests of the instance. self.session.headers["Accept-Language"] = language ... Returns: :class:`~uplink.session.Session` """ return self.__session @property def exceptions(self): """ An enum of standard HTTP client exceptions that can be handled. This property enables the handling of specific exceptions from the backing HTTP client. Example: .. code-block:: python try: github.get_user(user_id) except github.exceptions.ServerTimeout: # Handle the timeout of the request ... """ return self.__client.exceptions
def build(service_cls, *args, **kwargs): name = service_cls.__name__ warnings.warn( "`uplink.build` is deprecated and will be removed in v1.0.0. " "To construct a consumer instance, have `{0}` inherit " "`uplink.Consumer` then instantiate (e.g., `{0}(...)`). ".format(name), DeprecationWarning, ) consumer = type(name, (service_cls, Consumer), dict(service_cls.__dict__)) return consumer(*args, **kwargs)