| # Licensed to the Apache Software Foundation (ASF) under one or more |
| # contributor license agreements. See the NOTICE file distributed with |
| # this work for additional information regarding copyright ownership. |
| # The ASF licenses this file to You under the Apache License, Version 2.0 |
| # (the "License"); you may not use this file except in compliance with |
| # the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| """ |
| Utilities for instrumenting collections of models in storage. |
| """ |
| |
| from . import exceptions |
| |
| |
| class _InstrumentedCollection(object): |
| |
| def __init__(self, |
| mapi, |
| parent, |
| field_name, |
| field_cls, |
| seq=None, |
| is_top_level=True, |
| **kwargs): |
| self._mapi = mapi |
| self._parent = parent |
| self._field_name = field_name |
| self._is_top_level = is_top_level |
| self._field_cls = field_cls |
| self._load(seq, **kwargs) |
| |
| @property |
| def _raw(self): |
| raise NotImplementedError |
| |
| def _load(self, seq, **kwargs): |
| """ |
| Instantiates the object from existing seq. |
| |
| :param seq: the original sequence to load from |
| """ |
| raise NotImplementedError |
| |
| def _set(self, key, value): |
| """ |
| Sets the changes for the current object (not in the database). |
| |
| :param key: |
| :param value: |
| """ |
| raise NotImplementedError |
| |
| def _del(self, collection, key): |
| raise NotImplementedError |
| |
| def _instrument(self, key, value): |
| """ |
| Instruments any collection to track changes (and ease of access). |
| |
| :param key: |
| :param value: |
| """ |
| if isinstance(value, _InstrumentedCollection): |
| return value |
| elif isinstance(value, dict): |
| instrumentation_cls = _InstrumentedDict |
| elif isinstance(value, list): |
| instrumentation_cls = _InstrumentedList |
| else: |
| return value |
| |
| return instrumentation_cls(self._mapi, self, key, self._field_cls, value, False) |
| |
| def _raw_value(self, value): |
| """ |
| Gets the raw value. |
| |
| :param value: |
| """ |
| if isinstance(value, self._field_cls): |
| return value.value |
| return value |
| |
| def _encapsulate_value(self, key, value): |
| """ |
| Creates a new item class if needed. |
| |
| :param key: |
| :param value: |
| """ |
| if isinstance(value, self._field_cls): |
| return value |
| # If it is not wrapped |
| return self._field_cls.wrap(key, value) |
| |
| def __setitem__(self, key, value): |
| """ |
| Updates the values in both the local and the database locations. |
| |
| :param key: |
| :param value: |
| """ |
| self._set(key, value) |
| if self._is_top_level: |
| # We are at the top level |
| field = getattr(self._parent, self._field_name) |
| self._set_field( |
| field, key, value if key in field else self._encapsulate_value(key, value)) |
| self._mapi.update(self._parent) |
| else: |
| # We are not at the top level |
| self._set_field(self._parent, self._field_name, self) |
| |
| def _set_field(self, collection, key, value): |
| """ |
| Enables updating the current change in the ancestors. |
| |
| :param collection: collection to change |
| :param key: key for the specific field |
| :param value: new value |
| """ |
| if isinstance(value, _InstrumentedCollection): |
| value = value._raw |
| if key in collection and isinstance(collection[key], self._field_cls): |
| if isinstance(collection[key], _InstrumentedCollection): |
| self._del(collection, key) |
| collection[key].value = value |
| else: |
| collection[key] = value |
| return collection[key] |
| |
| def __deepcopy__(self, *args, **kwargs): |
| return self._raw |
| |
| |
| class _InstrumentedDict(_InstrumentedCollection, dict): |
| |
| def _load(self, dict_=None, **kwargs): |
| dict.__init__( |
| self, |
| tuple((key, self._raw_value(value)) for key, value in (dict_ or {}).iteritems()), |
| **kwargs) |
| |
| def update(self, dict_=None, **kwargs): |
| dict_ = dict_ or {} |
| for key, value in dict_.iteritems(): |
| self[key] = value |
| for key, value in kwargs.iteritems(): |
| self[key] = value |
| |
| def __getitem__(self, key): |
| return self._instrument(key, dict.__getitem__(self, key)) |
| |
| def _set(self, key, value): |
| dict.__setitem__(self, key, self._raw_value(value)) |
| |
| @property |
| def _raw(self): |
| return dict(self) |
| |
| def _del(self, collection, key): |
| del collection[key] |
| |
| |
| class _InstrumentedList(_InstrumentedCollection, list): |
| |
| def _load(self, list_=None, **kwargs): |
| list.__init__(self, list(item for item in list_ or [])) |
| |
| def append(self, value): |
| self.insert(len(self), value) |
| |
| def insert(self, index, value): |
| list.insert(self, index, self._raw_value(value)) |
| if self._is_top_level: |
| field = getattr(self._parent, self._field_name) |
| field.insert(index, self._encapsulate_value(index, value)) |
| else: |
| self._parent[self._field_name] = self |
| |
| def __getitem__(self, key): |
| return self._instrument(key, list.__getitem__(self, key)) |
| |
| def _set(self, key, value): |
| list.__setitem__(self, key, value) |
| |
| def _del(self, collection, key): |
| del collection[key] |
| |
| @property |
| def _raw(self): |
| return list(self) |
| |
| |
| class _WrappedBase(object): |
| |
| def __init__(self, wrapped, instrumentation, instrumentation_kwargs=None): |
| """ |
| :param wrapped: model to be instrumented |
| :param instrumentation: instrumentation dict |
| :param instrumentation_kwargs: arguments for instrumentation class |
| """ |
| self._wrapped = wrapped |
| self._instrumentation = instrumentation |
| self._instrumentation_kwargs = instrumentation_kwargs or {} |
| |
| def _wrap(self, value): |
| if value.__class__ in set(class_.class_ for class_ in self._instrumentation): |
| return _create_instrumented_model( |
| value, instrumentation=self._instrumentation, **self._instrumentation_kwargs) |
| # Check that the value is a SQLAlchemy model (it should have metadata) or a collection |
| elif hasattr(value, 'metadata') or isinstance(value, (dict, list)): |
| return _create_wrapped_model( |
| value, instrumentation=self._instrumentation, **self._instrumentation_kwargs) |
| return value |
| |
| def __getattr__(self, item): |
| if hasattr(self, '_wrapped'): |
| return self._wrap(getattr(self._wrapped, item)) |
| else: |
| super(_WrappedBase, self).__getattribute__(item) |
| |
| |
| class _InstrumentedModel(_WrappedBase): |
| |
| def __init__(self, mapi, *args, **kwargs): |
| """ |
| The original model. |
| |
| :param mapi: MAPI for the wrapped model |
| :param wrapped: model to be instrumented |
| :param instrumentation: instrumentation dict |
| :param instrumentation_kwargs: arguments for instrumentation class |
| """ |
| super(_InstrumentedModel, self).__init__(instrumentation_kwargs=dict(mapi=mapi), |
| *args, **kwargs) |
| self._mapi = mapi |
| self._apply_instrumentation() |
| |
| def _apply_instrumentation(self): |
| for field in self._instrumentation: |
| if not issubclass(type(self._wrapped), field.parent.class_): |
| # Do not apply if this field is not for our class |
| continue |
| |
| field_name = field.key |
| field_cls = field.mapper.class_ |
| |
| field = getattr(self._wrapped, field_name) |
| |
| # Preserve the original field, e.g. original "attributes" would be located under |
| # "_attributes" |
| setattr(self, '_{0}'.format(field_name), field) |
| |
| # Set instrumented value |
| if isinstance(field, dict): |
| instrumentation_cls = _InstrumentedDict |
| elif isinstance(field, list): |
| instrumentation_cls = _InstrumentedList |
| else: |
| # TODO: raise proper error |
| raise exceptions.StorageError( |
| "ARIA supports instrumentation for dict and list. Field {field} of the " |
| "class `{model}` is of type `{type}`.".format( |
| field=field, |
| model=self._wrapped, |
| type=type(field))) |
| |
| instrumented_class = instrumentation_cls(seq=field, |
| parent=self._wrapped, |
| mapi=self._mapi, |
| field_name=field_name, |
| field_cls=field_cls) |
| setattr(self, field_name, instrumented_class) |
| |
| |
| class _WrappedModel(_WrappedBase): |
| |
| def __getitem__(self, item): |
| return self._wrap(self._wrapped[item]) |
| |
| def __iter__(self): |
| for item in self._wrapped.__iter__(): |
| yield self._wrap(item) |
| |
| |
| def _create_instrumented_model(original_model, mapi, instrumentation): |
| return type('Instrumented{0}'.format(original_model.__class__.__name__), |
| (_InstrumentedModel,), |
| {})(wrapped=original_model, instrumentation=instrumentation, mapi=mapi) |
| |
| |
| def _create_wrapped_model(original_model, mapi, instrumentation): |
| return type('Wrapped{0}'.format(original_model.__class__.__name__), |
| (_WrappedModel, ), |
| {})(wrapped=original_model, |
| instrumentation=instrumentation, |
| instrumentation_kwargs=dict(mapi=mapi)) |
| |
| |
| def instrument(instrumentation, original_model, mapi): |
| for instrumented_field in instrumentation: |
| if isinstance(original_model, instrumented_field.class_): |
| return _create_instrumented_model(original_model, mapi, instrumentation) |
| |
| return _create_wrapped_model(original_model, mapi, instrumentation) |