Skip to content

Commit

Permalink
Database relink after creation (#446)
Browse files Browse the repository at this point in the history
* Add signal for relinking

* Extend DatabaseRelinkDialog with another classmethod

* Add strategy for relinking an sqlite database

* Add relinking action to database table and add check for writable database

* Add relink method to controller
  • Loading branch information
dgdekoning authored Sep 23, 2020
1 parent a29b7f5 commit 34238f2
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 2 deletions.
51 changes: 50 additions & 1 deletion activity_browser/app/bwutils/strategies.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
from typing import Collection

import brightway2 as bw
from bw2data.backends.peewee import ActivityDataset
from bw2data.backends.peewee import ActivityDataset, sqlite3_lci_db
from bw2data.errors import ValidityError
from bw2io.errors import StrategyError
from bw2io.strategies.generic import format_nonunique_key_error
from bw2io.utils import DEFAULT_FIELDS, activity_hash


def relink_exchanges_dbs(data: Collection, relink: dict) -> Collection:
Expand Down Expand Up @@ -37,3 +41,48 @@ def relink_exchanges_bw2package(data: dict, relink: dict) -> dict:
raise ValueError("Cannot relink exchange '{}', key '{}' not found.".format(exc, new_key)
).with_traceback(e.__traceback__)
return data


def relink_exchanges_existing_db(db: bw.Database, other: bw.Database) -> None:
"""Relink exchanges after the database has been created/written.
This means possibly doing a lot of sqlite update calls.
"""
assert db.backend == "sqlite", "Relinking only allowed for SQLITE backends"
assert other.backend == "sqlite", "Relinking only allowed for SQLITE backends"

duplicates, candidates = {}, {}
altered = 0

for ds in other:
key = activity_hash(ds, DEFAULT_FIELDS)
if key in candidates:
duplicates.setdefault(key, []).append(ds)
else:
candidates[key] = (ds['database'], ds['code'])

with sqlite3_lci_db.transaction() as transaction:
try:
# Only do relinking on external biosphere/technosphere exchanges.
for i, exc in enumerate(
exc for act in db for exc in act.exchanges()
if exc.get("type") in {"biosphere", "technosphere"} and exc.input[0] != db.name
):
# Use the input activity to generate the hash.
key = activity_hash(exc.input, DEFAULT_FIELDS)
if key in duplicates:
raise StrategyError(format_nonunique_key_error(exc.input, DEFAULT_FIELDS, duplicates[key]))
elif key in candidates:
exc["input"] = candidates[key]
altered += 1
exc.save()
if i % 10000 == 0:
# Commit changes every 10k exchanges.
transaction.commit()
except (StrategyError, ValidityError) as e:
print(e)
transaction.rollback()
# Process the database after the transaction is complete.
# this updates the 'depends' in metadata
db.process()
print("Finished relinking database, {} exchanges altered.".format(altered))
17 changes: 16 additions & 1 deletion activity_browser/app/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@

from .bwutils import commontasks as bc, AB_metadata
from .bwutils.presamples import clear_resource_by_name, get_package_path, remove_package
from .bwutils.strategies import relink_exchanges_existing_db
from .settings import ab_settings, project_settings
from .signals import signals
from .ui.widgets import CopyDatabaseDialog
from .ui.widgets import CopyDatabaseDialog, DatabaseRelinkDialog
from .ui.wizards.db_import_wizard import DatabaseImportWizard, DefaultBiosphereDialog


Expand Down Expand Up @@ -58,6 +59,7 @@ def connect_signals(self):
signals.copy_database.connect(self.copy_database)
signals.install_default_data.connect(self.install_default_data)
signals.import_database.connect(self.import_database_wizard)
signals.relink_database.connect(self.relink_database)
# Activity
signals.duplicate_activity.connect(self.duplicate_activity)
signals.activity_modified.connect(self.modify_activity)
Expand Down Expand Up @@ -277,6 +279,19 @@ def delete_database(self, name):
del bw.databases[name]
self.change_project(bw.projects.current, reload=True)

@Slot(str, QObject, name="relinkDatabase")
def relink_database(self, db_name: str, parent: QObject) -> None:
"""Relink technosphere exchanges within the given database."""
dialog = DatabaseRelinkDialog.relink_existing(
parent, db_name, [db for db in bw.databases if db != db_name]
)
if dialog.exec_() == DatabaseRelinkDialog.Accepted:
db = bw.Database(db_name)
other = bw.Database(dialog.new_db)
relink_exchanges_existing_db(db, other)
signals.database_changed.emit(db_name)
signals.databases_changed.emit()

# CALCULATION SETUP
def new_calculation_setup(self):
name, ok = QtWidgets.QInputDialog.getText(
Expand Down
1 change: 1 addition & 0 deletions activity_browser/app/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class Signals(QObject):
copy_database = Signal(str, QObject)
install_default_data = Signal()
import_database = Signal(QObject)
relink_database = Signal(str, QObject)

database_selected = Signal(str)
databases_changed = Signal()
Expand Down
12 changes: 12 additions & 0 deletions activity_browser/app/ui/tables/inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,19 +41,26 @@ def __init__(self, parent=None):
QtWidgets.QSizePolicy.Preferred,
QtWidgets.QSizePolicy.Maximum
))
self.relink_action = QtWidgets.QAction(
qicons.edit, "Relink database", None
)
self._connect_signals()

def _connect_signals(self):
signals.project_selected.connect(self.sync)
signals.databases_changed.connect(self.sync)
self.doubleClicked.connect(self.open_database)
self.relink_action.triggered.connect(
lambda: signals.relink_database.emit(self.selected_db_name, self)
)

def contextMenuEvent(self, a0) -> None:
menu = QtWidgets.QMenu(self)
menu.addAction(
qicons.delete, "Delete database",
lambda: signals.delete_database.emit(self.selected_db_name)
)
menu.addAction(self.relink_action)
menu.addAction(
qicons.duplicate_database, "Copy database",
lambda: signals.copy_database.emit(self.selected_db_name, self)
Expand All @@ -62,6 +69,11 @@ def contextMenuEvent(self, a0) -> None:
qicons.add, "Add new activity",
lambda: signals.new_activity.emit(self.selected_db_name)
)
proxy = self.indexAt(a0.pos())
if proxy.isValid():
index = self.get_source_index(proxy)
db_name = self.model.index(index.row(), 0).data()
self.relink_action.setEnabled(not project_settings.db_is_readonly(db_name))
menu.exec_(a0.globalPos())

def mousePressEvent(self, e):
Expand Down
13 changes: 13 additions & 0 deletions activity_browser/app/ui/widgets/dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,10 @@ class DatabaseRelinkDialog(QtWidgets.QDialog):
" exchanges to a different database?"
"\n\nReplace database '{}' with:"
)
RELINK_EXISTING = (
"Relink exchanges from database '{}' with another database?"
"\n\nLink with:"
)

def __init__(self, parent=None):
super().__init__(parent)
Expand Down Expand Up @@ -274,3 +278,12 @@ def start_relink(cls, parent: QtWidgets.QWidget, db: str, options: List[str]) ->
obj.choice.addItems(options)
obj.choice.setEnabled(True)
return obj

@classmethod
def relink_existing(cls, parent: QtWidgets.QWidget, db: str, options: List[str]) -> 'DatabaseRelinkDialog':
obj = cls(parent)
obj.label.setText(cls.RELINK_EXISTING.format(db))
obj.choice.clear()
obj.choice.addItems(options)
obj.choice.setEnabled(True)
return obj

0 comments on commit 34238f2

Please sign in to comment.