model2data 1.10.0__tar.gz → 1.10.1__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.0/model2data.egg-info → model2data-1.10.1}/PKG-INFO +1 -1
- {model2data-1.10.0 → model2data-1.10.1}/README.md +4 -4
- {model2data-1.10.0 → model2data-1.10.1}/model2data/cli.py +7 -4
- {model2data-1.10.0 → model2data-1.10.1}/model2data/model/_yaml.py +66 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/model/reader.py +11 -4
- {model2data-1.10.0 → model2data-1.10.1/model2data.egg-info}/PKG-INFO +1 -1
- {model2data-1.10.0 → model2data-1.10.1}/model2data.egg-info/SOURCES.txt +1 -0
- {model2data-1.10.0 → model2data-1.10.1}/pyproject.toml +1 -1
- {model2data-1.10.0 → model2data-1.10.1}/tests/test_cli_model.py +45 -5
- model2data-1.10.1/tests/test_issue_lines.py +87 -0
- {model2data-1.10.0 → model2data-1.10.1}/tests/test_model.py +3 -1
- {model2data-1.10.0 → model2data-1.10.1}/LICENSE +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/README_PYPI.md +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/__init__.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/__init__.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/LICENSE +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/VENDORED.md +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/__init__.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/_classes/__init__.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/_classes/base.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/_classes/column.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/_classes/enum.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/_classes/expression.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/_classes/index.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/_classes/note.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/_classes/project.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/_classes/reference.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/_classes/sticky_note.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/_classes/table.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/_classes/table_group.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/classes/__init__.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/constants.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/database.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/definitions/__init__.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/definitions/column.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/definitions/common.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/definitions/enum.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/definitions/generic.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/definitions/index.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/definitions/project.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/definitions/records.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/definitions/reference.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/definitions/sticky_note.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/definitions/table.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/definitions/table_group.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/definitions/table_partial.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/exceptions.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/parser/__init__.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/parser/blueprints.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/parser/parser.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/__init__.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/base.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/dbml/__init__.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/dbml/default/__init__.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/dbml/default/column.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/dbml/default/enum.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/dbml/default/expression.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/dbml/default/index.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/dbml/default/note.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/dbml/default/project.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/dbml/default/reference.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/dbml/default/renderer.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/dbml/default/sticky_note.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/dbml/default/table.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/dbml/default/table_group.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/dbml/default/utils.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/sql/__init__.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/sql/default/__init__.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/sql/default/column.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/sql/default/enum.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/sql/default/expression.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/sql/default/index.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/sql/default/note.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/sql/default/reference.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/sql/default/renderer.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/sql/default/table.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/sql/default/utils.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/tools.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/dbt/__init__.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/dbt/hint_tests.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/dbt/naming.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/dbt/project.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/dbt/templates/dbt_project.yml.jinja +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/dbt/templates/hint_macros/model2data_hint_tests.sql +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/dbt/templates/history_macros/model2data_history_tests.sql +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/dbt/templates/macros/generate_schema_name.sql +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/dbt/templates/profiles.yml.jinja +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/dbt/tests.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/defects/__init__.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/defects/apply.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/defects/checks.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/defects/presets.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/defects/report.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/generate/__init__.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/generate/core.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/generate/days.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/generate/faker.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/generate/hints.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/generate/history.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/generate/kinds.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/generate/options.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/generate/relationships.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/generate/timeline.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/model/__init__.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/model/dbml.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/model/document.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/model/dump.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/model/engine.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/model/errors.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/model/types.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/model/validate.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/output.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/parse/__init__.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/parse/dbml.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/spec/PUBLISHING.md +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/spec/README.md +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/spec/examples/coffee_webshop.model2data.yml +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/spec/model.schema.json +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data/utils.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data.egg-info/dependency_links.txt +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data.egg-info/entry_points.txt +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data.egg-info/requires.txt +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/model2data.egg-info/top_level.txt +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/setup.cfg +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/tests/test_as_of_anchor.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/tests/test_cli.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/tests/test_column_time_hints.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/tests/test_coverage_gaps.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/tests/test_days.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/tests/test_dbml_parser.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/tests/test_dbml_parser_fuzz.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/tests/test_dbt_integration.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/tests/test_dbt_naming.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/tests/test_dbt_project.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/tests/test_dbt_tests.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/tests/test_defects.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/tests/test_defects_dbt.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/tests/test_defects_replay.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/tests/test_defects_report.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/tests/test_defects_review.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/tests/test_distributions.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/tests/test_examples_model.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/tests/test_faker_name_inference.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/tests/test_generation.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/tests/test_hint_tests.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/tests/test_history.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/tests/test_lone_country.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/tests/test_model_dbml.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/tests/test_model_edges.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/tests/test_model_messages.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/tests/test_model_vocabulary.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/tests/test_options.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/tests/test_release_stress.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/tests/test_reproducibility.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/tests/test_row_identity.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/tests/test_shaping.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/tests/test_spec_alignment.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/tests/test_spec_schema.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/tests/test_table_seeds.py +0 -0
- {model2data-1.10.0 → model2data-1.10.1}/tests/test_timeline.py +0 -0
|
@@ -217,19 +217,19 @@ jobs:
|
|
|
217
217
|
validate:
|
|
218
218
|
runs-on: ubuntu-latest
|
|
219
219
|
steps:
|
|
220
|
-
- uses: actions/checkout@
|
|
220
|
+
- uses: actions/checkout@v5
|
|
221
221
|
- uses: JB-Analytica/model2data@v1
|
|
222
222
|
```
|
|
223
223
|
|
|
224
224
|
Inputs: `files` (glob, default `**/*.model2data.yml`), `version` (default: the release the tag
|
|
225
|
-
points at, e.g. `1.10.
|
|
225
|
+
points at, e.g. `1.10.1`; a specifier such as `>=1.10,<2` also works), `python-version` (3.12).
|
|
226
226
|
|
|
227
227
|
**pre-commit**
|
|
228
228
|
|
|
229
229
|
```yaml
|
|
230
230
|
repos:
|
|
231
231
|
- repo: https://github.com/JB-Analytica/model2data
|
|
232
|
-
rev: v1.10.
|
|
232
|
+
rev: v1.10.1
|
|
233
233
|
hooks:
|
|
234
234
|
- id: model2data-validate
|
|
235
235
|
```
|
|
@@ -243,7 +243,7 @@ model2data:
|
|
|
243
243
|
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
244
244
|
changes: ["**/*.model2data.yml"]
|
|
245
245
|
script:
|
|
246
|
-
- pip install model2data==1.10.
|
|
246
|
+
- pip install model2data==1.10.1
|
|
247
247
|
- model2data validate --glob "**/*.model2data.yml"
|
|
248
248
|
```
|
|
249
249
|
|
|
@@ -843,10 +843,13 @@ def validate_command(
|
|
|
843
843
|
f"✅ {file if len(targets) > 1 else file.name} conforms to spec {_spec_of(file)}."
|
|
844
844
|
)
|
|
845
845
|
if len(targets) > 1 or output_format == "github":
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
846
|
+
count = len(targets)
|
|
847
|
+
checked = f"{count} model file{'s' if count != 1 else ''}"
|
|
848
|
+
if failed:
|
|
849
|
+
verb = "does" if failed == 1 else "do"
|
|
850
|
+
typer.echo(f"❌ {failed} of {checked} {verb} not conform.")
|
|
851
|
+
else:
|
|
852
|
+
typer.echo(f"✅ {checked} {'conforms' if count == 1 else 'conform'}.")
|
|
850
853
|
if failed:
|
|
851
854
|
raise typer.Exit(1)
|
|
852
855
|
|
|
@@ -23,6 +23,7 @@ from __future__ import annotations
|
|
|
23
23
|
|
|
24
24
|
import math
|
|
25
25
|
import re
|
|
26
|
+
from dataclasses import replace
|
|
26
27
|
from typing import Any
|
|
27
28
|
|
|
28
29
|
import yaml
|
|
@@ -160,6 +161,71 @@ def _check_mappings(node: Node, path: list[PathPart], issues: list[Issue]) -> No
|
|
|
160
161
|
_check_mappings(item, [*path, index], issues)
|
|
161
162
|
|
|
162
163
|
|
|
164
|
+
def locate_issues(text: str, issues: list[Issue]) -> list[Issue]:
|
|
165
|
+
"""`issues` with a `line` on each one that has none, from where its path sits in `text`.
|
|
166
|
+
|
|
167
|
+
`text` is a YAML (or JSON) document; it is composed once, with the profile's
|
|
168
|
+
own loader so the node tree is the one `parse_yaml` read, and each issue's
|
|
169
|
+
dotted path is walked down it. A path that stops resolving (a key that is
|
|
170
|
+
missing, say) takes the line of the deepest node that did. Text that does
|
|
171
|
+
not compose leaves the issues as they are.
|
|
172
|
+
"""
|
|
173
|
+
if not any(issue.line is None and issue.path for issue in issues):
|
|
174
|
+
return issues
|
|
175
|
+
loader = _CoreLoader(text)
|
|
176
|
+
try:
|
|
177
|
+
root = loader.get_single_node()
|
|
178
|
+
except Exception:
|
|
179
|
+
return issues
|
|
180
|
+
finally:
|
|
181
|
+
loader.dispose()
|
|
182
|
+
if root is None:
|
|
183
|
+
return issues
|
|
184
|
+
located = []
|
|
185
|
+
for issue in issues:
|
|
186
|
+
if issue.line is None and issue.path:
|
|
187
|
+
line = _line_of_path(root, issue.path)
|
|
188
|
+
if line is not None:
|
|
189
|
+
issue = replace(issue, line=line)
|
|
190
|
+
located.append(issue)
|
|
191
|
+
return located
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _line_of_path(root: Node, path: str) -> int | None:
|
|
195
|
+
"""The line of the deepest node `path` reaches, or None if it reaches only the root."""
|
|
196
|
+
node = root
|
|
197
|
+
rest = path
|
|
198
|
+
line: int | None = None
|
|
199
|
+
while rest:
|
|
200
|
+
if isinstance(node, MappingNode):
|
|
201
|
+
best: tuple[Node, Node] | None = None
|
|
202
|
+
best_len = 0
|
|
203
|
+
for key_node, value_node in node.value:
|
|
204
|
+
key = _key_text(key_node)
|
|
205
|
+
if not isinstance(key, str) or not key or len(key) <= best_len:
|
|
206
|
+
continue
|
|
207
|
+
if rest == key or rest.startswith(key + "."):
|
|
208
|
+
best, best_len = (key_node, value_node), len(key)
|
|
209
|
+
if best is None:
|
|
210
|
+
break
|
|
211
|
+
line = best[0].start_mark.line + 1
|
|
212
|
+
node = best[1]
|
|
213
|
+
rest = rest[best_len + 1 :]
|
|
214
|
+
elif isinstance(node, SequenceNode):
|
|
215
|
+
match = re.match(r"^(?:\[(\d+)\]|(\d+))(?:\.|$)", rest)
|
|
216
|
+
if match is None:
|
|
217
|
+
break
|
|
218
|
+
index = int(match.group(1) or match.group(2))
|
|
219
|
+
if index >= len(node.value):
|
|
220
|
+
break
|
|
221
|
+
node = node.value[index]
|
|
222
|
+
line = node.start_mark.line + 1
|
|
223
|
+
rest = rest[match.end() :]
|
|
224
|
+
else:
|
|
225
|
+
break
|
|
226
|
+
return line
|
|
227
|
+
|
|
228
|
+
|
|
163
229
|
def parse_yaml(text: str) -> Any:
|
|
164
230
|
"""Read one YAML document under the spec's profile, or raise `ModelError`."""
|
|
165
231
|
loader = _CoreLoader(text)
|
|
@@ -7,7 +7,7 @@ import os
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import Literal, Optional, Union
|
|
9
9
|
|
|
10
|
-
from model2data.model._yaml import parse_yaml
|
|
10
|
+
from model2data.model._yaml import locate_issues, parse_yaml
|
|
11
11
|
from model2data.model.document import from_dict
|
|
12
12
|
from model2data.model.errors import Issue, ModelError
|
|
13
13
|
from model2data.model.types import Model
|
|
@@ -78,9 +78,16 @@ def load(source: Source, *, format: Optional[Format] = None) -> Model:
|
|
|
78
78
|
from model2data.model.dbml import from_dbml
|
|
79
79
|
|
|
80
80
|
return from_dbml(text)
|
|
81
|
-
if format == "json"
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
document = _parse_json(text) if format == "json" else parse_yaml(text)
|
|
82
|
+
try:
|
|
83
|
+
model = from_dict(document)
|
|
84
|
+
except ModelError as error:
|
|
85
|
+
# One composition of the text, however many issues: each gets its line.
|
|
86
|
+
raise ModelError(
|
|
87
|
+
locate_issues(text, error.issues), locate_issues(text, error.warnings)
|
|
88
|
+
) from None
|
|
89
|
+
model.warnings = locate_issues(text, model.warnings)
|
|
90
|
+
return model
|
|
84
91
|
|
|
85
92
|
|
|
86
93
|
def _parse_json(text: str):
|
|
@@ -187,8 +187,8 @@ def test_validate_prints_every_issue_with_its_path_and_exits_1(tmp_path):
|
|
|
187
187
|
result = runner.invoke(app, ["validate", str(model)])
|
|
188
188
|
assert result.exit_code == 1
|
|
189
189
|
assert "❌ m.model2data.yml: 2 errors" in result.output
|
|
190
|
-
assert " - tables.orders.columns.customer_id.nul: unknown key" in result.output
|
|
191
|
-
assert " - run.rows: must be 1 or more (got 0)" in result.output
|
|
190
|
+
assert " - tables.orders.columns.customer_id.nul (line 16): unknown key" in result.output
|
|
191
|
+
assert " - run.rows (line 21): must be 1 or more (got 0)" in result.output
|
|
192
192
|
|
|
193
193
|
|
|
194
194
|
def test_validate_exits_0_on_warnings_alone(tmp_path):
|
|
@@ -270,8 +270,8 @@ def test_validate_several_files_one_invalid_exits_1_with_path_prefixed_issues(tm
|
|
|
270
270
|
result = runner.invoke(app, ["validate", str(good), str(bad)])
|
|
271
271
|
assert result.exit_code == 1
|
|
272
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
|
|
273
|
+
assert " - run.rows (line 21): must be 1 or more (got 0)" in result.output
|
|
274
|
+
assert "❌ 1 of 2 model files does not conform." in result.output
|
|
275
275
|
|
|
276
276
|
|
|
277
277
|
def test_validate_glob_matches_nested_files(tmp_path, monkeypatch):
|
|
@@ -304,7 +304,10 @@ def test_validate_format_github_prints_annotations(tmp_path, monkeypatch):
|
|
|
304
304
|
monkeypatch.chdir(tmp_path)
|
|
305
305
|
result = runner.invoke(app, ["validate", "bad.model2data.yml", "--format", "github"])
|
|
306
306
|
assert result.exit_code == 1
|
|
307
|
-
assert
|
|
307
|
+
assert (
|
|
308
|
+
"::error file=bad.model2data.yml,line=21::run.rows: must be 1 or more (got 0)"
|
|
309
|
+
in result.output
|
|
310
|
+
)
|
|
308
311
|
yaml_error = _write(tmp_path, "dup.model2data.yml", MODEL + "name: again\n")
|
|
309
312
|
result = runner.invoke(app, ["validate", yaml_error.name, "--format", "github"])
|
|
310
313
|
assert "::error file=dup.model2data.yml,line=25::" in result.output
|
|
@@ -338,3 +341,40 @@ def test_ci_integration_files_run_the_validate_command():
|
|
|
338
341
|
assert hooks[0]["entry"] == "model2data validate"
|
|
339
342
|
assert hooks[0]["language"] == "python"
|
|
340
343
|
assert hooks[0]["pass_filenames"] is True
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def test_validate_format_github_puts_the_line_on_a_value_issue(tmp_path, monkeypatch):
|
|
347
|
+
_write(
|
|
348
|
+
tmp_path,
|
|
349
|
+
"m.model2data.yml",
|
|
350
|
+
MODEL.replace("pk: true}\n email", "pk: maybe}\n email"),
|
|
351
|
+
)
|
|
352
|
+
monkeypatch.chdir(tmp_path)
|
|
353
|
+
result = runner.invoke(app, ["validate", "m.model2data.yml", "--format", "github"])
|
|
354
|
+
assert result.exit_code == 1
|
|
355
|
+
assert (
|
|
356
|
+
"::error file=m.model2data.yml,line=10::tables.customers.columns.id.pk: "
|
|
357
|
+
'must be true or false, not the string "maybe"'
|
|
358
|
+
) in result.output
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def test_validate_summary_agrees_with_its_count(tmp_path):
|
|
362
|
+
good = _write(tmp_path, "good.model2data.yml", MODEL)
|
|
363
|
+
bad = _broken(tmp_path)
|
|
364
|
+
one_good = runner.invoke(app, ["validate", str(good), "--format", "github"])
|
|
365
|
+
assert "✅ 1 model file conforms." in one_good.output
|
|
366
|
+
one_bad = runner.invoke(app, ["validate", str(bad), "--format", "github"])
|
|
367
|
+
assert "❌ 1 of 1 model file does not conform." in one_bad.output
|
|
368
|
+
mixed = runner.invoke(app, ["validate", str(good), str(bad)])
|
|
369
|
+
assert "❌ 1 of 2 model files does not conform." in mixed.output
|
|
370
|
+
both = runner.invoke(app, ["validate", str(bad), str(_broken(tmp_path, "b2.model2data.yml"))])
|
|
371
|
+
assert "❌ 2 of 2 model files do not conform." in both.output
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def test_action_uses_setup_python_v6():
|
|
375
|
+
import yaml
|
|
376
|
+
|
|
377
|
+
root = Path(__file__).resolve().parent.parent
|
|
378
|
+
action = yaml.safe_load((root / "action.yml").read_text())
|
|
379
|
+
uses = [step.get("uses") for step in action["runs"]["steps"]]
|
|
380
|
+
assert "actions/setup-python@v6" in uses
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Validation issues carry the line of the node their path points at."""
|
|
2
|
+
|
|
3
|
+
from model2data.model import Issue, validate
|
|
4
|
+
from model2data.model._yaml import _CoreLoader, _line_of_path, locate_issues
|
|
5
|
+
|
|
6
|
+
DOC = """\
|
|
7
|
+
model2data: 0.2.0
|
|
8
|
+
name: shop
|
|
9
|
+
tables:
|
|
10
|
+
raw.users:
|
|
11
|
+
columns:
|
|
12
|
+
id: {type: bigint, pk: true}
|
|
13
|
+
email: {type: email, bogus: 1}
|
|
14
|
+
orders:
|
|
15
|
+
columns:
|
|
16
|
+
id: {type: bigint, pk: maybe}
|
|
17
|
+
keys:
|
|
18
|
+
- unique: [id]
|
|
19
|
+
- unique: 5
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _line(path, text=DOC):
|
|
24
|
+
loader = _CoreLoader(text)
|
|
25
|
+
try:
|
|
26
|
+
return _line_of_path(loader.get_single_node(), path)
|
|
27
|
+
finally:
|
|
28
|
+
loader.dispose()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_a_simple_path_resolves_to_its_key_line():
|
|
32
|
+
assert _line("tables.orders.columns.id.pk") == 10
|
|
33
|
+
assert _line("name") == 2
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_a_table_name_with_a_dot_resolves_by_longest_key():
|
|
37
|
+
assert _line("tables.raw.users.columns.email") == 7
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_a_sequence_index_resolves_in_either_spelling():
|
|
41
|
+
assert _line("tables.orders.keys.1") == 13
|
|
42
|
+
assert _line("tables.orders.keys.[1]") == 13
|
|
43
|
+
assert _line("tables.orders.keys.1.unique") == 13
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_an_unresolvable_tail_falls_back_to_the_deepest_node():
|
|
47
|
+
assert _line("tables.orders.columns.nope.type") == 9
|
|
48
|
+
assert _line("tables.orders.keys.9") == 11
|
|
49
|
+
assert _line("nothing.at.all") is None
|
|
50
|
+
assert _line("") is None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_validate_gives_issues_their_lines():
|
|
54
|
+
issues = validate(DOC)
|
|
55
|
+
by_path = {issue.path: issue for issue in issues}
|
|
56
|
+
assert by_path["tables.raw.users.columns.email.bogus"].line == 7
|
|
57
|
+
assert by_path["tables.orders.columns.id.pk"].line == 10
|
|
58
|
+
assert "(line 10)" in str(by_path["tables.orders.columns.id.pk"])
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_json_issues_get_lines_too():
|
|
62
|
+
text = '{\n "model2data": "0.2.0",\n "name": "m",\n "tables": {\n "t": {"columns": {"id": {"type": "bigint", "pk": "maybe"}}}\n }\n}'
|
|
63
|
+
issues = validate(text)
|
|
64
|
+
assert [issue.line for issue in issues] == [5]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_dbml_issues_keep_no_line():
|
|
68
|
+
assert all(issue.line is None for issue in validate("Table t { id int }\n", format="dbml"))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_a_sequence_followed_by_a_name_stops_at_the_sequence():
|
|
72
|
+
# `keys` is a list: a path that names a key inside it cannot be followed.
|
|
73
|
+
assert _line("tables.orders.keys.unique") == 11
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_a_path_past_a_plain_value_stops_at_that_value():
|
|
77
|
+
assert _line("name.first") == 2
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_text_that_does_not_compose_leaves_the_issues_as_they_were():
|
|
81
|
+
issue = Issue("tables", "broken")
|
|
82
|
+
assert locate_issues("a: [unclosed", [issue]) == [issue]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_an_empty_document_leaves_the_issues_as_they_were():
|
|
86
|
+
issue = Issue("tables", "missing")
|
|
87
|
+
assert locate_issues("", [issue]) == [issue]
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import copy
|
|
6
6
|
import json
|
|
7
|
+
from dataclasses import replace
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
|
|
9
10
|
import pytest
|
|
@@ -350,7 +351,8 @@ def test_2_foreign_key_columns_pair_up():
|
|
|
350
351
|
"are drawn from the ones it holds, but a parent of one row per value needs them to be "
|
|
351
352
|
"its primary key, one of its keys, or a unique column",
|
|
352
353
|
]
|
|
353
|
-
|
|
354
|
+
# `validate` on text adds the line each issue sits on; `from_dict` has no text.
|
|
355
|
+
assert [str(replace(issue, line=None)) for issue in validate(json.dumps(document))][1:] == [
|
|
354
356
|
str(issue) for issue in raised.value.warnings
|
|
355
357
|
]
|
|
356
358
|
|
|
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.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/definitions/sticky_note.py
RENAMED
|
File without changes
|
|
File without changes
|
{model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/definitions/table_group.py
RENAMED
|
File without changes
|
{model2data-1.10.0 → model2data-1.10.1}/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.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/dbml/default/__init__.py
RENAMED
|
File without changes
|
{model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/dbml/default/column.py
RENAMED
|
File without changes
|
{model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/dbml/default/enum.py
RENAMED
|
File without changes
|
|
File without changes
|
{model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/dbml/default/index.py
RENAMED
|
File without changes
|
{model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/dbml/default/note.py
RENAMED
|
File without changes
|
{model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/dbml/default/project.py
RENAMED
|
File without changes
|
{model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/dbml/default/reference.py
RENAMED
|
File without changes
|
{model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/dbml/default/renderer.py
RENAMED
|
File without changes
|
|
File without changes
|
{model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/dbml/default/table.py
RENAMED
|
File without changes
|
|
File without changes
|
{model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/dbml/default/utils.py
RENAMED
|
File without changes
|
|
File without changes
|
{model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/sql/default/__init__.py
RENAMED
|
File without changes
|
{model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/sql/default/column.py
RENAMED
|
File without changes
|
{model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/sql/default/enum.py
RENAMED
|
File without changes
|
{model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/sql/default/expression.py
RENAMED
|
File without changes
|
{model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/sql/default/index.py
RENAMED
|
File without changes
|
{model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/sql/default/note.py
RENAMED
|
File without changes
|
{model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/sql/default/reference.py
RENAMED
|
File without changes
|
{model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/sql/default/renderer.py
RENAMED
|
File without changes
|
{model2data-1.10.0 → model2data-1.10.1}/model2data/_vendor/pydbml/renderer/sql/default/table.py
RENAMED
|
File without changes
|
{model2data-1.10.0 → model2data-1.10.1}/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.10.0 → model2data-1.10.1}/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
|
{model2data-1.10.0 → model2data-1.10.1}/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
|