diff --git a/tests/test_tro_utils.py b/tests/test_tro_utils.py index fea30da..3f50c5d 100644 --- a/tests/test_tro_utils.py +++ b/tests/test_tro_utils.py @@ -620,6 +620,40 @@ def test_add_performance_mixed_strings_and_refs( assert contributed["trov:arrangement"]["@id"] == "arrangement/2" assert contributed["trov:boundTo"] == "/mnt/output" + def test_add_performance_extra_attrs( + self, temp_workspace, tmp_path, gpg_setup, trs_profile + ): + """Test that extra attributes passed to add_performance are included in the performance.""" + tro = create_tro_with_gpg( + filepath=str(tmp_path / "test_tro.jsonld"), + gpg_setup=gpg_setup, + profile=trs_profile, + ) + (temp_workspace / "input.txt").write_text("x") + tro.add_arrangement(str(temp_workspace), comment="A") + (temp_workspace / "output.txt").write_text("y") + tro.add_arrangement(str(temp_workspace), comment="B") + + tro.add_performance( + start_time=datetime.datetime(2024, 1, 1, 10, 0, 0), + end_time=datetime.datetime(2024, 1, 1, 11, 0, 0), + comment="with attrs", + accessed_arrangement="arrangement/0", + modified_arrangement="arrangement/1", + extra_attributes={"trov:customAttr": "customValue"}, + ) + perf = tro.data["@graph"][0]["trov:hasPerformance"][0] + assert perf["trov:customAttr"] == "customValue" + + tro.save() + tro = create_tro_with_gpg( + filepath=str(tmp_path / "test_tro.jsonld"), + gpg_setup=gpg_setup, + profile=trs_profile, + ) + perf = tro.data["@graph"][0]["trov:hasPerformance"][0] + assert perf["trov:customAttr"] == "customValue" + class TestTROSigning: """Test TRO signing and verification.""" diff --git a/tro_utils/models/performance.py b/tro_utils/models/performance.py index 6603138..938ef1f 100644 --- a/tro_utils/models/performance.py +++ b/tro_utils/models/performance.py @@ -92,6 +92,7 @@ class TrustedResearchPerformance(TROVModel): accessed_arrangements: list[ArrangementBinding] = field(default_factory=list) contributed_to_arrangements: list[ArrangementBinding] = field(default_factory=list) attributes: list[PerformanceAttribute] = field(default_factory=list) + extra_attributes: dict[str, Any] = field(default_factory=dict) # ------------------------------------------------------------------ # JSON-LD serialisation @@ -127,6 +128,8 @@ def to_jsonld(self) -> dict[str, Any]: result["trov:contributedToArrangement"] = [ ref.to_jsonld() for ref in self.contributed_to_arrangements ] + if self.extra_attributes: + result.update(self.extra_attributes) return result @classmethod @@ -157,6 +160,19 @@ def _parse_refs(value: Any) -> list[ArrangementBinding]: return [ArrangementBinding.from_jsonld(item) for item in value] return [ArrangementBinding.from_jsonld(value)] + known_keys = { + "@id", + "@type", + "rdfs:comment", + "trov:wasConductedBy", + "trov:hasPerformanceAttribute", + "trov:startedAtTime", + "trov:endedAtTime", + "trov:accessedArrangement", + "trov:contributedToArrangement", + } + extra = {k: v for k, v in data.items() if k not in known_keys} + return cls( performance_id=data["@id"], comment=data.get("rdfs:comment", ""), @@ -168,4 +184,5 @@ def _parse_refs(value: Any) -> list[ArrangementBinding]: data.get("trov:contributedToArrangement") ), attributes=attributes, + extra_attributes=extra, ) diff --git a/tro_utils/models/tro.py b/tro_utils/models/tro.py index ebf94d6..19eeb7b 100644 --- a/tro_utils/models/tro.py +++ b/tro_utils/models/tro.py @@ -280,10 +280,8 @@ def _normalise_refs(value: Any) -> list[tuple[str, str | None]]: attributes=performance_attributes, ) - # Merge extra_attributes as raw fields — stored as a side-channel dict - # via the JSON-LD round-trip (no typed model needed for forward compat) if extra_attributes: - trp._extra_attributes = extra_attributes # type: ignore[attr-defined] + trp.extra_attributes = extra_attributes self.performances.append(trp) return trp