|
| 1 | +# Plan: Mixed Catalog with DataReference Type Registry |
| 2 | + |
| 3 | +## Status |
| 4 | + |
| 5 | +- ⬜ Phase 1 — `dvue/registry.py`: `DataReferenceTypeRegistry` singleton + `register_ref_type` decorator |
| 6 | +- ⬜ Phase 2 — `DataReference.from_dict` base classmethod (catalog.py) |
| 7 | +- ⬜ Phase 3 — `MathDataReference.from_dict` (math_reference.py) |
| 8 | +- ⬜ Phase 4 — Updated `DataCatalog.from_csv` with registry dispatch + auto-wire |
| 9 | +- ⬜ Phase 5 — `DataCatalog.save_mixed` / `load_mixed` (CSV + YAML sidecar) |
| 10 | +- ⬜ Phase 6 — `from_dict` on dsm2ui subclasses (DSM2DSSDataReference, DSM2TidefileDataReference, DSM2EchoInputDataReference, CalibDataReference) |
| 11 | +- ⬜ Phase 7 — `dsm2ui/registry.py` + `dsm2ui/__init__.py` auto-registration |
| 12 | +- ⬜ Phase 8 — Export `ref_type_registry`, `register_ref_type` from `dvue/__init__.py` |
| 13 | + |
| 14 | +--- |
| 15 | + |
| 16 | +## Design Decisions |
| 17 | + |
| 18 | +| Question | Decision | Rationale | |
| 19 | +|----------|----------|-----------| |
| 20 | +| Registry location | Separate `dvue/registry.py`, module-level singleton `ref_type_registry` | Cleaner separation from DataCatalog; avoids growing catalog.py further | |
| 21 | +| Instantiation contract | `@classmethod from_dict(cls, d)` on each registered class — explicit, no default | Makes each subclass's construction requirements explicit; no hidden kwargs magic | |
| 22 | +| Math ref in mixed CSV | Stub row in CSV (name, ref_type, expression, flat attrs) + full definition in `{stem}_math.yaml` sidecar | CSV stays flat/readable; YAML sidecar retains full nested search_map; UI table shows all refs in one place | |
| 23 | +| variable_map in CSV | Serialize as JSON `{varname: ref_name}` column; on load resolve as catalog name lookups | variable_map holds live objects; names are the only portable representation | |
| 24 | +| Catalog back-link | Auto-wire in `from_csv` and `load_mixed` — no manual step | Math refs are always useless without a catalog; silent failure otherwise | |
| 25 | +| dsm2ui registration | `dsm2ui/registry.py` called from `dsm2ui/__init__.py` on import | All types registered the moment dsm2ui is imported; no action required by callers | |
| 26 | +| Unregistered ref_type | Warn + fallback to base `DataReference` | Graceful degradation; existing CSV files with no ref_type column continue to work | |
| 27 | +| Save API | New `save_mixed(path)` / `load_mixed(path)` on DataCatalog, leave `to_csv`/`from_csv` unchanged | Avoids surprising behavior change on existing single-type catalogs | |
| 28 | + |
| 29 | +--- |
| 30 | + |
| 31 | +## Context |
| 32 | + |
| 33 | +dvue supports self-contained `DataReference` objects that retrieve data without reference to |
| 34 | +the catalog containing them. dsm2ui defines several subclasses (`DSM2DSSDataReference`, |
| 35 | +`DSM2TidefileDataReference`, etc.), each with a distinct `ref_type` string. |
| 36 | + |
| 37 | +The goal is to allow **mixed catalogs** containing refs of different `ref_type` values to be |
| 38 | +saved and loaded from `.csv` files, with the correct subclass instantiated on load. A |
| 39 | +lightweight registry in dvue maps `ref_type` strings to subclasses; downstream packages |
| 40 | +(dsm2ui) populate the registry at import time. |
| 41 | + |
| 42 | +`MathDataReference` adds a wrinkle: its `search_map` is a nested dict that cannot be stored |
| 43 | +flat in CSV. The chosen solution is a **sidecar YAML** (`{stem}_math.yaml`) written alongside |
| 44 | +the CSV, containing full math ref definitions. The CSV contains only a stub row for each math |
| 45 | +ref (name, ref_type, expression, display attributes) so the UI table remains complete. |
| 46 | + |
| 47 | +--- |
| 48 | + |
| 49 | +## Phase 1: dvue/registry.py (new file) |
| 50 | + |
| 51 | +**Contents** |
| 52 | +- `DataReferenceTypeRegistry` class: |
| 53 | + - `_registry: Dict[str, type]` |
| 54 | + - `register(ref_type, cls)` → None |
| 55 | + - `get(ref_type) → Optional[type]` |
| 56 | + - `instantiate(ref_type, row_dict) → DataReference` — calls `cls.from_dict(row_dict)`, |
| 57 | + emits `logging.warning` and falls back to `DataReference` if type unknown |
| 58 | + - `__contains__` |
| 59 | +- Module-level singleton: `ref_type_registry = DataReferenceTypeRegistry()` |
| 60 | +- Decorator: `register_ref_type(ref_type: str)` → class decorator that calls |
| 61 | + `ref_type_registry.register(ref_type, cls)` and returns cls |
| 62 | +- At bottom of file: lazy-import `DataReference` and `MathDataReference`, register |
| 63 | + `"raw"` → `DataReference`, `"math"` → `MathDataReference` |
| 64 | + |
| 65 | +**Circular import rule**: `catalog.py` and `math_reference.py` must NEVER import |
| 66 | +`registry.py` at module level. The registry is accessed only via lazy imports inside |
| 67 | +`from_csv` / `load_mixed` methods. |
| 68 | + |
| 69 | +--- |
| 70 | + |
| 71 | +## Phase 2: DataReference.from_dict base (catalog.py) |
| 72 | + |
| 73 | +Add `@classmethod from_dict(cls, d: dict) -> "DataReference"` to base `DataReference`: |
| 74 | +- Strip NaN values: `{k: v for k, v in d.items() if not (isinstance(v, float) and math.isnan(v))}` |
| 75 | +- Pop standard fields: `name`, `source`, `reader`, `ref_type` |
| 76 | +- Return `cls(source=source, reader=reader, name=name, **remaining)` |
| 77 | +- Serves as the fallback implementation for unregistered types |
| 78 | + |
| 79 | +--- |
| 80 | + |
| 81 | +## Phase 3: MathDataReference.from_dict (math_reference.py) |
| 82 | + |
| 83 | +Add `@classmethod from_dict(cls, d: dict) -> "MathDataReference"`: |
| 84 | +- Strip NaN values |
| 85 | +- Pop `name`, `expression`, `ref_type`, `source`, `reader` (source/reader unused for math) |
| 86 | +- Pop `variable_map_refs` JSON column if present → parse to `{varname: ref_name}` dict |
| 87 | + for later name-lookup wiring via catalog |
| 88 | +- Return `cls(expression=expression, name=name, **remaining)` |
| 89 | +- **search_map is NOT reconstructed here** — the stub CSV row is display-only; the full |
| 90 | + definition (with search_map) comes from the sidecar YAML via `load_mixed` |
| 91 | + |
| 92 | +--- |
| 93 | + |
| 94 | +## Phase 4: DataCatalog.from_csv updated (catalog.py) |
| 95 | + |
| 96 | +Replace the current `from_csv` body: |
| 97 | +- For each row, pop `ref_type` (default `"raw"`) |
| 98 | +- Lazy import: `from dvue.registry import ref_type_registry` |
| 99 | +- Look up `cls_for_type = ref_type_registry.get(ref_type)` → if None, warn + use `DataReference` |
| 100 | +- Call `ref = cls_for_type.from_dict(row_dict)` |
| 101 | +- After building catalog, auto-wire catalog into math refs: |
| 102 | + ```python |
| 103 | + for ref in catalog._references.values(): |
| 104 | + if hasattr(ref, 'set_catalog') and getattr(ref, '_catalog', None) is None: |
| 105 | + ref.set_catalog(catalog) |
| 106 | + ``` |
| 107 | + |
| 108 | +--- |
| 109 | + |
| 110 | +## Phase 5: DataCatalog.save_mixed / load_mixed (catalog.py) |
| 111 | + |
| 112 | +### save_mixed(self, path) |
| 113 | +1. Call `self.to_csv(path)` — all refs written (math refs emit stub rows automatically |
| 114 | + since `expression` is stored in `_attributes` and appears in `to_dict()`) |
| 115 | +2. Collect math refs: `[r for r in self._references.values() if r.ref_type == "math"]` |
| 116 | +3. If any exist: compute sidecar path `Path(path).with_name(Path(path).stem + '_math.yaml')` |
| 117 | + and call `save_math_refs()` from `math_reference.py` filtered to those refs |
| 118 | + |
| 119 | +### load_mixed(cls, path) — classmethod |
| 120 | +1. Detect sidecar: `Path(path).with_name(Path(path).stem + '_math.yaml')` |
| 121 | +2. If sidecar exists: load via `MathDataCatalogReader` → list of `MathDataReference` objects |
| 122 | + (full definitions with search_map) |
| 123 | +3. Load CSV with registry dispatch (updated `from_csv` logic) but **skip rows where |
| 124 | + ref_type="math"** when sidecar exists — they are replaced by the full YAML definitions |
| 125 | +4. Add YAML-loaded math refs to catalog |
| 126 | +5. Auto-wire catalog into all math refs (`ref.set_catalog(catalog)`) |
| 127 | +6. Return catalog |
| 128 | + |
| 129 | +--- |
| 130 | + |
| 131 | +## Phase 6: from_dict on dsm2ui subclasses |
| 132 | + |
| 133 | +All follow the same pattern: strip NaN, pop `name`/`source`/`reader`/`ref_type`, |
| 134 | +return `cls(source=..., reader=..., name=..., **remaining)`. |
| 135 | + |
| 136 | +| Class | File | |
| 137 | +|-------|------| |
| 138 | +| `DSM2DSSDataReference` | `dsm2ui/dsm2ui.py` | |
| 139 | +| `DSM2TidefileDataReference` | `dsm2ui/dsm2ui.py` | |
| 140 | +| `DSM2EchoInputDataReference` | `dsm2ui/dsm2ui.py` | |
| 141 | +| `CalibDataReference` | `dsm2ui/calib/calibplotui.py` | |
| 142 | + |
| 143 | +--- |
| 144 | + |
| 145 | +## Phase 7: dsm2ui auto-registration |
| 146 | + |
| 147 | +### dsm2ui/registry.py (new) |
| 148 | +- `_register_all()` function with lazy imports of the four subclasses + `ref_type_registry.register(...)` calls |
| 149 | +- Registers: `"dsm2_dss"`, `"dsm2_hdf5"`, `"dsm2_echo_input"`, `"calib"` |
| 150 | +- Called at module bottom so registration happens on first import |
| 151 | + |
| 152 | +### dsm2ui/__init__.py |
| 153 | +- Add at end: `from . import registry as _ref_registry # registers DataReference types` |
| 154 | + |
| 155 | +--- |
| 156 | + |
| 157 | +## Phase 8: dvue/__init__.py exports |
| 158 | + |
| 159 | +Add: |
| 160 | +```python |
| 161 | +from .registry import ref_type_registry, register_ref_type |
| 162 | +``` |
| 163 | +Add both to `__all__`. |
| 164 | + |
| 165 | +--- |
| 166 | + |
| 167 | +## Relevant Files |
| 168 | + |
| 169 | +| File | Change | |
| 170 | +|------|--------| |
| 171 | +| `dvue/dvue/registry.py` | **new** — global registry singleton + decorator | |
| 172 | +| `dvue/dvue/catalog.py` | `DataReference.from_dict`, updated `from_csv`, new `save_mixed`/`load_mixed` | |
| 173 | +| `dvue/dvue/math_reference.py` | `MathDataReference.from_dict` | |
| 174 | +| `dvue/dvue/__init__.py` | export `ref_type_registry`, `register_ref_type` | |
| 175 | +| `dsm2ui/dsm2ui/registry.py` | **new** — registers dsm2ui ref types | |
| 176 | +| `dsm2ui/dsm2ui/__init__.py` | `from . import registry as _ref_registry` | |
| 177 | +| `dsm2ui/dsm2ui/dsm2ui.py` | `from_dict` on 3 subclasses | |
| 178 | +| `dsm2ui/dsm2ui/calib/calibplotui.py` | `from_dict` on `CalibDataReference` | |
| 179 | + |
| 180 | +--- |
| 181 | + |
| 182 | +## Verification |
| 183 | + |
| 184 | +1. `DataCatalog.from_csv` with `ref_type="dsm2_dss"` rows after `import dsm2ui` → instances are `DSM2DSSDataReference` |
| 185 | +2. Unknown `ref_type` in CSV → warning emitted, falls back to base `DataReference` |
| 186 | +3. `catalog.save_mixed(path)` with a mixed catalog → CSV has stub math rows + `_math.yaml` sidecar created |
| 187 | +4. `DataCatalog.load_mixed(path)` round-trip → math refs have correct `search_map` from YAML, raw refs are correct subclasses, catalog back-link set |
| 188 | +5. `import dsm2ui; from dvue.registry import ref_type_registry; ref_type_registry._registry` → shows all 4 dsm2ui types plus built-ins |
| 189 | +6. `pytest dvue/tests/ dsm2ui/tests/` passes with no regressions |
| 190 | + |
| 191 | +--- |
| 192 | + |
| 193 | +## Follow-up Consideration |
| 194 | + |
| 195 | +`dssui`'s `DSSDataUIManager` currently uses the base `DataReference` (no subclass, no |
| 196 | +`ref_type`). If generic DSS catalogs should also benefit from registry dispatch, a |
| 197 | +`DSSDataReference` subclass would need to be added to `dssui/dssui.py`. Deferred until |
| 198 | +needed. |
0 commit comments