model2data 1.13.0__tar.gz → 1.16.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.13.0/model2data.egg-info → model2data-1.16.0}/PKG-INFO +14 -8
- {model2data-1.13.0 → model2data-1.16.0}/README.md +13 -7
- {model2data-1.13.0 → model2data-1.16.0}/README_PYPI.md +13 -7
- {model2data-1.13.0 → model2data-1.16.0}/model2data/cli.py +80 -18
- {model2data-1.13.0 → model2data-1.16.0}/model2data/dbt/hint_tests.py +60 -7
- model2data-1.16.0/model2data/dbt/templates/hint_macros/model2data_parent_tests.sql +37 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/dbt/tests.py +15 -7
- {model2data-1.13.0 → model2data-1.16.0}/model2data/defects/apply.py +42 -1
- {model2data-1.13.0 → model2data-1.16.0}/model2data/defects/checks.py +37 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/generate/core.py +147 -4
- {model2data-1.13.0 → model2data-1.16.0}/model2data/generate/days.py +29 -1
- {model2data-1.13.0 → model2data-1.16.0}/model2data/generate/faker.py +42 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/generate/hints.py +31 -10
- model2data-1.16.0/model2data/generate/parents.py +437 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/generate/timeline.py +109 -18
- {model2data-1.13.0 → model2data-1.16.0}/model2data/guide/setup.md +3 -2
- {model2data-1.13.0 → model2data-1.16.0}/model2data/guide/triage.md +11 -1
- {model2data-1.13.0 → model2data-1.16.0}/model2data/guide/tune.md +26 -6
- {model2data-1.13.0 → model2data-1.16.0}/model2data/metrics/__init__.py +5 -0
- model2data-1.16.0/model2data/metrics/lightdash.py +597 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/model/__init__.py +3 -2
- {model2data-1.13.0 → model2data-1.16.0}/model2data/model/dbml.py +10 -5
- {model2data-1.13.0 → model2data-1.16.0}/model2data/model/document.py +19 -2
- {model2data-1.13.0 → model2data-1.16.0}/model2data/model/errors.py +22 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/model/types.py +1 -1
- {model2data-1.13.0 → model2data-1.16.0}/model2data/model/validate.py +227 -40
- {model2data-1.13.0 → model2data-1.16.0}/model2data/spec/PUBLISHING.md +14 -14
- {model2data-1.13.0 → model2data-1.16.0}/model2data/spec/README.md +48 -5
- {model2data-1.13.0 → model2data-1.16.0}/model2data/spec/examples/coffee_webshop.model2data.yml +3 -3
- {model2data-1.13.0 → model2data-1.16.0}/model2data/spec/metrics/README.md +3 -2
- {model2data-1.13.0 → model2data-1.16.0}/model2data/spec/model.schema.json +24 -9
- {model2data-1.13.0 → model2data-1.16.0/model2data.egg-info}/PKG-INFO +14 -8
- {model2data-1.13.0 → model2data-1.16.0}/model2data.egg-info/SOURCES.txt +5 -0
- {model2data-1.13.0 → model2data-1.16.0}/pyproject.toml +1 -1
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_cli.py +37 -0
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_cli_model.py +2 -2
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_column_time_hints.py +2 -1
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_defects.py +8 -2
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_defects_dbt.py +31 -0
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_defects_review.py +4 -2
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_distributions.py +8 -6
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_metrics_cli.py +58 -1
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_metrics_dbt.py +69 -0
- model2data-1.16.0/tests/test_metrics_lightdash.py +645 -0
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_model_dbml.py +3 -1
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_model_messages.py +4 -4
- model2data-1.16.0/tests/test_parent_dates.py +882 -0
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_shaping.py +28 -26
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_spec_alignment.py +133 -1
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_table_seeds.py +11 -5
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_timeline.py +31 -28
- {model2data-1.13.0 → model2data-1.16.0}/LICENSE +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/__init__.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/__init__.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/LICENSE +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/VENDORED.md +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/__init__.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/_classes/__init__.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/_classes/base.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/_classes/column.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/_classes/enum.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/_classes/expression.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/_classes/index.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/_classes/note.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/_classes/project.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/_classes/reference.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/_classes/sticky_note.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/_classes/table.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/_classes/table_group.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/classes/__init__.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/constants.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/database.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/definitions/__init__.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/definitions/column.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/definitions/common.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/definitions/enum.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/definitions/generic.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/definitions/index.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/definitions/project.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/definitions/records.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/definitions/reference.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/definitions/sticky_note.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/definitions/table.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/definitions/table_group.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/definitions/table_partial.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/exceptions.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/parser/__init__.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/parser/blueprints.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/parser/parser.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/renderer/__init__.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/renderer/base.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/renderer/dbml/__init__.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/renderer/dbml/default/__init__.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/renderer/dbml/default/column.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/renderer/dbml/default/enum.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/renderer/dbml/default/expression.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/renderer/dbml/default/index.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/renderer/dbml/default/note.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/renderer/dbml/default/project.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/renderer/dbml/default/reference.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/renderer/dbml/default/renderer.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/renderer/dbml/default/sticky_note.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/renderer/dbml/default/table.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/renderer/dbml/default/table_group.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/renderer/dbml/default/utils.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/renderer/sql/__init__.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/renderer/sql/default/__init__.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/renderer/sql/default/column.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/renderer/sql/default/enum.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/renderer/sql/default/expression.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/renderer/sql/default/index.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/renderer/sql/default/note.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/renderer/sql/default/reference.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/renderer/sql/default/renderer.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/renderer/sql/default/table.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/renderer/sql/default/utils.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/_vendor/pydbml/tools.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/dbt/__init__.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/dbt/naming.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/dbt/project.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/dbt/templates/dbt_project.yml.jinja +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/dbt/templates/hint_macros/model2data_hint_tests.sql +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/dbt/templates/hint_macros/model2data_when_tests.sql +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/dbt/templates/history_macros/model2data_history_tests.sql +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/dbt/templates/macros/generate_schema_name.sql +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/dbt/templates/profiles.yml.jinja +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/defects/__init__.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/defects/presets.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/defects/report.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/generate/__init__.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/generate/history.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/generate/kinds.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/generate/options.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/generate/relationships.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/generate/when.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/metrics/check.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/metrics/columns.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/metrics/dbt.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/metrics/expression.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/metrics/graph.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/metrics/infer.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/metrics/ossie.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/metrics/reader.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/metrics/semantic.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/metrics/sql.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/metrics/types.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/metrics/values.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/model/_yaml.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/model/dump.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/model/engine.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/model/reader.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/output.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/parse/__init__.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/parse/dbml.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/spec/examples/coffee_webshop.metrics.yml +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/spec/metrics/metrics.schema.json +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data/utils.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data.egg-info/dependency_links.txt +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data.egg-info/entry_points.txt +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data.egg-info/requires.txt +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/model2data.egg-info/top_level.txt +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/setup.cfg +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_as_of_anchor.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_coverage_gaps.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_days.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_dbml_parser.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_dbml_parser_fuzz.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_dbt_integration.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_dbt_naming.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_dbt_project.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_dbt_tests.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_defects_replay.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_defects_report.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_determinism.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_examples_model.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_faker_name_inference.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_fast_paths.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_generation.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_guide.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_hint_tests.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_history.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_issue_lines.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_lone_country.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_metrics_expression.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_metrics_ossie.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_metrics_reader.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_metrics_semantic.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_metrics_spec.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_metrics_values.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_model.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_model_edges.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_model_vocabulary.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_options.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_release_stress.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_reproducibility.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_row_identity.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_spec_schema.py +0 -0
- {model2data-1.13.0 → model2data-1.16.0}/tests/test_when.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: model2data
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.16.0
|
|
4
4
|
Summary: Turn a data model into realistic synthetic data, a runnable dbt project and metrics with known values
|
|
5
5
|
Author: JB Analytica
|
|
6
6
|
License-Expression: MIT
|
|
@@ -89,9 +89,12 @@ emails, not `Lorem ipsum`. Type a column with any Faker provider (`billing_count
|
|
|
89
89
|
come from one country. → [The model file](https://github.com/JB-Analytica/model2data/blob/main/docs/model-file.md)
|
|
90
90
|
|
|
91
91
|
**Foreign keys that resolve.** Tables are generated in dependency order and every foreign key
|
|
92
|
-
points at a parent row that exists — onto a primary key or a unique one
|
|
93
|
-
|
|
94
|
-
(`
|
|
92
|
+
points at a parent row that exists — onto a primary key or a unique one — and
|
|
93
|
+
`after: customers.created_at` keeps an order on or after the sign-up of the customer it points
|
|
94
|
+
at (`validate` suggests it where a date could fall before its parent's). Integer primary keys
|
|
95
|
+
number the rows 1..N, in order. Composite primary and unique keys hold, and a column that repeats a
|
|
96
|
+
parent's value through a foreign key (`orders.customer_email`) holds the email of the order's
|
|
97
|
+
own customer.
|
|
95
98
|
→ [The model file](https://github.com/JB-Analytica/model2data/blob/main/docs/model-file.md)
|
|
96
99
|
|
|
97
100
|
**A dbt project that runs.** Seeds, staging models that `ref()` them, `not_null`, `unique`,
|
|
@@ -115,8 +118,7 @@ byte-identical. → [The determinism promise](https://github.com/JB-Analytica/mo
|
|
|
115
118
|
|
|
116
119
|
**When things happen.** `--business-hours`, `--growth` and `--seasonality` shape every timestamp
|
|
117
120
|
(weekdays and working hours, a trend, a Q4 peak), or one column at a time. `updated_at` never
|
|
118
|
-
lands before `created_at`, `after` orders any two columns, and `when` gives `shipped_at` a value
|
|
119
|
-
on shipped and delivered orders only. → [When things happen](https://github.com/JB-Analytica/model2data/blob/main/docs/time-shapes.md)
|
|
121
|
+
lands before `created_at`, `after` orders any two columns, or a column after a parent's, and `when` gives `shipped_at` a value on shipped and delivered orders only. → [When things happen](https://github.com/JB-Analytica/model2data/blob/main/docs/time-shapes.md)
|
|
120
122
|
|
|
121
123
|
**How the data is spread.** `--skew` lets a few customers place most of the orders; `weights`,
|
|
122
124
|
`true_rate`, `null_rate` and `distinct` shape a column; `distribution` draws a number from a
|
|
@@ -131,6 +133,9 @@ orphans, nulls, invalid values, messy text, late-arriving rows. Every run with d
|
|
|
131
133
|
orders, average order value. The run writes each metric's value over the generated data to
|
|
132
134
|
`metric_values.json`, overall and per month, a dbt test per metric that must reproduce it on the
|
|
133
135
|
warehouse, and an [Apache Ossie](https://github.com/apache/ossie) semantic model under `osi/`.
|
|
136
|
+
New in 1.14.0: `--lightdash` writes the same metrics into the staging models' YAML as
|
|
137
|
+
[Lightdash](https://www.lightdash.com/) reads them, filters, joins and ratios included, so the
|
|
138
|
+
dashboard's revenue can be checked against the number the run already knows.
|
|
134
139
|
→ [Metrics with known values](https://github.com/JB-Analytica/model2data/blob/main/docs/metrics.md)
|
|
135
140
|
|
|
136
141
|
**Validate in CI.** `model2data validate` checks models and metrics files and prints GitHub
|
|
@@ -272,7 +277,7 @@ definition goes in; nothing but synthetic data comes out.
|
|
|
272
277
|
`accepted_values` fail for the right reason.
|
|
273
278
|
- **A team defining metrics once.** Write revenue in one metrics file, check every number the
|
|
274
279
|
semantic layer or dashboard shows against `metric_values.json`, let `dbt build` prove the SQL,
|
|
275
|
-
and hand the definitions on as Apache Ossie.
|
|
280
|
+
and hand the definitions on as Apache Ossie or straight to Lightdash.
|
|
276
281
|
|
|
277
282
|
## How it works
|
|
278
283
|
|
|
@@ -298,7 +303,8 @@ model (.model2data.yml or DBML) + metrics file (optional)
|
|
|
298
303
|
model's hints, table and column `description:` fields from the model, and a profile for
|
|
299
304
|
DuckDB (zero-config, file-based) or Postgres.
|
|
300
305
|
4. **Compute metrics.** With `--metrics`, computes each metric over the generated data and writes
|
|
301
|
-
its known value, a dbt test that checks it, and the Apache Ossie file
|
|
306
|
+
its known value, a dbt test that checks it, and the Apache Ossie file; with `--lightdash`,
|
|
307
|
+
Lightdash metrics and dimensions in the staging models' YAML.
|
|
302
308
|
|
|
303
309
|
---
|
|
304
310
|
|
|
@@ -42,9 +42,12 @@ emails, not `Lorem ipsum`. Type a column with any Faker provider (`billing_count
|
|
|
42
42
|
come from one country. → [The model file](docs/model-file.md)
|
|
43
43
|
|
|
44
44
|
**Foreign keys that resolve.** Tables are generated in dependency order and every foreign key
|
|
45
|
-
points at a parent row that exists — onto a primary key or a unique one
|
|
46
|
-
|
|
47
|
-
(`
|
|
45
|
+
points at a parent row that exists — onto a primary key or a unique one — and
|
|
46
|
+
`after: customers.created_at` keeps an order on or after the sign-up of the customer it points
|
|
47
|
+
at (`validate` suggests it where a date could fall before its parent's). Integer primary keys
|
|
48
|
+
number the rows 1..N, in order. Composite primary and unique keys hold, and a column that repeats a
|
|
49
|
+
parent's value through a foreign key (`orders.customer_email`) holds the email of the order's
|
|
50
|
+
own customer.
|
|
48
51
|
→ [The model file](docs/model-file.md)
|
|
49
52
|
|
|
50
53
|
**A dbt project that runs.** Seeds, staging models that `ref()` them, `not_null`, `unique`,
|
|
@@ -68,8 +71,7 @@ byte-identical. → [The determinism promise](docs/generating.md#the-determinism
|
|
|
68
71
|
|
|
69
72
|
**When things happen.** `--business-hours`, `--growth` and `--seasonality` shape every timestamp
|
|
70
73
|
(weekdays and working hours, a trend, a Q4 peak), or one column at a time. `updated_at` never
|
|
71
|
-
lands before `created_at`, `after` orders any two columns, and `when` gives `shipped_at` a value
|
|
72
|
-
on shipped and delivered orders only. → [When things happen](docs/time-shapes.md)
|
|
74
|
+
lands before `created_at`, `after` orders any two columns, or a column after a parent's, and `when` gives `shipped_at` a value on shipped and delivered orders only. → [When things happen](docs/time-shapes.md)
|
|
73
75
|
|
|
74
76
|
**How the data is spread.** `--skew` lets a few customers place most of the orders; `weights`,
|
|
75
77
|
`true_rate`, `null_rate` and `distinct` shape a column; `distribution` draws a number from a
|
|
@@ -84,6 +86,9 @@ orphans, nulls, invalid values, messy text, late-arriving rows. Every run with d
|
|
|
84
86
|
orders, average order value. The run writes each metric's value over the generated data to
|
|
85
87
|
`metric_values.json`, overall and per month, a dbt test per metric that must reproduce it on the
|
|
86
88
|
warehouse, and an [Apache Ossie](https://github.com/apache/ossie) semantic model under `osi/`.
|
|
89
|
+
New in 1.14.0: `--lightdash` writes the same metrics into the staging models' YAML as
|
|
90
|
+
[Lightdash](https://www.lightdash.com/) reads them, filters, joins and ratios included, so the
|
|
91
|
+
dashboard's revenue can be checked against the number the run already knows.
|
|
87
92
|
→ [Metrics with known values](docs/metrics.md)
|
|
88
93
|
|
|
89
94
|
**Validate in CI.** `model2data validate` checks models and metrics files and prints GitHub
|
|
@@ -225,7 +230,7 @@ definition goes in; nothing but synthetic data comes out.
|
|
|
225
230
|
`accepted_values` fail for the right reason.
|
|
226
231
|
- **A team defining metrics once.** Write revenue in one metrics file, check every number the
|
|
227
232
|
semantic layer or dashboard shows against `metric_values.json`, let `dbt build` prove the SQL,
|
|
228
|
-
and hand the definitions on as Apache Ossie.
|
|
233
|
+
and hand the definitions on as Apache Ossie or straight to Lightdash.
|
|
229
234
|
|
|
230
235
|
## How it works
|
|
231
236
|
|
|
@@ -281,7 +286,8 @@ flowchart LR
|
|
|
281
286
|
model's hints, table and column `description:` fields from the model, and a profile for
|
|
282
287
|
DuckDB (zero-config, file-based) or Postgres.
|
|
283
288
|
4. **Compute metrics.** With `--metrics`, computes each metric over the generated data and writes
|
|
284
|
-
its known value, a dbt test that checks it, and the Apache Ossie file
|
|
289
|
+
its known value, a dbt test that checks it, and the Apache Ossie file; with `--lightdash`,
|
|
290
|
+
Lightdash metrics and dimensions in the staging models' YAML.
|
|
285
291
|
|
|
286
292
|
---
|
|
287
293
|
|
|
@@ -42,9 +42,12 @@ emails, not `Lorem ipsum`. Type a column with any Faker provider (`billing_count
|
|
|
42
42
|
come from one country. → [The model file](https://github.com/JB-Analytica/model2data/blob/main/docs/model-file.md)
|
|
43
43
|
|
|
44
44
|
**Foreign keys that resolve.** Tables are generated in dependency order and every foreign key
|
|
45
|
-
points at a parent row that exists — onto a primary key or a unique one
|
|
46
|
-
|
|
47
|
-
(`
|
|
45
|
+
points at a parent row that exists — onto a primary key or a unique one — and
|
|
46
|
+
`after: customers.created_at` keeps an order on or after the sign-up of the customer it points
|
|
47
|
+
at (`validate` suggests it where a date could fall before its parent's). Integer primary keys
|
|
48
|
+
number the rows 1..N, in order. Composite primary and unique keys hold, and a column that repeats a
|
|
49
|
+
parent's value through a foreign key (`orders.customer_email`) holds the email of the order's
|
|
50
|
+
own customer.
|
|
48
51
|
→ [The model file](https://github.com/JB-Analytica/model2data/blob/main/docs/model-file.md)
|
|
49
52
|
|
|
50
53
|
**A dbt project that runs.** Seeds, staging models that `ref()` them, `not_null`, `unique`,
|
|
@@ -68,8 +71,7 @@ byte-identical. → [The determinism promise](https://github.com/JB-Analytica/mo
|
|
|
68
71
|
|
|
69
72
|
**When things happen.** `--business-hours`, `--growth` and `--seasonality` shape every timestamp
|
|
70
73
|
(weekdays and working hours, a trend, a Q4 peak), or one column at a time. `updated_at` never
|
|
71
|
-
lands before `created_at`, `after` orders any two columns, and `when` gives `shipped_at` a value
|
|
72
|
-
on shipped and delivered orders only. → [When things happen](https://github.com/JB-Analytica/model2data/blob/main/docs/time-shapes.md)
|
|
74
|
+
lands before `created_at`, `after` orders any two columns, or a column after a parent's, and `when` gives `shipped_at` a value on shipped and delivered orders only. → [When things happen](https://github.com/JB-Analytica/model2data/blob/main/docs/time-shapes.md)
|
|
73
75
|
|
|
74
76
|
**How the data is spread.** `--skew` lets a few customers place most of the orders; `weights`,
|
|
75
77
|
`true_rate`, `null_rate` and `distinct` shape a column; `distribution` draws a number from a
|
|
@@ -84,6 +86,9 @@ orphans, nulls, invalid values, messy text, late-arriving rows. Every run with d
|
|
|
84
86
|
orders, average order value. The run writes each metric's value over the generated data to
|
|
85
87
|
`metric_values.json`, overall and per month, a dbt test per metric that must reproduce it on the
|
|
86
88
|
warehouse, and an [Apache Ossie](https://github.com/apache/ossie) semantic model under `osi/`.
|
|
89
|
+
New in 1.14.0: `--lightdash` writes the same metrics into the staging models' YAML as
|
|
90
|
+
[Lightdash](https://www.lightdash.com/) reads them, filters, joins and ratios included, so the
|
|
91
|
+
dashboard's revenue can be checked against the number the run already knows.
|
|
87
92
|
→ [Metrics with known values](https://github.com/JB-Analytica/model2data/blob/main/docs/metrics.md)
|
|
88
93
|
|
|
89
94
|
**Validate in CI.** `model2data validate` checks models and metrics files and prints GitHub
|
|
@@ -225,7 +230,7 @@ definition goes in; nothing but synthetic data comes out.
|
|
|
225
230
|
`accepted_values` fail for the right reason.
|
|
226
231
|
- **A team defining metrics once.** Write revenue in one metrics file, check every number the
|
|
227
232
|
semantic layer or dashboard shows against `metric_values.json`, let `dbt build` prove the SQL,
|
|
228
|
-
and hand the definitions on as Apache Ossie.
|
|
233
|
+
and hand the definitions on as Apache Ossie or straight to Lightdash.
|
|
229
234
|
|
|
230
235
|
## How it works
|
|
231
236
|
|
|
@@ -251,7 +256,8 @@ model (.model2data.yml or DBML) + metrics file (optional)
|
|
|
251
256
|
model's hints, table and column `description:` fields from the model, and a profile for
|
|
252
257
|
DuckDB (zero-config, file-based) or Postgres.
|
|
253
258
|
4. **Compute metrics.** With `--metrics`, computes each metric over the generated data and writes
|
|
254
|
-
its known value, a dbt test that checks it, and the Apache Ossie file
|
|
259
|
+
its known value, a dbt test that checks it, and the Apache Ossie file; with `--lightdash`,
|
|
260
|
+
Lightdash metrics and dimensions in the staging models' YAML.
|
|
255
261
|
|
|
256
262
|
---
|
|
257
263
|
|
|
@@ -29,6 +29,7 @@ from model2data.defects import (
|
|
|
29
29
|
)
|
|
30
30
|
from model2data.generate.core import (
|
|
31
31
|
generate_data_from_dbml,
|
|
32
|
+
get_capped_tables,
|
|
32
33
|
get_cyclic_tables,
|
|
33
34
|
get_unresolved_composite_keys,
|
|
34
35
|
)
|
|
@@ -50,13 +51,16 @@ from model2data.metrics import (
|
|
|
50
51
|
SPEC_VERSION as METRICS_SPEC_VERSION,
|
|
51
52
|
)
|
|
52
53
|
from model2data.metrics import (
|
|
54
|
+
LightdashExport,
|
|
53
55
|
Metrics,
|
|
54
56
|
MetricsError,
|
|
55
57
|
is_metrics_file,
|
|
56
58
|
known_values,
|
|
57
59
|
metrics_stem,
|
|
58
60
|
sibling_model,
|
|
61
|
+
to_lightdash,
|
|
59
62
|
to_ossie,
|
|
63
|
+
write_lightdash,
|
|
60
64
|
write_metric_tests,
|
|
61
65
|
)
|
|
62
66
|
from model2data.metrics import load as load_metrics
|
|
@@ -181,7 +185,7 @@ app = typer.Typer(
|
|
|
181
185
|
cls=_GenerateByDefault,
|
|
182
186
|
help=(
|
|
183
187
|
"model2data: Generate analytics-ready datasets from a data model.\n\n"
|
|
184
|
-
"Given a model -- a .model2data.yml document (spec 0.
|
|
188
|
+
"Given a model -- a .model2data.yml document (spec 0.5.0), the same as JSON,\n"
|
|
185
189
|
"or a DBML file -- this tool produces:\n"
|
|
186
190
|
"• Synthetic but realistic data\n"
|
|
187
191
|
"• A runnable dbt project scaffold\n"
|
|
@@ -398,8 +402,9 @@ def main(
|
|
|
398
402
|
"warn",
|
|
399
403
|
"--hint-tests",
|
|
400
404
|
help=(
|
|
401
|
-
"Write the model's generation hints as dbt tests (min/max range, after
|
|
402
|
-
"null_rate, distinct, when, grain) at this severity:
|
|
405
|
+
"Write the model's generation hints as dbt tests (min/max range, after "
|
|
406
|
+
"(also a parent's column), null_rate, distinct, when, grain) at this severity: "
|
|
407
|
+
"error, warn or off. They "
|
|
403
408
|
"describe intent, so they warn by default rather than break a first dbt "
|
|
404
409
|
"build on real data."
|
|
405
410
|
),
|
|
@@ -471,6 +476,16 @@ def main(
|
|
|
471
476
|
"without it. Not picked up from beside the model: pass it."
|
|
472
477
|
),
|
|
473
478
|
),
|
|
479
|
+
lightdash: bool = typer.Option(
|
|
480
|
+
False,
|
|
481
|
+
"--lightdash",
|
|
482
|
+
help=(
|
|
483
|
+
"Add Lightdash metrics and dimensions to the staging models' YAML\n"
|
|
484
|
+
"(models/staging/stg_*.yml, under config.meta): the --metrics file's metrics,\n"
|
|
485
|
+
"or without one the metrics the model implies. Without it those files are\n"
|
|
486
|
+
"unchanged."
|
|
487
|
+
),
|
|
488
|
+
),
|
|
474
489
|
):
|
|
475
490
|
"""
|
|
476
491
|
Generate synthetic data and a dbt project from a model.
|
|
@@ -726,6 +741,13 @@ def main(
|
|
|
726
741
|
# Metrics: only added files, computed from what the seeds hold
|
|
727
742
|
# -------------------------
|
|
728
743
|
metrics_count: Optional[int] = None
|
|
744
|
+
lightdash_export = None
|
|
745
|
+
if _given(lightdash):
|
|
746
|
+
typer.echo("🔦 Adding Lightdash metrics and dimensions to the staging models...")
|
|
747
|
+
lightdash_export = to_lightdash(
|
|
748
|
+
resolve_metrics(model, metrics_doc, model_name=_model_stem(file))
|
|
749
|
+
)
|
|
750
|
+
write_lightdash(dest, lightdash_export)
|
|
729
751
|
if metrics_doc is not None and metrics_file is not None:
|
|
730
752
|
typer.echo("📏 Computing each metric's known value and its dbt test...")
|
|
731
753
|
semantic = resolve_metrics(model, metrics_doc, model_name=_model_stem(file))
|
|
@@ -752,6 +774,7 @@ def main(
|
|
|
752
774
|
cyclic_tables = get_cyclic_tables()
|
|
753
775
|
unresolved_keys = get_unresolved_composite_keys()
|
|
754
776
|
duplicate_unique = get_duplicate_unique_columns()
|
|
777
|
+
capped_tables = get_capped_tables()
|
|
755
778
|
|
|
756
779
|
typer.echo("\n📊 Summary")
|
|
757
780
|
typer.echo(f" Tables generated: {len(generated_tables)}")
|
|
@@ -790,6 +813,13 @@ def main(
|
|
|
790
813
|
)
|
|
791
814
|
for label in duplicate_unique:
|
|
792
815
|
typer.echo(f" - {label}")
|
|
816
|
+
if capped_tables:
|
|
817
|
+
typer.echo(
|
|
818
|
+
" ⚠️ Tables generated with fewer rows than asked, so a unique reference "
|
|
819
|
+
"keeps one row per parent value:"
|
|
820
|
+
)
|
|
821
|
+
for label in capped_tables:
|
|
822
|
+
typer.echo(f" - {label}")
|
|
793
823
|
|
|
794
824
|
if metrics_count is not None:
|
|
795
825
|
typer.echo(
|
|
@@ -797,6 +827,13 @@ def main(
|
|
|
797
827
|
f"osi/{project_name}.yml, data-tests/metrics/)"
|
|
798
828
|
)
|
|
799
829
|
|
|
830
|
+
if lightdash_export is not None:
|
|
831
|
+
typer.echo(
|
|
832
|
+
f" Lightdash: {len(lightdash_export.metrics)} metrics "
|
|
833
|
+
"(models/staging/stg_*.yml, config.meta)"
|
|
834
|
+
)
|
|
835
|
+
_print_losses("Not expressible in Lightdash, left out", lightdash_export.lossiness)
|
|
836
|
+
|
|
800
837
|
if report is not None:
|
|
801
838
|
_print_defects(report)
|
|
802
839
|
|
|
@@ -810,12 +847,13 @@ def main(
|
|
|
810
847
|
|
|
811
848
|
|
|
812
849
|
def _spec_of(file: Path) -> str:
|
|
813
|
-
"""The spec a conforming model is written against: 0.4.0, 0.3.0, or 0.2.0 (DBML
|
|
814
|
-
`when` included)."""
|
|
850
|
+
"""The spec a conforming model is written against: 0.5.0, 0.4.0, 0.3.0, or 0.2.0 (DBML
|
|
851
|
+
without `when` or a 0.5.0 `after` included)."""
|
|
815
852
|
version = str(load(file).version)
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
853
|
+
for minor in ("0.5", "0.4", "0.3"):
|
|
854
|
+
if version.startswith(minor):
|
|
855
|
+
return f"{minor}.0"
|
|
856
|
+
return "0.2.0"
|
|
819
857
|
|
|
820
858
|
|
|
821
859
|
def _print_defects(report: DefectsReport) -> None:
|
|
@@ -907,11 +945,12 @@ def validate_command(
|
|
|
907
945
|
),
|
|
908
946
|
),
|
|
909
947
|
):
|
|
910
|
-
"""Check that models conform to spec 0.
|
|
948
|
+
"""Check that models conform to spec 0.5.0 (or 0.2.x-0.4.x), printing every issue with its path.
|
|
911
949
|
|
|
912
950
|
Takes one or more files and/or --glob patterns. A metrics file (*.metrics.yml) is checked
|
|
913
951
|
against metrics spec 0.1.0 and against its model. Exits 1 when any file has an error;
|
|
914
|
-
warnings are printed, and the file conforms.
|
|
952
|
+
warnings are printed, and the file conforms. A warning that a child's date `can fall
|
|
953
|
+
before` its parent's names the `after: <table>.<column>` to add.
|
|
915
954
|
"""
|
|
916
955
|
if output_format not in ("text", "github"):
|
|
917
956
|
raise typer.BadParameter("must be 'text' or 'github'", param_hint="--format")
|
|
@@ -1002,7 +1041,8 @@ def convert_command(
|
|
|
1002
1041
|
force: bool = typer.Option(False, "--force", help="Overwrite the output file if it exists."),
|
|
1003
1042
|
):
|
|
1004
1043
|
"""Convert a model -- DBML, typically -- to a .model2data.yml document (spec 0.2.0, or
|
|
1005
|
-
0.3.0 when it has defects, 0.4.0 when it has `when`
|
|
1044
|
+
0.3.0 when it has defects, 0.4.0 when it has `when`, 0.5.0 when an `after` is a list or
|
|
1045
|
+
names a parent's column)."""
|
|
1006
1046
|
try:
|
|
1007
1047
|
model = load(file)
|
|
1008
1048
|
except ModelError as error:
|
|
@@ -1074,7 +1114,14 @@ metrics_app = typer.Typer(
|
|
|
1074
1114
|
)
|
|
1075
1115
|
app.add_typer(metrics_app, name="metrics")
|
|
1076
1116
|
|
|
1077
|
-
_METRICS_EXPORTS = ("ossie",)
|
|
1117
|
+
_METRICS_EXPORTS = ("ossie", "lightdash")
|
|
1118
|
+
|
|
1119
|
+
|
|
1120
|
+
def _print_losses(heading: str, losses: list) -> None:
|
|
1121
|
+
if losses:
|
|
1122
|
+
typer.echo(f"ℹ️ {heading}:")
|
|
1123
|
+
for loss in losses:
|
|
1124
|
+
typer.echo(f" - {loss}")
|
|
1078
1125
|
|
|
1079
1126
|
|
|
1080
1127
|
def _metrics_inputs(file: Path, metrics_file: Optional[Path]) -> Any:
|
|
@@ -1138,7 +1185,14 @@ def metrics_list_command(
|
|
|
1138
1185
|
def metrics_export_command(
|
|
1139
1186
|
file: Path = _MODEL_OPTION,
|
|
1140
1187
|
metrics_file: Optional[Path] = _METRICS_OPTION,
|
|
1141
|
-
to: str = typer.Option(
|
|
1188
|
+
to: str = typer.Option(
|
|
1189
|
+
"ossie",
|
|
1190
|
+
"--to",
|
|
1191
|
+
help=(
|
|
1192
|
+
"The format: 'ossie' (Apache Ossie 0.1.1, YAML) or 'lightdash' (dbt model "
|
|
1193
|
+
"properties with Lightdash's config.meta, YAML)."
|
|
1194
|
+
),
|
|
1195
|
+
),
|
|
1142
1196
|
output: Optional[Path] = typer.Option( # noqa: B008
|
|
1143
1197
|
None,
|
|
1144
1198
|
"--output",
|
|
@@ -1153,6 +1207,11 @@ def metrics_export_command(
|
|
|
1153
1207
|
Ossie datasets read the staging models of the dbt project `generate` writes
|
|
1154
1208
|
(staging.stg_<table>). What Ossie 0.1.1 cannot say is listed at the top of the file,
|
|
1155
1209
|
and carried in custom_extensions.
|
|
1210
|
+
|
|
1211
|
+
Lightdash's file describes those staging models (stg_<table>) as a dbt properties
|
|
1212
|
+
file. dbt reads a model's properties from one file only, so in a project `generate`
|
|
1213
|
+
wrote, use `generate --lightdash` instead: it merges the same meta into the staging
|
|
1214
|
+
models' own YAML. What Lightdash cannot say is listed at the top of the file.
|
|
1156
1215
|
"""
|
|
1157
1216
|
to = (_given(to) or "ossie").lower()
|
|
1158
1217
|
if to not in _METRICS_EXPORTS:
|
|
@@ -1160,7 +1219,7 @@ def metrics_export_command(
|
|
|
1160
1219
|
f"Choose one of: {', '.join(_METRICS_EXPORTS)}.", param_hint="--to"
|
|
1161
1220
|
)
|
|
1162
1221
|
semantic = _metrics_inputs(file, _given(metrics_file))
|
|
1163
|
-
export = to_ossie(semantic)
|
|
1222
|
+
export = to_ossie(semantic) if to == "ossie" else to_lightdash(semantic)
|
|
1164
1223
|
text = export.to_yaml()
|
|
1165
1224
|
output = _given(output)
|
|
1166
1225
|
if output is None:
|
|
@@ -1170,8 +1229,11 @@ def metrics_export_command(
|
|
|
1170
1229
|
typer.echo(f"❌ {output} already exists. Use --force to overwrite it.")
|
|
1171
1230
|
raise typer.Exit(1)
|
|
1172
1231
|
output.write_text(text, encoding="utf-8")
|
|
1232
|
+
if isinstance(export, LightdashExport):
|
|
1233
|
+
typer.echo(f"✅ Wrote {output} (Lightdash, dbt YAML, {len(export.metrics)} metrics)")
|
|
1234
|
+
_print_losses("Not expressible in Lightdash, left out", export.lossiness)
|
|
1235
|
+
return
|
|
1173
1236
|
typer.echo(f"✅ Wrote {output} (Apache Ossie 0.1.1, {len(semantic.metrics)} metrics)")
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
typer.echo(f" - {loss}")
|
|
1237
|
+
_print_losses(
|
|
1238
|
+
"Not expressible in Ossie 0.1.1 itself, carried in custom_extensions", export.lossiness
|
|
1239
|
+
)
|
|
@@ -12,11 +12,17 @@ What becomes a test, and what does not:
|
|
|
12
12
|
|---------------------------|----------------------------------------|-----------|
|
|
13
13
|
| `generate.min` / `max` | `model2data_between` | none |
|
|
14
14
|
| `generate.after: other` | `model2data_not_before` | none |
|
|
15
|
+
| `generate.after: table.column` | `model2data_not_before_parent` | none |
|
|
15
16
|
| `generate.null_rate` | `model2data_max_null_share` | yes |
|
|
16
17
|
| `generate.distinct: n` | `model2data_max_distinct` | none |
|
|
17
18
|
| `generate.when` | `model2data_when` | none |
|
|
18
19
|
| table `grain` | `model2data_unique_combination` | none |
|
|
19
20
|
|
|
21
|
+
An `after` listing several columns writes one test for each. A parent's column
|
|
22
|
+
(`after: customers.created_at`, see `model2data.generate.parents`) is tested
|
|
23
|
+
through the foreign key that reaches it, so that test needs the model's
|
|
24
|
+
references: a caller passing bare tables passes `refs` too.
|
|
25
|
+
|
|
20
26
|
A column with `when` and `null_rate` has its null share tested among the rows
|
|
21
27
|
`when` matches (`model2data_when_max_null_share`), since only those rows can
|
|
22
28
|
hold a value, and its `model2data_when` test lets those rows be null.
|
|
@@ -28,8 +34,9 @@ constraint a row can break, so they produce no test.
|
|
|
28
34
|
|
|
29
35
|
The generic tests live in `macros/model2data_hint_tests.sql` of the generated
|
|
30
36
|
project (see `write_hint_macros`), and the `when` ones in
|
|
31
|
-
`macros/model2data_when_tests.sql
|
|
32
|
-
|
|
37
|
+
`macros/model2data_when_tests.sql` and the parent one in
|
|
38
|
+
`macros/model2data_parent_tests.sql`, each written only for a model that has
|
|
39
|
+
one, so a project without either is what it was: plain SQL, no dbt package.
|
|
33
40
|
"""
|
|
34
41
|
|
|
35
42
|
from __future__ import annotations
|
|
@@ -41,6 +48,9 @@ from pathlib import Path
|
|
|
41
48
|
from typing import Any, Literal, Optional, Union
|
|
42
49
|
|
|
43
50
|
from model2data.generate import kinds
|
|
51
|
+
from model2data.generate.core import parent_rules_for
|
|
52
|
+
from model2data.generate.parents import ParentRule
|
|
53
|
+
from model2data.generate.timeline import same_row_after
|
|
44
54
|
from model2data.model.engine import EngineInputs, to_engine
|
|
45
55
|
from model2data.model.types import Model
|
|
46
56
|
from model2data.parse.dbml import ColumnDef, TableDef
|
|
@@ -53,6 +63,8 @@ MACROS_FILE = "model2data_hint_tests.sql"
|
|
|
53
63
|
_MACROS_SOURCE = Path(__file__).parent / "templates" / "hint_macros" / MACROS_FILE
|
|
54
64
|
WHEN_MACROS_FILE = "model2data_when_tests.sql"
|
|
55
65
|
_WHEN_MACROS_SOURCE = Path(__file__).parent / "templates" / "hint_macros" / WHEN_MACROS_FILE
|
|
66
|
+
PARENT_MACROS_FILE = "model2data_parent_tests.sql"
|
|
67
|
+
_PARENT_MACROS_SOURCE = Path(__file__).parent / "templates" / "hint_macros" / PARENT_MACROS_FILE
|
|
56
68
|
|
|
57
69
|
|
|
58
70
|
@dataclass(frozen=True)
|
|
@@ -64,6 +76,8 @@ class HintTest:
|
|
|
64
76
|
test's name, `arguments` its parameters as dbt's `arguments:` block, and
|
|
65
77
|
`hint` the hint it comes from (`min`, `max`, `after`, `null_rate`,
|
|
66
78
|
`distinct`, `when`, `grain`). `min` and `max` of one column share one test.
|
|
79
|
+
`parent` is the `(table, column)` a test looks rows up in: the parent's key
|
|
80
|
+
for `model2data_not_before_parent`, None for every other test.
|
|
67
81
|
"""
|
|
68
82
|
|
|
69
83
|
table: str
|
|
@@ -71,6 +85,7 @@ class HintTest:
|
|
|
71
85
|
test: str
|
|
72
86
|
arguments: dict[str, Any] = field(default_factory=dict)
|
|
73
87
|
hint: str = ""
|
|
88
|
+
parent: Optional[tuple[str, str]] = None
|
|
74
89
|
|
|
75
90
|
def to_dbt(self, severity: str = "warn") -> dict[str, dict[str, Any]]:
|
|
76
91
|
"""The entry for a `tests:` list: `{name: {arguments: ..., config: {severity: ...}}}`."""
|
|
@@ -83,12 +98,16 @@ def hint_tests_for(
|
|
|
83
98
|
model_or_tables: Union[Model, EngineInputs, Mapping[str, TableDef]],
|
|
84
99
|
*,
|
|
85
100
|
tolerance: float = DEFAULT_TOLERANCE,
|
|
101
|
+
refs: Optional[list[dict]] = None,
|
|
86
102
|
) -> list[HintTest]:
|
|
87
103
|
"""The tests implied by the hints of a model, in table then column order.
|
|
88
104
|
|
|
89
105
|
Takes a `Model`, the `EngineInputs` of one, or the `tables` dict (as
|
|
90
106
|
`to_engine(model).tables`, or the renamed tables the CLI hands
|
|
91
107
|
`generate_dbt_yml`). Table-level tests follow the table's column tests.
|
|
108
|
+
With `tables`, `refs` are the references as `to_engine` gives them; without
|
|
109
|
+
them no test of a parent's column is written (a `Model` or `EngineInputs`
|
|
110
|
+
brings its own).
|
|
92
111
|
|
|
93
112
|
`tolerance` is the absolute slack of the statistical test (the null share
|
|
94
113
|
may be `null_rate + tolerance`); hard constraints take none. Severity is
|
|
@@ -97,18 +116,25 @@ def hint_tests_for(
|
|
|
97
116
|
if tolerance < 0:
|
|
98
117
|
raise ValueError("tolerance must not be negative")
|
|
99
118
|
if isinstance(model_or_tables, Model):
|
|
100
|
-
|
|
119
|
+
inputs = to_engine(model_or_tables)
|
|
120
|
+
tables: Mapping[str, TableDef] = inputs.tables
|
|
121
|
+
refs = inputs.refs
|
|
101
122
|
elif isinstance(model_or_tables, EngineInputs):
|
|
102
123
|
tables = model_or_tables.tables
|
|
124
|
+
refs = model_or_tables.refs
|
|
103
125
|
else:
|
|
104
126
|
tables = model_or_tables
|
|
127
|
+
rules = parent_rules_for(dict(tables), refs) if refs else {}
|
|
105
128
|
|
|
106
129
|
found: list[HintTest] = []
|
|
107
130
|
for key, table in tables.items():
|
|
108
131
|
by_name = {column.name: column for column in table.columns}
|
|
109
132
|
names = set(by_name)
|
|
133
|
+
parents = {rule.column: rule for rule in rules.get(key, [])}
|
|
110
134
|
for column in table.columns:
|
|
111
135
|
found.extend(_column_tests(key, column, by_name, tolerance))
|
|
136
|
+
if column.name in parents:
|
|
137
|
+
found.extend(_parent_tests(key, parents[column.name]))
|
|
112
138
|
grain = (table.note or {}).get("grain")
|
|
113
139
|
if grain and all(name in names for name in grain):
|
|
114
140
|
found.append(
|
|
@@ -146,8 +172,7 @@ def _column_tests(
|
|
|
146
172
|
)
|
|
147
173
|
)
|
|
148
174
|
|
|
149
|
-
after
|
|
150
|
-
if isinstance(after, str) and after in kinds_of and after != column.name:
|
|
175
|
+
for after in [a for a in same_row_after(note) if a in kinds_of and a != column.name]:
|
|
151
176
|
arguments: dict[str, Any] = {"other": after}
|
|
152
177
|
# The generator compares a date with a timestamp at day granularity.
|
|
153
178
|
if (
|
|
@@ -202,6 +227,31 @@ def _column_tests(
|
|
|
202
227
|
return tests
|
|
203
228
|
|
|
204
229
|
|
|
230
|
+
def _parent_tests(table: str, rule: ParentRule) -> list[HintTest]:
|
|
231
|
+
"""One `model2data_not_before_parent` test per parent column the `after` names."""
|
|
232
|
+
tests = []
|
|
233
|
+
for link in rule.links:
|
|
234
|
+
arguments: dict[str, Any] = {
|
|
235
|
+
"foreign_key": link.fk,
|
|
236
|
+
"to": f"ref('stg_{link.parent_table}')",
|
|
237
|
+
"field": link.parent_key,
|
|
238
|
+
"parent_column": link.parent_column,
|
|
239
|
+
}
|
|
240
|
+
if link.by_day:
|
|
241
|
+
arguments["granularity"] = "day"
|
|
242
|
+
tests.append(
|
|
243
|
+
HintTest(
|
|
244
|
+
table,
|
|
245
|
+
rule.column,
|
|
246
|
+
"model2data_not_before_parent",
|
|
247
|
+
arguments,
|
|
248
|
+
"after",
|
|
249
|
+
parent=(link.parent_table, link.parent_key),
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
return tests
|
|
253
|
+
|
|
254
|
+
|
|
205
255
|
def _when_conditions(when: Any, kinds_of: dict[str, ColumnDef]) -> Optional[dict[str, list]]:
|
|
206
256
|
"""A `when` hint as the test's `conditions`: an enum's values as the member text it holds."""
|
|
207
257
|
if not isinstance(when, dict) or not when or not all(name in kinds_of for name in when):
|
|
@@ -212,13 +262,14 @@ def _when_conditions(when: Any, kinds_of: dict[str, ColumnDef]) -> Optional[dict
|
|
|
212
262
|
}
|
|
213
263
|
|
|
214
264
|
|
|
215
|
-
def write_hint_macros(dest: Path, *, when: bool = False) -> Path:
|
|
265
|
+
def write_hint_macros(dest: Path, *, when: bool = False, parents: bool = False) -> Path:
|
|
216
266
|
"""Write the generic tests into `dest/macros/`, and return the file's path.
|
|
217
267
|
|
|
218
268
|
A project whose YAML holds `hint_tests_for` tests needs this file next to
|
|
219
269
|
it; `generate_dbt_yml` writes it itself when it emits any. With `when`, the
|
|
220
270
|
file of the `when` tests is written beside it (`WHEN_MACROS_FILE`), which a
|
|
221
|
-
project holding a `model2data_when` test needs too
|
|
271
|
+
project holding a `model2data_when` test needs too; with `parents`, the
|
|
272
|
+
file of `model2data_not_before_parent` (`PARENT_MACROS_FILE`).
|
|
222
273
|
"""
|
|
223
274
|
macros = dest / "macros"
|
|
224
275
|
macros.mkdir(parents=True, exist_ok=True)
|
|
@@ -226,4 +277,6 @@ def write_hint_macros(dest: Path, *, when: bool = False) -> Path:
|
|
|
226
277
|
target.write_text(_MACROS_SOURCE.read_text())
|
|
227
278
|
if when:
|
|
228
279
|
(macros / WHEN_MACROS_FILE).write_text(_WHEN_MACROS_SOURCE.read_text())
|
|
280
|
+
if parents:
|
|
281
|
+
(macros / PARENT_MACROS_FILE).write_text(_PARENT_MACROS_SOURCE.read_text())
|
|
229
282
|
return target
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{#-
|
|
2
|
+
The generic test model2data writes for a cross-table `after` (`after:
|
|
3
|
+
customers.created_at` on orders.order_date): a row is not dated before the
|
|
4
|
+
parent row its foreign key points at. Written beside
|
|
5
|
+
model2data_hint_tests.sql, and only by a model that has such a hint. Plain
|
|
6
|
+
ANSI SQL, runs on DuckDB and Postgres. Returns the rows that break it.
|
|
7
|
+
`column_name` arrives already quoted by the schema YAML; `foreign_key`,
|
|
8
|
+
`field` (the parent's key) and `parent_column` (the column `after` names)
|
|
9
|
+
are raw names and are quoted here. A row whose foreign key is null, or points
|
|
10
|
+
at no parent row, is left to the not_null and relationships tests, and a key
|
|
11
|
+
several parent rows share (left to the unique test) counts its earliest one.
|
|
12
|
+
`granularity='day'` compares a date with a timestamp by day.
|
|
13
|
+
-#}
|
|
14
|
+
|
|
15
|
+
{% test model2data_not_before_parent(model, column_name, foreign_key, to, field, parent_column, granularity=none) %}
|
|
16
|
+
{%- set mine = 'child.' ~ column_name -%}
|
|
17
|
+
{%- set theirs = 'parent.parent_value' -%}
|
|
18
|
+
{%- if granularity == 'day' -%}
|
|
19
|
+
{%- set mine = 'cast(' ~ mine ~ ' as date)' -%}
|
|
20
|
+
{%- set theirs = 'cast(' ~ theirs ~ ' as date)' -%}
|
|
21
|
+
{%- endif %}
|
|
22
|
+
select
|
|
23
|
+
child.{{ column_name }} as value,
|
|
24
|
+
parent.parent_value
|
|
25
|
+
from {{ model }} as child
|
|
26
|
+
join (
|
|
27
|
+
select
|
|
28
|
+
{{ adapter.quote(field) }} as parent_key,
|
|
29
|
+
min({{ adapter.quote(parent_column) }}) as parent_value
|
|
30
|
+
from {{ to }}
|
|
31
|
+
where {{ adapter.quote(parent_column) }} is not null
|
|
32
|
+
group by {{ adapter.quote(field) }}
|
|
33
|
+
) as parent
|
|
34
|
+
on child.{{ adapter.quote(foreign_key) }} = parent.parent_key
|
|
35
|
+
where child.{{ column_name }} is not null
|
|
36
|
+
and {{ mine }} < {{ theirs }}
|
|
37
|
+
{% endtest %}
|