JSONLines (Pydantic) Pipeline Specification¶
This document specifies how an ENTITY_MODELS registry of Pydantic BaseModel classes and the
JSONLines (Pydantic) ingestion pipeline
(cdm_data_loaders.pipelines.jsonlines.extract_pydantic_validate_pipeline,
cdm_data_loaders.pipelines.jsonlines.settings.JsonlPydanticIngestSettings, and
cdm_data_loaders.converters.facades.pydantic_to_pyarrow) determine:
- Entity model registration and upfront PyArrow schema generation (
load_registry_dict,build_output_schema_registry,PydanticToPyArrow). - Single-pass Pydantic validation,
model_dump(mode="python")coercion, and dead-letter (_rejected) routing (_make_validator_callbacks,make_page_router,REJECTED_PYARROW_SCHEMA). - Multi-entity directory discovery, MD5 sidecar pre-processing, table transforms, and reporting (
build_entity_resource,run_jsonlines_with_validation_pipeline).
| Property | Value |
|---|---|
| Base Pipeline Family | jsonlines_pydantic |
| Base CLI Entry Point | jsonlines_pydantic |
| Base Settings Class | cdm_data_loaders.pipelines.jsonlines.settings.JsonlPydanticIngestSettings |
| Pipeline Runner | cdm_data_loaders.pipelines.jsonlines.extract_pydantic_validate_pipeline.run_jsonlines_with_validation_pipeline |
| Preset Subclasses Covered | — |
| Specification Directory | specs/pipelines/jsonlines_pydantic/ |
Running Smoke Tests Across Environments¶
# Run all base and preset smoke tests for jsonlines_pydantic across built-in environments
uv run pytest tests/cdm_data_loaders/pipelines/test_pipeline_smoke_spec.py --smoke-pipeline jsonlines_pydantic
# Run against a custom environment directory (config.toml, secrets.toml, settings.toml)
uv run pytest tests/cdm_data_loaders/pipelines/test_pipeline_smoke_spec.py \
--smoke-pipeline jsonlines_pydantic \
--dlt-config-dir /path/to/env_dlt_config \
--settings-file /path/to/env_dlt_config/settings.toml
1. Conversion & Pipeline Rules¶
1.1 Entity Model Registry and PyArrow Schema Generation¶
- R-JSONL-PYDANTIC-ENTITY-REGISTRY:
JsonlPydanticIngestSettingsloadsENTITY_MODELSfrom the module named byentity_models(-m/--entity-models) viaload_registry_dict(self.entity_models, "ENTITY_MODELS")(ModelRegistry). Every key must be a non-empty table name (excluding the reserved_rejectedtable) and every value must be apydantic.BaseModelsubclass.
Cases: spec/01_multi_entity_validation_and_rejections
- R-JSONL-PYDANTIC-SCHEMA-CONVERT: When
use_pyarrow=True(default),build_output_schema_registryconverts each entityBaseModelinENTITY_MODELSto apa.Schemaup front viaPydanticToPyArrow().convert(model)per the scalar mapping table below and thepydantic_to_pyarrowspecification.
Cases: spec/01_multi_entity_validation_and_rejections
- R-JSONL-PYDANTIC-SCHEMA-SKIP: Automatic
PydanticToPyArrowconversion is skipped for any entity table that has a registered transform inTABLE_TRANSFORMS(transforms_module) or an explicit PyArrow schema override inOUTPUT_SCHEMAS(output_schemas_module), allowing transforms to reshape validated models before PyArrow coercion.
Cases: spec/03_table_transforms_and_output_schemas
| Pydantic type / annotation | Python type (coerced) | PyArrow DataType |
metadata["pydantic_type"] |
|---|---|---|---|
bool, StrictBool |
bool |
pa.bool_() |
boolean |
int, StrictInt, PositiveInt, NegativeInt, NonNegativeInt, NonPositiveInt |
int |
pa.int64() |
integer |
float, StrictFloat, PositiveFloat, NegativeFloat, NonNegativeFloat, NonPositiveFloat |
float |
pa.float64() |
number |
str, StrictStr, SecretStr, EmailStr, NameEmail, AnyUrl, HttpUrl, Base64Str |
str |
pa.string() |
string |
Decimal (unconstrained) |
float |
pa.float64() |
number |
Annotated[Decimal, Field(max_digits=10, decimal_places=2)] |
decimal.Decimal |
pa.decimal128(10, 2) |
decimal |
datetime.date, PastDate, FutureDate |
datetime.date |
pa.date32() |
date |
datetime.datetime, AwareDatetime, PastDatetime, FutureDatetime |
datetime.datetime (UTC) |
pa.timestamp("us", tz="UTC") |
date-time |
NaiveDatetime |
datetime.datetime (naive) |
pa.timestamp("us") |
date-time-naive |
datetime.time |
datetime.time |
pa.time64("us") |
time |
datetime.timedelta |
str |
pa.string() |
duration |
UUID, UUID1..UUID5 |
str |
pa.string() |
uuid |
Path, PurePath, FilePath, DirectoryPath, NewPath |
str |
pa.string() |
path |
IPv4Address, IPv6Address, IPv4Network, IPv6Network, IPvAnyAddress, IPvAnyInterface, IPvAnyNetwork |
str |
pa.string() |
ip |
bytes, bytearray, SecretBytes |
bytes |
pa.binary() |
binary |
Base64Bytes |
bytes |
pa.binary() |
binary (base64) |
StrEnum, Enum (str values) |
str |
pa.string() |
string |
IntEnum, Enum (int values) |
int |
pa.int64() |
integer |
Literal[1, 2] |
int |
pa.int64() |
integer |
Literal[1, 2.5] |
float |
pa.float64() |
integer \| number |
Literal[1, "two", True] |
str |
pa.string() |
integer \| string \| boolean |
int \| float |
float |
pa.float64() |
integer \| number |
int \| str |
str |
pa.string() |
integer \| string |
str \| None |
str |
pa.string() |
string |
1.2 Single-Pass Record Validation, Dumping, and Dead-Letter Routing¶
- R-JSONL-PYDANTIC-VALIDATE-COERCE:
_make_validator_callbacksvalidates each non-blank JSON object at most once viamodel.model_validate(record).model_dump(mode="python"), caching the dumped Python dictionary across_validateand_dump_validatedso default values, type coercions, and nested models are materialized before routing intoDictBuffer(schema_map=settings.schema_map).
Cases: spec/01_multi_entity_validation_and_rejections, spec/03_table_transforms_and_output_schemas
- R-JSONL-PYDANTIC-BLANK-AND-REJECT: Blank or whitespace-only lines are skipped without error while preserving 1-based
line_nooffsets. Lines that failjson.loads(error_detail=str(e)) bypass Pydantic validation, and lines that failmodel.model_validate(error_detail=json.dumps(e.errors(), default=str)) are routed to_rejected(REJECTED_PYARROW_SCHEMA) withtable_name=<entity>,source_filerelative toinput_dir, 1-basedline_no, and strippedraw_record.
Cases: spec/01_multi_entity_validation_and_rejections
| Column | PyArrow DataType |
Field.nullable |
Description |
|---|---|---|---|
table_name |
pa.string() |
True |
Target entity table name (None for level-mode file pre-processor errors) |
source_file |
pa.string() |
True |
Input file path relative to input_dir |
line_no |
pa.int64() |
True |
1-based physical line number in source_file (None for file-level errors) |
raw_record |
pa.string() |
True |
Stripped raw input line (None for file-level errors) |
error_detail |
pa.string() |
True |
Parser error message, JSON-encoded list of schema errors, or pre-processor error |
1.3 Multi-Entity Discovery, Pre-processing, Transforms, and Reporting¶
- R-JSONL-PYDANTIC-MULTI-ENTITY-GLOB:
run_jsonlines_with_validation_pipelinebuilds one resource per entity insorted(ENTITY_MODELS)scanning<input_dir>/<table_name>/<settings.file_glob>(default*.jsonl*) and transparently decompressing.gzfiles. Entities whose<input_dir>/<table_name>directory does not exist produce zero rows without error.
Cases: spec/01_multi_entity_validation_and_rejections
- R-JSONL-PYDANTIC-PREPROCESS-MD5: Before reading an entity file,
build_filesystem_resourcerunssettings.get_file_preprocessors(table_name)(frompreprocessors_module, e.g.validate_md5_sidecar). When a pre-processor raisesFilePreprocessError, the file is skipped, omitted fromingested_files, recorded inreport.txtload_errors(status: completed with errors), and routed to_rejectedwithtable_name=<entity>,source_file=<entity>/<file>,line_no=None, andraw_record=None.
Cases: spec/02_md5_sidecar_preprocessing
- R-JSONL-PYDANTIC-TRANSFORMS: When
transforms_module(TABLE_TRANSFORMS) andoutput_schemas_module(OUTPUT_SCHEMAS) are configured, validatedmodel_dump(mode="python")records are routed throughroute_and_coerce_record, allowing transforms to filter rows ({}/None), compute derived columns, or fan out records across multiple destination tables coerced to theirOUTPUT_SCHEMAS.
Cases: spec/03_table_transforms_and_output_schemas
- R-JSONL-PYDANTIC-REPORT: All entity resources execute in a single
pipeline.run(resources)call withmax_table_nesting=0and writereport.txt(status,ingested_files,load_errors,pipeline_load_info_errs) and_load_info.
Cases: spec/01_multi_entity_validation_and_rejections, spec/02_md5_sidecar_preprocessing, spec/03_table_transforms_and_output_schemas
2. Case Coverage Matrix¶
| Suite | Target CLI | Settings Class | Case | Title | Rules | Expected Table Counts |
|---|---|---|---|---|---|---|
spec |
jsonlines_pydantic |
JsonlPydanticIngestSettings |
01_multi_entity_validation_and_rejections |
Multi-Entity Validation, Model Coercion, Gzip Decompression, and _rejected Routing |
R-JSONL-PYDANTIC-ENTITY-REGISTRY, R-JSONL-PYDANTIC-SCHEMA-CONVERT, R-JSONL-PYDANTIC-VALIDATE-COERCE, R-JSONL-PYDANTIC-BLANK-AND-REJECT, R-JSONL-PYDANTIC-MULTI-ENTITY-GLOB, R-JSONL-PYDANTIC-REPORT |
default: {'instruments': 1, 'samples': 2, '_rejected': 2} |
spec |
jsonlines_pydantic |
JsonlPydanticIngestSettings |
02_md5_sidecar_preprocessing |
MD5 Sidecar Pre-processing and File-Level _rejected Routing |
R-JSONL-PYDANTIC-PREPROCESS-MD5, R-JSONL-PYDANTIC-REPORT |
partial_md5_failure: {'samples': 1, '_rejected': 1} |
spec |
jsonlines_pydantic |
JsonlPydanticIngestSettings |
03_table_transforms_and_output_schemas |
Table Transforms, Schema Conversion Skip, Multi-Table Fan-Out, and Output Schemas | R-JSONL-PYDANTIC-SCHEMA-SKIP, R-JSONL-PYDANTIC-VALIDATE-COERCE, R-JSONL-PYDANTIC-TRANSFORMS, R-JSONL-PYDANTIC-REPORT |
default: {'assays': 2, 'assay_metrics': 3} |
3. Base Pipeline Specification Cases (jsonlines_pydantic / spec/)¶
Shared Fixture Module (specs/pipelines/jsonlines_pydantic/sample_registry.py)
"""Entity Pydantic model registry for JSONLines (Pydantic) specification cases."""
from typing import Final
from pydantic import BaseModel, Field
class SampleModel(BaseModel):
"""Validated biological sample record."""
sample_id: str
ph: float
biome: str = "terrestrial"
tags: list[str] = Field(default_factory=list)
class InstrumentModel(BaseModel):
"""Sequencing instrument metadata record."""
instrument_id: str
platform: str
active: bool = True
ENTITY_MODELS: Final[dict[str, type[BaseModel]]] = {
"samples": SampleModel,
"instruments": InstrumentModel,
"unpopulated": InstrumentModel,
}
Shared Fixture Module (specs/pipelines/jsonlines_pydantic/transforms.py)
"""Entity models, file pre-processors, table transforms, and output schemas for JSONLines (Pydantic) spec cases."""
from typing import Any, Final
import pyarrow as pa
from pydantic import BaseModel, Field
from cdm_data_loaders.core.preprocessors import validate_md5_sidecar
from cdm_data_loaders.core.registry import FilePreprocessFn, RecordTransformFn
NEUTRAL_PH: Final[float] = 7.0
FILE_PREPROCESSORS: tuple[FilePreprocessFn, ...] = (validate_md5_sidecar,)
class AssayModel(BaseModel):
"""Input assay record validated by Pydantic before table transformation."""
assay_id: str
ph: float
exclude: bool = False
metrics: list[str] = Field(default_factory=list)
ASSAY_SUMMARY_SCHEMA: Final[pa.Schema] = pa.schema(
[
pa.field("assay_id", pa.string(), nullable=False),
pa.field("ph", pa.float64(), nullable=False),
pa.field("ph_bucket", pa.string(), nullable=False),
]
)
ASSAY_METRIC_SCHEMA: Final[pa.Schema] = pa.schema(
[
pa.field("assay_id", pa.string(), nullable=False),
pa.field("metric", pa.string(), nullable=False),
]
)
def transform_assay(row: dict[str, Any]) -> dict[str, list[dict[str, Any]]]:
"""Drop excluded assays, classify pH bucket, and fan out metrics to ``assay_metrics``.
:param row: Validated and dumped ``AssayModel`` dictionary.
:type row: dict[str, Any]
:return: Mapping of target table names to lists of output row dictionaries.
:rtype: dict[str, list[dict[str, Any]]]
"""
if row.get("exclude") is True:
return {}
assay_id = str(row["assay_id"]).upper()
ph = float(row["ph"])
ph_bucket = "acidic" if ph < NEUTRAL_PH else "neutral_or_basic"
routed: dict[str, list[dict[str, Any]]] = {
"assays": [
{
"assay_id": assay_id,
"ph": ph,
"ph_bucket": ph_bucket,
}
]
}
metrics = row.get("metrics") or []
if metrics:
routed["assay_metrics"] = [{"assay_id": assay_id, "metric": str(m)} for m in metrics]
return routed
ENTITY_MODELS: Final[dict[str, type[BaseModel]]] = {
"assays": AssayModel,
}
TABLE_TRANSFORMS: Final[dict[str, RecordTransformFn]] = {
"assays": transform_assay,
}
OUTPUT_SCHEMAS: Final[dict[str, pa.Schema]] = {
"assays": ASSAY_SUMMARY_SCHEMA,
"assay_metrics": ASSAY_METRIC_SCHEMA,
}
Multi-Entity Validation, Model Coercion, Gzip Decompression, and _rejected Routing (spec/01_multi_entity_validation_and_rejections)¶
Suite: spec · Target CLI: jsonlines_pydantic · Settings Class: JsonlPydanticIngestSettings
Directory: specs/pipelines/jsonlines_pydantic/spec/01_multi_entity_validation_and_rejections/
Rules: R-JSONL-PYDANTIC-ENTITY-REGISTRY, R-JSONL-PYDANTIC-SCHEMA-CONVERT, R-JSONL-PYDANTIC-VALIDATE-COERCE, R-JSONL-PYDANTIC-BLANK-AND-REJECT, R-JSONL-PYDANTIC-MULTI-ENTITY-GLOB, R-JSONL-PYDANTIC-REPORT
entity_models = "specs.pipelines.jsonlines_pydantic.sample_registry"registerssamples(SampleModel),instruments(InstrumentModel), andunpopulated(InstrumentModel).samples/samples.jsonlexercisesmodel_validate(...).model_dump(mode="python")(coercing"ph": "6.5"to6.5and populating defaultbiome = "terrestrial"), skips a blank line at line 2, and routes a JSON syntax error (line 3) and a PydanticValidationError(line 4) to_rejected.instruments/instruments.jsonl.gzis transparently decompressed and loaded intoinstruments, while the absentunpopulated/directory produces zero rows without error.
Settings (specs/pipelines/jsonlines_pydantic/spec/01_multi_entity_validation_and_rejections/settings.toml)
dataset_name = "jsonlines_pydantic_spec"
entity_models = "specs.pipelines.jsonlines_pydantic.sample_registry"
loader_file_format = "parquet"
use_output_dir_for_pipeline_metadata = true
Input instruments/instruments.jsonl.gz (specs/pipelines/jsonlines_pydantic/spec/01_multi_entity_validation_and_rejections/input/instruments/instruments.jsonl.gz) (decompressed preview)
Input samples/samples.jsonl (specs/pipelines/jsonlines_pydantic/spec/01_multi_entity_validation_and_rejections/input/samples/samples.jsonl)
{"sample_id": "s-1", "ph": "6.5", "tags": ["soil", "core"]}
{"sample_id": "s-bad-json", "ph": }
{"sample_id": "s-bad-model", "ph": "not-a-float"}
{"sample_id": "s-2", "ph": 7.8, "biome": "marine", "tags": ["water"]}
Expected report.txt Summary & Row Counts ({'instruments': 1, 'samples': 2, '_rejected': 2})
status: completed
ingested_files:
- instruments/instruments.jsonl.gz
- samples/samples.jsonl
load_errors: []
pipeline_load_info_errs: null
Expected PyArrow Tables (specs/pipelines/jsonlines_pydantic/spec/01_multi_entity_validation_and_rejections/expected_tables.py — EXPECTED_TABLES)
"""Expected PyArrow tables for jsonlines_pydantic 01_multi_entity_validation_and_rejections."""
import json
import pyarrow as pa
from pydantic import ValidationError
from cdm_data_loaders.converters.facades.pydantic_to_pyarrow import PydanticToPyArrow
from cdm_data_loaders.utils.buffer import REJECTED_PYARROW_SCHEMA
from specs.pipelines.jsonlines_pydantic.sample_registry import InstrumentModel, SampleModel
CONVERTER = PydanticToPyArrow()
SAMPLES_SCHEMA = CONVERTER.convert(SampleModel)
INSTRUMENTS_SCHEMA = CONVERTER.convert(InstrumentModel)
def _sample_validation_error() -> str:
try:
SampleModel.model_validate({"sample_id": "s-bad-model", "ph": "not-a-float"})
except ValidationError as exc:
return json.dumps(exc.errors(), default=str)
return ""
EXPECTED_TABLES: dict[str, pa.Table] = {
"samples": pa.Table.from_pylist(
[
{"sample_id": "s-1", "ph": 6.5, "biome": "terrestrial", "tags": ["soil", "core"]},
{"sample_id": "s-2", "ph": 7.8, "biome": "marine", "tags": ["water"]},
],
schema=SAMPLES_SCHEMA,
),
"instruments": pa.Table.from_pylist(
[
{"instrument_id": "inst-1", "platform": "Illumina", "active": True},
],
schema=INSTRUMENTS_SCHEMA,
),
"_rejected": pa.Table.from_pylist(
[
{
"table_name": "samples",
"source_file": "samples/samples.jsonl",
"line_no": 3,
"raw_record": '{"sample_id": "s-bad-json", "ph": }',
"error_detail": "Expecting value: line 1 column 35 (char 34)",
},
{
"table_name": "samples",
"source_file": "samples/samples.jsonl",
"line_no": 4,
"raw_record": '{"sample_id": "s-bad-model", "ph": "not-a-float"}',
"error_detail": _sample_validation_error(),
},
],
schema=REJECTED_PYARROW_SCHEMA,
),
}
Expected Canonical Table Rows (specs/pipelines/jsonlines_pydantic/spec/01_multi_entity_validation_and_rejections/expected.json)
{
"_rejected": [
{
"error_detail": "Expecting value: line 1 column 35 (char 34)",
"line_no": 3,
"raw_record": "{\"sample_id\": \"s-bad-json\", \"ph\": }",
"source_file": "samples/samples.jsonl",
"table_name": "samples"
},
{
"error_detail": "[{\"type\": \"float_parsing\", \"loc\": [\"ph\"], \"msg\": \"Input should be a valid number, unable to parse string as a number\", \"input\": \"not-a-float\", \"url\": \"https://errors.pydantic.dev/2.14/v/float_parsing\"}]",
"line_no": 4,
"raw_record": "{\"sample_id\": \"s-bad-model\", \"ph\": \"not-a-float\"}",
"source_file": "samples/samples.jsonl",
"table_name": "samples"
}
],
"instruments": [
{
"active": true,
"instrument_id": "inst-1",
"platform": "Illumina"
}
],
"samples": [
{
"biome": "terrestrial",
"ph": 6.5,
"sample_id": "s-1",
"tags": [
"soil",
"core"
]
},
{
"biome": "marine",
"ph": 7.8,
"sample_id": "s-2",
"tags": [
"water"
]
}
]
}
MD5 Sidecar Pre-processing and File-Level _rejected Routing (spec/02_md5_sidecar_preprocessing)¶
Suite: spec · Target CLI: jsonlines_pydantic · Settings Class: JsonlPydanticIngestSettings
Directory: specs/pipelines/jsonlines_pydantic/spec/02_md5_sidecar_preprocessing/
Rules: R-JSONL-PYDANTIC-PREPROCESS-MD5, R-JSONL-PYDANTIC-REPORT
preprocessors_module = "specs.pipelines.jsonlines_pydantic.transforms"registersvalidate_md5_sidecar.samples/good.jsonlmatchessamples/good.jsonl.md5and loads intosamples.samples/bad.jsonlfails MD5 validation againstsamples/bad.jsonl.md5; the file is skipped before line streaming, omitted fromingested_files, recorded inreport.txtload_errors(status: completed with errors), and emitted as a file-level row (line_no=None,raw_record=None) in_rejected.
Settings (specs/pipelines/jsonlines_pydantic/spec/02_md5_sidecar_preprocessing/settings.toml)
dataset_name = "jsonlines_pydantic_spec"
entity_models = "specs.pipelines.jsonlines_pydantic.sample_registry"
file_glob = "*.jsonl"
preprocessors_module = "specs.pipelines.jsonlines_pydantic.transforms"
loader_file_format = "parquet"
use_output_dir_for_pipeline_metadata = true
Input samples/bad.jsonl (specs/pipelines/jsonlines_pydantic/spec/02_md5_sidecar_preprocessing/input/samples/bad.jsonl)
Input samples/bad.jsonl.md5 (specs/pipelines/jsonlines_pydantic/spec/02_md5_sidecar_preprocessing/input/samples/bad.jsonl.md5)
Input samples/good.jsonl (specs/pipelines/jsonlines_pydantic/spec/02_md5_sidecar_preprocessing/input/samples/good.jsonl)
Input samples/good.jsonl.md5 (specs/pipelines/jsonlines_pydantic/spec/02_md5_sidecar_preprocessing/input/samples/good.jsonl.md5)
Expected report.txt Summary & Row Counts ({'samples': 1, '_rejected': 1})
status: completed with errors
ingested_files:
- samples/good.jsonl
load_errors:
- 'File pre-processing failed for samples/bad.jsonl: MD5 mismatch for samples/bad.jsonl'
pipeline_load_info_errs: null
Expected PyArrow Tables (specs/pipelines/jsonlines_pydantic/spec/02_md5_sidecar_preprocessing/expected_tables.py — EXPECTED_TABLES)
"""Expected PyArrow tables for jsonlines_pydantic 02_md5_sidecar_preprocessing."""
import pyarrow as pa
from cdm_data_loaders.converters.facades.pydantic_to_pyarrow import PydanticToPyArrow
from cdm_data_loaders.utils.buffer import REJECTED_PYARROW_SCHEMA
from specs.pipelines.jsonlines_pydantic.sample_registry import SampleModel
SAMPLES_SCHEMA = PydanticToPyArrow().convert(SampleModel)
EXPECTED_TABLES: dict[str, pa.Table] = {
"samples": pa.Table.from_pylist(
[
{"sample_id": "s-10", "ph": 7.1, "biome": "aquatic", "tags": ["lake"]},
],
schema=SAMPLES_SCHEMA,
),
"_rejected": pa.Table.from_pylist(
[
{
"table_name": "samples",
"source_file": "samples/bad.jsonl",
"line_no": None,
"raw_record": None,
"error_detail": (
"MD5 mismatch for samples/bad.jsonl: "
"expected 00000000000000000000000000000000, got d8981895f8c957b2edf853991bd4f5c4"
),
}
],
schema=REJECTED_PYARROW_SCHEMA,
),
}
Expected Canonical Table Rows (specs/pipelines/jsonlines_pydantic/spec/02_md5_sidecar_preprocessing/expected.json)
{
"_rejected": [
{
"error_detail": "MD5 mismatch for samples/bad.jsonl: expected 00000000000000000000000000000000, got d8981895f8c957b2edf853991bd4f5c4",
"source_file": "samples/bad.jsonl",
"table_name": "samples"
}
],
"samples": [
{
"biome": "aquatic",
"ph": 7.1,
"sample_id": "s-10",
"tags": [
"lake"
]
}
]
}
Table Transforms, Schema Conversion Skip, Multi-Table Fan-Out, and Output Schemas (spec/03_table_transforms_and_output_schemas)¶
Suite: spec · Target CLI: jsonlines_pydantic · Settings Class: JsonlPydanticIngestSettings
Directory: specs/pipelines/jsonlines_pydantic/spec/03_table_transforms_and_output_schemas/
Rules: R-JSONL-PYDANTIC-SCHEMA-SKIP, R-JSONL-PYDANTIC-VALIDATE-COERCE, R-JSONL-PYDANTIC-TRANSFORMS, R-JSONL-PYDANTIC-REPORT
entity_models,transforms_module, andoutput_schemas_moduleall point tospecs.pipelines.jsonlines_pydantic.transforms.- Because
assayshas a registeredTABLE_TRANSFORMSentry and an explicitOUTPUT_SCHEMASentry,build_output_schema_registryskips automaticPydanticToPyArrowconversion forAssayModel. - Validated
AssayModeldictionaries pass throughtransform_assay, droppinga-drop(exclude: true), computingph_bucket(acidicvsneutral_or_basic), and fanning outmetricstoassay_metrics.
Settings (specs/pipelines/jsonlines_pydantic/spec/03_table_transforms_and_output_schemas/settings.toml)
dataset_name = "jsonlines_pydantic_spec"
entity_models = "specs.pipelines.jsonlines_pydantic.transforms"
file_glob = "*.jsonl"
transforms_module = "specs.pipelines.jsonlines_pydantic.transforms"
output_schemas_module = "specs.pipelines.jsonlines_pydantic.transforms"
loader_file_format = "parquet"
use_output_dir_for_pipeline_metadata = true
Input assays/assays.jsonl (specs/pipelines/jsonlines_pydantic/spec/03_table_transforms_and_output_schemas/input/assays/assays.jsonl)
{"assay_id": "a-1", "ph": "5.4", "metrics": ["od600", "temp"]}
{"assay_id": "a-drop", "ph": 7.0, "exclude": true, "metrics": ["ignored"]}
{"assay_id": "a-2", "ph": 7.4, "metrics": ["dissolved_o2"]}
Expected report.txt Summary & Row Counts ({'assays': 2, 'assay_metrics': 3})
status: completed
ingested_files:
- assays/assays.jsonl
load_errors: []
pipeline_load_info_errs: null
Expected PyArrow Tables (specs/pipelines/jsonlines_pydantic/spec/03_table_transforms_and_output_schemas/expected_tables.py — EXPECTED_TABLES)
"""Expected PyArrow tables for jsonlines_pydantic 03_table_transforms_and_output_schemas."""
import pyarrow as pa
from specs.pipelines.jsonlines_pydantic.transforms import (
ASSAY_METRIC_SCHEMA,
ASSAY_SUMMARY_SCHEMA,
)
EXPECTED_TABLES: dict[str, pa.Table] = {
"assays": pa.Table.from_pylist(
[
{"assay_id": "A-1", "ph": 5.4, "ph_bucket": "acidic"},
{"assay_id": "A-2", "ph": 7.4, "ph_bucket": "neutral_or_basic"},
],
schema=ASSAY_SUMMARY_SCHEMA,
),
"assay_metrics": pa.Table.from_pylist(
[
{"assay_id": "A-1", "metric": "od600"},
{"assay_id": "A-1", "metric": "temp"},
{"assay_id": "A-2", "metric": "dissolved_o2"},
],
schema=ASSAY_METRIC_SCHEMA,
),
}
Expected Canonical Table Rows (specs/pipelines/jsonlines_pydantic/spec/03_table_transforms_and_output_schemas/expected.json)
{
"assay_metrics": [
{
"assay_id": "A-1",
"metric": "od600"
},
{
"assay_id": "A-1",
"metric": "temp"
},
{
"assay_id": "A-2",
"metric": "dissolved_o2"
}
],
"assays": [
{
"assay_id": "A-1",
"ph": 5.4,
"ph_bucket": "acidic"
},
{
"assay_id": "A-2",
"ph": 7.4,
"ph_bucket": "neutral_or_basic"
}
]
}
4. Fixture Index¶
Every case directory contains a case.yaml manifest; the test suites and this document are both driven by it.
| Case | Target CLI | Manifest | Runs |
|---|---|---|---|
spec/01_multi_entity_validation_and_rejections |
jsonlines_pydantic |
case.yaml |
default |
spec/02_md5_sidecar_preprocessing |
jsonlines_pydantic |
case.yaml |
partial_md5_failure |
spec/03_table_transforms_and_output_schemas |
jsonlines_pydantic |
case.yaml |
default |