Repository navigation
[PreProcessor] Validate config.json with a pydantic model - #845
Conversation
Not up to standards ⛔🔴 Issues
|
| Category | Results |
|---|---|
| Documentation | 8 minor |
| CodeStyle | 5 minor |
🟢 Metrics 35 complexity
Metric Results Complexity 35
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.
There was a problem hiding this comment.
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.
61fa089 to
5125e12
Compare
…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.
5125e12 to
b79cc87
Compare
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.
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.
config.jsonis now described by pydantic models (ImportConfig,InputFileConfiginstats/config.py) and validated in one place,parse_config(). pydantic 2.9.2 is already a dependency.Scope: this models every field
config.jsonaccepts 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_nameskeys 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
extra="forbid"). Models are frozen, with camelCase aliases.INVALID_CONFIGURATIONerror. Existing messages are kept (unknown keys with the allowed keys, entities block,format, reference columns); new checks readInvalid '<key>' in 'inputFiles' entry for '<pattern>': <reason>.Configkeeps its getters and per-file lookup;for_file()andpatterns()replace private accessors, andconfig.datais gone._import_name/_dir_import_nameskeys are removed.groupStatVarsByPropertykeeps its lenient parsing, with a deprecation TODO.annotated-typesandtyping-extensions(the installed versions). config.md gets a short "Validation" section.Configs that passed before and now fail
ignoreColumns: "x",columnMappings: [...],nullfor a list).pattern(the last entry used to win silently).rowEntityType(was anAssertionError).includeInputSubdirsthat is not a boolean or a boolean string:true/falseand the documented"true"/"false"still work, but"false"now means false (it meant true), and values like"maybe"are rejected.importTypeor**in a file name now fails when the config is read.dataDownloadUrlin input-dir mode.Verification
test_config.pyfor each change above, for error rendering (unknown keys grouped per object, nested locations), and for whichinputFilesentry applies to a file. e2e goldens unchanged. Lint clean; full suite passes.