Skip to content

[PreProcessor] Validate config.json with a pydantic model - #845

Merged
gmechali merged 9 commits into
datacommonsorg:masterfrom
gmechali:config-model
Nov 13, 2026
Merged

gmechali merged 9 commits into
datacommonsorg:masterfrom
gmechali:config-model

Conversation

@gmechali

@gmechali gmechali commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

config.json is now described by pydantic models (ImportConfig, InputFileConfig in stats/config.py) and validated in one place, parse_config(). pydantic 2.9.2 is already a dependency.

Scope: this models every field config.json accepts today, as it is today, including fields we plan to remove (filename, importName, format, dataDownloadUrl, referenceColumns/columnsToResolve, groupStatVarsByProperty). No documented field is added or dropped here (only the internal, write-only _import_name / _dir_import_names keys are gone). Follow-up PRs will start dropping fields from the model, one at a time, so each removal is its own small, clear change.

Changes

  • Unknown keys are still rejected (extra="forbid"). Models are frozen, with camelCase aliases.
  • Every problem is reported at once, as one INVALID_CONFIGURATION error. Existing messages are kept (unknown keys with the allowed keys, entities block, format, reference columns); new checks read Invalid '<key>' in 'inputFiles' entry for '<pattern>': <reason>.
  • Config keeps its getters and per-file lookup; for_file() and patterns() replace private accessors, and config.data is gone.
  • Multi-import runs merge the validated models; workers receive the model instead of re-parsing JSON. The write-only _import_name / _dir_import_names keys are removed.
  • groupStatVarsByProperty keeps its lenient parsing, with a deprecation TODO.
  • Pins annotated-types and typing-extensions (the installed versions). config.md gets a short "Validation" section.

Configs that passed before and now fail

  1. Wrong JSON types (e.g. ignoreColumns: "x", columnMappings: [...], null for a list).
  2. Duplicate pattern (the last entry used to win silently).
  3. Entities entry without rowEntityType (was an AssertionError).
  4. includeInputSubdirs that is not a boolean or a boolean string: true/false and the documented "true"/"false" still work, but "false" now means false (it meant true), and values like "maybe" are rejected.
  5. Unsupported importType or ** in a file name now fails when the config is read.
  6. Non-list dataDownloadUrl in input-dir mode.

Verification

  • New output-level cases in test_config.py for each change above, for error rendering (unknown keys grouped per object, nested locations), and for which inputFiles entry applies to a file. e2e goldens unchanged. Lint clean; full suite passes.
  • All 19 UN configs and 5 e2e configs validate.
  • UN 10-import subset vs master: identical output, no perf change.

@codacy-production

codacy-production Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 13 minor

Alerts:
⚠ 13 issues (≤ 0 issues of at least minor severity)

Results:
13 new issues

Category Results
Documentation 8 minor
CodeStyle 5 minor

View in Codacy

🟢 Metrics 35 complexity

Metric Results
Complexity 35

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the configuration parsing and validation logic by adopting Pydantic models (InputFileConfig and ImportConfig) in simple/stats/config.py. This replaces manual dictionary key validation, improves error reporting, and updates the runner, validation, and test suites to integrate with the new Pydantic-based configuration. The review feedback suggests two improvements: checking if value is already an instance of ImportType in _check_import_type to ensure robustness during model copy/update operations, and adding defensive checks when accessing e["ctx"]["error"] in _render_errors to prevent potential KeyError exceptions.

Comment thread simple/stats/config.py Resolved
Comment thread simple/stats/config.py Resolved
@gmechali
gmechali force-pushed the config-model branch 2 times, most recently from 61fa089 to 5125e12 Compare November 11, 2026 21:36
gmechali added a commit that referenced this pull request Nov 11, 2026
…844)

Today a bad `config.json` fails in `Runner.__init__`, before `run()`'s
failure handling, so it writes no `report.json`, no ingestion report and
no handshake. The ingestion workflow gets no structured error.

### Changes
- `Runner.__init__` only sets up the output store, reporter and summary
generator. `run()` calls the new `_load_config()` inside its existing
`try`, which reads the config and opens the input stores (through a
small `_open_input_store` helper). A config failure therefore produces
the same `report.json`, `ingestion_report.json` and `{"status":
"FAILURE", "errors": [...]}` handshake as any other failure. No schema
or field changes.
- Config errors (validation, bad JSON, non-object config, missing
`dataDownloadUrl`) carry `INVALID_CONFIGURATION`. A missing config file
stays `GENERIC_ERROR`.
- `run()` uses `e.error_type` when set, instead of always
`GENERIC_ERROR`. This changes the reported type of three existing
failures:

| Failure | Before | After |
|---|---|---|
| Undefined provenance | `GENERIC_ERROR` | `MISSING_PROVENANCE` |
| Provenance without a source | `GENERIC_ERROR` | `MISSING_SOURCE` |
| Provenance without a namespace prefix | `GENERIC_ERROR` |
`INVALID_CONFIGURATION` |

### Verification
Six scenarios ran through the real `Runner` (testsuite harness:
`WORKFLOW_EXECUTION_ID` set, output and handshake on fake GCS), on
master and on this PR:

| Scenario | master | this PR |
|---|---|---|
| Valid config | SUCCESS | Byte-identical |
| Unknown key | Nothing written | `FAILURE`, `INVALID_CONFIGURATION` |
| Malformed JSON | Nothing written | `FAILURE`, `INVALID_CONFIGURATION`
|
| No config.json | Nothing written | `FAILURE`, `GENERIC_ERROR` |
| Undefined provenance | `GENERIC_ERROR` | `MISSING_PROVENANCE` (only
change) |
| Multi-import, bad subdir config | Nothing written | `FAILURE`,
`INVALID_CONFIGURATION`, message names the subdir config |

