brightpath 0.0.3__tar.gz → 0.0.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. {brightpath-0.0.3/brightpath.egg-info → brightpath-0.0.4}/PKG-INFO +13 -2
  2. {brightpath-0.0.3 → brightpath-0.0.4}/README.md +11 -1
  3. {brightpath-0.0.3 → brightpath-0.0.4}/brightpath/__init__.py +1 -1
  4. {brightpath-0.0.3 → brightpath-0.0.4}/brightpath/simaproconverter.py +162 -1
  5. {brightpath-0.0.3 → brightpath-0.0.4/brightpath.egg-info}/PKG-INFO +13 -2
  6. {brightpath-0.0.3 → brightpath-0.0.4}/brightpath.egg-info/requires.txt +1 -0
  7. {brightpath-0.0.3 → brightpath-0.0.4}/pyproject.toml +1 -1
  8. {brightpath-0.0.3 → brightpath-0.0.4}/requirements.txt +1 -0
  9. {brightpath-0.0.3 → brightpath-0.0.4}/tests/test_simaproconverter.py +64 -0
  10. {brightpath-0.0.3 → brightpath-0.0.4}/LICENSE +0 -0
  11. {brightpath-0.0.3 → brightpath-0.0.4}/MANIFEST.in +0 -0
  12. {brightpath-0.0.3 → brightpath-0.0.4}/brightpath/bwconverter.py +0 -0
  13. {brightpath-0.0.3 → brightpath-0.0.4}/brightpath/data/export/correspondence_biosphere_flows.yaml +0 -0
  14. {brightpath-0.0.3 → brightpath-0.0.4}/brightpath/data/export/ecoinvent_to_uvek_mapping.csv +0 -0
  15. {brightpath-0.0.3 → brightpath-0.0.4}/brightpath/data/export/ei_transport.csv +0 -0
  16. {brightpath-0.0.3 → brightpath-0.0.4}/brightpath/data/export/flows_biosphere_39.csv +0 -0
  17. {brightpath-0.0.3 → brightpath-0.0.4}/brightpath/data/export/simapro-biosphere.json +0 -0
  18. {brightpath-0.0.3 → brightpath-0.0.4}/brightpath/data/export/simapro-technosphere-3.5.csv +0 -0
  19. {brightpath-0.0.3 → brightpath-0.0.4}/brightpath/data/export/simapro_blacklist.yaml +0 -0
  20. {brightpath-0.0.3 → brightpath-0.0.4}/brightpath/data/export/simapro_ei_exceptions.yaml +0 -0
  21. {brightpath-0.0.3 → brightpath-0.0.4}/brightpath/data/export/simapro_fields.yaml +0 -0
  22. {brightpath-0.0.3 → brightpath-0.0.4}/brightpath/data/export/simapro_headers.yaml +0 -0
  23. {brightpath-0.0.3 → brightpath-0.0.4}/brightpath/data/export/simapro_subcompartments.yaml +0 -0
  24. {brightpath-0.0.3 → brightpath-0.0.4}/brightpath/data/export/simapro_to_brightway_biosphere_ei310.yaml +0 -0
  25. {brightpath-0.0.3 → brightpath-0.0.4}/brightpath/data/export/simapro_units.yaml +0 -0
  26. {brightpath-0.0.3 → brightpath-0.0.4}/brightpath/data/export/uvek_blacklist.yaml +0 -0
  27. {brightpath-0.0.3 → brightpath-0.0.4}/brightpath/data/export/uvek_conversion_factors.yaml +0 -0
  28. {brightpath-0.0.3 → brightpath-0.0.4}/brightpath/data/export/waste_exchange_names.yaml +0 -0
  29. {brightpath-0.0.3 → brightpath-0.0.4}/brightpath/utils.py +0 -0
  30. {brightpath-0.0.3 → brightpath-0.0.4}/brightpath.egg-info/SOURCES.txt +0 -0
  31. {brightpath-0.0.3 → brightpath-0.0.4}/brightpath.egg-info/dependency_links.txt +0 -0
  32. {brightpath-0.0.3 → brightpath-0.0.4}/brightpath.egg-info/top_level.txt +0 -0
  33. {brightpath-0.0.3 → brightpath-0.0.4}/setup.cfg +0 -0
  34. {brightpath-0.0.3 → brightpath-0.0.4}/tests/test_bwconverter.py +0 -0
  35. {brightpath-0.0.3 → brightpath-0.0.4}/tests/test_regressions.py +0 -0
  36. {brightpath-0.0.3 → brightpath-0.0.4}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: brightpath
