model2data 1.9.0__tar.gz → 1.10.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.9.0/model2data.egg-info → model2data-1.10.0}/PKG-INFO +7 -1
- {model2data-1.9.0 → model2data-1.10.0}/README.md +48 -0
- {model2data-1.9.0 → model2data-1.10.0}/README_PYPI.md +6 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/cli.py +84 -11
- {model2data-1.9.0 → model2data-1.10.0/model2data.egg-info}/PKG-INFO +7 -1
- {model2data-1.9.0 → model2data-1.10.0}/pyproject.toml +1 -1
- {model2data-1.9.0 → model2data-1.10.0}/tests/test_cli_model.py +92 -0
- {model2data-1.9.0 → model2data-1.10.0}/LICENSE +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/__init__.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/__init__.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/LICENSE +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/VENDORED.md +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/__init__.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/_classes/__init__.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/_classes/base.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/_classes/column.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/_classes/enum.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/_classes/expression.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/_classes/index.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/_classes/note.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/_classes/project.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/_classes/reference.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/_classes/sticky_note.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/_classes/table.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/_classes/table_group.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/classes/__init__.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/constants.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/database.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/definitions/__init__.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/definitions/column.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/definitions/common.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/definitions/enum.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/definitions/generic.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/definitions/index.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/definitions/project.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/definitions/records.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/definitions/reference.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/definitions/sticky_note.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/definitions/table.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/definitions/table_group.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/definitions/table_partial.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/exceptions.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/parser/__init__.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/parser/blueprints.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/parser/parser.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/__init__.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/base.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/dbml/__init__.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/dbml/default/__init__.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/dbml/default/column.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/dbml/default/enum.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/dbml/default/expression.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/dbml/default/index.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/dbml/default/note.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/dbml/default/project.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/dbml/default/reference.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/dbml/default/renderer.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/dbml/default/sticky_note.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/dbml/default/table.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/dbml/default/table_group.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/dbml/default/utils.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/sql/__init__.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/sql/default/__init__.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/sql/default/column.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/sql/default/enum.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/sql/default/expression.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/sql/default/index.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/sql/default/note.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/sql/default/reference.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/sql/default/renderer.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/sql/default/table.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/sql/default/utils.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/tools.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/dbt/__init__.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/dbt/hint_tests.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/dbt/naming.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/dbt/project.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/dbt/templates/dbt_project.yml.jinja +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/dbt/templates/hint_macros/model2data_hint_tests.sql +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/dbt/templates/history_macros/model2data_history_tests.sql +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/dbt/templates/macros/generate_schema_name.sql +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/dbt/templates/profiles.yml.jinja +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/dbt/tests.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/defects/__init__.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/defects/apply.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/defects/checks.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/defects/presets.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/defects/report.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/generate/__init__.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/generate/core.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/generate/days.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/generate/faker.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/generate/hints.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/generate/history.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/generate/kinds.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/generate/options.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/generate/relationships.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/generate/timeline.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/model/__init__.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/model/_yaml.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/model/dbml.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/model/document.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/model/dump.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/model/engine.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/model/errors.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/model/reader.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/model/types.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/model/validate.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/output.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/parse/__init__.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/parse/dbml.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/spec/PUBLISHING.md +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/spec/README.md +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/spec/examples/coffee_webshop.model2data.yml +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/spec/model.schema.json +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data/utils.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data.egg-info/SOURCES.txt +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data.egg-info/dependency_links.txt +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data.egg-info/entry_points.txt +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data.egg-info/requires.txt +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/model2data.egg-info/top_level.txt +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/setup.cfg +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/tests/test_as_of_anchor.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/tests/test_cli.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/tests/test_column_time_hints.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/tests/test_coverage_gaps.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/tests/test_days.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/tests/test_dbml_parser.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/tests/test_dbml_parser_fuzz.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/tests/test_dbt_integration.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/tests/test_dbt_naming.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/tests/test_dbt_project.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/tests/test_dbt_tests.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/tests/test_defects.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/tests/test_defects_dbt.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/tests/test_defects_replay.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/tests/test_defects_report.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/tests/test_defects_review.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/tests/test_distributions.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/tests/test_examples_model.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/tests/test_faker_name_inference.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/tests/test_generation.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/tests/test_hint_tests.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/tests/test_history.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/tests/test_lone_country.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/tests/test_model.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/tests/test_model_dbml.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/tests/test_model_edges.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/tests/test_model_messages.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/tests/test_model_vocabulary.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/tests/test_options.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/tests/test_release_stress.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/tests/test_reproducibility.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/tests/test_row_identity.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/tests/test_shaping.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/tests/test_spec_alignment.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/tests/test_spec_schema.py +0 -0
- {model2data-1.9.0 → model2data-1.10.0}/tests/test_table_seeds.py +0 -0
- {model2data-1.9.0 → model2data-1.10.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.10.0
|
|
4
4
|
Summary: Generate analytics-ready datasets from DBML models
|
|
5
5
|
Author: JB Analytica
|
|
6
6
|
License-Expression: MIT
|
|
@@ -177,6 +177,12 @@ run:
|
|
|
177
177
|
CLI option overrides the one it names. `model2data validate FILE` checks a model and prints every
|
|
178
178
|
issue with its path in the document.
|
|
179
179
|
|
|
180
|
+
**Validate models in CI.** `model2data validate a.model2data.yml b.model2data.yml` (or
|
|
181
|
+
`--glob "**/*.model2data.yml"`, `--format github` for inline pull request annotations) exits 1
|
|
182
|
+
when any model has an error. A GitHub Action (`uses: JB-Analytica/model2data@v1`), a pre-commit
|
|
183
|
+
hook (`model2data-validate`) and a GitLab CI snippet are in the
|
|
184
|
+
[README](https://github.com/JB-Analytica/model2data#validate-models-in-ci).
|
|
185
|
+
|
|
180
186
|
**DBML is supported input.** `--file` also takes a `.dbml` file, converted to the same model
|
|
181
187
|
before generating; a JSON note on a column (`[note: '{"min": 1}']`) becomes its `generate`, any
|
|
182
188
|
other note its description. `model2data convert schema.dbml -o schema.model2data.yml` writes the
|
|
@@ -199,6 +199,54 @@ unquoted non-ASCII names — can't be read yet, and are refused with the line an
|
|
|
199
199
|
|
|
200
200
|
---
|
|
201
201
|
|
|
202
|
+
## Validate models in CI
|
|
203
|
+
|
|
204
|
+
Teams that keep `*.model2data.yml` models in git can fail a pull request that breaks one.
|
|
205
|
+
`model2data validate` takes several files and `--glob` patterns, prefixes each issue with its
|
|
206
|
+
file, exits 1 if any model has an error, and with `--format github` prints annotations that show
|
|
207
|
+
inline on the pull request's Files tab. `--require-files` fails when nothing matched.
|
|
208
|
+
|
|
209
|
+
**GitHub Actions**
|
|
210
|
+
|
|
211
|
+
```yaml
|
|
212
|
+
name: model2data
|
|
213
|
+
on:
|
|
214
|
+
pull_request:
|
|
215
|
+
paths: ["**/*.model2data.yml"]
|
|
216
|
+
jobs:
|
|
217
|
+
validate:
|
|
218
|
+
runs-on: ubuntu-latest
|
|
219
|
+
steps:
|
|
220
|
+
- uses: actions/checkout@v4
|
|
221
|
+
- uses: JB-Analytica/model2data@v1
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Inputs: `files` (glob, default `**/*.model2data.yml`), `version` (default: the release the tag
|
|
225
|
+
points at, e.g. `1.10.0`; a specifier such as `>=1.10,<2` also works), `python-version` (3.12).
|
|
226
|
+
|
|
227
|
+
**pre-commit**
|
|
228
|
+
|
|
229
|
+
```yaml
|
|
230
|
+
repos:
|
|
231
|
+
- repo: https://github.com/JB-Analytica/model2data
|
|
232
|
+
rev: v1.10.0
|
|
233
|
+
hooks:
|
|
234
|
+
- id: model2data-validate
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**GitLab CI**
|
|
238
|
+
|
|
239
|
+
```yaml
|
|
240
|
+
model2data:
|
|
241
|
+
image: python:3.12-slim
|
|
242
|
+
rules:
|
|
243
|
+
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
244
|
+
changes: ["**/*.model2data.yml"]
|
|
245
|
+
script:
|
|
246
|
+
- pip install model2data==1.10.0
|
|
247
|
+
- model2data validate --glob "**/*.model2data.yml"
|
|
248
|
+
```
|
|
249
|
+
|
|
202
250
|
## model2data studio — the same engine, in the browser
|
|
203
251
|
|
|
204
252
|
[**model2data studio**](https://studio.jbanalytica.com) puts everything on this page behind a
|
|
@@ -130,6 +130,12 @@ run:
|
|
|
130
130
|
CLI option overrides the one it names. `model2data validate FILE` checks a model and prints every
|
|
131
131
|
issue with its path in the document.
|
|
132
132
|
|
|
133
|
+
**Validate models in CI.** `model2data validate a.model2data.yml b.model2data.yml` (or
|
|
134
|
+
`--glob "**/*.model2data.yml"`, `--format github` for inline pull request annotations) exits 1
|
|
135
|
+
when any model has an error. A GitHub Action (`uses: JB-Analytica/model2data@v1`), a pre-commit
|
|
136
|
+
hook (`model2data-validate`) and a GitLab CI snippet are in the
|
|
137
|
+
[README](https://github.com/JB-Analytica/model2data#validate-models-in-ci).
|
|
138
|
+
|
|
133
139
|
**DBML is supported input.** `--file` also takes a `.dbml` file, converted to the same model
|
|
134
140
|
before generating; a JSON note on a column (`[note: '{"min": 1}']`) becomes its `generate`, any
|
|
135
141
|
other note its description. `model2data convert schema.dbml -o schema.model2data.yml` writes the
|
|
@@ -196,15 +196,16 @@ def _read_model(file: Path) -> Model:
|
|
|
196
196
|
return model
|
|
197
197
|
|
|
198
198
|
|
|
199
|
-
def _print_issues(file: Path, issues: list[Issue]) -> None:
|
|
199
|
+
def _print_issues(file: Path, issues: list[Issue], label: Optional[str] = None) -> None:
|
|
200
200
|
"""Errors, then warnings, each with its document path."""
|
|
201
|
+
label = label or file.name
|
|
201
202
|
errors = [issue for issue in issues if issue.is_error]
|
|
202
203
|
warnings = [issue for issue in issues if not issue.is_error]
|
|
203
204
|
for marker, group, word in (("❌", errors, "error"), ("⚠️ ", warnings, "warning")):
|
|
204
205
|
if not group:
|
|
205
206
|
continue
|
|
206
207
|
count = f"1 {word}" if len(group) == 1 else f"{len(group)} {word}s"
|
|
207
|
-
typer.echo(f"{marker} {
|
|
208
|
+
typer.echo(f"{marker} {label}: {count}")
|
|
208
209
|
for issue in group:
|
|
209
210
|
text = str(issue).removeprefix("warning: ").replace("\n", "\n ")
|
|
210
211
|
typer.echo(f" - {text}")
|
|
@@ -756,26 +757,98 @@ def _print_defects(report: DefectsReport) -> None:
|
|
|
756
757
|
typer.echo(f" - {failure.test}{warn}")
|
|
757
758
|
|
|
758
759
|
|
|
760
|
+
_SKIPPED_DIRS = {".git", ".venv", "node_modules"}
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
def _expand_globs(patterns: list[str]) -> list[Path]:
|
|
764
|
+
"""Files matching recursive glob patterns relative to the cwd, sorted, without duplicates."""
|
|
765
|
+
found: dict[Path, None] = {}
|
|
766
|
+
for pattern in patterns:
|
|
767
|
+
for match in sorted(Path.cwd().glob(pattern)):
|
|
768
|
+
relative = match.relative_to(Path.cwd())
|
|
769
|
+
if match.is_file() and not _SKIPPED_DIRS.intersection(relative.parts):
|
|
770
|
+
found[relative] = None
|
|
771
|
+
return list(found)
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
def _github_escape(text: str, *, prop: bool = False) -> str:
|
|
775
|
+
"""Escape a workflow-command message (or property value) the way GitHub expects."""
|
|
776
|
+
text = text.replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A")
|
|
777
|
+
return text.replace(":", "%3A").replace(",", "%2C") if prop else text
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
def _print_github_issues(file: Path, issues: list[Issue]) -> None:
|
|
781
|
+
"""One `::error` / `::warning` annotation per issue, shown inline on a pull request."""
|
|
782
|
+
for issue in issues:
|
|
783
|
+
level = "error" if issue.is_error else "warning"
|
|
784
|
+
where = f"file={_github_escape(file.as_posix(), prop=True)}"
|
|
785
|
+
if issue.line:
|
|
786
|
+
where += f",line={issue.line}"
|
|
787
|
+
text = f"{issue.path}: {issue.message}" if issue.path else issue.message
|
|
788
|
+
typer.echo(f"::{level} {where}::{_github_escape(text)}")
|
|
789
|
+
|
|
790
|
+
|
|
759
791
|
@app.command("validate")
|
|
760
792
|
def validate_command(
|
|
761
|
-
|
|
762
|
-
|
|
793
|
+
files: Optional[list[Path]] = typer.Argument( # noqa: B008
|
|
794
|
+
None,
|
|
763
795
|
exists=True,
|
|
764
796
|
file_okay=True,
|
|
765
797
|
dir_okay=False,
|
|
766
798
|
readable=True,
|
|
767
|
-
help="The
|
|
799
|
+
help="The models: .model2data.yml / .yaml / .json documents, or .dbml files.",
|
|
800
|
+
),
|
|
801
|
+
glob: Optional[list[str]] = typer.Option( # noqa: B008
|
|
802
|
+
None,
|
|
803
|
+
"--glob",
|
|
804
|
+
help=(
|
|
805
|
+
"A recursive glob relative to the current directory, e.g. '**/*.model2data.yml'. "
|
|
806
|
+
"Repeatable; expands without the shell, so CI needs no globstar."
|
|
807
|
+
),
|
|
808
|
+
),
|
|
809
|
+
output_format: str = typer.Option(
|
|
810
|
+
"text",
|
|
811
|
+
"--format",
|
|
812
|
+
help="'text' (default) or 'github' for ::error / ::warning annotations on a pull request.",
|
|
813
|
+
),
|
|
814
|
+
require_files: bool = typer.Option(
|
|
815
|
+
False, "--require-files", help="Exit 1 when no model file was given or matched."
|
|
768
816
|
),
|
|
769
817
|
):
|
|
770
|
-
"""Check that
|
|
818
|
+
"""Check that models conform to spec 0.3.0 (or 0.2.x), printing every issue with its path.
|
|
771
819
|
|
|
772
|
-
|
|
820
|
+
Takes one or more files and/or --glob patterns. Exits 1 when any file has an error;
|
|
821
|
+
warnings are printed, and the model conforms.
|
|
773
822
|
"""
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
823
|
+
if output_format not in ("text", "github"):
|
|
824
|
+
raise typer.BadParameter("must be 'text' or 'github'", param_hint="--format")
|
|
825
|
+
targets = list(files or [])
|
|
826
|
+
if glob:
|
|
827
|
+
targets += [path for path in _expand_globs(glob) if path not in targets]
|
|
828
|
+
if not targets:
|
|
829
|
+
typer.echo("no model files matched")
|
|
830
|
+
raise typer.Exit(1 if require_files else 0)
|
|
831
|
+
|
|
832
|
+
failed = 0
|
|
833
|
+
for file in targets:
|
|
834
|
+
issues = validate(file)
|
|
835
|
+
if output_format == "github":
|
|
836
|
+
_print_github_issues(file, issues)
|
|
837
|
+
else:
|
|
838
|
+
_print_issues(file, issues, label=str(file) if len(targets) > 1 else file.name)
|
|
839
|
+
if any(issue.is_error for issue in issues):
|
|
840
|
+
failed += 1
|
|
841
|
+
elif output_format == "text":
|
|
842
|
+
typer.echo(
|
|
843
|
+
f"✅ {file if len(targets) > 1 else file.name} conforms to spec {_spec_of(file)}."
|
|
844
|
+
)
|
|
845
|
+
if len(targets) > 1 or output_format == "github":
|
|
846
|
+
checked = f"{len(targets)} model file{'s' if len(targets) != 1 else ''}"
|
|
847
|
+
typer.echo(
|
|
848
|
+
f"❌ {failed} of {checked} do not conform." if failed else f"✅ {checked} conform."
|
|
849
|
+
)
|
|
850
|
+
if failed:
|
|
777
851
|
raise typer.Exit(1)
|
|
778
|
-
typer.echo(f"✅ {file.name} conforms to spec {_spec_of(file)}.")
|
|
779
852
|
|
|
780
853
|
|
|
781
854
|
@app.command("convert")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: model2data
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.10.0
|
|
4
4
|
Summary: Generate analytics-ready datasets from DBML models
|
|
5
5
|
Author: JB Analytica
|
|
6
6
|
License-Expression: MIT
|
|
@@ -177,6 +177,12 @@ run:
|
|
|
177
177
|
CLI option overrides the one it names. `model2data validate FILE` checks a model and prints every
|
|
178
178
|
issue with its path in the document.
|
|
179
179
|
|
|
180
|
+
**Validate models in CI.** `model2data validate a.model2data.yml b.model2data.yml` (or
|
|
181
|
+
`--glob "**/*.model2data.yml"`, `--format github` for inline pull request annotations) exits 1
|
|
182
|
+
when any model has an error. A GitHub Action (`uses: JB-Analytica/model2data@v1`), a pre-commit
|
|
183
|
+
hook (`model2data-validate`) and a GitLab CI snippet are in the
|
|
184
|
+
[README](https://github.com/JB-Analytica/model2data#validate-models-in-ci).
|
|
185
|
+
|
|
180
186
|
**DBML is supported input.** `--file` also takes a `.dbml` file, converted to the same model
|
|
181
187
|
before generating; a JSON note on a column (`[note: '{"min": 1}']`) becomes its `generate`, any
|
|
182
188
|
other note its description. `model2data convert schema.dbml -o schema.model2data.yml` writes the
|
|
@@ -246,3 +246,95 @@ def test_help_lists_the_commands():
|
|
|
246
246
|
assert result.exit_code == 0
|
|
247
247
|
for command in ("generate", "validate", "convert"):
|
|
248
248
|
assert command in result.output
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
# ---------------------------------------------------------------------------
|
|
252
|
+
# validate: several files, --glob, --format github, and the CI integration files
|
|
253
|
+
# ---------------------------------------------------------------------------
|
|
254
|
+
def _broken(tmp_path, name="bad.model2data.yml"):
|
|
255
|
+
return _write(tmp_path, name, MODEL.replace("rows: 12", "rows: 0"))
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def test_validate_several_files_all_valid(tmp_path):
|
|
259
|
+
a = _write(tmp_path, "a.model2data.yml", MODEL)
|
|
260
|
+
b = _write(tmp_path, "b.model2data.yml", MODEL)
|
|
261
|
+
result = runner.invoke(app, ["validate", str(a), str(b)])
|
|
262
|
+
assert result.exit_code == 0, result.output
|
|
263
|
+
assert f"✅ {a} conforms to spec 0.2.0." in result.output
|
|
264
|
+
assert "✅ 2 model files conform." in result.output
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def test_validate_several_files_one_invalid_exits_1_with_path_prefixed_issues(tmp_path):
|
|
268
|
+
good = _write(tmp_path, "good.model2data.yml", MODEL)
|
|
269
|
+
bad = _broken(tmp_path)
|
|
270
|
+
result = runner.invoke(app, ["validate", str(good), str(bad)])
|
|
271
|
+
assert result.exit_code == 1
|
|
272
|
+
assert f"❌ {bad}: 1 error" in result.output
|
|
273
|
+
assert " - run.rows: must be 1 or more (got 0)" in result.output
|
|
274
|
+
assert "❌ 1 of 2 model files do not conform." in result.output
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def test_validate_glob_matches_nested_files(tmp_path, monkeypatch):
|
|
278
|
+
(tmp_path / "deep" / "er").mkdir(parents=True)
|
|
279
|
+
_write(tmp_path, "top.model2data.yml", MODEL)
|
|
280
|
+
_write(tmp_path / "deep" / "er", "nested.model2data.yml", MODEL)
|
|
281
|
+
(tmp_path / "node_modules").mkdir()
|
|
282
|
+
_write(tmp_path / "node_modules", "skipped.model2data.yml", MODEL)
|
|
283
|
+
monkeypatch.chdir(tmp_path)
|
|
284
|
+
result = runner.invoke(app, ["validate", "--glob", "**/*.model2data.yml"])
|
|
285
|
+
assert result.exit_code == 0, result.output
|
|
286
|
+
assert "top.model2data.yml" in result.output
|
|
287
|
+
assert "nested.model2data.yml" in result.output
|
|
288
|
+
assert "skipped" not in result.output
|
|
289
|
+
assert "2 model files conform" in result.output
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def test_validate_glob_with_no_match_is_a_note_unless_files_are_required(tmp_path, monkeypatch):
|
|
293
|
+
monkeypatch.chdir(tmp_path)
|
|
294
|
+
result = runner.invoke(app, ["validate", "--glob", "**/*.model2data.yml"])
|
|
295
|
+
assert result.exit_code == 0
|
|
296
|
+
assert "no model files matched" in result.output
|
|
297
|
+
required = runner.invoke(app, ["validate", "--glob", "**/*.model2data.yml", "--require-files"])
|
|
298
|
+
assert required.exit_code == 1
|
|
299
|
+
assert "no model files matched" in required.output
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def test_validate_format_github_prints_annotations(tmp_path, monkeypatch):
|
|
303
|
+
_broken(tmp_path)
|
|
304
|
+
monkeypatch.chdir(tmp_path)
|
|
305
|
+
result = runner.invoke(app, ["validate", "bad.model2data.yml", "--format", "github"])
|
|
306
|
+
assert result.exit_code == 1
|
|
307
|
+
assert "::error file=bad.model2data.yml::run.rows: must be 1 or more (got 0)" in result.output
|
|
308
|
+
yaml_error = _write(tmp_path, "dup.model2data.yml", MODEL + "name: again\n")
|
|
309
|
+
result = runner.invoke(app, ["validate", yaml_error.name, "--format", "github"])
|
|
310
|
+
assert "::error file=dup.model2data.yml,line=25::" in result.output
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def test_validate_format_github_prints_warnings_and_exits_0():
|
|
314
|
+
result = runner.invoke(
|
|
315
|
+
app, ["validate", str(EXAMPLES / "hackernews.dbml"), "--format", "github"]
|
|
316
|
+
)
|
|
317
|
+
assert result.exit_code == 0
|
|
318
|
+
assert "::warning file=" in result.output
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def test_validate_rejects_an_unknown_format(tmp_path):
|
|
322
|
+
model = _write(tmp_path, "m.model2data.yml", MODEL)
|
|
323
|
+
assert runner.invoke(app, ["validate", str(model), "--format", "xml"]).exit_code != 0
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def test_ci_integration_files_run_the_validate_command():
|
|
327
|
+
import yaml
|
|
328
|
+
|
|
329
|
+
root = Path(__file__).resolve().parent.parent
|
|
330
|
+
action = yaml.safe_load((root / "action.yml").read_text())
|
|
331
|
+
assert action["runs"]["using"] == "composite"
|
|
332
|
+
assert set(action["inputs"]) == {"files", "version", "python-version"}
|
|
333
|
+
steps = " ".join(str(step.get("run", "")) for step in action["runs"]["steps"])
|
|
334
|
+
assert "model2data validate --glob" in steps
|
|
335
|
+
assert "--format github" in steps
|
|
336
|
+
hooks = yaml.safe_load((root / ".pre-commit-hooks.yaml").read_text())
|
|
337
|
+
assert hooks[0]["id"] == "model2data-validate"
|
|
338
|
+
assert hooks[0]["entry"] == "model2data validate"
|
|
339
|
+
assert hooks[0]["language"] == "python"
|
|
340
|
+
assert hooks[0]["pass_filenames"] is True
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/definitions/table_partial.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/dbml/default/__init__.py
RENAMED
|
File without changes
|
{model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/dbml/default/column.py
RENAMED
|
File without changes
|
{model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/dbml/default/enum.py
RENAMED
|
File without changes
|
{model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/dbml/default/expression.py
RENAMED
|
File without changes
|
{model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/dbml/default/index.py
RENAMED
|
File without changes
|
{model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/dbml/default/note.py
RENAMED
|
File without changes
|
{model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/dbml/default/project.py
RENAMED
|
File without changes
|
{model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/dbml/default/reference.py
RENAMED
|
File without changes
|
{model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/dbml/default/renderer.py
RENAMED
|
File without changes
|
|
File without changes
|
{model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/dbml/default/table.py
RENAMED
|
File without changes
|
|
File without changes
|
{model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/dbml/default/utils.py
RENAMED
|
File without changes
|
|
File without changes
|
{model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/sql/default/__init__.py
RENAMED
|
File without changes
|
{model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/sql/default/column.py
RENAMED
|
File without changes
|
{model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/sql/default/enum.py
RENAMED
|
File without changes
|
{model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/sql/default/expression.py
RENAMED
|
File without changes
|
{model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/sql/default/index.py
RENAMED
|
File without changes
|
{model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/sql/default/note.py
RENAMED
|
File without changes
|
{model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/sql/default/reference.py
RENAMED
|
File without changes
|
{model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/sql/default/renderer.py
RENAMED
|
File without changes
|
{model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/sql/default/table.py
RENAMED
|
File without changes
|
{model2data-1.9.0 → model2data-1.10.0}/model2data/_vendor/pydbml/renderer/sql/default/utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{model2data-1.9.0 → model2data-1.10.0}/model2data/dbt/templates/macros/generate_schema_name.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{model2data-1.9.0 → model2data-1.10.0}/model2data/spec/examples/coffee_webshop.model2data.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|