diff --git a/.issueflows/03-solved-issues/issue151_original.md b/.issueflows/03-solved-issues/issue151_original.md new file mode 100644 index 0000000..a900184 --- /dev/null +++ b/.issueflows/03-solved-issues/issue151_original.md @@ -0,0 +1,25 @@ +# Issue #151: CellMeta.uuid: cell-level join key for external metadata sources + +Source: https://github.com/cellpy/cellpy-core/issues/151 +Labels: enhancement +Captured: 2026-09-26 + +--- + +## Context + +jepegit/cellpy#784 (Epic M, external metadata sources) adds a `MetadataSource` Protocol and a persisted back-link (`ExternalLink`: `source_name`, `external_id`, `source_uri`) on the cellpy side. The design (cellpy-design-and-development `active/cellpy2-metadata-source-integration.md` §3.3, metadata-plan OQ6) also parks a **cell-level** id — `CellMeta.uuid` — next to the existing `TestMeta.uuid`, so a physical cell can be linked to a lab database record independently of any one test run. + +`CellMeta` lives in `cellpycore.metadata.models`, so this is a core-first change (then PyPI release, then cellpy re-pin per jepegit/cellpy Epic S). + +## Spec + +- Add `uuid: Optional[str] = None` to `CellMeta` (same conventions as `TestMeta.uuid`: minted by the consumer, never required, round-trips through `to_dict` / `from_dict` and the legacy mapping tables as a core-only field). +- Add `"uuid"` to `meta_mapping.CORE_ONLY_CELL` so `apply_test_meta_to_legacy` in cellpy skips it. +- Update `docs/specifications/harmonized-raw.md` CellMeta table. + +## Acceptance + +- `CellMeta(uuid="…")` round-trips through the metadata archive helpers. +- Existing cellpy tests that build `CellMeta()` are unaffected (default `None`). +- Release note entry; cellpy re-pin tracked in jepegit/cellpy. diff --git a/.issueflows/03-solved-issues/issue151_status.md b/.issueflows/03-solved-issues/issue151_status.md new file mode 100644 index 0000000..6e33ea2 --- /dev/null +++ b/.issueflows/03-solved-issues/issue151_status.md @@ -0,0 +1,18 @@ +# Status: #151 CellMeta.uuid + +- [x] Done + +## What was done (2026-09-26) + +- `CellMeta.uuid: Optional[str] = None` (docstring: cell-level id, consumer-minted, + distinct from `TestMeta.uuid`). +- `legacy.meta_mapping.CORE_ONLY_CELL` gains `"uuid"` so totality tests hold and + cellpy's `apply_test_meta_to_legacy` skips it. +- Spec table in `docs/specifications/harmonized-raw.md` (core-only CellMeta fields). +- Tests: JSON round-trip + core-only mapping; suite 289 passed, ruff clean. +- HISTORY.md `[Unreleased]` entry. + +## Follow-up (not here) + +PyPI release of cellpy-core and cellpy re-pin (jepegit/cellpy Epic S); cellpy's +BatBase adapter can then set `cell.uuid` from the journal row. diff --git a/HISTORY.md b/HISTORY.md index 16831ea..7a8ecde 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +- `CellMeta.uuid`: optional cell-level id (join key for external metadata sources), + core-only in the legacy mapping so `apply_test_meta_to_legacy` skips it. (#151) + ## [0.2.6] - 2026-09-08 - `update_core_data` treats an empty `new_raw` as a no-op (skips the derived refresh diff --git a/docs/specifications/harmonized-raw.md b/docs/specifications/harmonized-raw.md index 21a5b61..3a42496 100644 --- a/docs/specifications/harmonized-raw.md +++ b/docs/specifications/harmonized-raw.md @@ -152,6 +152,13 @@ geometry) and `CellpyMetaIndividualTest` (test-dependent: `channel_index`, `crea full field set; they can sit in `TestMeta` or a sibling `CellMeta` record (decision deferred — see the design note). +The `CellMeta` dataclass additionally carries two core-only fields legacy never had: + +| Field | Data type | Unit | Sample data | Comment | +| --- | --- | --- | --- | --- | +| uuid | str(36) | - | "0d1f3a7c-…" | stable, globally-unique id for the **physical cell**, independent of any test run (`TestMeta.uuid` identifies the run); minted by the consumer, e.g. to join the cell to a lab-database record (issue #151, jepegit/cellpy#784) | +| volume | float | cm**3 | 0.12 | cell / electrode volume for the volumetric specific-capacity mode (issue #117) | + ## Follow-ups - **step_types** - 'charge', 'discharge', 'rest' diff --git a/src/cellpycore/legacy/meta_mapping.py b/src/cellpycore/legacy/meta_mapping.py index 7a63f2d..f6657a5 100644 --- a/src/cellpycore/legacy/meta_mapping.py +++ b/src/cellpycore/legacy/meta_mapping.py @@ -141,8 +141,9 @@ } # Core fields legacy never had. Migration fills them from context (provenance -# stamped by the loading framework; ``volume`` is the new volumetric-mode field). -CORE_ONLY_CELL = frozenset({"volume"}) +# stamped by the loading framework; ``volume`` is the new volumetric-mode field; +# ``uuid`` is the cell-level join key for external metadata sources, #151). +CORE_ONLY_CELL = frozenset({"volume", "uuid"}) CORE_ONLY_TEST = frozenset( { "uuid", diff --git a/src/cellpycore/metadata/models.py b/src/cellpycore/metadata/models.py index 26e003e..b5ce81d 100644 --- a/src/cellpycore/metadata/models.py +++ b/src/cellpycore/metadata/models.py @@ -70,6 +70,11 @@ class CellMeta: ``CellMeta()`` is valid scaffolding and carries no obligation to be populated. Attributes: + uuid: Stable, globally-unique id for the physical cell, independent of + any one test run (``TestMeta.uuid`` identifies the run). Minted by + the consumer — typically to join the cell to a lab-database record + (external metadata sources, jepegit/cellpy#784) — never required + (issue #151). material: Active-material name / identifier. mass: Active-material mass. tot_mass: Total material mass. @@ -95,6 +100,7 @@ class CellMeta: comment: Free-text comment. """ + uuid: Optional[str] = None material: Optional[str] = None mass: Optional[float] = None tot_mass: Optional[float] = None diff --git a/tests/test_meta_mapping.py b/tests/test_meta_mapping.py index f0df2b9..6d0dd45 100644 --- a/tests/test_meta_mapping.py +++ b/tests/test_meta_mapping.py @@ -168,3 +168,10 @@ class LegacyCommon: cell_empty, test_empty = meta_mapping.legacy_meta_to_core(None, None) assert cell_empty == CellMeta() assert test_empty == TestMeta() + + +def test_cell_uuid_is_core_only(): + """``CellMeta.uuid`` is never filled from legacy meta (issue #151).""" + assert "uuid" in meta_mapping.CORE_ONLY_CELL + cell, _ = meta_mapping.legacy_meta_to_core({"mass": 1.0}, {}) + assert cell.uuid is None diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 5eb81c8..047d4fd 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -117,3 +117,16 @@ def test_merge_without_renumber_raises_on_collision(): def test_persistence_stubs_raise_not_implemented(call): with pytest.raises(NotImplementedError): call() + + +def test_cell_meta_uuid_round_trips_and_defaults_to_none(): + """CellMeta.uuid is a consumer-minted cell-level id (issue #151).""" + assert CellMeta().uuid is None + meta = TestMeta( + test_id=0, + uuid="test-run-uuid", + cell=CellMeta(uuid="0d1f3a7c-cell", mass=1.0), + ) + restored = from_json(TestMeta, to_json(meta)) + assert restored.cell.uuid == "0d1f3a7c-cell" + assert restored.uuid == "test-run-uuid" # test-run id stays separate