Source code for boxsdk.util.translator

# coding: utf-8

from __future__ import absolute_import, unicode_literals

import inspect

from .chain_map import ChainMap


__all__ = list(map(str, ['Translator']))

# pylint: disable=invalid-name
inspect_signature = None
try:
    inspect_signature = inspect.signature
except AttributeError:  # pragma: no cover
    import funcsigs
    inspect_signature = funcsigs.signature


def _get_object_id(obj):
    """
    Gets the ID for an API object.

    :param obj:
        The API object
    :type obj:
        `dict`
    :return:
    """
    return obj.get('id', None)


[docs]class Translator(ChainMap): """ Translate item responses from the Box API to Box objects. Also acts as a :class:`Mapping` from type names to Box object classes. There exists a global default `Translator`, containing the default API object classes defined by the SDK. Custom `Translator` instances can be created to extend the default `Translator` with custom subclasses. A `Translator` is a :class:`ChainMap`, so that one translator can "extend" others. The most common scenario would be a custom, non-global `Translator` that extends only the default translator, to register 0 or more new classes. But more complex inheritance is also allowed, in case that is useful. """ __slots__ = () # :attr _default_translator: # A global `Translator` containing the default API object classes # defined by the SDK. By default, new `Translator` instances will # "extend" this one, so that the global registrations are reflected # automatically. # # NOTE: For convenience and backwards-compatability, developers are # allowed to register their own custom subclasses with # `_default_translator`, but are encouraged not to. The default # translator may change or be removed in any major or minor release. # Additionally, it has the usual hazards of mutable global state. # The supported and recommended ways for registering custom subclasses # are: # # - Constructing a new `Translator`, calling `Translator.register()` as # necessary, and passing it to the `BoxSession` constructor. # - Calling `session.translator.register()` on an existing # `BoxSession`. # - Calling `client.translator.register()` on an existing `Client`. # :type _default_translator: :class:`Translator` _default_translator = {} # Will be set to a `Translator` instance below, after the class is defined. def __init__(self, *translation_maps, **kwargs): """Baseclass override. :param translation_maps: (variadic) The same as the `maps` variadic parameter to :class:`ChainMap`, except restricted to maps from type names to Box object classes. :type translation_maps: `tuple` of (:class:`Mapping` of `unicode` to :class:`BaseAPIJSONObjectMeta`) :param extend_default_translator: (optional, keyword-only) If `True` (the default), `_default_translator` is appended to the end of `translation_maps`. When this functionality is used, the new `Translator` will inherit all of the global registrations. :type extend_default_translator: `bool` :param new_child: (optional, keyword-only) If `True` (the default), a new empty `dict` is prepended to the front of `translation_maps`. Either way, the resulting `Translator` starts out with the same key-value pairs. But when this is `False`, the first item in `translation_maps` will be mutated by `__setitem__()` and `__delitem()__` calls, which will affect other references to it. Whereas when this is `True`, all items in `translation_maps` are safe from mutation in normal usage scenarios. :type new_child: `bool` """ translation_maps = list(translation_maps) extend_default_translator = kwargs.pop('extend_default_translator', True) new_child = kwargs.pop('new_child', True) if extend_default_translator: translation_maps.append(self._default_translator) if new_child: translation_maps.insert(0, {}) super(Translator, self).__init__(*translation_maps, **kwargs)
[docs] def register(self, type_name, box_cls): """Associate a Box object class to handle Box API item responses with the given type name. :param type_name: The type name to be registered. :type type_name: `unicode` :param box_cls: The Box object class, which will be associated with the type name provided. :type box_cls: :class:`BaseAPIJSONObjectMeta` """ self[type_name] = box_cls
[docs] def get(self, key, default=None): """Get the box object class associated with the given type name. :param key: The type name to be translated. :type key: `unicode` :param default: (optional) The default Box object class to return. Defaults to `BaseObject`. :type default: :class:`BaseAPIJSONObjectMeta` :rtype: :class:`BaseAPIJSONObjectMeta` """ from boxsdk.object.base_object import BaseObject if default is None: default = BaseObject return super(Translator, self).get(key, default)
[docs] def translate(self, session, response_object): """ Translate a given API response object into SDK classes, rescursively translating any subobjects. :param session: The SDK session to use for any objects that require a session (i.e. classes that make API calls) :type session: class:`Session` :param response_object: The JSON response object from the API, which will be translated :type response_object: `dict` :return: The translated object """ if not isinstance(response_object, dict): return response_object translated_obj = {} object_type = response_object.get('type', None) object_class = self.get(object_type) if object_type is not None else None # Parent classes have the ability to "blacklist" fields that they do not want translated blacklisted_fields = object_class.untranslated_fields() if object_class is not None else () for key in response_object: if key in blacklisted_fields: translated_obj[key] = response_object[key] continue if isinstance(response_object[key], dict): translated_obj[key] = self.translate(session, response_object[key]) elif isinstance(response_object[key], list): translated_obj[key] = [self.translate(session, o) for o in response_object[key]] else: translated_obj[key] = response_object[key] # Try to translate any API object with a `type` property, except for metadata instances # The $type value in metadata instances isn't directly usable, so we avoid it altogether # NOTE: Currently, we represent metadata as just a `dict`, so there's no need to translate it anyway # Metadata field objects are another issue; they contain a 'type' property that doesn't really # map to a Box object. We probably want to treat these as just `dict`s, so they're excluded here if object_class is not None and '$type' not in translated_obj: param_values = { 'session': session, 'response_object': translated_obj, 'object_id': _get_object_id(translated_obj), } params = inspect_signature(object_class.__init__).parameters param_values = {p: param_values[p] for p in params if p in param_values} return object_class(**param_values) return translated_obj
Translator._default_translator = Translator(extend_default_translator=False) # pylint:disable=protected-access