_stage_example
StageExample subclass for MEDS_extract’s raw-ingestion stages.
The upstream :class:MEDS_transforms.stages.examples.StageExample validates outputs as either
a :class:MEDSDataset (data/*.parquet shards) or a single metadata/codes.parquet file.
MEDS_extract’s early-pipeline stages don’t fit either mold:
convert_to_parquetwrites normalized parquets todata/<prefix>.parquet.split_and_shard_subjectswrites a JSON file atmetadata/.shards.json.convert_to_subject_sharded,convert_to_MEDS_events, andextract_code_metadataall write intermediate parquet files whose schemas aren’t MEDS-format.
MEDSExtractStageExample handles all of those by declaring inputs AND outputs as
:class:~pathlib.Path references to yaml_to_disk spec files. :meth:check_outputs
materializes out_data.yaml into a temp directory and compares every file in there to the
stage’s actual output — .parquet via :func:polars.testing.assert_frame_equal, .json
via parsed-dict equality, .yaml via parsed-dict equality. Subclasses of this class can add
more suffix handlers as needed.
MEDSExtractStageExample
dataclass
Bases: StageExample
StageExample variant that reads inputs/outputs as yaml_to_disk specs on disk.
Overrides:
- :meth:
is_example_dir: an example dir carries anout_data.yamlorout_metadata.yamlfile. - :meth:
from_dir: loadsin.yamlandout_data.yaml/out_metadata.yamlas :class:~pathlib.Pathreferences (never as parsed :class:MEDSDatasetobjects), loads optionalcfg.yaml+pipeline_cfg.yamlas plain dicts, and optional_test_cfg.yamlas keyword overrides. - :meth:
check_outputs: materializeswant_data/want_metadataviayaml_to_diskand compares each expected file to the actual output by suffix (.parquet→ :func:polars.testing.assert_frame_equal;.json,.yaml,.yml→ parsed-dict equality).
Wiring MESSY_config_fp and shards_map_fp happens through
pipeline_cfg.yaml: e.g. setting MESSY_config_fp: ${input_dir}/messy.yaml
there flows into the auto-generated pipeline YAML that MEDS_transform-stage reads.
Examples:
Construction requires exactly one of want_data / want_metadata:
>>> MEDSExtractStageExample(stage_name="ex")
Traceback (most recent call last):
...
ValueError: Either want_data or want_metadata must be provided.
>>> MEDSExtractStageExample(stage_name="ex", want_data=Path("a"), want_metadata=Path("b"))
Traceback (most recent call last):
...
ValueError: Either want_data or want_metadata must be provided, but not both.
The "." scenario name (used by singleton example dirs) is canonicalized to None,
and setting a pipeline_cfg implicitly sets do_use_config_yaml:
>>> ex = MEDSExtractStageExample(
... stage_name="ex", scenario_name=".", want_data=Path("a.yaml"),
... pipeline_cfg={"MESSY_config_fp": "x"},
... )
>>> print(ex.scenario_name)
None
>>> ex.do_use_config_yaml
True
The default :attr:df_check_kwargs is loose about dtypes and column order so that
yaml_to_disk-materialized expected frames (which can’t carry polars dtypes) diff
cleanly against actual parquet outputs:
>>> ex = MEDSExtractStageExample(stage_name="ex", want_data=Path("a.yaml"))
>>> ex.df_check_kwargs
{'check_dtypes': False, 'check_column_order': False}
Source code in MEDS_extract/_stage_example.py
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 | |
_collect_actual(root, top_dirs)
Walk top_dirs under root and return {rel: fp} for comparable files.
Skips :attr:_SKIP_DIRS and :attr:_SKIP_FILES so hydra logs and stage-byproduct
config copies don’t blow up the diff.
Source code in MEDS_extract/_stage_example.py
check_outputs(output_dir, is_resolved_dir=False)
Compare the stage’s actual output tree to the expected tree from want_data / want_metadata.
When is_resolved_dir is set (the pipeline_tester case), the caller has already
resolved output_dir to ${cohort}/<stage_name>/, so any leading data/ /
metadata/ segments in the expected paths are absorbed by that resolution and must
be stripped before the file-by-file diff.
In pipeline mode, some stages write to globally-shared pipeline paths rather than
their own stage_cfg.output_dir — e.g. split_and_shard_subjects writes
metadata/.shards.json via the pipeline-level shards_map_fp config, which by
convention lives at ${output_dir}/metadata/.shards.json (the cohort root). When
is_resolved_dir is set and a file isn’t found under output_dir, the search
falls back to output_dir.parent (the cohort root) with the full unstripped
relative path — matching the canonical MEDS_extract pipeline layout.
Source code in MEDS_extract/_stage_example.py
from_dir(stage_name, scenario_name, example_dir, **schema_updates)
classmethod
Load an example from <stage>/examples/<scenario>/.
Expected files (all optional except at least one of out_data.yaml / out_metadata.yaml):
in.yaml— yaml_to_disk spec materializing the stage’s input treeout_data.yaml— yaml_to_disk spec describing expecteddata/treeout_metadata.yaml— yaml_to_disk spec describing expectedmetadata/treecfg.yaml— stage-specific config (maps ontostage_cfg)pipeline_cfg.yaml— top-level pipeline overrides (e.g.MESSY_config_fp)_test_cfg.yaml— kwargs forwarded to the dataclass constructor (e.g.df_check_kwargs)
Source code in MEDS_extract/_stage_example.py
render_content(example_dir=None)
Override StageExample.render_content to render want_metadata as a YAML code block when it’s
a :class:~pathlib.Path.
The upstream default assumes want_metadata is a :class:polars.DataFrame and calls
df_to_markdown, which blows up on a path. For MEDS_extract stages whose outputs are
declared as out_metadata.yaml specs, we emit the YAML source under an
**Expected output metadata:** heading — mirroring the built-in want_data-as-path
handling for data stages.
Examples:
Metadata-Path examples get an **Expected output metadata:** block rendered
inline above the shell-invocation hint:
>>> with yaml_disk({"out_metadata.yaml": "metadata/codes.parquet:\n code: [HR]\n"}) as d:
... ex = MEDSExtractStageExample(stage_name="demo", want_metadata=d / "out_metadata.yaml")
... rendered = "\n".join(ex.render_content())
>>> "**Expected output metadata:**" in rendered
True
>>> "metadata/codes.parquet:" in rendered
True
>>> "MEDS_transform-stage" in rendered
True
When want_metadata isn’t a Path the upstream default applies unchanged:
>>> import polars as pl
>>> df = pl.DataFrame({"code": ["HR"], "description": ["Heart Rate"]})
>>> ex = MEDSExtractStageExample(stage_name="demo", want_metadata=df)
>>> "**Expected output metadata:**" in "\n".join(ex.render_content())
True
Source code in MEDS_extract/_stage_example.py
_assert_struct_equal(got, want, rel)
Compare two structured blobs (dicts/lists) recursively, treating dict values as unordered sets whenever they are lists of hashables at any nesting level.
Motivated by .shards.json — split_and_shard_subjects output where per-split subject
lists aren’t deterministically ordered run-to-run. The unordered-list semantics apply
recursively rather than only at the top level because nested pipeline-output YAMLs can
carry similarly non-deterministic lists (e.g. a future {split: {shard: [subjects]}}
layout). Plain dicts recurse; anything else is a strict == comparison.
Examples:
Hashable-list dict values compare order-independently:
Missing/extra keys raise:
>>> _assert_struct_equal({"a": 1, "b": 2}, {"a": 1}, Path("shards.json"))
Traceback (most recent call last):
...
AssertionError: shards.json: key mismatch.
Missing: []
Extra: ['b']
Differing hashable-list contents raise with both sides sorted for readability:
>>> _assert_struct_equal({"x": [1, 2]}, {"x": [1, 3]}, Path("shards.json"))
Traceback (most recent call last):
...
AssertionError: shards.json['x']: contents differ (unordered).
Got: [1, 2]
Want: [1, 3]
Nested dicts recurse:
Non-dict scalar mismatch raises:
>>> _assert_struct_equal("foo", "bar", Path("x.json"))
Traceback (most recent call last):
...
AssertionError: x.json: differs.
Got: 'foo'
Want: 'bar'
Source code in MEDS_extract/_stage_example.py
_compare(expected_fp, actual_fp, rel, df_check_kwargs)
Per-file comparison dispatcher keyed off the expected file’s suffix.
.parquet uses :func:polars.testing.assert_frame_equal; .json / .yaml /
.yml parse both sides and compare the resulting Python objects. The caller is
responsible for filtering to supported suffixes; reaching the _ branch is a
programming error.
Examples:
Matching parquets pass silently:
>>> with yaml_disk({"a.parquet": {"x": [1, 2]}, "b.parquet": {"x": [1, 2]}}) as d:
... _compare(d / "a.parquet", d / "b.parquet", Path("x.parquet"), {})
Differing parquets raise with the rel path and both frames in the message:
>>> with yaml_disk({"a.parquet": {"x": [1, 2]}, "b.parquet": {"x": [1, 999]}}) as d:
... _compare(d / "a.parquet", d / "b.parquet", Path("shard/0.parquet"), {})
Traceback (most recent call last):
...
AssertionError: Parquet shard/0.parquet differs...
JSON comparison uses the recursive struct-equal helper (so e.g. .shards.json
hashable-list values diff order-independently):
>>> import json, tempfile
>>> with tempfile.TemporaryDirectory() as d:
... a, b = Path(d) / "a.json", Path(d) / "b.json"
... _ = a.write_text(json.dumps({"train/0": [1, 2]}))
... _ = b.write_text(json.dumps({"train/0": [2, 1]}))
... _compare(a, b, Path("shards.json"), {})
YAML files dispatch through the same struct-equal helper:
>>> with yaml_disk({"a.yaml": "foo: 1\n", "b.yaml": "foo: 1\n"}) as d:
... _compare(d / "a.yaml", d / "b.yaml", Path("cfg.yaml"), {})
>>> with yaml_disk({"a.yaml": "foo: 1\n", "b.yaml": "foo: 2\n"}) as d:
... _compare(d / "a.yaml", d / "b.yaml", Path("cfg.yaml"), {})
Traceback (most recent call last):
...
AssertionError: cfg.yaml: differs.
Got: 2
Want: 1
Unsupported suffixes raise — the caller should have filtered these out:
>>> with yaml_disk({"a.txt": "hello", "b.txt": "hello"}) as d:
... _compare(d / "a.txt", d / "b.txt", Path("x.txt"), {})
Traceback (most recent call last):
...
AssertionError: Unsupported output suffix '.txt' for x.txt.
Source code in MEDS_extract/_stage_example.py
296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 | |
_is_hashable_list(v)
True iff v is a :class:list of hashable elements.
Used by :func:_assert_struct_equal to decide whether a dict value can be compared
order-independently via a set.
Examples:
>>> _is_hashable_list([1, 2, 3])
True
>>> _is_hashable_list(["a", "b"])
True
>>> _is_hashable_list([])
True
>>> _is_hashable_list([[1, 2], [3]]) # inner lists are unhashable
False
>>> _is_hashable_list("not a list")
False
>>> _is_hashable_list({"x": 1})
False