3
- Version: 0.0.3
3
+ Version: 0.0.4
4
4
  Summary: Convert Brightway2 LCI data to Simapro and back.
5
5
  Author-email: Romain Sacchi <romain.sacchi@psi.ch>
6
6
  License: BSD 3-Clause License
@@ -53,6 +53,7 @@ License-File: LICENSE
53
53
  Requires-Dist: bw2io
54
54
  Requires-Dist: prettytable>=3
55
55
  Requires-Dist: PyYAML>=6
56
+ Requires-Dist: XlsxWriter>=3
56
57
  Provides-Extra: test
57
58
  Requires-Dist: pytest; extra == "test"
58
59
  Requires-Dist: pytest-cov; extra == "test"
@@ -136,6 +137,17 @@ converter.convert_to_brightway()
136
137
  # converter.i.write_database()
137
138
  ```
138
139
 
140
+ You can also write a Brightway Excel workbook directly, without first creating
141
+ a Brightway database:
142
+
143
+ ```python
144
+ path = converter.convert_to_brightway(
145
+ format="excel",
146
+ filename="lci-my-simapro-import.xlsx",
147
+ )
148
+ print(path)
149
+ ```
150
+
139
151
  ## Development
140
152
 
141
153
  * Source code is formatted with standard Python tools and tested with
@@ -148,4 +160,3 @@ converter.convert_to_brightway()
148
160
 
149
161
  BrightPath is distributed under the
150
162
  [BSD-3-Clause license](LICENSE).
151
-
@@ -68,6 +68,17 @@ converter.convert_to_brightway()
68
68
  # converter.i.write_database()
69
69
  ```
70
70
 
71
+ You can also write a Brightway Excel workbook directly, without first creating
72
+ a Brightway database:
73
+
74
+ ```python
75
+ path = converter.convert_to_brightway(
76
+ format="excel",
77
+ filename="lci-my-simapro-import.xlsx",
78
+ )
79
+ print(path)
80
+ ```
81
+
71
82
  ## Development
72
83
 
73
84
  * Source code is formatted with standard Python tools and tested with
@@ -80,4 +91,3 @@ converter.convert_to_brightway()
80
91
 
81
92
  BrightPath is distributed under the
82
93
  [BSD-3-Clause license](LICENSE).
83
-
@@ -1,5 +1,5 @@
1
1
  __all__ = ("BrightwayConverter", "SimaproConverter", "DATA_DIR")
2
- __version__ = (0, 0, 3)
2
+ __version__ = (0, 0, 4)
3
3
 
4
4
  from pathlib import Path
5
5
 
@@ -5,12 +5,14 @@ Simapro inventories to Brightway inventory files.
5
5
 
6
6
  import csv
7
7
  import logging
8
+ import numbers
8
9
  import re
9
10
  import tempfile
10
11
  from copy import deepcopy
11
12
  from pathlib import Path
12
13
 
13
14
  import bw2io
15
+ import xlsxwriter
14
16
 
15
17
  from . import DATA_DIR
