JSONLines Extract Pipeline Specification¶
This document specifies how the JSONLines Extract ingestion pipeline
(cdm_data_loaders.pipelines.jsonlines.extract_pipeline and cdm_data_loaders.readers.jsonlines)
discovers, pre-processes, streams, transforms, and loads JSON Lines (.jsonl and .jsonl.gz)
files into a single target table (or transform-routed tables) without an upfront schema:
- File discovery,
.gzdecompression, and file pre-processing (get_matching_files,open_file,preprocess_and_record). - Line streaming, blank-line skipping, and dead-letter (
_rejected) routing (route_jsonl_lines,REJECTED_PYARROW_SCHEMA). - Table transforms, PyArrow output schema coercion, and pipeline reporting (
route_and_coerce_record,DictBuffer,run_jsonlines_pipeline).
| Property | Value |
|---|---|
| Base Pipeline Family | jsonlines_extract |
| Base CLI Entry Point | jsonlines_extract |
| Base Settings Class | cdm_data_loaders.pipelines.jsonlines.settings.JsonlExtractSettings |
| Pipeline Runner | cdm_data_loaders.pipelines.jsonlines.extract_pipeline.run_jsonlines_pipeline |
| Preset Subclasses Covered | — |
| Specification Directory | specs/pipelines/jsonlines_extract/ |
Running Smoke Tests Across Environments¶
# Run all base and preset smoke tests for jsonlines_extract across built-in environments
uv run pytest tests/cdm_data_loaders/pipelines/test_pipeline_smoke_spec.py --smoke-pipeline jsonlines_extract
# 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_extract \
--dlt-config-dir /path/to/env_dlt_config \
--settings-file /path/to/env_dlt_config/settings.toml
1. Conversion & Pipeline Rules¶
1.1 File Discovery, Decompression, and Pre-processing¶
- R-JSONL-EXTRACT-DISCOVERY:
run_jsonlines_pipelinescanssettings.input_dirusingget_matching_files(settings.input_dir, settings.file_glob)(defaultfile_glob="*.jsonl*"), discovering matching files across nested subdirectories in sorted order and transparently decompressing.gzarchives viaopen_file. Non-matching files are ignored.
Cases: spec/01_nested_glob_gzip_and_parse_rejections
- R-JSONL-EXTRACT-PREPROCESS-MD5: Before opening a matched file,
preprocess_and_recordruns each callable insettings.file_preprocessors(resolved frompreprocessors_module, e.g.validate_md5_sidecar). When a pre-processor returns a failure reason, the file is skipped, omitted fromingested_files, recorded inreport.txtload_errorsas"File pre-processing failed for <rel_path>: <reason>"(status: completed with errors), and written to_rejectedwithtable_name=settings.table_name,source_file=<rel_path>,line_no=None,raw_record=None, anderror_detail=<reason>.
Cases: spec/02_md5_sidecar_preprocessing
1.2 Line Streaming, Blank-Line Skipping, and Dead-Letter Routing¶
- R-JSONL-EXTRACT-STREAM:
route_jsonl_linesstreams each file line by line with 1-based line numbers (enumerate(f, start=1)), skipping empty or whitespace-only lines without recording an error while preserving exact 1-basedline_nooffsets for subsequent lines, and parses each non-blank line withjson.loads.
Cases: spec/01_nested_glob_gzip_and_parse_rejections, spec/03_table_transforms_and_output_schemas
- R-JSONL-EXTRACT-REJECT: Lines that raise
json.JSONDecodeErrorare routed to the_rejecteddead-letter table (REJECTED_PYARROW_SCHEMA) withtable_name=settings.table_name,source_fileset to the POSIX path relative toinput_dir, 1-basedline_no, strippedraw_record, anderror_detail=str(e). Valid lines in the same file continue loading and the file is recorded iningested_files.
Cases: spec/01_nested_glob_gzip_and_parse_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 Table Transforms, PyArrow Output Schemas, and Reporting¶
- R-JSONL-EXTRACT-TRANSFORMS: Each parsed JSON object is routed through
route_and_coerce_recordwithdefault_table=settings.table_name,transforms=settings.table_transforms(transforms_module), andoutput_schemas=settings.output_schemas(output_schemas_module). Transforms may drop records ({}/None), enrich fields, or fan out records across multiple destination tables, and any output table with a registeredpa.SchemainOUTPUT_SCHEMASis buffered inDictBuffer(batch_size=settings.batch_size) and emitted as a typedpa.Table.
Cases: spec/03_table_transforms_and_output_schemas
- R-JSONL-EXTRACT-REPORT:
run_jsonlines_pipelineloads all emitted table resources and_rejectedbatches intosettings.dataset_namein a singlepipeline.run(resources)call and writesreport.txtsummarizingstatus(completedorcompleted with errors),ingested_files,load_errors, and_load_info.
Cases: spec/01_nested_glob_gzip_and_parse_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_extract |
JsonlExtractSettings |
01_nested_glob_gzip_and_parse_rejections |
Nested Subdirectory Globbing, Gzip Decompression, Blank Lines, and JSON Parse Rejections | R-JSONL-EXTRACT-DISCOVERY, R-JSONL-EXTRACT-STREAM, R-JSONL-EXTRACT-REJECT, R-JSONL-EXTRACT-REPORT |
default: {'events': 3, '_rejected': 1} |
spec |
jsonlines_extract |
JsonlExtractSettings |
02_md5_sidecar_preprocessing |
MD5 Sidecar Pre-processing and File-Level _rejected Routing |
R-JSONL-EXTRACT-PREPROCESS-MD5, R-JSONL-EXTRACT-REPORT |
partial_md5_failure: {'events': 1, '_rejected': 1} |
spec |
jsonlines_extract |
JsonlExtractSettings |
03_table_transforms_and_output_schemas |
Table Transforms, Record Filtering, Multi-Table Fan-Out, and PyArrow Output Schemas | R-JSONL-EXTRACT-STREAM, R-JSONL-EXTRACT-TRANSFORMS, R-JSONL-EXTRACT-REPORT |
default: {'events': 2, 'event_tags': 3} |
3. Base Pipeline Specification Cases (jsonlines_extract / spec/)¶
Shared Fixture Module (specs/pipelines/jsonlines_extract/transforms.py)
"""File pre-processors, table transforms, and PyArrow output schemas for JSONLines Extract specification cases."""
from typing import Any, Final
import pyarrow as pa
from cdm_data_loaders.core.preprocessors import validate_md5_sidecar
from cdm_data_loaders.core.registry import FilePreprocessFn, RecordTransformFn
HIGH_LATENCY_MS: Final[int] = 100
FILE_PREPROCESSORS: tuple[FilePreprocessFn, ...] = (validate_md5_sidecar,)
EVENTS_SUMMARY_SCHEMA: Final[pa.Schema] = pa.schema(
[
pa.field("event_id", pa.string(), nullable=False),
pa.field("service", pa.string(), nullable=False),
pa.field("latency_ms", pa.int64(), nullable=False),
pa.field("is_slow", pa.bool_(), nullable=False),
]
)
EVENT_TAGS_SCHEMA: Final[pa.Schema] = pa.schema(
[
pa.field("event_id", pa.string(), nullable=False),
pa.field("tag", pa.string(), nullable=False),
]
)
def transform_event(row: dict[str, Any]) -> dict[str, list[dict[str, Any]]]:
"""Drop ignored events, normalize service names, compute ``is_slow``, and fan out tags to ``event_tags``.
:param row: Raw parsed JSONL event 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("drop") is True:
return {}
event_id = str(row["event_id"]).upper()
service = str(row["service"]).lower()
latency_ms = int(row["latency_ms"])
routed: dict[str, list[dict[str, Any]]] = {
"events": [
{
"event_id": event_id,
"service": service,
"latency_ms": latency_ms,
"is_slow": latency_ms >= HIGH_LATENCY_MS,
}
]
}
tags = row.get("tags") or []
if tags:
routed["event_tags"] = [{"event_id": event_id, "tag": str(tag)} for tag in tags]
return routed
TABLE_TRANSFORMS: Final[dict[str, RecordTransformFn]] = {
"events": transform_event,
}
OUTPUT_SCHEMAS: Final[dict[str, pa.Schema]] = {
"events": EVENTS_SUMMARY_SCHEMA,
"event_tags": EVENT_TAGS_SCHEMA,
}
Nested Subdirectory Globbing, Gzip Decompression, Blank Lines, and JSON Parse Rejections (spec/01_nested_glob_gzip_and_parse_rejections)¶
Suite: spec · Target CLI: jsonlines_extract · Settings Class: JsonlExtractSettings
Directory: specs/pipelines/jsonlines_extract/spec/01_nested_glob_gzip_and_parse_rejections/
Rules: R-JSONL-EXTRACT-DISCOVERY, R-JSONL-EXTRACT-STREAM, R-JSONL-EXTRACT-REJECT, R-JSONL-EXTRACT-REPORT
file_glob = "**/*.jsonl*"discoversbatch_a/part1.jsonlandbatch_b/part2.jsonl.gzacross nested subdirectories while ignoringbatch_b/ignored.txt.batch_a/part1.jsonlcontains a blank line at line 2 (skipped without error) and a malformed JSON object at line 3 (routed to_rejectedwithline_no=3and the verbatimraw_record), while valid records on lines 1 and 4 load intoevents.
Settings (specs/pipelines/jsonlines_extract/spec/01_nested_glob_gzip_and_parse_rejections/settings.toml)
dataset_name = "jsonlines_extract_spec"
table_name = "events"
file_glob = "**/*.jsonl*"
loader_file_format = "parquet"
use_output_dir_for_pipeline_metadata = true
Input batch_a/part1.jsonl (specs/pipelines/jsonlines_extract/spec/01_nested_glob_gzip_and_parse_rejections/input/batch_a/part1.jsonl)
{"event_id": "evt-1", "kind": "login", "latency_ms": 12}
{"event_id": "evt-bad", "kind": }
{"event_id": "evt-2", "kind": "query", "latency_ms": 45}
Input batch_b/ignored.txt (specs/pipelines/jsonlines_extract/spec/01_nested_glob_gzip_and_parse_rejections/input/batch_b/ignored.txt)
Input batch_b/part2.jsonl.gz (specs/pipelines/jsonlines_extract/spec/01_nested_glob_gzip_and_parse_rejections/input/batch_b/part2.jsonl.gz) (decompressed preview)
Expected report.txt Summary & Row Counts ({'events': 3, '_rejected': 1})
status: completed
ingested_files:
- batch_a/part1.jsonl
- batch_b/part2.jsonl.gz
load_errors: []
pipeline_load_info_errs: null
Expected PyArrow Tables (specs/pipelines/jsonlines_extract/spec/01_nested_glob_gzip_and_parse_rejections/expected_tables.py — EXPECTED_TABLES)
"""Expected PyArrow tables for jsonlines_extract 01_nested_glob_gzip_and_parse_rejections."""
import pyarrow as pa
from cdm_data_loaders.utils.buffer import REJECTED_PYARROW_SCHEMA
EVENTS_SCHEMA = pa.schema(
[
pa.field("event_id", pa.string(), nullable=False),
pa.field("kind", pa.string(), nullable=False),
pa.field("latency_ms", pa.int64(), nullable=False),
]
)
EXPECTED_TABLES: dict[str, pa.Table] = {
"events": pa.Table.from_pylist(
[
{"event_id": "evt-1", "kind": "login", "latency_ms": 12},
{"event_id": "evt-2", "kind": "query", "latency_ms": 45},
{"event_id": "evt-3", "kind": "logout", "latency_ms": 8},
],
schema=EVENTS_SCHEMA,
),
"_rejected": pa.Table.from_pylist(
[
{
"table_name": "events",
"source_file": "batch_a/part1.jsonl",
"line_no": 3,
"raw_record": '{"event_id": "evt-bad", "kind": }',
"error_detail": "Expecting value: line 1 column 33 (char 32)",
}
],
schema=REJECTED_PYARROW_SCHEMA,
),
}
Expected Canonical Table Rows (specs/pipelines/jsonlines_extract/spec/01_nested_glob_gzip_and_parse_rejections/expected.json)
{
"_rejected": [
{
"error_detail": "Expecting value: line 1 column 33 (char 32)",
"line_no": 3,
"raw_record": "{\"event_id\": \"evt-bad\", \"kind\": }",
"source_file": "batch_a/part1.jsonl",
"table_name": "events"
}
],
"events": [
{
"event_id": "evt-1",
"kind": "login",
"latency_ms": 12
},
{
"event_id": "evt-2",
"kind": "query",
"latency_ms": 45
},
{
"event_id": "evt-3",
"kind": "logout",
"latency_ms": 8
}
]
}
MD5 Sidecar Pre-processing and File-Level _rejected Routing (spec/02_md5_sidecar_preprocessing)¶
Suite: spec · Target CLI: jsonlines_extract · Settings Class: JsonlExtractSettings
Directory: specs/pipelines/jsonlines_extract/spec/02_md5_sidecar_preprocessing/
Rules: R-JSONL-EXTRACT-PREPROCESS-MD5, R-JSONL-EXTRACT-REPORT
preprocessors_module = "specs.pipelines.jsonlines_extract.transforms"registersvalidate_md5_sidecar.good.jsonlmatchesgood.jsonl.md5and loads intoevents.bad.jsonlfails MD5 validation againstbad.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_extract/spec/02_md5_sidecar_preprocessing/settings.toml)
dataset_name = "jsonlines_extract_spec"
table_name = "events"
file_glob = "*.jsonl"
preprocessors_module = "specs.pipelines.jsonlines_extract.transforms"
loader_file_format = "parquet"
use_output_dir_for_pipeline_metadata = true
Input bad.jsonl (specs/pipelines/jsonlines_extract/spec/02_md5_sidecar_preprocessing/input/bad.jsonl)
Input bad.jsonl.md5 (specs/pipelines/jsonlines_extract/spec/02_md5_sidecar_preprocessing/input/bad.jsonl.md5)
Input good.jsonl (specs/pipelines/jsonlines_extract/spec/02_md5_sidecar_preprocessing/input/good.jsonl)
Input good.jsonl.md5 (specs/pipelines/jsonlines_extract/spec/02_md5_sidecar_preprocessing/input/good.jsonl.md5)
Expected report.txt Summary & Row Counts ({'events': 1, '_rejected': 1})
status: completed with errors
ingested_files:
- good.jsonl
load_errors:
- 'File pre-processing failed for bad.jsonl: MD5 mismatch for bad.jsonl'
pipeline_load_info_errs: null
Expected PyArrow Tables (specs/pipelines/jsonlines_extract/spec/02_md5_sidecar_preprocessing/expected_tables.py — EXPECTED_TABLES)
"""Expected PyArrow tables for jsonlines_extract 02_md5_sidecar_preprocessing."""
import pyarrow as pa
from cdm_data_loaders.utils.buffer import REJECTED_PYARROW_SCHEMA
EVENTS_SCHEMA = pa.schema(
[
pa.field("event_id", pa.string(), nullable=False),
pa.field("kind", pa.string(), nullable=False),
pa.field("latency_ms", pa.int64(), nullable=False),
]
)
EXPECTED_TABLES: dict[str, pa.Table] = {
"events": pa.Table.from_pylist(
[
{"event_id": "evt-10", "kind": "ingest", "latency_ms": 21},
],
schema=EVENTS_SCHEMA,
),
"_rejected": pa.Table.from_pylist(
[
{
"table_name": "events",
"source_file": "bad.jsonl",
"line_no": None,
"raw_record": None,
"error_detail": (
"MD5 mismatch for bad.jsonl: "
"expected 00000000000000000000000000000000, got f23fdd271c6003eac5a5e788c4e86b84"
),
}
],
schema=REJECTED_PYARROW_SCHEMA,
),
}
Expected Canonical Table Rows (specs/pipelines/jsonlines_extract/spec/02_md5_sidecar_preprocessing/expected.json)
{
"_rejected": [
{
"error_detail": "MD5 mismatch for bad.jsonl: expected 00000000000000000000000000000000, got f23fdd271c6003eac5a5e788c4e86b84",
"source_file": "bad.jsonl",
"table_name": "events"
}
],
"events": [
{
"event_id": "evt-10",
"kind": "ingest",
"latency_ms": 21
}
]
}
Table Transforms, Record Filtering, Multi-Table Fan-Out, and PyArrow Output Schemas (spec/03_table_transforms_and_output_schemas)¶
Suite: spec · Target CLI: jsonlines_extract · Settings Class: JsonlExtractSettings
Directory: specs/pipelines/jsonlines_extract/spec/03_table_transforms_and_output_schemas/
Rules: R-JSONL-EXTRACT-STREAM, R-JSONL-EXTRACT-TRANSFORMS, R-JSONL-EXTRACT-REPORT
transforms_module = "specs.pipelines.jsonlines_extract.transforms"andoutput_schemas_module = "specs.pipelines.jsonlines_extract.transforms"registerTABLE_TRANSFORMS["events"]andOUTPUT_SCHEMASforeventsandevent_tags.evt-drop(drop: true) is filtered out ({}),evt-1andevt-2are normalized and enriched withis_slow = latency_ms >= 100, andtagsarrays fan out toevent_tags.
Settings (specs/pipelines/jsonlines_extract/spec/03_table_transforms_and_output_schemas/settings.toml)
dataset_name = "jsonlines_extract_spec"
table_name = "events"
file_glob = "*.jsonl"
transforms_module = "specs.pipelines.jsonlines_extract.transforms"
output_schemas_module = "specs.pipelines.jsonlines_extract.transforms"
loader_file_format = "parquet"
use_output_dir_for_pipeline_metadata = true
Input events.jsonl (specs/pipelines/jsonlines_extract/spec/03_table_transforms_and_output_schemas/input/events.jsonl)
{"event_id": "evt-1", "service": "Auth", "latency_ms": 25, "tags": ["prod", "edge"]}
{"event_id": "evt-drop", "service": "Search", "latency_ms": 5, "drop": true, "tags": ["ignored"]}
{"event_id": "evt-2", "service": "Storage", "latency_ms": 140, "tags": ["batch"]}
Expected report.txt Summary & Row Counts ({'events': 2, 'event_tags': 3})
Expected PyArrow Tables (specs/pipelines/jsonlines_extract/spec/03_table_transforms_and_output_schemas/expected_tables.py — EXPECTED_TABLES)
"""Expected PyArrow tables for jsonlines_extract 03_table_transforms_and_output_schemas."""
import pyarrow as pa
from specs.pipelines.jsonlines_extract.transforms import (
EVENT_TAGS_SCHEMA,
EVENTS_SUMMARY_SCHEMA,
)
EXPECTED_TABLES: dict[str, pa.Table] = {
"events": pa.Table.from_pylist(
[
{"event_id": "EVT-1", "service": "auth", "latency_ms": 25, "is_slow": False},
{"event_id": "EVT-2", "service": "storage", "latency_ms": 140, "is_slow": True},
],
schema=EVENTS_SUMMARY_SCHEMA,
),
"event_tags": pa.Table.from_pylist(
[
{"event_id": "EVT-1", "tag": "prod"},
{"event_id": "EVT-1", "tag": "edge"},
{"event_id": "EVT-2", "tag": "batch"},
],
schema=EVENT_TAGS_SCHEMA,
),
}
Expected Canonical Table Rows (specs/pipelines/jsonlines_extract/spec/03_table_transforms_and_output_schemas/expected.json)
{
"event_tags": [
{
"event_id": "EVT-1",
"tag": "edge"
},
{
"event_id": "EVT-1",
"tag": "prod"
},
{
"event_id": "EVT-2",
"tag": "batch"
}
],
"events": [
{
"event_id": "EVT-1",
"is_slow": false,
"latency_ms": 25,
"service": "auth"
},
{
"event_id": "EVT-2",
"is_slow": true,
"latency_ms": 140,
"service": "storage"
}
]
}
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_nested_glob_gzip_and_parse_rejections |
jsonlines_extract |
case.yaml |
default |
spec/02_md5_sidecar_preprocessing |
jsonlines_extract |
case.yaml |
partial_md5_failure |
spec/03_table_transforms_and_output_schemas |
jsonlines_extract |
case.yaml |
default |