- Config failures use the existing failure handshake; no new fields. UN
10-import subset: identical output.
- Tests: `handshake_error_types=()` expectations now expect
`INVALID_CONFIGURATION` (or `GENERIC_ERROR` for missing files). New:
`test_invalid_config_is_reported_like_any_failed_run` pins the
handshake, report.json and ingestion report for a bad subdirectory
config. Lint clean; full suite passes.

Stack: **this PR** → #845
config.json was parsed and checked ad hoc: allow-lists of keys, a few
hand-written checks, and no type checks, so a wrong type was silently misread
or failed far from its cause. It is now described by two pydantic models,
ImportConfig and InputFileConfig, and validated in one place, parse_config(),
when the config is read.

- Every problem is reported at once, as one INVALID_CONFIGURATION error.
  Existing messages are kept: unknown keys are still grouped per object with
  the allowed keys, and the entities block, format, reference columns and
  inputFiles shape errors keep their wording. New checks use pydantic's.
- New errors: values of the wrong type, duplicate patterns in one config, an
  entities entry without rowEntityType (was an assert), an unparseable
  includeInputSubdirs ("false" now means false). An unsupported importType
  or a "**" file-name pattern now fails when the config is read, not when a
  file is imported or matched.
- groupStatVarsByProperty keeps its lenient parsing; it is slated for
  deprecation.
- Config stays as the facade with the same getters and per-file lookup.
  Multi-import runs merge the validated models, and workers receive the
  model instead of re-parsing JSON. The write-only _import_name and
  _dir_import_names keys are gone.
- Pin pydantic's dependencies annotated-types and typing-extensions.

All 19 UN configs and the 5 e2e configs validate; e2e goldens are unchanged.
@gmechali
gmechali changed the base branch from config-errors to master November 12, 2026 03:17

@clincoln8 clincoln8 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

Comment thread simple/stats/config.py Resolved
Comment thread simple/stats/config.py Resolved
A constant for one key, used only in this module, read worse than the key.
_ConfigModel generates each key as the camelCase field name and holds the
model config both models shared, so fields no longer need Field(alias=...)
or default factories. The keys, defaults and errors are unchanged.
e.get("ctx", {}) only covered a missing ctx; pydantic can also set it to
None.
@gmechali
gmechali merged commit b623fe1 into datacommonsorg:master Nov 13, 2026
9 of 10 checks passed
@gmechali
gmechali deleted the config-model branch November 13, 2026 00:27
gmechali added a commit that referenced this pull request Nov 13, 2026
Stacked on #845. Removes the `config.json` keys that no real config
uses, so the format matches what the DCP docs describe. Each removal is
its own commit if you want to review step by step.

### What stays
`inputFiles` entries with `pattern`, `provenance`, `columnMappings`,
`ignoreColumns`, `importType` and `observationProperties`.
`includeInputSubdirs` is still accepted but ignored, with a deprecation
warning naming the config.json (16 of 19 UN configs and the DCP docs set
it).

### What changes
- **No more `--config_file` / `dataDownloadUrl`.** The preprocessor
always reads an input directory (`run_stats.sh -c` is gone too). Runner
raises `ValueError` instead of asserting when the input or output dir is
missing.
- **Subdirectories are always searched;** `pattern` alone decides what
is imported. `archives/` is still excluded.
- **References:** `columnsToResolve` / `referenceColumns` are gone. A
cell value with a namespace prefix (e.g. `dcid:geoId/06`) is a
reference, as it already was for every known config. Both keys fail with
a migration hint.
- **Entity CSVs** map their ids and types from columns: `dcid:dcid` and
`dcid:typeOf` in `columnMappings`, checked at header validation.
`rowEntityType` / `idColumn` are gone (migration hint), and there are no
more generated `<type>_<index>` ids.
- **StatVarGroups:** `groupStatVarsByProperty` is gone, and the
handshake no longer carries `generateStatVarGroups` (the DCP workflow
already defaults it to `false`). The root groups `dc/g/Root` and
`c/g/Root` are now written on every run.
- **Smaller removals:** `filename` (alias of `pattern`), `format`
(no-op), `importName` (only named the output run folder), and the
special message for the long-removed top-level `entities` block. These
now get the generic `Unknown key(s) ... Allowed keys are: [...]` error.

### Performance
No impact. The removed code was config parsing and dead branches. The UN
regression shows wall time +0.9%, CPU −0.8% and peak memory +1.5% (5
imports, 3 runs each), all within noise.

### Verification
- **Regression tool, UN data vs #845:** ran successfully. Output
identical on 10 imports, and on 5 imports × 3 runs. Those runs predate
`c/g/Root` becoming unconditional, which adds one node per run.
- **Goldens:** every diff is explained.
  - `config_validation_failure`: the "Allowed keys" list shrinks.
- `kitchen_sink` and `multi_import`: lose `generateStatVarGroups`, and
`multi_import` gains `c/g/Root`. kitchen_sink's entities CSV now maps a
`dcid` column to `dcid:dcid` and keeps `author_id` as a property, so its
Author nodes are unchanged.
- The old `config_file_driven` dataset becomes `nested_data_dir`: data
in a `data/` subdirectory, where only pattern matches are imported. Its
golden matches the old one apart from `c/g/Root`.
- All 19 UN configs validate. Lint and the full suite pass on every
commit; 98% coverage of the changed lines.

**Follow-ups:** in the DCP docs, document `ignoreColumns` and entity
CSVs (`dcid:dcid` / `dcid:typeOf`), and drop `includeInputSubdirs` from
the example. The root `config.json` (no `--imports`) is removed in the
next PR.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants