Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix for excel export #451

Merged
merged 8 commits into from
Sep 26, 2020
11 changes: 11 additions & 0 deletions activity_browser/app/bwutils/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-


class ImportCanceledError(Exception):
"""Import of data was cancelled by the user."""
pass


class LinkingFailed(Exception):
"""Unlinked exchanges remain after relinking."""
pass
20 changes: 18 additions & 2 deletions activity_browser/app/bwutils/exporters.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from pathlib import Path
from typing import Union

import brightway2 as bw
from bw2data.utils import safe_filename
from bw2io.export.excel import CSVFormatter, create_valid_worksheet_name
import xlsxwriter
Expand All @@ -16,6 +15,23 @@
# - Add handler for pedigree data to exporter


class ABCSVFormatter(CSVFormatter):
def exchange_as_dict(self, exc):
"""Same as CSVFormatter, but explicitly pull the database from the
input activity.

This ensures that the database value is always included, even when
it is not stored in the exchange _data.
"""
inp = exc.input
inp_fields = ("name", "unit", "location", "categories", "database")
skip_fields = ("input", "output")
data = {k: v for k, v in exc._data.items()
if k not in skip_fields}
data.update(**{k: inp[k] for k in inp_fields if inp.get(k)})
return data


def format_pedigree(data: dict) -> str:
"""Converts pedigree dict to tuple."""
try:
Expand Down Expand Up @@ -54,7 +70,7 @@ def write_lci_excel(db_name: str, path: str, objs=None, sections=None) -> Path:

sheet = workbook.add_worksheet(create_valid_worksheet_name(db_name))

data = CSVFormatter(db_name, objs).get_formatted_data(sections)
data = ABCSVFormatter(db_name, objs).get_formatted_data(sections)

for row_index, row in enumerate(data):
for col_index, value in enumerate(row):
Expand Down
19 changes: 9 additions & 10 deletions activity_browser/app/bwutils/importers.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
convert_activity_parameters_to_list
)

from .errors import LinkingFailed
from .strategies import (
relink_exchanges_bw2package, alter_database_name, hash_parameter_group,
relink_exchanges_with_db, link_exchanges_without_db,
Expand Down Expand Up @@ -87,18 +88,24 @@ def simple_automated_import(cls, filepath, db_name: str, relink: dict = None) ->
obj.apply_strategies()
if any(obj.unlinked) and relink:
for db, new_db in relink.items():
if db == "missing_db":
if db == "(name missing)":
obj.apply_strategy(functools.partial(
link_exchanges_without_db, db=new_db
))
else:
obj.apply_strategy(functools.partial(
relink_exchanges_with_db, old=db, new=new_db
))
# Relinking failed (some exchanges still unlinked)
if any(obj.unlinked):
# Raise a different exception.
excs = [exc for exc in obj.unlinked][:10]
databases = {exc.get("database", "(name missing)") for exc in obj.unlinked}
raise LinkingFailed(excs, databases)
if any(obj.unlinked):
# Still have unlinked fields? Raise exception.
excs = [exc for exc in obj.unlinked][:10]
databases = {exc.get("database", "missing_db") for exc in obj.unlinked}
databases = {exc.get("database", "(name missing)") for exc in obj.unlinked}
raise StrategyError(excs, databases)
if obj.project_parameters:
obj.write_project_parameters(delete_existing=False)
Expand All @@ -107,14 +114,6 @@ def simple_automated_import(cls, filepath, db_name: str, relink: dict = None) ->
bw.parameters.recalculate()
return [db]

def link_to_technosphere(self, db_name: str, fields: tuple = None) -> None:
"""Apply the 'link to technosphere' strategy with some flexibility."""
fields = fields or LINK_FIELDS
self.apply_strategy(functools.partial(
link_technosphere_by_activity_hash,
external_db_name=db_name, fields=fields
))


class ABPackage(bw.BW2Package):
""" Inherits from brightway2 `BW2Package` and handles importing BW2Packages.
Expand Down
18 changes: 16 additions & 2 deletions activity_browser/app/ui/wizards/db_export_wizard.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
# related to that database as an Excel file.
"Excel": write_lci_excel,
}
EXTENSIONS = {
"BW2Package": ".bw2package",
"Excel": ".xlsx",
}


class DatabaseExportWizard(QtWidgets.QWizard):
Expand All @@ -40,10 +44,20 @@ def perform_export(self) -> None:
db_name = self.field("database_choice")
export_as = self.field("export_option")
out_path = self.field("output_path")
# Ensure that extension matches export_option.
path, ext = os.path.splitext(out_path)
if ext and not ext == EXTENSIONS[export_as]:
ext = EXTENSIONS[export_as]
out_path = path + ext
EXPORTERS[export_as](db_name, out_path)


class ExportDatabasePage(QtWidgets.QWizardPage):
FILTERS = {
"BW2Package": "BW2Package Files (*.bw2package);; All Files (*.*)",
"Excel": "Excel Files (*.xlsx);; All Files (*.*)",
}

def __init__(self, parent=None):
super().__init__(parent=parent)
self.wizard = parent
Expand Down Expand Up @@ -94,9 +108,9 @@ def isComplete(self):

@Slot(name="browseFile")
def browse(self) -> None:
file_filter = self.FILTERS[self.field("export_option")]
path, _ = QtWidgets.QFileDialog.getSaveFileName(
parent=self, caption="Save database",
filter="Database Files (*.xlsx *.bw2package);; All Files (*.*)"
parent=self, caption="Save database", filter=file_filter
)
if path:
self.output_dir.setText(path)
19 changes: 12 additions & 7 deletions activity_browser/app/ui/wizards/db_import_wizard.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
import os
import io
from pprint import pprint
import subprocess
import tempfile
import zipfile
Expand All @@ -16,6 +17,7 @@
from PySide2 import QtWidgets, QtCore
from PySide2.QtCore import Signal, Slot

from ...bwutils.errors import ImportCanceledError, LinkingFailed
from ...bwutils.importers import ABExcelImporter, ABPackage
from ...signals import signals
from ..style import style_group_box
Expand Down Expand Up @@ -664,6 +666,8 @@ def update(self, db_name: str, archive_path=None, datasets_path=None,
self.relink = relink or {}

def run(self):
# Set the cancel sentinal to false whenever the thread (re-)starts
import_signals.cancel_sentinel = False
if self.use_forwast:
self.run_forwast()
elif self.use_local:
Expand All @@ -672,7 +676,6 @@ def run(self):
self.run_ecoinvent()

def run_ecoinvent(self):
import_signals.cancel_sentinel = False
with tempfile.TemporaryDirectory() as tempdir:
dataset_dir = self.datasets_path or os.path.join(tempdir, "datasets")
if not os.path.isdir(dataset_dir):
Expand All @@ -693,7 +696,6 @@ def run_forwast(self):
"""
adapted from pjamesjoyce/lcopt
"""
import_signals.cancel_sentinel = False
response = requests.get(self.forwast_url)
forwast_zip = zipfile.ZipFile(io.BytesIO(response.content))
import_signals.download_complete.emit()
Expand Down Expand Up @@ -789,11 +791,18 @@ def run_local_import(self):
("Unknown object", str(e))
)
except StrategyError as e:
from pprint import pprint
print("Could not link exchanges, here are 10 examples.:")
pprint(e.args[0])
self.delete_canceled_db()
import_signals.links_required.emit(e.args[0], e.args[1])
except LinkingFailed as e:
msg = QtWidgets.QMessageBox(
QtWidgets.QMessageBox.Critical, "Unlinked exchanges",
"Some exchanges could not be linked in databases: '[{}]'".format(", ".join(e.args[1])),
QtWidgets.QMessageBox.Ok, self
)
msg.setDetailedText("\n\n".join(str(e) for e in e.args[0]))
msg.exec_()

def delete_canceled_db(self):
if self.db_name in bw.databases:
Expand Down Expand Up @@ -1113,10 +1122,6 @@ def _efficient_write_dataset(self, *args, **kwargs):
bw.config.backends['activitybrowser'] = ActivityBrowserBackend


class ImportCanceledError(Exception):
pass


class ImportSignals(QtCore.QObject):
extraction_progress = Signal(int, int)
strategy_progress = Signal(int, int)
Expand Down