16
18
  from .utils import (
@@ -30,6 +32,44 @@ from .utils import (
30
32
  WASTE_TERMS = get_waste_exchange_names()
31
33
  logger = logging.getLogger(__name__)
32
34
 
35
+ EXCEL_ACTIVITY_SKIP_FIELDS = {
36
+ "database",
37
+ "exchanges",
38
+ "name",
39
+ "parameters",
40
+ "simapro metadata",
41
+ }
42
+ EXCEL_EXCHANGE_SKIP_FIELDS = {"input", "output"}
43
+ EXCEL_ACTIVITY_FIELD_ORDER = [
44
+ "reference product",
45
+ "unit",
46
+ "location",
47
+ "code",
48
+ "comment",
49
+ "type",
50
+ "categories",
51
+ ]
52
+ EXCEL_EXCHANGE_FIELD_ORDER = [
53
+ "name",
54
+ "amount",
55
+ "database",
56
+ "reference product",
57
+ "product",
58
+ "location",
59
+ "unit",
60
+ "categories",
61
+ "type",
62
+ "formula",
63
+ "uncertainty type",
64
+ "loc",
65
+ "scale",
66
+ "shape",
67
+ "minimum",
68
+ "maximum",
69
+ "simapro name",
70
+ "comment",
71
+ ]
72
+
33
73
 
34
74
  def _version_tuple(version: str) -> tuple:
35
75
  try:
@@ -209,6 +249,57 @@ def is_simapro_final_waste_flow(exchange: dict) -> bool:
209
249
  )
210
250
 
211
251
 
252
+ def _safe_excel_filename(name: str) -> str:
253
+ safe_name = re.sub(r"[^A-Za-z0-9_.-]+", "_", name).strip("_")
254
+ return safe_name or "brightway-inventory"
255
+
256
+
257
+ def _valid_worksheet_name(name: str) -> str:
258
+ cleaned = name
259
+ for char in {"\\", "/", "*", "[", "]", ":", "?"}:
260
+ cleaned = cleaned.replace(char, "#")
261
+ if cleaned == "History":
262
+ cleaned = "History-worksheet"
263
+ return (cleaned or "Inventory")[:30]
264
+
265
+
266
+ def _is_excel_exportable(value) -> bool:
267
+ if value is None:
268
+ return True
269
+ if isinstance(value, (str, bool, numbers.Number)):
270
+ return True
271
+ if isinstance(value, (tuple, list)):
272
+ return all(_is_excel_exportable(item) for item in value)
273
+ return False
274
+
275
+
276
+ def _format_excel_value(value):
277
+ if isinstance(value, (tuple, list)):
278
+ return "::".join("" if item is None else str(_format_excel_value(item)) for item in value)
279
+ return value
280
+
281
+
282
+ def _ordered_export_fields(data: list[dict], preferred: list[str], skipped: set[str]) -> list[str]:
283
+ fields = {
284
+ field for row in data for field, value in row.items() if field not in skipped and _is_excel_exportable(value)
285
+ }
286
+ ordered = [field for field in preferred if field in fields]
287
+ ordered.extend(sorted(fields.difference(ordered)))
288
+ return ordered
289
+
290
+
291
+ def _write_excel_cell(sheet, row: int, column: int, value, cell_format=None) -> None:
292
+ value = _format_excel_value(value)
293
+ if value is None:
294
+ return
295
+ if isinstance(value, bool):
296
+ sheet.write_boolean(row, column, value, cell_format)
297
+ elif isinstance(value, numbers.Number):
298
+ sheet.write_number(row, column, value, cell_format)
299
+ else:
300
+ sheet.write_string(row, column, str(value), cell_format)
301
+
302
+
212
303
  def load_ecoinvent_activities(version: str) -> list:
213
304
  if not re.fullmatch(r"\d+(?:\.\d+){1,2}", version):
214
305
  raise ValueError(f"Unsupported ecoinvent version format: {version!r}.")
@@ -396,7 +487,9 @@ class SimaproConverter:
396
487
  if exc["input"][0] is None:
397
488
  exc["input"] = (self.i.db_name, exc["input"][1])
398
489
 
399
- def convert_to_brightway(self):
490
+ def convert_to_brightway(self, format: str = "data", filename=None):
491
+ if format not in ("data", "excel"):
492
+ raise ValueError("Format must be either `data` or `excel`.")
400
493
 
401
494
  logger.info("Formatting exchanges.")
402
495
  for ds in self.i.data:
@@ -473,6 +566,74 @@ class SimaproConverter:
473
566
  self.check_inventories()
474
567
  logger.info("SimaPro conversion completed.")
475
568
 
569
+ if format == "excel":
570
+ return self.write_brightway_excel(filename)
571
+
572
+ return self.i.data
573
+
574
+ def write_brightway_excel(self, filename=None) -> Path:
575
+ filepath = self._excel_filepath(filename)
576
+ filepath.parent.mkdir(parents=True, exist_ok=True)
577
+
578
+ workbook = xlsxwriter.Workbook(filepath)
579
+ bold = workbook.add_format({"bold": True})
580
+ bold.set_font_size(12)
581
+ sheet = workbook.add_worksheet(_valid_worksheet_name(self.db_name))
582
+
583
+ row = 0
584
+ row = self._write_database_excel_section(sheet, row, bold)
585
+ row += 1
586
+
587
+ for activity in self.i.data:
588
+ row = self._write_activity_excel_section(sheet, row, activity, bold)
589
+ row += 1
590
+
591
+ workbook.close()
592
+ return filepath
593
+
594
+ def _excel_filepath(self, filename=None) -> Path:
595
+ if filename is None:
596
+ return Path.cwd() / f"lci-{_safe_excel_filename(self.db_name)}.xlsx"
597
+
598
+ filepath = Path(filename).expanduser()
599
+ if filepath.suffix == "":
600
+ filepath = filepath.with_suffix(".xlsx")
601
+ if filepath.suffix.lower() != ".xlsx":
602
+ raise ValueError("Brightway Excel exports must use a .xlsx filename.")
603
+ return filepath.resolve()
604
+
605
+ def _write_database_excel_section(self, sheet, row: int, bold) -> int:
606
+ _write_excel_cell(sheet, row, 0, "Database", bold)
607
+ _write_excel_cell(sheet, row, 1, self.db_name)
608
+ return row + 1
609
+
610
+ def _write_activity_excel_section(self, sheet, row: int, activity: dict, bold) -> int:
611
+ _write_excel_cell(sheet, row, 0, "Activity", bold)
612
+ _write_excel_cell(sheet, row, 1, activity["name"])
613
+ row += 1
614
+
615
+ for field in _ordered_export_fields([activity], EXCEL_ACTIVITY_FIELD_ORDER, EXCEL_ACTIVITY_SKIP_FIELDS):
616
+ _write_excel_cell(sheet, row, 0, field)
617
+ _write_excel_cell(sheet, row, 1, activity.get(field))
618
+ row += 1
619
+
620
+ exchanges = activity.get("exchanges", [])
621
+ _write_excel_cell(sheet, row, 0, "Exchanges", bold)
622
+ row += 1
623
+
624
+ if exchanges:
625
+ columns = _ordered_export_fields(exchanges, EXCEL_EXCHANGE_FIELD_ORDER, EXCEL_EXCHANGE_SKIP_FIELDS)
626
+ for column, field in enumerate(columns):
627
+ _write_excel_cell(sheet, row, column, field)
628
+ row += 1
629
+
630
+ for exchange in exchanges:
631
+ for column, field in enumerate(columns):
632
+ _write_excel_cell(sheet, row, column, exchange.get(field))
633
+ row += 1
634
+
635
+ return row
636
+
476
637
  def remove_empty_datasets(self):
477
638
  self.i.data = [ds for ds in self.i.data if len(ds["exchanges"]) >= 1]
478
639
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: brightpath
3
- Version: 0.0.3
3
+ Version: 0.0.4
4
4
  Summary: Convert Brightway2 LCI data to Simapro and back.
5
5
  Author-email: Romain Sacchi <romain.sacchi@psi.ch>
6
6
  License: BSD 3-Clause License
@@ -53,6 +53,7 @@ License-File: LICENSE
53
53
  Requires-Dist: bw2io
54
54
  Requires-Dist: prettytable>=3
55
55
  Requires-Dist: PyYAML>=6
56
+ Requires-Dist: XlsxWriter>=3
56
57
  Provides-Extra: test
57
58
  Requires-Dist: pytest; extra == "test"
58
59
  Requires-Dist: pytest-cov; extra == "test"
@@ -136,6 +137,17 @@ converter.convert_to_brightway()
136
137
  # converter.i.write_database()
137
138
  ```
138
139
 
140
+ You can also write a Brightway Excel workbook directly, without first creating
141
+ a Brightway database:
142
+
143
+ ```python
144
+ path = converter.convert_to_brightway(
145
+ format="excel",
146
+ filename="lci-my-simapro-import.xlsx",
147
+ )
148
+ print(path)
149
+ ```
150
+
139
151
  ## Development
140
152
 
141
153
  * Source code is formatted with standard Python tools and tested with
@@ -148,4 +160,3 @@ converter.convert_to_brightway()
148
160
 
149
161
  BrightPath is distributed under the
150
162
  [BSD-3-Clause license](LICENSE).
151
-
@@ -1,6 +1,7 @@
1
1
  bw2io
2
2
  prettytable>=3
3
3
  PyYAML>=6
4
+ XlsxWriter>=3
4
5
 
5
6
  [dev]
6
7
  bandit
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "brightpath"
7
- version = "0.0.3"
7
+ version = "0.0.4"
8
8
  description = "Convert Brightway2 LCI data to Simapro and back."
9
9
  dynamic = ["dependencies"]
10
10
  authors = [
@@ -1,3 +1,4 @@
1
1
  bw2io
2
2
  prettytable>=3
3
3
  PyYAML>=6
4
+ XlsxWriter>=3
@@ -1,5 +1,6 @@
1
1
  from types import SimpleNamespace
2
2
 
3
+ import bw2io
3
4
  import pytest
4
5
 
5
6
  from brightpath import simaproconverter
@@ -338,6 +339,69 @@ def test_convert_to_brightway_formats_dataset_and_exchanges():
338
339
  assert biosphere["categories"] == ("natural resource", "in water")
339
340
 
340
341
 
342
+ def test_convert_to_brightway_rejects_unknown_output_format():
343
+ converter = make_converter([])
344
+
345
+ with pytest.raises(ValueError, match="Format must be"):
346
+ converter.convert_to_brightway(format="json")
347
+
348
+
349
+ def test_convert_to_brightway_writes_excel_importable_by_bw2io(tmp_path):
350
+ data = [
351
+ {
352
+ "name": "Electricity {CH}| market for | Cut-off, U",
353
+ "unit": "kilowatt hour",
354
+ "simapro metadata": {"Comment": "dataset comment"},
355
+ "exchanges": [
356
+ {
357
+ "type": "production",
358
+ "name": "Electricity {CH}| market for | Cut-off, U",
359
+ "unit": "kilowatt hour",
360
+ "amount": 1.0,
361
+ "input": (None, "production"),
362
+ },
363
+ {
364
+ "type": "biosphere",
365
+ "name": "Water, lake",
366
+ "categories": ("natural resource", "unspecified"),
367
+ "unit": "cubic meter",
368
+ "amount": 4.0,
369
+ },
370
+ ],
371
+ }
372
+ ]
373
+ converter = make_converter(data, db_name="converted_db")
374
+
375
+ output = converter.convert_to_brightway(format="excel", filename=tmp_path / "converted")
376
+
377
+ assert output == (tmp_path / "converted.xlsx").resolve()
378
+ importer = bw2io.ExcelImporter(output)
379
+ assert importer.db_name == "converted_db"
380
+ assert len(importer.data) == 1
381
+
382
+ dataset = importer.data[0]
383
+ assert dataset["name"] == "market for electricity"
384
+ assert dataset["reference product"] == "electricity"
385
+ assert dataset["location"] == "CH"
386
+ assert dataset["unit"] == "kilowatt hour"
387
+ assert dataset["comment"] == "dataset comment"
388
+
389
+ production = next(exc for exc in dataset["exchanges"] if exc["type"] == "production")
390
+ biosphere = next(exc for exc in dataset["exchanges"] if exc["type"] == "biosphere")
391
+ assert production["name"] == "market for electricity"
392
+ assert production["reference product"] == "electricity"
393
+ assert production["amount"] == 1
394
+ assert biosphere["name"] == "Water, lake"
395
+ assert biosphere["categories"] == "natural resource::in water"
396
+
397
+
398
+ def test_write_brightway_excel_requires_xlsx_suffix(tmp_path):
399
+ converter = make_converter([])
400
+
401
+ with pytest.raises(ValueError, match="\\.xlsx"):
402
+ converter.write_brightway_excel(tmp_path / "converted.csv")
403
+
404
+
341
405
  def test_convert_to_brightway_drops_simapro_final_waste_indicators():
342
406
  data = [
343
407
  {
File without changes
File without changes
File without changes