Module layout¶
The target, once the split below is done:
datarecord/ # the standalone concept
├── schema.py # Dimension, AttributeSpec, Schema
├── record.py # Record, Frames, LazyFrames, Flags
├── mutable.py # WorkingRecord, the edit/commit path
├── layered/ # Record and its resolution
│ ├── revision.py # Revision, the node tree
│ ├── resolve.py # owner-map fold
│ ├── sources.py # LayerSource: where a layer's files are
│ └── write.py # write_record
└── duck.py # connection setup, path derivation
"Depends on duckdb, narwhals and pydantic, and on nothing else" is achieved at this layer — datarecord/tools/ is where a framework-specific tool lives instead (below), and nothing under it is imported by the package's own __init__.py.
The protocols live with their implementations rather than with any one consumer, because there are several: a tool both implements and consumes RecordLike, WorkingRecord satisfies it, and write_record takes a LayerData — the write-side protocol a source and a resolver share — wrapping a RecordLike in an adapter where a tool hands one over instead.
A tool lives outside this core, under datarecord/tools/<name>.py — one module per modelling framework, imported explicitly (from datarecord.tools.pypsa import PyPSA) rather than through datarecord.tools itself, which imports none of them.
What decides the side is one question: does it name a modelling framework? Nothing in datarecord's core may.
The dependency runs strictly one way, so importing the record layer pulls in no framework, and importing one tool pulls in no other.