Source code for binx.adapter

""" The adapter module helps the user mutate one collection into another. This works similar to
a calc class in calc_factory.
AbstractAdapter's call takes a collection's data attribute as the first argument. An adapt() method must be
overridden by the user which must return an instance of AdapterOutputContainer. This is enforced by returning the
helper method

Once the class is declared the user register's the adapter using the register static method
"""
import abc
from .registry import register_adapter_to_collection, register_adaptable_collection
from .exceptions import AdapterFunctionError

import copy
import inspect

[docs]def check_adapter_call(method): """ a helper decorater for the __call__ method that does some type checking """ def inner(*args, **kwargs): self = args[0] coll = args[1] if not isinstance(coll, self.from_collection_class): raise TypeError('Cannot adapt from type {}'.format(coll)) result = method(*args, **kwargs) if not isinstance(result, AdapterOutputContainer): raise TypeError('Adapters must return an instance of AdapterOutputContainer') return result return inner
[docs]class AdapterOutputContainer(object): """ A generic container class for moving data out of adapters. It holds the target output collection along with any context data that might need to be passed on to the caller or another adapter. Essentially 'side effects' from the adaptation that might be needed further along in the adapter chain. NOTE that the context in a container instance only relates to its immediate adapter call. It does contain any of the surrounding context. This gets accumulated in BaseCollection._resolve_adapter_chain This is used internally in Adapter.__call__ """ def __init__(self, collection, **context): self.collection = collection self._context = {} for k,v in context.items(): setattr(self, k, v) # set on class and load into a context dict for easy access self._context[k] = v # load in context @property def context(self): return self._context
[docs]class AbstractAdapter(abc.ABC): """ Concrete Adapters subclass this class and override its adapt method with an implementation. Other methods may be added as helper classes """ target_collection_class = None from_collection_class = None is_registered = False #NOTE set to True in the adapter registery
[docs] def render_return(self, data, **context): """ A helper method renders the data response to the target_collection_class and passes context data. Must return the data in an instance of AdapterOutputContainer """ coll = self.target_collection_class() coll.load_data(data) return AdapterOutputContainer(coll, **context)
[docs] @abc.abstractmethod def adapt(self, collection, **context): """ The user must overrides this method to do the data cleaning. Any additional methods needed for clean should be considered private to the Adapter subclass. Must call render_return """
# get the data from the input_collection (collection.data or collection.to_dataframe() or whatever...) # get the context args you need... context.pop('some-key') or context['some-key'] #return self.render_return(data, **context) #NOTE this is an example of how to return from adapt @check_adapter_call def __call__(self, collection, **context): """ Wraps the adapt call and does some type checking. Makes sure that input collection if the from_class and the output collection and context are delivered in an AdapterOutputContainer. An adapter should be used as """ return self.adapt(collection, **context)
[docs]class PluggableAdapter(AbstractAdapter): """ creates a pluggable interface for Adapters. A user should subclass this class and provide a calc object that """ calc = None def __init__(self): self._check_calc(self.__class__.calc) # perform this check once on init def _check_calc(self, calc): """ some checking logic that happens on init""" assert calc is not None, 'A callable must be set on the calc field' if not callable(calc): raise TypeError('calc field must be callable') sig = inspect.signature(calc).parameters assert sorted(list(sig.keys())) == ['collection', 'context'], 'Incorrect signature provided to Adapter.calc. Must have signature "collection", "**context"' assert sig['context'].kind == inspect.Parameter.VAR_KEYWORD, '"context" must be kwargs variable (**context)' return True
[docs] def adapt(self, collection, **context): """Calls the adaptation function set on the calc field. """ try: data, context = self.__class__.calc(collection, **context) except ValueError: # we enforce returning a two-tuple raise AdapterFunctionError('Return type of PluggableAdapter.calc must be a 2-tuple of data and context dict') if not isinstance(context, dict): raise AdapterFunctionError('The second return value of PluggableAdapter.calc must be a dictionary') return self.render_return(data, **context)
[docs]def register_adapter(adapter_class): """ Registers the adapter class in the graph chain by setting its to and from classes """ # adapter_class.target_collection_class = target_class # assign the class a from and target # adapter_class.from_collection_class = from_class target_lookup_path = adapter_class.target_collection_class.get_fully_qualified_class_path() # get some path names from_lookup_path = adapter_class.from_collection_class.get_fully_qualified_class_path() # register adapter to the 'from' classes 'adapters' list and link the two classes on the 'target' # in its 'adaptable_from' list register_adapter_to_collection(from_lookup_path, adapter_class) register_adaptable_collection(target_lookup_path, adapter_class.from_collection_class) adapter_class.is_registered = True