model2data 1.10.3__tar.gz → 1.11.0__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.3/model2data.egg-info → model2data-1.11.0}/PKG-INFO +3 -2
- {model2data-1.10.3 → model2data-1.11.0}/README.md +30 -4
- {model2data-1.10.3 → model2data-1.11.0}/README_PYPI.md +2 -1
- {model2data-1.10.3 → model2data-1.11.0}/model2data/cli.py +57 -7
- {model2data-1.10.3 → model2data-1.11.0}/model2data/dbt/hint_tests.py +58 -9
- model2data-1.11.0/model2data/dbt/templates/hint_macros/model2data_when_tests.sql +56 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/dbt/tests.py +2 -1
- {model2data-1.10.3 → model2data-1.11.0}/model2data/defects/checks.py +49 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/generate/core.py +203 -55
- {model2data-1.10.3 → model2data-1.11.0}/model2data/generate/days.py +54 -4
- {model2data-1.10.3 → model2data-1.11.0}/model2data/generate/faker.py +30 -1
- {model2data-1.10.3 → model2data-1.11.0}/model2data/generate/hints.py +27 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/generate/kinds.py +6 -2
- model2data-1.11.0/model2data/generate/when.py +385 -0
- model2data-1.11.0/model2data/guide/setup.md +76 -0
- model2data-1.11.0/model2data/guide/triage.md +68 -0
- model2data-1.11.0/model2data/guide/tune.md +139 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/model/__init__.py +1 -1
- {model2data-1.10.3 → model2data-1.11.0}/model2data/model/dbml.py +7 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/model/document.py +17 -4
- {model2data-1.10.3 → model2data-1.11.0}/model2data/model/types.py +1 -1
- {model2data-1.10.3 → model2data-1.11.0}/model2data/model/validate.py +157 -11
- {model2data-1.10.3 → model2data-1.11.0}/model2data/spec/PUBLISHING.md +15 -11
- {model2data-1.10.3 → model2data-1.11.0}/model2data/spec/README.md +37 -4
- {model2data-1.10.3 → model2data-1.11.0}/model2data/spec/model.schema.json +19 -5
- {model2data-1.10.3 → model2data-1.11.0/model2data.egg-info}/PKG-INFO +3 -2
- {model2data-1.10.3 → model2data-1.11.0}/model2data.egg-info/SOURCES.txt +8 -1
- {model2data-1.10.3 → model2data-1.11.0}/pyproject.toml +2 -1
- {model2data-1.10.3 → model2data-1.11.0}/tests/test_cli.py +2 -2
- {model2data-1.10.3 → model2data-1.11.0}/tests/test_cli_model.py +31 -0
- {model2data-1.10.3 → model2data-1.11.0}/tests/test_days.py +26 -0
- {model2data-1.10.3 → model2data-1.11.0}/tests/test_defects.py +3 -1
- {model2data-1.10.3 → model2data-1.11.0}/tests/test_generation.py +134 -6
- model2data-1.11.0/tests/test_guide.py +186 -0
- {model2data-1.10.3 → model2data-1.11.0}/tests/test_model_messages.py +4 -4
- {model2data-1.10.3 → model2data-1.11.0}/tests/test_spec_alignment.py +109 -0
- model2data-1.11.0/tests/test_when.py +974 -0
- {model2data-1.10.3 → model2data-1.11.0}/LICENSE +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/__init__.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/__init__.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/LICENSE +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/VENDORED.md +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/__init__.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/_classes/__init__.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/_classes/base.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/_classes/column.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/_classes/enum.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/_classes/expression.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/_classes/index.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/_classes/note.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/_classes/project.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/_classes/reference.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/_classes/sticky_note.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/_classes/table.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/_classes/table_group.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/classes/__init__.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/constants.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/database.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/definitions/__init__.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/definitions/column.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/definitions/common.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/definitions/enum.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/definitions/generic.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/definitions/index.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/definitions/project.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/definitions/records.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/definitions/reference.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/definitions/sticky_note.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/definitions/table.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/definitions/table_group.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/definitions/table_partial.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/exceptions.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/parser/__init__.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/parser/blueprints.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/parser/parser.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/renderer/__init__.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/renderer/base.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/renderer/dbml/__init__.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/renderer/dbml/default/__init__.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/renderer/dbml/default/column.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/renderer/dbml/default/enum.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/renderer/dbml/default/expression.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/renderer/dbml/default/index.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/renderer/dbml/default/note.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/renderer/dbml/default/project.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/renderer/dbml/default/reference.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/renderer/dbml/default/renderer.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/renderer/dbml/default/sticky_note.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/renderer/dbml/default/table.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/renderer/dbml/default/table_group.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/renderer/dbml/default/utils.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/renderer/sql/__init__.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/renderer/sql/default/__init__.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/renderer/sql/default/column.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/renderer/sql/default/enum.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/renderer/sql/default/expression.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/renderer/sql/default/index.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/renderer/sql/default/note.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/renderer/sql/default/reference.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/renderer/sql/default/renderer.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/renderer/sql/default/table.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/renderer/sql/default/utils.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/_vendor/pydbml/tools.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/dbt/__init__.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/dbt/naming.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/dbt/project.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/dbt/templates/dbt_project.yml.jinja +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/dbt/templates/hint_macros/model2data_hint_tests.sql +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/dbt/templates/history_macros/model2data_history_tests.sql +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/dbt/templates/macros/generate_schema_name.sql +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/dbt/templates/profiles.yml.jinja +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/defects/__init__.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/defects/apply.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/defects/presets.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/defects/report.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/generate/__init__.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/generate/history.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/generate/options.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/generate/relationships.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/generate/timeline.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/model/_yaml.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/model/dump.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/model/engine.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/model/errors.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/model/reader.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/output.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/parse/__init__.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/parse/dbml.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/spec/examples/coffee_webshop.model2data.yml +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data/utils.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data.egg-info/dependency_links.txt +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data.egg-info/entry_points.txt +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data.egg-info/requires.txt +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/model2data.egg-info/top_level.txt +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/setup.cfg +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/tests/test_as_of_anchor.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/tests/test_column_time_hints.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/tests/test_coverage_gaps.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/tests/test_dbml_parser.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/tests/test_dbml_parser_fuzz.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/tests/test_dbt_integration.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/tests/test_dbt_naming.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/tests/test_dbt_project.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/tests/test_dbt_tests.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/tests/test_defects_dbt.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/tests/test_defects_replay.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/tests/test_defects_report.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/tests/test_defects_review.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/tests/test_determinism.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/tests/test_distributions.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/tests/test_examples_model.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/tests/test_faker_name_inference.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/tests/test_fast_paths.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/tests/test_hint_tests.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/tests/test_history.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/tests/test_issue_lines.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/tests/test_lone_country.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/tests/test_model.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/tests/test_model_dbml.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/tests/test_model_edges.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/tests/test_model_vocabulary.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/tests/test_options.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/tests/test_release_stress.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/tests/test_reproducibility.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/tests/test_row_identity.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/tests/test_shaping.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/tests/test_spec_schema.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/tests/test_table_seeds.py +0 -0
- {model2data-1.10.3 → model2data-1.11.0}/tests/test_timeline.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: model2data
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.11.0
|
|
4
4
|
Summary: Generate analytics-ready datasets from DBML models
|
|
5
5
|
Author: JB Analytica
|
|
6
6
|
License-Expression: MIT
|
|
@@ -132,7 +132,7 @@ access required.
|
|
|
132
132
|
## The model file
|
|
133
133
|
|
|
134
134
|
A model is one YAML document, `<name>.model2data.yml` — or the same document as JSON. Its format
|
|
135
|
-
is [spec 0.
|
|
135
|
+
is [spec 0.4.0](https://github.com/JB-Analytica/model2data/blob/main/model2data/spec/README.md), with a JSON Schema your editor can autocomplete and
|
|
136
136
|
check against:
|
|
137
137
|
|
|
138
138
|
```yaml
|
|
@@ -292,6 +292,7 @@ dbt package, so `dbt build` still works offline):
|
|
|
292
292
|
| `generate: {after: other}` | `model2data_not_before` (where both are not null) | none |
|
|
293
293
|
| `generate: {null_rate}` | `model2data_max_null_share`: nulls at most `null_rate` + tolerance | `--test-tolerance`, default 0.1 |
|
|
294
294
|
| `generate: {distinct: n}` | `model2data_max_distinct`: at most `n` distinct values | none |
|
|
295
|
+
| `generate: {when}` | `model2data_when`: null exactly where the condition does not hold (with `null_rate`, only there; its null share is then tested among the matching rows) | none |
|
|
295
296
|
| `grain` on a table | `model2data_unique_combination` | none |
|
|
296
297
|
| an enum-typed column | `accepted_values` (always written) | none |
|
|
297
298
|
|
|
@@ -16,6 +16,13 @@ relationship-preserving synthetic data
|
|
|
16
16
|
DuckDB or Postgres profile. No sample data to hunt down, no dbt boilerplate to hand-write, no
|
|
17
17
|
production data to risk exposing.
|
|
18
18
|
|
|
19
|
+
Driving model2data from a coding agent? Have it start here; the instructions ship with the
|
|
20
|
+
installed version and pick a topic from the current directory:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
uvx model2data@latest guide
|
|
24
|
+
```
|
|
25
|
+
|
|
19
26
|
A working analytics stack — real (synthetic) data, tested dbt models, queryable in
|
|
20
27
|
DuckDB — from a schema file, in seconds:
|
|
21
28
|
|
|
@@ -123,7 +130,7 @@ flowchart LR
|
|
|
123
130
|
## The model file
|
|
124
131
|
|
|
125
132
|
A model is one YAML document, `<name>.model2data.yml` — or the same document as JSON. Its
|
|
126
|
-
format is [spec 0.
|
|
133
|
+
format is [spec 0.4.0](model2data/spec/README.md), with a JSON Schema
|
|
127
134
|
([`model.schema.json`](model2data/spec/model.schema.json)) your editor can autocomplete and check
|
|
128
135
|
against:
|
|
129
136
|
|
|
@@ -222,14 +229,14 @@ jobs:
|
|
|
222
229
|
```
|
|
223
230
|
|
|
224
231
|
Inputs: `files` (glob, default `**/*.model2data.yml`), `version` (default: the release the tag
|
|
225
|
-
points at, e.g. `1.
|
|
232
|
+
points at, e.g. `1.11.0`; a specifier such as `>=1.10,<2` also works), `python-version` (3.12).
|
|
226
233
|
|
|
227
234
|
**pre-commit**
|
|
228
235
|
|
|
229
236
|
```yaml
|
|
230
237
|
repos:
|
|
231
238
|
- repo: https://github.com/JB-Analytica/model2data
|
|
232
|
-
rev: v1.
|
|
239
|
+
rev: v1.11.0
|
|
233
240
|
hooks:
|
|
234
241
|
- id: model2data-validate
|
|
235
242
|
```
|
|
@@ -243,7 +250,7 @@ model2data:
|
|
|
243
250
|
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
244
251
|
changes: ["**/*.model2data.yml"]
|
|
245
252
|
script:
|
|
246
|
-
- pip install model2data==1.
|
|
253
|
+
- pip install model2data==1.11.0
|
|
247
254
|
- model2data validate --glob "**/*.model2data.yml"
|
|
248
255
|
```
|
|
249
256
|
|
|
@@ -394,6 +401,24 @@ shipped_at:
|
|
|
394
401
|
generate: {after: ordered_at}
|
|
395
402
|
```
|
|
396
403
|
|
|
404
|
+
A column that only has a value in some states says which with `when` (spec 0.4.0: the model says
|
|
405
|
+
`model2data: 0.4.0`): it holds a value on
|
|
406
|
+
exactly the rows whose named column holds one of the listed values, and is null on every other
|
|
407
|
+
row — no `pending` order with a `shipped_at`, no `cancelled` subscription without a
|
|
408
|
+
`cancelled_at`:
|
|
409
|
+
|
|
410
|
+
```yaml
|
|
411
|
+
shipped_at:
|
|
412
|
+
type: timestamp
|
|
413
|
+
generate: {after: ordered_at, when: {status: [shipped, delivered]}}
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
The named column is another column of the table (an enum, boolean, number or text column; several
|
|
417
|
+
named must all match), and the column carrying `when` is nullable. On the matching rows it is
|
|
418
|
+
never null, unless it has a `null_rate`, which then counts only those rows. On days after the
|
|
419
|
+
first (`--days`), a row that moves into a listed status gets its value that day and a row that
|
|
420
|
+
moves out loses it. Every other column comes out exactly as it would without the hint.
|
|
421
|
+
|
|
397
422
|
The flags above (or `run.shape` in the model) shape every date and timestamp column the same
|
|
398
423
|
way, run-wide. `business_hours`, `growth`, and `seasonality` column hints override that for one
|
|
399
424
|
column at a time — the whole point being a run can be uniform everywhere except the one column
|
|
@@ -525,6 +550,7 @@ dbt package, so `dbt build` still works offline):
|
|
|
525
550
|
| `generate: {after: other}` | `model2data_not_before` (where both are not null) | none |
|
|
526
551
|
| `generate: {null_rate}` | `model2data_max_null_share`: nulls at most `null_rate` + tolerance | `--test-tolerance`, default 0.1 |
|
|
527
552
|
| `generate: {distinct: n}` | `model2data_max_distinct`: at most `n` distinct values | none |
|
|
553
|
+
| `generate: {when}` | `model2data_when`: null exactly where the condition does not hold (with `null_rate`, only there; its null share is then tested among the matching rows) | none |
|
|
528
554
|
| `grain` on a table | `model2data_unique_combination` | none |
|
|
529
555
|
| an enum-typed column | `accepted_values` (always written) | none |
|
|
530
556
|
|
|
@@ -85,7 +85,7 @@ access required.
|
|
|
85
85
|
## The model file
|
|
86
86
|
|
|
87
87
|
A model is one YAML document, `<name>.model2data.yml` — or the same document as JSON. Its format
|
|
88
|
-
is [spec 0.
|
|
88
|
+
is [spec 0.4.0](https://github.com/JB-Analytica/model2data/blob/main/model2data/spec/README.md), with a JSON Schema your editor can autocomplete and
|
|
89
89
|
check against:
|
|
90
90
|
|
|
91
91
|
```yaml
|
|
@@ -245,6 +245,7 @@ dbt package, so `dbt build` still works offline):
|
|
|
245
245
|
| `generate: {after: other}` | `model2data_not_before` (where both are not null) | none |
|
|
246
246
|
| `generate: {null_rate}` | `model2data_max_null_share`: nulls at most `null_rate` + tolerance | `--test-tolerance`, default 0.1 |
|
|
247
247
|
| `generate: {distinct: n}` | `model2data_max_distinct`: at most `n` distinct values | none |
|
|
248
|
+
| `generate: {when}` | `model2data_when`: null exactly where the condition does not hold (with `null_rate`, only there; its null share is then tested among the matching rows) | none |
|
|
248
249
|
| `grain` on a table | `model2data_unique_combination` | none |
|
|
249
250
|
| an enum-typed column | `accepted_values` (always written) | none |
|
|
250
251
|
|
|
@@ -2,6 +2,8 @@ import random
|
|
|
2
2
|
import shutil
|
|
3
3
|
from collections.abc import Iterable
|
|
4
4
|
from datetime import date, datetime
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from importlib import resources
|
|
5
7
|
from pathlib import Path
|
|
6
8
|
from typing import Any, Optional
|
|
7
9
|
|
|
@@ -163,13 +165,13 @@ app = typer.Typer(
|
|
|
163
165
|
cls=_GenerateByDefault,
|
|
164
166
|
help=(
|
|
165
167
|
"model2data: Generate analytics-ready datasets from a data model.\n\n"
|
|
166
|
-
"Given a model -- a .model2data.yml document (spec 0.
|
|
168
|
+
"Given a model -- a .model2data.yml document (spec 0.4.0), the same as JSON,\n"
|
|
167
169
|
"or a DBML file -- this tool produces:\n"
|
|
168
170
|
"• Synthetic but realistic data\n"
|
|
169
171
|
"• A runnable dbt project scaffold\n"
|
|
170
172
|
"• dbt seeds, staging models, and profiles\n\n"
|
|
171
173
|
"`model2data --file MODEL` generates; `validate` and `convert` check and\n"
|
|
172
|
-
"convert a model."
|
|
174
|
+
"convert a model. Agents: run `model2data guide` first."
|
|
173
175
|
),
|
|
174
176
|
add_completion=False,
|
|
175
177
|
)
|
|
@@ -369,7 +371,7 @@ def main(
|
|
|
369
371
|
"--hint-tests",
|
|
370
372
|
help=(
|
|
371
373
|
"Write the model's generation hints as dbt tests (min/max range, after, "
|
|
372
|
-
"null_rate, distinct, grain) at this severity: error, warn or off. They "
|
|
374
|
+
"null_rate, distinct, when, grain) at this severity: error, warn or off. They "
|
|
373
375
|
"describe intent, so they warn by default rather than break a first dbt "
|
|
374
376
|
"build on real data."
|
|
375
377
|
),
|
|
@@ -735,8 +737,12 @@ def main(
|
|
|
735
737
|
|
|
736
738
|
|
|
737
739
|
def _spec_of(file: Path) -> str:
|
|
738
|
-
"""The spec a conforming model is written against: 0.3.0, or 0.2.0 (DBML
|
|
739
|
-
|
|
740
|
+
"""The spec a conforming model is written against: 0.4.0, 0.3.0, or 0.2.0 (DBML without
|
|
741
|
+
`when` included)."""
|
|
742
|
+
version = str(load(file).version)
|
|
743
|
+
return (
|
|
744
|
+
"0.4.0" if version.startswith("0.4") else "0.3.0" if version.startswith("0.3") else "0.2.0"
|
|
745
|
+
)
|
|
740
746
|
|
|
741
747
|
|
|
742
748
|
def _print_defects(report: DefectsReport) -> None:
|
|
@@ -815,7 +821,7 @@ def validate_command(
|
|
|
815
821
|
False, "--require-files", help="Exit 1 when no model file was given or matched."
|
|
816
822
|
),
|
|
817
823
|
):
|
|
818
|
-
"""Check that models conform to spec 0.
|
|
824
|
+
"""Check that models conform to spec 0.4.0 (or 0.2.x, 0.3.x), printing every issue with its path.
|
|
819
825
|
|
|
820
826
|
Takes one or more files and/or --glob patterns. Exits 1 when any file has an error;
|
|
821
827
|
warnings are printed, and the model conforms.
|
|
@@ -874,7 +880,7 @@ def convert_command(
|
|
|
874
880
|
force: bool = typer.Option(False, "--force", help="Overwrite the output file if it exists."),
|
|
875
881
|
):
|
|
876
882
|
"""Convert a model -- DBML, typically -- to a .model2data.yml document (spec 0.2.0, or
|
|
877
|
-
0.3.0 when it has defects)."""
|
|
883
|
+
0.3.0 when it has defects, 0.4.0 when it has `when`)."""
|
|
878
884
|
try:
|
|
879
885
|
model = load(file)
|
|
880
886
|
except ModelError as error:
|
|
@@ -891,3 +897,47 @@ def convert_command(
|
|
|
891
897
|
raise typer.Exit(1)
|
|
892
898
|
output.write_text(text, encoding="utf-8")
|
|
893
899
|
typer.echo(f"✅ Wrote {output}")
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
class GuideTopic(str, Enum):
|
|
903
|
+
setup = "setup"
|
|
904
|
+
triage = "triage"
|
|
905
|
+
tune = "tune"
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
_MODEL_PATTERNS = ("*.model2data.yml", "*.model2data.yaml", "*.model2data.json", "*.dbml")
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
def _detect_topic(cwd: Path) -> tuple[GuideTopic, str]:
|
|
912
|
+
"""`triage` when the current directory holds a model, else `setup`, and why.
|
|
913
|
+
|
|
914
|
+
Reads file names in `cwd` only: no subdirectories, no network.
|
|
915
|
+
"""
|
|
916
|
+
models = sorted(path.name for pattern in _MODEL_PATTERNS for path in cwd.glob(pattern))
|
|
917
|
+
if models:
|
|
918
|
+
return GuideTopic.triage, f"found {', '.join(models)}"
|
|
919
|
+
return GuideTopic.setup, "no *.model2data.yml or *.dbml in this directory"
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
@app.command("guide")
|
|
923
|
+
def guide_command(
|
|
924
|
+
topic: Optional[GuideTopic] = typer.Argument( # noqa: B008
|
|
925
|
+
None,
|
|
926
|
+
help=(
|
|
927
|
+
"setup: no model here yet -- install, write one, first run. "
|
|
928
|
+
"triage: what validate, generate and dbt build printed, and what to do. "
|
|
929
|
+
"tune: every option, model key and exit code. "
|
|
930
|
+
"Default: picked from the current directory (a *.model2data.yml or *.dbml "
|
|
931
|
+
"file in it means triage)."
|
|
932
|
+
),
|
|
933
|
+
),
|
|
934
|
+
):
|
|
935
|
+
"""Print instructions for an agent about to use model2data, as Markdown.
|
|
936
|
+
|
|
937
|
+
Reads nothing but file names in the current directory and writes nothing.
|
|
938
|
+
"""
|
|
939
|
+
if topic is None:
|
|
940
|
+
topic, reason = _detect_topic(Path.cwd())
|
|
941
|
+
typer.echo(f"# model2data guide: {reason} -> {topic.value}\n")
|
|
942
|
+
page = resources.files("model2data").joinpath(f"guide/{topic.value}.md")
|
|
943
|
+
typer.echo(page.read_text("utf-8"), nl=False)
|
|
@@ -14,19 +14,27 @@ What becomes a test, and what does not:
|
|
|
14
14
|
| `generate.after: other` | `model2data_not_before` | none |
|
|
15
15
|
| `generate.null_rate` | `model2data_max_null_share` | yes |
|
|
16
16
|
| `generate.distinct: n` | `model2data_max_distinct` | none |
|
|
17
|
+
| `generate.when` | `model2data_when` | none |
|
|
17
18
|
| table `grain` | `model2data_unique_combination` | none |
|
|
18
19
|
|
|
20
|
+
A column with `when` and `null_rate` has its null share tested among the rows
|
|
21
|
+
`when` matches (`model2data_when_max_null_share`), since only those rows can
|
|
22
|
+
hold a value, and its `model2data_when` test lets those rows be null.
|
|
23
|
+
|
|
19
24
|
Enum columns keep the `accepted_values` test `generate_dbt_yml` always wrote.
|
|
20
25
|
`true_rate`, `weights`, `skew`, `distribution` and the temporal shape hints
|
|
21
26
|
(`business_hours`, `growth`, `seasonality`) describe a statistical shape, not a
|
|
22
27
|
constraint a row can break, so they produce no test.
|
|
23
28
|
|
|
24
29
|
The generic tests live in `macros/model2data_hint_tests.sql` of the generated
|
|
25
|
-
project (see `write_hint_macros`)
|
|
30
|
+
project (see `write_hint_macros`), and the `when` ones in
|
|
31
|
+
`macros/model2data_when_tests.sql`, written only for a model that has one, so
|
|
32
|
+
a project without `when` is what it was: plain SQL, no dbt package.
|
|
26
33
|
"""
|
|
27
34
|
|
|
28
35
|
from __future__ import annotations
|
|
29
36
|
|
|
37
|
+
import copy
|
|
30
38
|
from collections.abc import Mapping
|
|
31
39
|
from dataclasses import dataclass, field
|
|
32
40
|
from pathlib import Path
|
|
@@ -43,6 +51,8 @@ DEFAULT_TOLERANCE = 0.1
|
|
|
43
51
|
|
|
44
52
|
MACROS_FILE = "model2data_hint_tests.sql"
|
|
45
53
|
_MACROS_SOURCE = Path(__file__).parent / "templates" / "hint_macros" / MACROS_FILE
|
|
54
|
+
WHEN_MACROS_FILE = "model2data_when_tests.sql"
|
|
55
|
+
_WHEN_MACROS_SOURCE = Path(__file__).parent / "templates" / "hint_macros" / WHEN_MACROS_FILE
|
|
46
56
|
|
|
47
57
|
|
|
48
58
|
@dataclass(frozen=True)
|
|
@@ -53,7 +63,7 @@ class HintTest:
|
|
|
53
63
|
`column` is None for a table-level test (grain). `test` is the generic
|
|
54
64
|
test's name, `arguments` its parameters as dbt's `arguments:` block, and
|
|
55
65
|
`hint` the hint it comes from (`min`, `max`, `after`, `null_rate`,
|
|
56
|
-
`distinct`, `grain`). `min` and `max` of one column share one test.
|
|
66
|
+
`distinct`, `when`, `grain`). `min` and `max` of one column share one test.
|
|
57
67
|
"""
|
|
58
68
|
|
|
59
69
|
table: str
|
|
@@ -148,14 +158,39 @@ def _column_tests(
|
|
|
148
158
|
tests.append(HintTest(table, column.name, "model2data_not_before", arguments, "after"))
|
|
149
159
|
|
|
150
160
|
null_rate = note.get("null_rate")
|
|
151
|
-
if isinstance(null_rate, (int, float))
|
|
161
|
+
if not isinstance(null_rate, (int, float)) or isinstance(null_rate, bool):
|
|
162
|
+
null_rate = None
|
|
163
|
+
conditions = _when_conditions(note.get("when"), kinds_of)
|
|
164
|
+
if conditions is not None:
|
|
165
|
+
when_arguments: dict[str, Any] = {"conditions": conditions}
|
|
166
|
+
if null_rate is not None:
|
|
167
|
+
when_arguments["required"] = False
|
|
168
|
+
tests.append(HintTest(table, column.name, "model2data_when", when_arguments, "when"))
|
|
169
|
+
|
|
170
|
+
if null_rate is not None:
|
|
152
171
|
# Rounded so 0.1 + 0.1 reaches the YAML as 0.2, not 0.2000000000000001.
|
|
153
172
|
limit = round(min(1.0, null_rate + tolerance), 10)
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
173
|
+
if conditions is None:
|
|
174
|
+
tests.append(
|
|
175
|
+
HintTest(
|
|
176
|
+
table,
|
|
177
|
+
column.name,
|
|
178
|
+
"model2data_max_null_share",
|
|
179
|
+
{"max_share": limit},
|
|
180
|
+
"null_rate",
|
|
181
|
+
)
|
|
182
|
+
)
|
|
183
|
+
else:
|
|
184
|
+
tests.append(
|
|
185
|
+
HintTest(
|
|
186
|
+
table,
|
|
187
|
+
column.name,
|
|
188
|
+
"model2data_when_max_null_share",
|
|
189
|
+
# Its own copy, or the YAML would share the mapping as an anchor.
|
|
190
|
+
{"conditions": copy.deepcopy(conditions), "max_share": limit},
|
|
191
|
+
"null_rate",
|
|
192
|
+
)
|
|
157
193
|
)
|
|
158
|
-
)
|
|
159
194
|
|
|
160
195
|
distinct = note.get("distinct")
|
|
161
196
|
if isinstance(distinct, int) and not isinstance(distinct, bool) and distinct > 0:
|
|
@@ -167,14 +202,28 @@ def _column_tests(
|
|
|
167
202
|
return tests
|
|
168
203
|
|
|
169
204
|
|
|
170
|
-
def
|
|
205
|
+
def _when_conditions(when: Any, kinds_of: dict[str, ColumnDef]) -> Optional[dict[str, list]]:
|
|
206
|
+
"""A `when` hint as the test's `conditions`: an enum's values as the member text it holds."""
|
|
207
|
+
if not isinstance(when, dict) or not when or not all(name in kinds_of for name in when):
|
|
208
|
+
return None
|
|
209
|
+
return {
|
|
210
|
+
name: [str(value) for value in values] if kinds_of[name].enum_values else list(values)
|
|
211
|
+
for name, values in when.items()
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def write_hint_macros(dest: Path, *, when: bool = False) -> Path:
|
|
171
216
|
"""Write the generic tests into `dest/macros/`, and return the file's path.
|
|
172
217
|
|
|
173
218
|
A project whose YAML holds `hint_tests_for` tests needs this file next to
|
|
174
|
-
it; `generate_dbt_yml` writes it itself when it emits any.
|
|
219
|
+
it; `generate_dbt_yml` writes it itself when it emits any. With `when`, the
|
|
220
|
+
file of the `when` tests is written beside it (`WHEN_MACROS_FILE`), which a
|
|
221
|
+
project holding a `model2data_when` test needs too.
|
|
175
222
|
"""
|
|
176
223
|
macros = dest / "macros"
|
|
177
224
|
macros.mkdir(parents=True, exist_ok=True)
|
|
178
225
|
target = macros / MACROS_FILE
|
|
179
226
|
target.write_text(_MACROS_SOURCE.read_text())
|
|
227
|
+
if when:
|
|
228
|
+
(macros / WHEN_MACROS_FILE).write_text(_WHEN_MACROS_SOURCE.read_text())
|
|
180
229
|
return target
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{#-
|
|
2
|
+
Generic tests model2data writes for a column's `when` hint: the column holds a
|
|
3
|
+
value only on the rows whose named columns hold one of the listed values.
|
|
4
|
+
Written beside model2data_hint_tests.sql, and only by a model that has a
|
|
5
|
+
`when`. Plain ANSI SQL, runs on DuckDB and Postgres.
|
|
6
|
+
`conditions` maps a raw column name to the values it must hold; a row whose
|
|
7
|
+
named column is null does not match.
|
|
8
|
+
-#}
|
|
9
|
+
|
|
10
|
+
{% macro model2data_when_literal(value) -%}
|
|
11
|
+
{%- if value is sameas true -%}true
|
|
12
|
+
{%- elif value is sameas false -%}false
|
|
13
|
+
{%- elif value is number -%}{{ value }}
|
|
14
|
+
{%- else -%}'{{ value | string | replace("'", "''") }}'
|
|
15
|
+
{%- endif -%}
|
|
16
|
+
{%- endmacro %}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
{% macro model2data_when_matches(conditions) -%}
|
|
20
|
+
(case when true
|
|
21
|
+
{%- for column, values in conditions.items() %}
|
|
22
|
+
and {{ adapter.quote(column) }} in (
|
|
23
|
+
{%- for value in values -%}
|
|
24
|
+
{{ model2data_when_literal(value) }}{% if not loop.last %}, {% endif %}
|
|
25
|
+
{%- endfor -%}
|
|
26
|
+
)
|
|
27
|
+
{%- endfor %}
|
|
28
|
+
then 1 else 0 end = 1)
|
|
29
|
+
{%- endmacro %}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
{#-
|
|
33
|
+
A row that does not match holds a value, or (unless `required` is false, for
|
|
34
|
+
a column with a `null_rate`) a row that matches holds none.
|
|
35
|
+
-#}
|
|
36
|
+
{% test model2data_when(model, column_name, conditions, required=true) %}
|
|
37
|
+
select {{ column_name }} as value
|
|
38
|
+
from {{ model }}
|
|
39
|
+
where (not {{ model2data_when_matches(conditions) }} and {{ column_name }} is not null)
|
|
40
|
+
{%- if required %}
|
|
41
|
+
or ({{ model2data_when_matches(conditions) }} and {{ column_name }} is null)
|
|
42
|
+
{%- endif %}
|
|
43
|
+
{% endtest %}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
{#- `null_rate` with `when`: the null share among the matching rows only. -#}
|
|
47
|
+
{% test model2data_when_max_null_share(model, column_name, conditions, max_share) %}
|
|
48
|
+
select null_share
|
|
49
|
+
from (
|
|
50
|
+
select
|
|
51
|
+
avg(case when {{ column_name }} is null then 1.0 else 0.0 end) as null_share
|
|
52
|
+
from {{ model }}
|
|
53
|
+
where {{ model2data_when_matches(conditions) }}
|
|
54
|
+
) as shares
|
|
55
|
+
where null_share > {{ max_share }}
|
|
56
|
+
{% endtest %}
|
|
@@ -68,7 +68,8 @@ def generate_dbt_yml(
|
|
|
68
68
|
|
|
69
69
|
hinted = _hinted(tables, hint_tests, test_tolerance)
|
|
70
70
|
if hinted:
|
|
71
|
-
|
|
71
|
+
when = any(test.hint == "when" for tests in hinted.values() for test in tests)
|
|
72
|
+
write_hint_macros(dest, when=when)
|
|
72
73
|
if any(_history_entries(table) for table in tables.values()):
|
|
73
74
|
write_history_macros(dest)
|
|
74
75
|
fk_map = _fk_map(tables, refs)
|
|
@@ -14,6 +14,10 @@ SQL reads:
|
|
|
14
14
|
and `model2data_max_distinct`: as their macros in
|
|
15
15
|
`dbt/templates/hint_macros/model2data_hint_tests.sql`, nulls skipped where the
|
|
16
16
|
SQL skips them.
|
|
17
|
+
- `model2data_when` and `model2data_when_max_null_share`: as their macros in
|
|
18
|
+
`dbt/templates/hint_macros/model2data_when_tests.sql`, a row matching when
|
|
19
|
+
each named column's text is one of its values (a boolean as `true`/`false`
|
|
20
|
+
in any case, a number as a number).
|
|
17
21
|
- `model2data_unique_combination` and a composite key's `unique_combination`: a
|
|
18
22
|
combination of values on two rows, nulls grouping together as in `group by`.
|
|
19
23
|
- A history table's `model2data_one_current_row` (a key with other than one
|
|
@@ -342,6 +346,49 @@ def _max_null_share(test: DbtTest, frame: pd.DataFrame, seeded: Mapping, cache:
|
|
|
342
346
|
return nulls / len(frame) > test.arguments["max_share"]
|
|
343
347
|
|
|
344
348
|
|
|
349
|
+
def _matches(test: DbtTest, frame: pd.DataFrame, cache: Columns) -> list[bool]:
|
|
350
|
+
"""Per row: does each column of the test's `conditions` hold one of its values?"""
|
|
351
|
+
result = [True] * len(frame)
|
|
352
|
+
for column, values in test.arguments["conditions"].items():
|
|
353
|
+
texts = {value for value in values if isinstance(value, str)}
|
|
354
|
+
flags = {str(value).lower() for value in values if isinstance(value, bool)}
|
|
355
|
+
numbers = {
|
|
356
|
+
float(value)
|
|
357
|
+
for value in values
|
|
358
|
+
if isinstance(value, (int, float)) and not isinstance(value, bool)
|
|
359
|
+
}
|
|
360
|
+
for row, text in enumerate(cache.values(frame, column)):
|
|
361
|
+
if text is None or not (
|
|
362
|
+
text in texts or text.lower() in flags or _number(text) in numbers
|
|
363
|
+
):
|
|
364
|
+
result[row] = False
|
|
365
|
+
return result
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _when(test: DbtTest, frame: pd.DataFrame, seeded: Mapping, cache: Columns) -> bool:
|
|
369
|
+
required = test.arguments.get("required", True)
|
|
370
|
+
values = cache.values(frame, str(test.column))
|
|
371
|
+
for matches, value in zip(_matches(test, frame, cache), values, strict=True):
|
|
372
|
+
if (not matches and value is not None) or (required and matches and value is None):
|
|
373
|
+
return True
|
|
374
|
+
return False
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _when_max_null_share(
|
|
378
|
+
test: DbtTest, frame: pd.DataFrame, seeded: Mapping, cache: Columns
|
|
379
|
+
) -> bool:
|
|
380
|
+
values = cache.values(frame, str(test.column))
|
|
381
|
+
held = [
|
|
382
|
+
value
|
|
383
|
+
for matches, value in zip(_matches(test, frame, cache), values, strict=True)
|
|
384
|
+
if matches
|
|
385
|
+
]
|
|
386
|
+
if not held:
|
|
387
|
+
return False
|
|
388
|
+
nulls = sum(value is None for value in held)
|
|
389
|
+
return nulls / len(held) > test.arguments["max_share"]
|
|
390
|
+
|
|
391
|
+
|
|
345
392
|
def _max_distinct(test: DbtTest, frame: pd.DataFrame, seeded: Mapping, cache: Columns) -> bool:
|
|
346
393
|
return len(cache.number_set(frame, str(test.column))) > test.arguments["max_count"]
|
|
347
394
|
|
|
@@ -388,6 +435,8 @@ _CHECKS: dict[str, Callable[[DbtTest, pd.DataFrame, Mapping, Columns], bool]] =
|
|
|
388
435
|
"model2data_not_before": _not_before,
|
|
389
436
|
"model2data_max_null_share": _max_null_share,
|
|
390
437
|
"model2data_max_distinct": _max_distinct,
|
|
438
|
+
"model2data_when": _when,
|
|
439
|
+
"model2data_when_max_null_share": _when_max_null_share,
|
|
391
440
|
"model2data_unique_combination": _unique_combination,
|
|
392
441
|
"unique_combination": _unique_combination,
|
|
393
442
|
"model2data_one_current_row": _one_current_row,
|