ferrero-opentext/Python-Version/venv/lib/python3.12/site-packages/boxsdk/util/translator.py

172 lines
7.6 KiB
Python

from collections import ChainMap
import inspect
__all__ = list(map(str, ['Translator']))
from typing import Any, TYPE_CHECKING, Dict
if TYPE_CHECKING:
from boxsdk.object.base_api_json_object import BaseAPIJSONObjectMeta
from boxsdk.session.session import Session
def _get_object_id(obj: dict) -> Any:
"""
Gets the ID for an API object.
:param obj:
The API object
:return:
"""
return obj.get('id', None)
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: Dict[str, 'BaseAPIJSONObjectMeta'], **kwargs: Any):
"""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.
: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().__init__(*translation_maps, **kwargs)
def register(self, type_name: str, box_cls: 'BaseAPIJSONObjectMeta') -> Any:
"""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.
:param box_cls:
The Box object class, which will be associated with the type name provided.
"""
self[type_name] = box_cls
def get(self, key: str, default: 'BaseAPIJSONObjectMeta' = None) -> 'BaseAPIJSONObjectMeta':
"""Get the box object class associated with the given type name.
:param key:
The type name to be translated.
:param default:
(optional) The default Box object class to return.
Defaults to `BaseObject`.
"""
# pylint:disable=import-outside-toplevel
from boxsdk.object.base_object import BaseObject
if default is None:
default = BaseObject
return super().get(key, default)
def translate(self, session: 'Session', response_object: dict) -> Any:
"""
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)
:param response_object:
The JSON response object from the API, which will be translated
: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 skip fields that they do not want translated
fields_to_skip = object_class.untranslated_fields() if object_class is not None else ()
for key in response_object:
if key in fields_to_skip:
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