Source code for save_to_db.core.utils.mapper
from collections import defaultdict
from weakref import WeakSet
[docs]class Mapper(object):
""" This class automatically sets up reverse relations for all items. """
def __init__(self):
# [collection_id][item_cls] = list_of_items
self._map = defaultdict(lambda: defaultdict(WeakSet))
def __contains__(self, item):
return item in self.__get_item_set(item)
def __get_item_set(self, item):
collection_id = item.get_collection_id()
item_cls = item.get_item_cls()
return self._map[collection_id][item_cls]
def __get_item_collection_set(self, item):
collection_id = item.get_collection_id()
return self._map[collection_id]
[docs] def item_created(self, item):
"""Called when an instance of
:py:class:`~save_to_db.core.item_base.ItemBase` is created.
:param item: An instance of created item.
"""
self.__get_item_set(item).add(item)
[docs] def item_value_set(self, item, key, old_value, new_value):
"""Called when an item attribute is set, for example:
.. code-block:: Python
item[key] = new_value
:param item: An item whose attribute is set.
:param key: Attribute name of the `item`.
:param old_value: Old attribute value.
:param new_value: New attribute value.
"""
if item.is_bulk_item():
return # default value was set
if key not in item.relations:
return
# --- forward relation ---
relation = item.relations[key]
if new_value is not None and relation["relation_type"].is_one_to_x():
for that_key, that_item in self.iter_pointing_items(
new_value, type(item), key
):
if that_item is item:
continue
del that_item.data[that_key]
# --- reverse relation ---
reverse_key = relation["reverse_key"]
this_item, that_item_new = item, new_value
that_item_old = old_value
if not reverse_key:
return
if relation["relation_type"].is_one_to_one():
# reverse for old
if that_item_old and reverse_key in that_item_old.data:
del that_item_old.data[reverse_key]
# reverse for new
if that_item_new is None:
return
that_item_data = that_item_new.data
if reverse_key in that_item_data and that_item_data is this_item.data:
return
that_item_new.data[reverse_key] = this_item
elif relation["relation_type"].is_one_to_many():
# reverse for old
if that_item_old:
for item_in_bulk in that_item_old.bulk:
del item_in_bulk.data[reverse_key]
# reverse for new
if that_item_new:
for item_in_bulk in that_item_new.bulk:
item_in_bulk.data[reverse_key] = this_item
elif relation["relation_type"].is_many_to_one():
# reverse for old item
if that_item_old:
if item in that_item_old[reverse_key].bulk:
that_item_old[reverse_key].bulk.remove(item)
# reverse for new item
if that_item_new:
if item not in that_item_new[reverse_key].bulk:
that_item_new[reverse_key].bulk.append(item)
elif relation["relation_type"].is_many_to_many():
# reverse for old item
if that_item_old:
for item_in_bulk in that_item_old.bulk:
if item in item_in_bulk[reverse_key].bulk:
item_in_bulk[reverse_key].bulk.remove(item)
# reverse for new item
if that_item_new:
for item_in_bulk in that_item_new.bulk:
if item not in item_in_bulk[reverse_key].bulk:
item_in_bulk[reverse_key].bulk.append(item)
[docs] def item_value_deleted(self, item, key, old_value):
"""Called when an item attribute is deleted, for example:
.. code-block:: Python
del item[key]
:param item: An item whose attribute is deleted.
:param key: Deleted attribute name.
:param old_value: Deleted attribute value.
"""
if item.is_bulk_item():
return # default value deleted
if key not in item.relations or old_value is None:
return
relation = item.relations[key]
if not relation["reverse_key"]:
return
if relation["relation_type"].is_one_to_one():
if relation["reverse_key"] in old_value:
del old_value.data[relation["reverse_key"]]
elif relation["relation_type"].is_one_to_many():
for item_in_bulk in old_value.bulk:
del item_in_bulk.data[relation["reverse_key"]]
elif relation["relation_type"].is_many_to_one():
old_value[relation["reverse_key"]].bulk.remove(item)
elif relation["relation_type"].is_many_to_many():
for item_in_bulk in old_value:
item_in_bulk[relation["reverse_key"]].bulk.remove(item)
[docs] def iter_pointing_items(self, item, pointing_cls=None, pointing_key=None):
"""A generator that yields items that point to a curtain item.
to automatically adjust relationships.
:param item: an instance of :py:class:`~save_to_db.core.item.Item` class
to which other items must point.
:param pointing_cls: Subclass of :py:class:`~save_to_db.core.item.Item`. If
it is not `None` then only instances of this class are yielded.
:param pointing_key: If it is not `None` then only items that point using the
same key are yielded.
:returns: A generator of tupltes with two items, pointing key and pointing item.
"""
collection_set = self.__get_item_collection_set(item)
if pointing_cls:
pointing_cls_list = [pointing_cls]
else:
pointing_cls_list = set(collection_set)
# pointing single items
for that_item_cls in pointing_cls_list:
for that_item in collection_set[that_item_cls]:
if pointing_key:
if pointing_key not in that_item.data:
continue
pointing_key_list = [pointing_key]
else:
pointing_key_list = set(that_item.data)
for key in pointing_key_list:
if that_item.data[key] is item:
yield key, that_item
[docs] def item_deleted(self, item):
"""Called when an item is deleted, for example:
.. code-block:: Python
item.delete()
:param item: Deleted item.
"""
# clearing the item
# (defaults in bulk items and directs in single items)
item.data.clear()
for key, that_item in self.iter_pointing_items(item):
relation_type = that_item.relations[key]["relation_type"]
reverse_key = that_item.relations[key]["reverse_key"]
if reverse_key:
if item.is_bulk_item() and relation_type.is_one_to_many():
for item_in_bulk in item.bulk:
del item_in_bulk.data[reverse_key]
elif (
item.is_bulk_item()
and item.bulk
and relation_type.is_many_to_many()
):
for this_item_in_bulk in item.bulk:
that_bulk = this_item_in_bulk.data[reverse_key]
for that_item_in_bulk in that_bulk.bulk:
if that_item_in_bulk.data[key] is item:
that_bulk.bulk.remove(that_item_in_bulk)
# defaults in bulk items and directs in single items
del that_item.data[key]
# removing from any bulks
if item.is_single_item():
collection_set = self.__get_item_collection_set(item)
for that_item_cls in collection_set:
for that_item in collection_set[that_item_cls]:
if that_item.is_bulk_item():
if item in that_item.bulk:
that_item.bulk.remove(item)
else:
item.bulk.clear()
[docs] def item_added_to_bulk(self, item, bulk_item):
"""Called when an item is added to a bulk item, for example:
.. code-block:: Python
bulk_item.add(single_item)
:param item: A single item added to the `bulk_item`.
:param bulk_item: Bulk item containing `item`.
"""
collection_set = self.__get_item_collection_set(item)
for that_item_cls in set(collection_set):
for that_item in set(collection_set[that_item_cls]):
if not that_item.is_single_item():
continue
for key in set(that_item.data):
if that_item.data[key] is not bulk_item:
continue
relation = that_item.relations[key]
reverse_key = that_item.relations[key]["reverse_key"]
if not reverse_key:
continue
if relation["relation_type"].is_one_to_many():
item.data[reverse_key] = that_item
elif relation["relation_type"].is_many_to_many():
if that_item not in item[reverse_key].bulk:
item[reverse_key].bulk.append(that_item)
[docs] def item_removed_from_bulk(self, item, bulk_item):
"""Called when an item is removed from a bulk item, for example:
.. code-block:: Python
bulk_item.remove(single_item)
:param item: A single item removed from the `bulk_item`.
:param bulk_item: Bulk item that was containing `item`.
"""
collection_set = self.__get_item_collection_set(item)
for that_item_cls in collection_set:
for that_item in collection_set[that_item_cls]:
if not that_item.is_single_item():
continue
for key in set(that_item.data):
# `key not in that_item.data` can happen in case of self
# relationships
if (
key not in that_item.data
or that_item.data[key] is not bulk_item
):
continue
relation = that_item.relations[key]
reverse_key = that_item.relations[key]["reverse_key"]
if reverse_key:
if relation["relation_type"].is_one_to_many():
del item.data[reverse_key]
elif relation["relation_type"].is_many_to_many():
if that_item in item.data[reverse_key].bulk:
item.data[reverse_key].bulk.remove(that_item)
mapper = Mapper()