model2data 1.10.1__tar.gz → 1.10.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {model2data-1.10.1/model2data.egg-info → model2data-1.10.4}/PKG-INFO +1 -1
- {model2data-1.10.1 → model2data-1.10.4}/README.md +21 -6
- {model2data-1.10.1 → model2data-1.10.4}/model2data/defects/apply.py +148 -22
- model2data-1.10.4/model2data/defects/checks.py +395 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/generate/core.py +7 -2
- {model2data-1.10.1 → model2data-1.10.4}/model2data/generate/days.py +16 -3
- {model2data-1.10.1 → model2data-1.10.4}/model2data/generate/faker.py +312 -9
- {model2data-1.10.1 → model2data-1.10.4}/model2data/generate/timeline.py +41 -3
- {model2data-1.10.1 → model2data-1.10.4/model2data.egg-info}/PKG-INFO +1 -1
- {model2data-1.10.1 → model2data-1.10.4}/model2data.egg-info/SOURCES.txt +2 -0
- {model2data-1.10.1 → model2data-1.10.4}/pyproject.toml +1 -1
- {model2data-1.10.1 → model2data-1.10.4}/tests/test_cli_model.py +31 -0
- {model2data-1.10.1 → model2data-1.10.4}/tests/test_days.py +26 -0
- model2data-1.10.4/tests/test_determinism.py +133 -0
- model2data-1.10.4/tests/test_fast_paths.py +512 -0
- {model2data-1.10.1 → model2data-1.10.4}/tests/test_generation.py +64 -0
- model2data-1.10.1/model2data/defects/checks.py +0 -179
- {model2data-1.10.1 → model2data-1.10.4}/LICENSE +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/README_PYPI.md +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/__init__.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/__init__.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/LICENSE +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/VENDORED.md +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/__init__.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/_classes/__init__.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/_classes/base.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/_classes/column.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/_classes/enum.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/_classes/expression.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/_classes/index.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/_classes/note.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/_classes/project.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/_classes/reference.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/_classes/sticky_note.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/_classes/table.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/_classes/table_group.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/classes/__init__.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/constants.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/database.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/definitions/__init__.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/definitions/column.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/definitions/common.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/definitions/enum.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/definitions/generic.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/definitions/index.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/definitions/project.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/definitions/records.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/definitions/reference.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/definitions/sticky_note.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/definitions/table.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/definitions/table_group.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/definitions/table_partial.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/exceptions.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/parser/__init__.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/parser/blueprints.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/parser/parser.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/__init__.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/base.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/dbml/__init__.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/dbml/default/__init__.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/dbml/default/column.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/dbml/default/enum.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/dbml/default/expression.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/dbml/default/index.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/dbml/default/note.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/dbml/default/project.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/dbml/default/reference.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/dbml/default/renderer.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/dbml/default/sticky_note.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/dbml/default/table.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/dbml/default/table_group.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/dbml/default/utils.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/sql/__init__.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/sql/default/__init__.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/sql/default/column.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/sql/default/enum.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/sql/default/expression.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/sql/default/index.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/sql/default/note.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/sql/default/reference.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/sql/default/renderer.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/sql/default/table.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/renderer/sql/default/utils.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/_vendor/pydbml/tools.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/cli.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/dbt/__init__.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/dbt/hint_tests.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/dbt/naming.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/dbt/project.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/dbt/templates/dbt_project.yml.jinja +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/dbt/templates/hint_macros/model2data_hint_tests.sql +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/dbt/templates/history_macros/model2data_history_tests.sql +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/dbt/templates/macros/generate_schema_name.sql +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/dbt/templates/profiles.yml.jinja +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/dbt/tests.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/defects/__init__.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/defects/presets.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/defects/report.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/generate/__init__.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/generate/hints.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/generate/history.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/generate/kinds.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/generate/options.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/generate/relationships.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/model/__init__.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/model/_yaml.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/model/dbml.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/model/document.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/model/dump.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/model/engine.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/model/errors.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/model/reader.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/model/types.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/model/validate.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/output.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/parse/__init__.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/parse/dbml.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/spec/PUBLISHING.md +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/spec/README.md +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/spec/examples/coffee_webshop.model2data.yml +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/spec/model.schema.json +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data/utils.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data.egg-info/dependency_links.txt +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data.egg-info/entry_points.txt +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data.egg-info/requires.txt +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/model2data.egg-info/top_level.txt +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/setup.cfg +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/tests/test_as_of_anchor.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/tests/test_cli.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/tests/test_column_time_hints.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/tests/test_coverage_gaps.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/tests/test_dbml_parser.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/tests/test_dbml_parser_fuzz.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/tests/test_dbt_integration.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/tests/test_dbt_naming.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/tests/test_dbt_project.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/tests/test_dbt_tests.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/tests/test_defects.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/tests/test_defects_dbt.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/tests/test_defects_replay.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/tests/test_defects_report.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/tests/test_defects_review.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/tests/test_distributions.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/tests/test_examples_model.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/tests/test_faker_name_inference.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/tests/test_hint_tests.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/tests/test_history.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/tests/test_issue_lines.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/tests/test_lone_country.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/tests/test_model.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/tests/test_model_dbml.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/tests/test_model_edges.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/tests/test_model_messages.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/tests/test_model_vocabulary.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/tests/test_options.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/tests/test_release_stress.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/tests/test_reproducibility.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/tests/test_row_identity.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/tests/test_shaping.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/tests/test_spec_alignment.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/tests/test_spec_schema.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/tests/test_table_seeds.py +0 -0
- {model2data-1.10.1 → model2data-1.10.4}/tests/test_timeline.py +0 -0
|
@@ -49,9 +49,9 @@ access required.
|
|
|
49
49
|
`sku ean13`) to pick its generator outright when the name is wrong for the data.
|
|
50
50
|
- **Relationship-preserving.** Foreign keys resolve to real parent rows; tables are generated in
|
|
51
51
|
dependency order.
|
|
52
|
-
- **Deterministic.**
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
- **Deterministic, byte for byte.** The same model, `--seed` and `--as-of` (and the same options)
|
|
53
|
+
give the same files, byte for byte, on any machine and in every 1.x release — safe to commit
|
|
54
|
+
fixtures, safe to diff across CI runs and upgrades. See [The determinism promise](#the-determinism-promise).
|
|
55
55
|
- **Re-rollable one table at a time.** `--table-seed orders=7` regenerates a single table and
|
|
56
56
|
leaves every other table byte-identical, so you can keep the four tables that look right.
|
|
57
57
|
- **A real dbt project, not just CSVs.** Seeds, staging models that `ref()` them, schema tests,
|
|
@@ -222,14 +222,14 @@ jobs:
|
|
|
222
222
|
```
|
|
223
223
|
|
|
224
224
|
Inputs: `files` (glob, default `**/*.model2data.yml`), `version` (default: the release the tag
|
|
225
|
-
points at, e.g. `1.10.
|
|
225
|
+
points at, e.g. `1.10.4`; a specifier such as `>=1.10,<2` also works), `python-version` (3.12).
|
|
226
226
|
|
|
227
227
|
**pre-commit**
|
|
228
228
|
|
|
229
229
|
```yaml
|
|
230
230
|
repos:
|
|
231
231
|
- repo: https://github.com/JB-Analytica/model2data
|
|
232
|
-
rev: v1.10.
|
|
232
|
+
rev: v1.10.4
|
|
233
233
|
hooks:
|
|
234
234
|
- id: model2data-validate
|
|
235
235
|
```
|
|
@@ -243,7 +243,7 @@ model2data:
|
|
|
243
243
|
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
244
244
|
changes: ["**/*.model2data.yml"]
|
|
245
245
|
script:
|
|
246
|
-
- pip install model2data==1.10.
|
|
246
|
+
- pip install model2data==1.10.4
|
|
247
247
|
- model2data validate --glob "**/*.model2data.yml"
|
|
248
248
|
```
|
|
249
249
|
|
|
@@ -309,6 +309,21 @@ fixture safe to commit:
|
|
|
309
309
|
model2data --file examples/ecommerce.model2data.yml --rows 200 --seed 42 --as-of 2026-01-31
|
|
310
310
|
```
|
|
311
311
|
|
|
312
|
+
### The determinism promise
|
|
313
|
+
|
|
314
|
+
The same model, seed and `--as-of` — with the same options (`--rows`, `--rows-for`,
|
|
315
|
+
`--table-seed`, `--locale`, `--defects`, `--days`, …) — produce the same files, byte for byte:
|
|
316
|
+
|
|
317
|
+
- on any machine and operating system, and on every supported Python version;
|
|
318
|
+
- with any Faker or pandas version model2data accepts;
|
|
319
|
+
- in every 1.x release. A release may make generation faster or add options, but never changes
|
|
320
|
+
what an existing model, seed and set of options produce. A fix that cannot keep that waits for
|
|
321
|
+
a new major version, and the changelog says so.
|
|
322
|
+
|
|
323
|
+
`tests/test_determinism.py` holds the file hashes of a spread of models, presets, multi-day runs
|
|
324
|
+
and locales, and runs on every pull request — against the newest Faker and pandas too — so a
|
|
325
|
+
change to a single output byte fails the build rather than reaching a release.
|
|
326
|
+
|
|
312
327
|
If one table comes out wrong and the rest looks right, `--table-seed` re-rolls just that table.
|
|
313
328
|
Every other table's seed CSV stays byte-identical, and children of the re-rolled table still
|
|
314
329
|
reference rows that exist, so there's nothing to re-check but the table you asked to change:
|
|
@@ -64,7 +64,7 @@ from model2data import __version__
|
|
|
64
64
|
from model2data.dbt.hint_tests import DEFAULT_TOLERANCE
|
|
65
65
|
from model2data.dbt.naming import dbt_names, for_dbt
|
|
66
66
|
from model2data.dbt.tests import DbtTest, dbt_tests
|
|
67
|
-
from model2data.defects.checks import as_seeded, failing
|
|
67
|
+
from model2data.defects.checks import Columns, as_seeded, failing, fails
|
|
68
68
|
from model2data.defects.report import AppliedDefect, DefectsReport, ExpectedFailure
|
|
69
69
|
from model2data.generate import kinds
|
|
70
70
|
from model2data.generate.days import DayResult, TableDay
|
|
@@ -123,6 +123,8 @@ class _Table:
|
|
|
123
123
|
# so no defect undoes another and every count holds.
|
|
124
124
|
self.claimed: dict[str, set[int]] = {}
|
|
125
125
|
self.patches: list[_Patch] = []
|
|
126
|
+
# What `referenced` found, by the columns asked about: the clean frames never change.
|
|
127
|
+
self._referenced: dict[tuple[str, ...], frozenset[int]] = {}
|
|
126
128
|
rows = len(self.frame)
|
|
127
129
|
if results is not None and key not in results[0].tables:
|
|
128
130
|
results = None # a table the days do not hold: a history table
|
|
@@ -162,15 +164,20 @@ class _Table:
|
|
|
162
164
|
|
|
163
165
|
def referenced(self, columns: Sequence[str]) -> set[int]:
|
|
164
166
|
"""The rows whose value in one of `columns` some child row holds."""
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
167
|
+
asked = tuple(columns)
|
|
168
|
+
if asked not in self._referenced:
|
|
169
|
+
found: set[int] = set()
|
|
170
|
+
for ref in self.refs:
|
|
171
|
+
if ref["target_table"] != self.key or ref["target_column"] not in asked:
|
|
172
|
+
continue
|
|
173
|
+
child = self.frames[ref["source_table"]][ref["source_column"]].tolist()
|
|
174
|
+
held = {_comparable(v) for v in child if _present(v)}
|
|
175
|
+
values = self.frame[ref["target_column"]].tolist()
|
|
176
|
+
found.update(
|
|
177
|
+
p for p, v in enumerate(values) if _present(v) and _comparable(v) in held
|
|
178
|
+
)
|
|
179
|
+
self._referenced[asked] = frozenset(found)
|
|
180
|
+
return set(self._referenced[asked])
|
|
174
181
|
|
|
175
182
|
def free(self, candidates: list[int], columns: Sequence[str]) -> list[int]:
|
|
176
183
|
"""`candidates` without the rows an earlier defect broke one of `columns` in."""
|
|
@@ -218,6 +225,9 @@ class _Table:
|
|
|
218
225
|
|
|
219
226
|
|
|
220
227
|
def _present(value: Any) -> bool:
|
|
228
|
+
kind = type(value)
|
|
229
|
+
if kind is str or kind is int:
|
|
230
|
+
return True
|
|
221
231
|
return (
|
|
222
232
|
value is not None
|
|
223
233
|
and not (isinstance(value, float) and math.isnan(value))
|
|
@@ -227,6 +237,9 @@ def _present(value: Any) -> bool:
|
|
|
227
237
|
|
|
228
238
|
def _comparable(value: Any) -> Any:
|
|
229
239
|
"""A value as it compares to the warehouse: 7, 7.0 and numpy's 7 are one value."""
|
|
240
|
+
kind = type(value)
|
|
241
|
+
if kind is str or kind is int:
|
|
242
|
+
return value
|
|
230
243
|
item = getattr(value, "item", None)
|
|
231
244
|
if callable(item):
|
|
232
245
|
value = item()
|
|
@@ -290,7 +303,9 @@ def _duplicate_keys(table: _Table, defect: Defect, rng: random.Random, count: in
|
|
|
290
303
|
if not columns:
|
|
291
304
|
return _Outcome(None, note=f"{table.key} has no primary key to repeat")
|
|
292
305
|
values = {name: table.frame[name].tolist() for name in columns}
|
|
293
|
-
complete = [
|
|
306
|
+
complete = [
|
|
307
|
+
p for p, row in enumerate(zip(*values.values(), strict=True)) if all(map(_present, row))
|
|
308
|
+
]
|
|
294
309
|
avoid = table.referenced(columns)
|
|
295
310
|
wanted = min(count, max(len(complete) - 1, 0))
|
|
296
311
|
targets = _choose(rng, table.free(complete, columns), wanted, avoid)
|
|
@@ -668,15 +683,54 @@ def requested_rows(defect: Defect, rows: int) -> int:
|
|
|
668
683
|
def _patched(
|
|
669
684
|
frame: pd.DataFrame, patches: Sequence[_Patch], day: Optional[int] = None
|
|
670
685
|
) -> pd.DataFrame:
|
|
671
|
-
"""A copy of `frame` with `patches` set, those from day `day` on when it is given.
|
|
686
|
+
"""A copy of `frame` with `patches` set, those from day `day` on when it is given.
|
|
687
|
+
|
|
688
|
+
A column of objects is set as a list, in one go; any other cell by cell, as
|
|
689
|
+
`_set` does, for what pandas makes of a value its column's type cannot hold.
|
|
690
|
+
"""
|
|
672
691
|
out = frame.copy()
|
|
692
|
+
by_column: dict[str, list[_Patch]] = {}
|
|
673
693
|
for patch in patches:
|
|
674
694
|
if patch.position >= len(out) or (day is not None and patch.since > day):
|
|
675
695
|
continue
|
|
676
|
-
|
|
696
|
+
by_column.setdefault(patch.column, []).append(patch)
|
|
697
|
+
for column, cells in by_column.items():
|
|
698
|
+
if out[column].dtype == object:
|
|
699
|
+
values = out[column].tolist()
|
|
700
|
+
for patch in cells:
|
|
701
|
+
values[patch.position] = patch.value
|
|
702
|
+
out[column] = pd.Series(values, index=out.index, dtype=object)
|
|
703
|
+
continue
|
|
704
|
+
if isinstance(out[column].dtype, _NULLABLE_INTEGERS) and all(
|
|
705
|
+
type(patch.value) is int for patch in cells
|
|
706
|
+
):
|
|
707
|
+
# What `_set` does to each cell, done to all of them at once.
|
|
708
|
+
final = {patch.position: patch.value for patch in cells}
|
|
709
|
+
array = out[column].array.copy()
|
|
710
|
+
try:
|
|
711
|
+
array[list(final)] = list(final.values())
|
|
712
|
+
except (TypeError, ValueError):
|
|
713
|
+
pass # too large for the column: cell by cell, as before
|
|
714
|
+
else:
|
|
715
|
+
out[column] = pd.Series(array, index=out.index)
|
|
716
|
+
continue
|
|
717
|
+
for patch in cells:
|
|
718
|
+
_set(out, patch.position, column, patch.value)
|
|
677
719
|
return out
|
|
678
720
|
|
|
679
721
|
|
|
722
|
+
_NULLABLE_INTEGERS = (
|
|
723
|
+
pd.Int8Dtype,
|
|
724
|
+
pd.Int16Dtype,
|
|
725
|
+
pd.Int32Dtype,
|
|
726
|
+
pd.Int64Dtype,
|
|
727
|
+
pd.UInt8Dtype,
|
|
728
|
+
pd.UInt16Dtype,
|
|
729
|
+
pd.UInt32Dtype,
|
|
730
|
+
pd.UInt64Dtype,
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
|
|
680
734
|
def _set(frame: pd.DataFrame, position: int, column: str, value: Any) -> None:
|
|
681
735
|
index = frame.columns.get_loc(column)
|
|
682
736
|
try:
|
|
@@ -823,18 +877,41 @@ def _report(
|
|
|
823
877
|
dbt_tables, dbt_refs = for_dbt(inputs.tables, inputs.refs, names)
|
|
824
878
|
tests = dbt_tests(dbt_tables, dbt_refs, hint_tests=hint_tests, test_tolerance=test_tolerance)
|
|
825
879
|
keys = {name: key for key, name in names.items()}
|
|
880
|
+
cache = Columns()
|
|
826
881
|
seeded_clean = {names[key]: as_seeded(frame) for key, frame in clean.items()}
|
|
827
|
-
seeded_broken =
|
|
828
|
-
|
|
829
|
-
|
|
882
|
+
seeded_broken = dict(seeded_clean)
|
|
883
|
+
done: dict[str, tuple[pd.DataFrame, Sequence[_Patch]]] = {}
|
|
884
|
+
for key, frame in broken.items():
|
|
885
|
+
if frame is not clean.get(key):
|
|
886
|
+
patches = [p for k, _, _, o in applied if k == key for p in o.patches]
|
|
887
|
+
seeded = _seeded_patched(clean[key], seeded_clean[names[key]], patches, cache)
|
|
888
|
+
seeded_broken[names[key]] = seeded
|
|
889
|
+
done[key] = (seeded, patches)
|
|
890
|
+
fails_clean = [fails(test, seeded_clean, cache=cache) for test in tests]
|
|
891
|
+
failing_clean = {test.name for test, failed in zip(tests, fails_clean, strict=True) if failed}
|
|
892
|
+
failing_broken = failing(tests, seeded_broken, cache=cache)
|
|
830
893
|
alone: list[set[str]] = []
|
|
831
894
|
for key, _, _, outcome in applied:
|
|
832
895
|
if not outcome.patches:
|
|
833
896
|
alone.append(set())
|
|
834
897
|
continue
|
|
898
|
+
# A test that does not read this table fails as it does on the clean data.
|
|
899
|
+
name = names[key]
|
|
835
900
|
seeded = dict(seeded_clean)
|
|
836
|
-
seeded[
|
|
837
|
-
|
|
901
|
+
seeded[name] = _seeded_patched(
|
|
902
|
+
clean[key], seeded_clean[name], outcome.patches, cache, done.get(key)
|
|
903
|
+
)
|
|
904
|
+
alone.append(
|
|
905
|
+
{
|
|
906
|
+
test.name
|
|
907
|
+
for test, failed in zip(tests, fails_clean, strict=True)
|
|
908
|
+
if (
|
|
909
|
+
fails(test, seeded, cache=cache)
|
|
910
|
+
if test.table == name or (test.parent is not None and test.parent[0] == name)
|
|
911
|
+
else failed
|
|
912
|
+
)
|
|
913
|
+
}
|
|
914
|
+
)
|
|
838
915
|
|
|
839
916
|
expected: list[ExpectedFailure] = []
|
|
840
917
|
by_defect: list[list[str]] = [[] for _ in applied]
|
|
@@ -871,6 +948,52 @@ def _report(
|
|
|
871
948
|
return report
|
|
872
949
|
|
|
873
950
|
|
|
951
|
+
def _seeded_patched(
|
|
952
|
+
frame: pd.DataFrame,
|
|
953
|
+
base: pd.DataFrame,
|
|
954
|
+
patches: Sequence[_Patch],
|
|
955
|
+
cache: Columns,
|
|
956
|
+
done: Optional[tuple[pd.DataFrame, Sequence[_Patch]]] = None,
|
|
957
|
+
) -> pd.DataFrame:
|
|
958
|
+
"""`as_seeded(_patched(frame, patches))`, given `base`, `as_seeded(frame)`.
|
|
959
|
+
|
|
960
|
+
Only the columns `patches` set are written out and read back, as a column
|
|
961
|
+
reads back the same whichever columns stand beside it; the others, and what
|
|
962
|
+
`cache` knows of them, are `base`'s. `done` is another seeded frame of
|
|
963
|
+
`frame` and the patches it was made with: a column those patches set exactly
|
|
964
|
+
as `patches` do is taken from it rather than written out again.
|
|
965
|
+
"""
|
|
966
|
+
mine = _by_column(patches)
|
|
967
|
+
theirs = _by_column(done[1]) if done is not None else {}
|
|
968
|
+
reuse = [
|
|
969
|
+
column
|
|
970
|
+
for column, own in mine.items()
|
|
971
|
+
if len(own) == len(theirs.get(column, ()))
|
|
972
|
+
and all(a is b for a, b in zip(own, theirs[column], strict=True))
|
|
973
|
+
]
|
|
974
|
+
fresh = [column for column in mine if column not in reuse]
|
|
975
|
+
seeded = base.copy()
|
|
976
|
+
if fresh:
|
|
977
|
+
part = as_seeded(
|
|
978
|
+
_patched(frame[fresh], [patch for patch in patches if patch.column in fresh])
|
|
979
|
+
)
|
|
980
|
+
for column in fresh:
|
|
981
|
+
seeded[column] = part[column]
|
|
982
|
+
cache.share(seeded, base, [column for column in base.columns if column not in mine])
|
|
983
|
+
if done is not None and reuse:
|
|
984
|
+
for column in reuse:
|
|
985
|
+
seeded[column] = done[0][column]
|
|
986
|
+
cache.share(seeded, done[0], reuse)
|
|
987
|
+
return seeded
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
def _by_column(patches: Sequence[_Patch]) -> dict[str, list[_Patch]]:
|
|
991
|
+
by_column: dict[str, list[_Patch]] = {}
|
|
992
|
+
for patch in patches:
|
|
993
|
+
by_column.setdefault(patch.column, []).append(patch)
|
|
994
|
+
return by_column
|
|
995
|
+
|
|
996
|
+
|
|
874
997
|
def _culprits(
|
|
875
998
|
test: DbtTest,
|
|
876
999
|
alone: list[set[str]],
|
|
@@ -932,15 +1055,17 @@ def _recount(defect: Defect, outcome: _Outcome, frame: Optional[pd.DataFrame]) -
|
|
|
932
1055
|
by_row: dict[int, list[_Patch]] = {}
|
|
933
1056
|
for patch in outcome.patches:
|
|
934
1057
|
by_row.setdefault(patch.position, []).append(patch)
|
|
1058
|
+
columns = sorted({patch.column for patch in outcome.patches})
|
|
1059
|
+
cells = {column: frame[column].tolist() for column in columns}
|
|
935
1060
|
held = [
|
|
936
1061
|
p
|
|
937
1062
|
for p in outcome.positions
|
|
938
|
-
if all(_same(
|
|
1063
|
+
if all(_same(cells[c.column][p], c.value) for c in by_row.get(p, []))
|
|
939
1064
|
]
|
|
940
1065
|
if defect.type == "duplicate_keys":
|
|
941
|
-
columns = sorted({patch.column for patch in outcome.patches})
|
|
942
1066
|
keys = [
|
|
943
|
-
tuple(_comparable
|
|
1067
|
+
tuple(map(_comparable, row))
|
|
1068
|
+
for row in zip(*(cells[column] for column in columns), strict=True)
|
|
944
1069
|
]
|
|
945
1070
|
counts: dict[tuple, int] = {}
|
|
946
1071
|
for key in keys:
|
|
@@ -998,7 +1123,8 @@ def _applied(
|
|
|
998
1123
|
# A defect with no table to break (a history not kept) broke no row.
|
|
999
1124
|
primary = primary_key(inputs.tables[key]) if key in inputs.tables else []
|
|
1000
1125
|
numbers = [position + 1 for position in outcome.positions]
|
|
1001
|
-
|
|
1126
|
+
held = {name: frame[name].tolist() for name in primary} if outcome.positions else {}
|
|
1127
|
+
keys = [[_native(held[name][p]) for name in primary] for p in outcome.positions]
|
|
1002
1128
|
rows: list[Any] = [values[0] if len(values) == 1 else values for values in keys]
|
|
1003
1129
|
row_key: Optional[list[str]] = list(primary) or None
|
|
1004
1130
|
if not primary:
|