Source code for spinetoolbox.fetch_parent

######################################################################################################################
# Copyright (C) 2017-2022 Spine project consortium
# Copyright Spine Toolbox contributors
# This file is part of Spine Toolbox.
# Spine Toolbox is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option)
# any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General
# Public License for more details. You should have received a copy of the GNU Lesser General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
######################################################################################################################

"""The FetchParent and FlexibleFetchParent classes."""
from PySide6.QtCore import QTimer, Signal, QObject, Qt
from .helpers import busy_effect


[docs]class FetchParent(QObject):
[docs] _changes_pending = Signal()
def __init__(self, index=None, owner=None, chunk_size=1000): """ Args: index (FetchIndex, optional): an index to speedup looking up fetched items owner (object, optional): somebody who owns this FetchParent. If it's a QObject instance, then this FetchParent becomes obsolete whenever the owner is destroyed chunk_size (int, optional): the number of items this parent should be happy with fetching at a time. If None, then no limit is imposed and the parent should fetch the entire contents of the DB. """ super().__init__() self._version = 0 self._restore_item_callbacks = {} self._update_item_callbacks = {} self._remove_item_callbacks = {} self._changes_by_db_map = {} self._obsolete = False self._fetched = False self._busy = False self._position = {} self._timer = QTimer() self._timer.setSingleShot(True) self._timer.setInterval(0) self._timer.timeout.connect(self._apply_pending_changes) self._changes_pending.connect(self._timer.start) self._index = index self._owner = owner if isinstance(self._owner, QObject): self._owner.destroyed.connect(lambda obj=None: self.set_obsolete(True)) self.setParent(self._owner) self.chunk_size = chunk_size
[docs] def apply_changes_immediately(self): # For tests self._changes_pending.connect(self._apply_pending_changes, Qt.UniqueConnection)
@property
[docs] def index(self): return self._index
[docs] def reset(self): """Resets fetch parent as if nothing was ever fetched.""" if self.is_obsolete: return self._version += 1 self._restore_item_callbacks.clear() self._update_item_callbacks.clear() self._remove_item_callbacks.clear() self._timer.stop() self._changes_by_db_map.clear() self._fetched = False self._busy = False self._position.clear() if self.index is not None: self.index.reset()
[docs] def position(self, db_map): return self._position.setdefault(db_map, 0)
[docs] def increment_position(self, db_map): self._position[db_map] += 1
@busy_effect
[docs] def _apply_pending_changes(self): if self.is_obsolete: return for db_map in list(self._changes_by_db_map): changes = self._changes_by_db_map.pop(db_map) last_handler = None items = [] for handler, item in changes: if handler == last_handler: items.append(item) continue if items: last_handler({db_map: items}) # pylint: disable=not-callable items = [item] last_handler = handler last_handler({db_map: items}) QTimer.singleShot(0, lambda: self.set_busy(False))
[docs] def bind_item(self, item, db_map): # NOTE: If `item` is in the process of calling callbacks in another thread, # the ones added below won't be called. # So, it is important to call this function before self.add_item() item.add_restore_callback(self._make_restore_item_callback(db_map)) item.add_update_callback(self._make_update_item_callback(db_map)) item.add_remove_callback(self._make_remove_item_callback(db_map))
[docs] def _make_restore_item_callback(self, db_map): if db_map not in self._restore_item_callbacks: self._restore_item_callbacks[db_map] = _ItemCallback(self.add_item, db_map, self._version) return self._restore_item_callbacks[db_map]
[docs] def _make_update_item_callback(self, db_map): if db_map not in self._update_item_callbacks: self._update_item_callbacks[db_map] = _ItemCallback(self.update_item, db_map, self._version) return self._update_item_callbacks[db_map]
[docs] def _make_remove_item_callback(self, db_map): if db_map not in self._remove_item_callbacks: self._remove_item_callbacks[db_map] = _ItemCallback(self.remove_item, db_map, self._version) return self._remove_item_callbacks[db_map]
[docs] def _is_valid(self, version): return (version is None or version == self._version) and not self.is_obsolete
[docs] def _change_item(self, handler, item, db_map, version): if not self._is_valid(version): return False self._changes_by_db_map.setdefault(db_map, []).append((handler, item)) self._changes_pending.emit() return True
[docs] def add_item(self, item, db_map, version=None): return self._change_item(self.handle_items_added, item, db_map, version)
[docs] def update_item(self, item, db_map, version=None): return self._change_item(self.handle_items_updated, item, db_map, version)
[docs] def remove_item(self, item, db_map, version=None): return self._change_item(self.handle_items_removed, item, db_map, version)
@property
[docs] def fetch_item_type(self): """Returns the DB item type to fetch, e.g., "entity_class". Returns: str """ raise NotImplementedError()
[docs] def key_for_index(self, db_map): """Returns the key for this parent in the index. Args: db_map (DatabaseMapping) Returns: Any """ return None
# pylint: disable=no-self-use
[docs] def accepts_item(self, item, db_map): """Called by the associated SpineDBWorker whenever items are fetched and also added/updated/removed. Returns whether this parent accepts that item as a children. In case of modifications, the SpineDBWorker will call one or more of ``handle_items_added()``, ``handle_items_updated()``, or ``handle_items_removed()`` with all the items that pass this test. Args: item (dict): The item db_map (DatabaseMapping) Returns: bool """ return True
# pylint: disable=no-self-use
[docs] def shows_item(self, item, db_map): """Called by the associated SpineDBWorker whenever items are fetched and accepted. Returns whether this parent will show this item to the user. Args: item (dict): The item db_map (DatabaseMapping) Returns: bool """ return True
@property
[docs] def is_obsolete(self): return self._obsolete
[docs] def set_obsolete(self, obsolete): """Sets the obsolete status. Args: obsolete (bool): whether parent has become obsolete """ if obsolete: self.set_busy(False) self._obsolete = obsolete
@property
[docs] def is_fetched(self): return self._fetched
[docs] def set_fetched(self, fetched): """Sets the fetched status. Args: fetched (bool): whether parent has been fetched completely """ if fetched: self.set_busy(False) self._fetched = fetched
@property
[docs] def is_busy(self): return self._busy
[docs] def set_busy(self, busy): """Sets the busy status. Args: busy (bool): whether parent is busy fetching """ self._busy = busy
[docs] def handle_items_added(self, db_map_data): """ Called by SpineDBWorker when items are added to the DB. Args: db_map_data (dict): Mapping DatabaseMapping instances to list of dict-items for which ``accepts_item()`` returns True. """ raise NotImplementedError(self.fetch_item_type)
[docs] def handle_items_removed(self, db_map_data): """ Called by SpineDBWorker when items are removed from the DB. Args: db_map_data (dict): Mapping DatabaseMapping instances to list of dict-items for which ``accepts_item()`` returns True. """ raise NotImplementedError(self.fetch_item_type)
[docs] def handle_items_updated(self, db_map_data): """ Called by SpineDBWorker when items are updated in the DB. Args: db_map_data (dict): Mapping DatabaseMapping instances to list of dict-items for which ``accepts_item()`` returns True. """ raise NotImplementedError(self.fetch_item_type)
[docs]class ItemTypeFetchParent(FetchParent): def __init__(self, fetch_item_type, index=None, owner=None, chunk_size=1000): super().__init__(index=index, owner=owner, chunk_size=chunk_size) self._fetch_item_type = fetch_item_type @property
[docs] def fetch_item_type(self): return self._fetch_item_type
@fetch_item_type.setter def fetch_item_type(self, fetch_item_type): self._fetch_item_type = fetch_item_type
[docs] def handle_items_added(self, db_map_data): raise NotImplementedError(self.fetch_item_type)
[docs] def handle_items_removed(self, db_map_data): raise NotImplementedError(self.fetch_item_type)
[docs] def handle_items_updated(self, db_map_data): raise NotImplementedError(self.fetch_item_type)
[docs] def __str__(self): return f"{super().__str__()} fetching {self.fetch_item_type} items owned by {self._owner}"
[docs]class FlexibleFetchParent(ItemTypeFetchParent): def __init__( self, fetch_item_type, handle_items_added=None, handle_items_removed=None, handle_items_updated=None, accepts_item=None, shows_item=None, key_for_index=None, index=None, owner=None, chunk_size=1000, ): super().__init__(fetch_item_type, index=index, owner=owner, chunk_size=chunk_size) self._handle_items_added = handle_items_added self._handle_items_removed = handle_items_removed self._handle_items_updated = handle_items_updated self._accepts_item = accepts_item self._shows_item = shows_item self._key_for_index = key_for_index
[docs] def key_for_index(self, db_map): if self._key_for_index is None: return None return self._key_for_index(db_map)
[docs] def handle_items_added(self, db_map_data): if self._handle_items_added is None: return self._handle_items_added(db_map_data)
[docs] def handle_items_removed(self, db_map_data): if self._handle_items_removed is None: return self._handle_items_removed(db_map_data)
[docs] def handle_items_updated(self, db_map_data): if self._handle_items_updated is None: return self._handle_items_updated(db_map_data)
[docs] def accepts_item(self, item, db_map): if self._accepts_item is None: return super().accepts_item(item, db_map) return self._accepts_item(item, db_map)
[docs] def shows_item(self, item, db_map): if self._shows_item is None: return super().shows_item(item, db_map) return self._shows_item(item, db_map)
[docs]class FetchIndex(dict): def __init__(self): super().__init__() self._position = {}
[docs] def reset(self): self._position.clear() self.clear()
[docs] def process_item(self, item, db_map): raise NotImplementedError()
[docs] def position(self, db_map): return self._position.setdefault(db_map, 0)
[docs] def increment_position(self, db_map): self._position[db_map] += 1
[docs] def get_items(self, key, db_map): return self.get(db_map, {}).get(key, [])
[docs]class _ItemCallback: def __init__(self, fn, *args): self._fn = fn self._args = args
[docs] def __call__(self, item): return self._fn(item, *self._args)
[docs] def __str__(self): return str(self._fn) + " with " + str(self._args)