Pipeline Specifications Developer Guide¶
This guide explains how to author, edit, and maintain pipeline smoke test rules, error scenarios, fixture manifests, and generated specification pages under specs/pipelines/.
1. Architecture and Directory Layout¶
Each pipeline family specification suite lives under specs/pipelines/<family>/ and is loaded by tests/pipeline_spec.py:
specs/pipelines/<family>/
├── rules.yaml # Rule catalogue (base rules + preset subclass rules)
├── spec/ # Generic base pipeline smoke test cases
│ ├── 01_<case_name>/
│ │ ├── case.yaml # Case manifest (title, rules, notes, runs)
│ │ ├── settings.toml # Optional case/run Settings overrides (.toml, .yaml, or .json)
│ │ ├── input/ # Staged input directory tree (files, subdirectories)
│ │ ├── expected_tables.py # EXPECTED_TABLES: dict[str, pa.Table] (including _rejected if applicable)
│ │ └── expected.json # Canonical JSON rows keyed by table name
│ └── 02_<error_or_edge_case>/
│ └── ...
└── presets/ # Domain-specific preset subclass smoke test cases
└── <preset_pipeline>/
├── 01_<case_name>/
│ ├── case.yaml # References both base rules (R-*) and preset rules
│ ├── settings.toml
│ ├── input/ # Domain fixtures (e.g., .xml.gz + .md5 sidecars)
│ ├── expected_tables.py
│ └── expected.json
└── 02_<error_or_edge_case>/
└── ...
The manifests in specs/pipelines/ drive two outputs:
- End-to-end smoke and unit specification tests:
tests/cdm_data_loaders/pipelines/test_pipeline_smoke_spec.py,tests/cdm_data_loaders/pipelines/xmltopyarrow/test_xmltopyarrow_spec.py,tests/cdm_data_loaders/pipelines/jsonlines/test_jsonlines_jsonschema_spec.py, andtests/cdm_data_loaders/pipelines/xsv/test_xsv_spec.py - Generated Markdown specification pages:
- XML-to-PyArrow Pipeline Specification (
xmltopyarrowbase pipeline +pubmedpreset subclass) - XML-to-Dict Pipeline Specification (
xmltodictpipeline) - JSONLines Extract Pipeline Specification (
jsonlines_extractpipeline) - JSONLines (JSON Schema) Pipeline Specification (
jsonlines_jsonschemabase pipeline +ncbi_datasetspreset subclass) - JSONLines (Pydantic) Pipeline Specification (
jsonlines_pydanticpipeline) - XSV (CSV/TSV) Schema and Pipeline Specification (
xsvpipeline)
2. How Pipeline Rules Are Created and Structured¶
Rules in specs/pipelines/<pipeline>/rules.yaml define the end-to-end behavioral contract of a pipeline across three stages:
- File Discovery and Pre-processing (for example,
P1-DISCOVERY,P1-MD5-SIDECAR,N1-ENTITY-DIRS): - Which files
input_dirandfile_globmatch. - How subdirectories map to entity tables.
- What file pre-processors (such as
validate_md5_sidecarincdm_data_loaders.core.preprocessors) verify before reading a file. - Parsing, Validation, Transforms and Coercion (for example,
P2-LEVEL-ROUTING,N2-JSONSCHEMA-VALIDATE,N3-FILTER-ANNOTATION): - How records are parsed and validated against XSD or JSON Schema.
- How
TABLE_TRANSFORMSfilter, mutate, or split records across tables. - How records are coerced to
OUTPUT_SCHEMASor precomputed PyArrow schemas (PUBMED_SCHEMAS,SCHEMAS). - Error Routing and Pipeline Reporting (for example,
P3-PREPROCESS-ERROR,P3-REPORT,N4-REJECTED,N4-REPORT): - How file pre-processing failures, JSON syntax errors, and schema validation errors populate
_rejected. - What
report.txtand_load_inforecord forstatus,ingested_files,load_errors, andpipeline_load_info_errs.
Rule Catalogue Format (rules.yaml)¶
title: PubMed XML-to-PyArrow Pipeline Specification
intro: |
End-to-end smoke test specification for the `pubmed` pipeline
(`cdm_data_loaders.pipelines.xmltopyarrow.pipeline:pubmed_cli`, `PubMedXmlToPyArrowSettings`).
sections:
- id: discovery_and_preprocessing
title: File Discovery and MD5 Pre-processing
rules:
- id: P1-DISCOVERY
name: Gzip XML discovery
rule: |
The pipeline scans `input_dir` for files matching `file_glob` (default `*.xml.gz`).
Bidirectional Rule Invariants¶
test_pipeline_smoke_spec.py enforces two invariants on rules.yaml and case.yaml:
- Every rule is exercised (
test_manifests_pass_every_rule_is_exercised): every rule ID inrules.yamlmust appear in therules:list of at least onecase.yamlacrossspec/orpresets/for that pipeline family. - Every referenced rule exists (
test_manifests_pass_every_case_rule_exists): every rule ID listed in anycase.yamlmust exist in that family'srules.yaml. - Preset subclasses inherit base Settings and runner (
test_manifests_pass_preset_subclasses_inherit_base_settings_and_runner): every preset pipeline inpresets/<preset>/must use aSettingsclass that subclasses the family's baseSettingsclass and must delegate to the samerun()callable.
3. How Pipeline Errors Are Modeled and Tested¶
Unlike schema converters (which raise exceptions at schema-translation time), KBase CTS ingestion pipelines run on the CTS (CDM Task Service) must exit with code 0 so the container service preserves output tables, _rejected rows, _load_info, and report.txt (see Core Pipeline and KBase CTS Execution Guide).
Pipeline error scenarios are modeled as case.yaml runs that verify three error outputs:
1. _rejected Table Rows¶
Failed files and failed records are routed to the _rejected table (REJECTED_PYARROW_SCHEMA in cdm_data_loaders.utils.buffer) with five columns:
| Column | File Pre-processor Failure | Record Parse / Validation Failure |
|---|---|---|
table_name |
Target table name or None (multi-table XML level mode) |
Entity table name (for example, "annotation_report") |
source_file |
Relative path of the rejected file | Relative path of the input file |
line_no |
None |
1-based line number in the input file |
raw_record |
None |
Raw unparsed text of the rejected line |
error_detail |
Pre-processor error message (for example, "MD5 mismatch for ...") |
JSON decode error string or JSON list of schema validation errors |
2. expected_report in case.yaml¶
Each run in case.yaml declares an expected_report block checked by assert_smoke_run_output():
runs:
- id: mismatched_md5
input_dir: input
settings_file: settings.toml
expected_tables: expected_tables.py
expected_json: expected.json
expected_counts:
_rejected: 1
expected_report:
status: completed with errors
ingested_files: []
load_errors:
- "File pre-processing failed for corrupt_sample.xml.gz: MD5 mismatch for corrupt_sample.xml.gz"
pipeline_load_info_errs: null
- File pre-processing errors (missing, empty, malformed, or mismatched
.md5sidecar): - Skip reading the failed file and omit it from
ingested_files. - Append
"File pre-processing failed for <file>: <reason>"toreport.txtload_errors. - Set
report.txtstatusto"completed with errors". - Write one provenance row per failed file to
_rejected. - Record parse and schema validation errors (malformed JSON lines or JSON Schema violations):
- Keep the file in
ingested_filesbecause the file itself passed pre-processing and was read. - Route valid records in the same file to their destination table(s) and invalid lines to
_rejected. - Keep
report.txtstatus: completedwhen no file pre-processor ordltload job failed.
4. Common Developer Workflows¶
Adding a New Rule and Smoke Test Case¶
- Add or select rule IDs in
specs/pipelines/<pipeline>/rules.yaml: - Add a new rule entry (
id,name,rule) under the appropriate section, or reuse existing rule IDs. - Create
specs/pipelines/<pipeline>/<NN_case_name>/: input/: Place small, deterministic input files underinput/.- When creating
.gzfiles or.md5sidecars forpubmed, compress withgzip.compress(data, mtime=0)so the gzip header timestamp is deterministic (0) and the MD5 digest never drifts.
- When creating
settings.toml: Add case-levelSettingsoverrides (for example,loader_file_format = "parquet"andbuffer_size = 10).expected_tables.py: DefineEXPECTED_TABLES: dict[str, pa.Table]using the pipeline's schemas (PUBMED_SCHEMAS,SCHEMAS, andREJECTED_PYARROW_SCHEMAwhen_rejectedhas rows).expected.json: Provide the canonical JSON representation of each expected table's rows (withnullfields omitted).case.yaml: Reference the rule IDs inrules:and define one or moreruns:withexpected_countsandexpected_report.- Regenerate the pipeline specification page:
- Verify fixture invariants, end-to-end execution, and documentation sync:
Editing Existing Rules, Cases, or Error Expectations¶
- Updating a rule description:
- Edit
specs/pipelines/<pipeline>/rules.yamland runuv run python -m scripts.gen_pipeline_spec. - Updating input fixtures or expected outputs:
- Update
input/,expected_tables.py,expected.json, andcase.yaml(expected_counts/expected_report) together. test_manifests_pass_expected_tables_validatechecks that everypa.Tableinexpected_tables.pypassestable.validate(full=True), matchesexpected_counts, and normalizes toexpected.json.- Run
uv run python -m scripts.gen_pipeline_specto refresh the embedded fixtures and expected JSON indocs/<pipeline>_specification.md.
Onboarding a New Pipeline to specs/pipelines/¶
- Ensure the pipeline is registered in
PIPELINE_TARGETS: - Check
PIPELINE_TARGETSintests/cdm_data_loaders/pipelines/framework.pysoresolve_pipeline("<pipeline>")resolves its CLI command, module path,Settingsclass, and runner function. - Create
specs/pipelines/<pipeline>/: - Add
rules.yamland at least one<case>/directory withcase.yaml,settings.toml,input/,expected_tables.py, andexpected.json. - Register the pipeline in
scripts/gen_pipeline_spec.py: - Add a
PipelineSpecConfigentry toPIPELINE_SPECSpointing tospecs/pipelines/<pipeline>anddocs/<pipeline>_specification.md. - Generate and index the specification page:
- Run
uv run python -m scripts.gen_pipeline_spec. - Add
docs/<pipeline>_specification.mdtozensical.toml([project].nav),docs/index.md, andREADME.md. - Run verification: