diff --git a/bw2data/backends/proxies.py b/bw2data/backends/proxies.py index 278bf480..79725bb2 100644 --- a/bw2data/backends/proxies.py +++ b/bw2data/backends/proxies.py @@ -561,6 +561,10 @@ def __init__(self, document=None, **kwargs): self._document.output_code, ) + @property + def id(self): + return self._document.id + def save(self, signal: bool = True, data_already_set: bool = False, force_insert: bool = False): if not data_already_set and not self.valid(): raise ValidityError( diff --git a/bw2data/parameters.py b/bw2data/parameters.py index 7c6eb55d..e9276145 100644 --- a/bw2data/parameters.py +++ b/bw2data/parameters.py @@ -1090,7 +1090,7 @@ def save(self, *args, **kwargs): exc = ExchangeDataset.get_or_none(id=self.exchange) if exc and exc.data.get("formula") != self.formula: exc.data["formula"] = self.formula - exc.save() + exc.save(signal=False) @staticmethod def load(group): diff --git a/bw2data/revisions.py b/bw2data/revisions.py index 7d419bce..15a6dbac 100644 --- a/bw2data/revisions.py +++ b/bw2data/revisions.py @@ -9,7 +9,13 @@ from bw2data.backends.utils import dict_as_activitydataset, dict_as_exchangedataset from bw2data.database import DatabaseChooser from bw2data.errors import DifferentObjects, IncompatibleClasses -from bw2data.parameters import ActivityParameter, DatabaseParameter, ParameterBase, ProjectParameter +from bw2data.parameters import ( + ActivityParameter, + DatabaseParameter, + ParameterBase, + ParameterizedExchange, + ProjectParameter, +) from bw2data.signals import SignaledDataset from bw2data.snowflake_ids import snowflake_id_generator from bw2data.utils import get_node @@ -316,6 +322,11 @@ def _unwrap_diff_dict(cls, data: dict) -> dict: } +class RevisionedParameterizedExchange(RevisionedParameter): + KEYS = ("id", "group", "formula", "exchange") + ORM_CLASS = ParameterizedExchange + + class RevisionedProjectParameter(RevisionedParameter): KEYS = ("id", "name", "formula", "amount", "data") ORM_CLASS = ProjectParameter @@ -453,6 +464,7 @@ def handle(cls, revision_data: dict) -> None: ProjectParameter: "project_parameter", DatabaseParameter: "database_parameter", ActivityParameter: "activity_parameter", + ParameterizedExchange: "parameterized_exchange", } REVISIONED_LABEL_AS_OBJECT = { "lci_node": RevisionedNode, @@ -461,5 +473,6 @@ def handle(cls, revision_data: dict) -> None: "project_parameter": RevisionedProjectParameter, "database_parameter": RevisionedDatabaseParameter, "activity_parameter": RevisionedActivityParameter, + "parameterized_exchange": RevisionedParameterizedExchange, } REVISIONS_OBJECT_AS_LABEL = {v: k for k, v in REVISIONED_LABEL_AS_OBJECT.items()} diff --git a/tests/unit/test_parameterized_exchange_events.py b/tests/unit/test_parameterized_exchange_events.py new file mode 100644 index 00000000..6f937f88 --- /dev/null +++ b/tests/unit/test_parameterized_exchange_events.py @@ -0,0 +1,341 @@ +import json + +from bw2data.database import DatabaseChooser +from bw2data.parameters import ActivityParameter, ParameterizedExchange +from bw2data.project import projects +from bw2data.snowflake_ids import snowflake_id_generator +from bw2data.tests import bw2test + + +@bw2test +def test_parameterized_exchange_revision_expected_format_create(num_revisions): + projects.set_current("activity-event") + + database = DatabaseChooser("db") + database.register() + node = database.new_node(code="A", name="A") + node.save() + other = database.new_node(code="B", name="B2", type="product") + other.save() + edge = node.new_edge(input=other, type="technosphere", amount=0.1, arbitrary="foo") + edge.save() + ActivityParameter.insert_dummy("test-group", (node["database"], node["code"])) + + assert not ParameterizedExchange.select().count() + assert projects.dataset.revision is None + + projects.dataset.set_sourced() + + pe = ParameterizedExchange.create( + group="test-group", + exchange=edge.id, + formula="1 * 2 + 3", + ) + assert pe.id > 1e6 + assert num_revisions(projects) == 1 + + assert projects.dataset.revision is not None + with open(projects.dataset.dir / "revisions" / f"{projects.dataset.revision}.rev", "r") as f: + revision = json.load(f) + + expected = { + "metadata": { + "parent_revision": None, + "revision": projects.dataset.revision, + "authors": "Anonymous", + "title": "Untitled revision", + "description": "No description", + }, + "data": [ + { + "type": "parameterized_exchange", + "id": pe.id, + "change_type": "create", + "delta": { + "type_changes": { + "root": { + "old_type": "NoneType", + "new_type": "dict", + "new_value": { + "id": pe.id, + "group": "test-group", + "formula": "1 * 2 + 3", + "exchange": edge.id, + }, + } + } + }, + } + ], + } + + assert revision == expected + + +@bw2test +def test_parameterized_exchange_revision_apply_create(num_revisions): + projects.set_current("activity-event") + + database = DatabaseChooser("db") + database.register() + node = database.new_node(code="A", name="A") + node.save() + other = database.new_node(code="B", name="B2", type="product") + other.save() + edge = node.new_edge(input=other, type="technosphere", amount=0.1, arbitrary="foo") + edge.save() + ActivityParameter.insert_dummy("test-group", (node["database"], node["code"])) + + assert projects.dataset.revision is None + + revision_id = next(snowflake_id_generator) + pe_id = next(snowflake_id_generator) + revision = { + "metadata": { + "parent_revision": None, + "revision": revision_id, + "authors": "Anonymous", + "title": "Untitled revision", + "description": "No description", + }, + "data": [ + { + "type": "parameterized_exchange", + "id": pe_id, + "change_type": "create", + "delta": { + "type_changes": { + "root": { + "old_type": "NoneType", + "new_type": "dict", + "new_value": { + "id": pe_id, + "group": "test-group", + "formula": "1 * 2 + 3", + "exchange": edge.id, + }, + } + } + }, + } + ], + } + + projects.dataset.apply_revision(revision) + assert projects.dataset.revision == revision_id + + assert not num_revisions(projects) + + assert ParameterizedExchange.select().count() == 1 + pe = ParameterizedExchange.get(id=pe_id) + assert pe.group == "test-group" + assert pe.formula == "1 * 2 + 3" + assert pe.exchange == edge.id + + +@bw2test +def test_parameterized_exchange_revision_expected_format_update(num_revisions): + projects.set_current("activity-event") + + database = DatabaseChooser("db") + database.register() + node = database.new_node(code="A", name="A") + node.save() + other = database.new_node(code="B", name="B2", type="product") + other.save() + edge = node.new_edge(input=other, type="technosphere", amount=0.1, arbitrary="foo") + edge.save() + ActivityParameter.insert_dummy("test-group", (node["database"], node["code"])) + pe = ParameterizedExchange.create( + group="test-group", + exchange=edge.id, + formula="1 * 2 + 3", + ) + + assert projects.dataset.revision is None + projects.dataset.set_sourced() + + pe.formula = "7 / 3.141" + pe.save() + + assert num_revisions(projects) == 1 + + assert projects.dataset.revision is not None + with open(projects.dataset.dir / "revisions" / f"{projects.dataset.revision}.rev", "r") as f: + revision = json.load(f) + + expected = { + "metadata": { + "parent_revision": None, + "revision": projects.dataset.revision, + "authors": "Anonymous", + "title": "Untitled revision", + "description": "No description", + }, + "data": [ + { + "type": "parameterized_exchange", + "id": pe.id, + "change_type": "update", + "delta": {"values_changed": {"root['formula']": {"new_value": "7 / 3.141"}}}, + } + ], + } + + assert revision == expected + + +@bw2test +def test_parameterized_exchange_revision_apply_update(num_revisions): + projects.set_current("activity-event") + + database = DatabaseChooser("db") + database.register() + node = database.new_node(code="A", name="A") + node.save() + other = database.new_node(code="B", name="B2", type="product") + other.save() + edge = node.new_edge(input=other, type="technosphere", amount=0.1, arbitrary="foo") + edge.save() + ActivityParameter.insert_dummy("test-group", (node["database"], node["code"])) + pe = ParameterizedExchange.create( + group="test-group", + exchange=edge.id, + formula="1 * 2 + 3", + ) + + assert projects.dataset.revision is None + + revision_id = next(snowflake_id_generator) + revision = { + "metadata": { + "parent_revision": None, + "revision": revision_id, + "authors": "Anonymous", + "title": "Untitled revision", + "description": "No description", + }, + "data": [ + { + "type": "parameterized_exchange", + "id": pe.id, + "change_type": "update", + "delta": {"values_changed": {"root['formula']": {"new_value": "7 / 3.141"}}}, + } + ], + } + + projects.dataset.apply_revision(revision) + assert projects.dataset.revision == revision_id + + assert not num_revisions(projects) + + assert ParameterizedExchange.select().count() == 1 + dp = ParameterizedExchange.get(id=pe.id) + assert dp.formula == "7 / 3.141" + assert pe.group == "test-group" + assert pe.exchange == edge.id + + +@bw2test +def test_parameterized_exchange_revision_expected_format_delete(num_revisions): + projects.set_current("activity-event") + + database = DatabaseChooser("db") + database.register() + node = database.new_node(code="A", name="A") + node.save() + other = database.new_node(code="B", name="B2", type="product") + other.save() + edge = node.new_edge(input=other, type="technosphere", amount=0.1, arbitrary="foo") + edge.save() + ActivityParameter.insert_dummy("test-group", (node["database"], node["code"])) + pe = ParameterizedExchange.create( + group="test-group", + exchange=edge.id, + formula="1 * 2 + 3", + ) + + assert projects.dataset.revision is None + projects.dataset.set_sourced() + + pe.delete_instance() + + assert num_revisions(projects) == 1 + + assert projects.dataset.revision is not None + with open(projects.dataset.dir / "revisions" / f"{projects.dataset.revision}.rev", "r") as f: + revision = json.load(f) + + expected = { + "metadata": { + "parent_revision": None, + "revision": projects.dataset.revision, + "authors": "Anonymous", + "title": "Untitled revision", + "description": "No description", + }, + "data": [ + { + "type": "parameterized_exchange", + "id": pe.id, + "change_type": "delete", + "delta": { + "type_changes": { + "root": {"old_type": "dict", "new_type": "NoneType", "new_value": None} + } + }, + } + ], + } + + assert revision == expected + + +@bw2test +def test_parameterized_exchange_revision_apply_delete(num_revisions): + projects.set_current("activity-event") + + database = DatabaseChooser("db") + database.register() + node = database.new_node(code="A", name="A") + node.save() + other = database.new_node(code="B", name="B2", type="product") + other.save() + edge = node.new_edge(input=other, type="technosphere", amount=0.1, arbitrary="foo") + edge.save() + ActivityParameter.insert_dummy("test-group", (node["database"], node["code"])) + pe = ParameterizedExchange.create( + group="test-group", + exchange=edge.id, + formula="1 * 2 + 3", + ) + + revision_id = next(snowflake_id_generator) + revision = { + "metadata": { + "parent_revision": None, + "revision": revision_id, + "authors": "Anonymous", + "title": "Untitled revision", + "description": "No description", + }, + "data": [ + { + "type": "parameterized_exchange", + "id": pe.id, + "change_type": "delete", + "delta": { + "type_changes": { + "root": {"old_type": "dict", "new_type": "NoneType", "new_value": None} + } + }, + } + ], + } + + projects.dataset.apply_revision(revision) + assert projects.dataset.revision == revision_id + + assert not num_revisions(projects) + assert not ParameterizedExchange.select().count()