# Standard library imports import functools import warnings # Local imports from uplink import ( arguments, clients, compat, exceptions, helpers, interfaces, session, utils, ) from uplink import ( auth as auth_, ) from uplink import ( converters as converters_, ) from uplink import ( hooks as hooks_, ) from uplink.clients import io __all__ = ["Consumer", "build"] class RequestPreparer: 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: 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: """ A wrapper around a [`interfaces.RequestDefinitionBuilder`][uplink.interfaces.RequestDefinitionBuilder] instance bound to a [`Consumer`][uplink.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: raise exceptions.UplinkBuilderError( self._owner_name, self._attr_name, error ) from 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().__new__(mcs, name, bases, namespace) def __setattr__(cls, key, value): value = cls._wrap_if_definition(cls.__name__, key, value) super().__setattr__(key, value) _Consumer = ConsumerMeta("_Consumer", (), {}) class Consumer(interfaces.Consumer, _Consumer): """ Base consumer class with which to define custom consumers. Example usage: ```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 (str, optional): The base URL for any request sent from this consumer instance. client (optional): A supported HTTP client instance (e.g., a `requests.Session`) or an adapter (e.g., `uplink.RequestsClient`). converters (ConverterFactory, optional): One or more objects that encapsulate custom (de)serialization strategies for request properties and/or the response body. (E.g., `uplink.converters.MarshmallowConverter`) auth (tuple or callable, optional): The authentication object for this consumer instance. hooks (uplink.hooks.TransactionHook, optional): One or more hooks to modify behavior of request execution and response handling (see `uplink.response_handler` or `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 [Session][uplink.session.Session] object for this consumer instance. Exposes the configuration of this [Consumer][uplink.Consumer] instance and allows the persistence of certain properties across all requests sent from that instance. Example usage: ```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: [Session][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: ```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. " f"To construct a consumer instance, have `{name}` inherit " f"`uplink.Consumer` then instantiate (e.g., `{name}(...)`). ", DeprecationWarning, stacklevel=2, ) consumer = type(name, (service_cls, Consumer), dict(service_cls.__dict__)) return consumer(*args, **kwargs)