datacontract-cli 0.10.14__py3-none-any.whl → 0.10.15__py3-none-any.whl
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.
Potentially problematic release.
This version of datacontract-cli might be problematic. Click here for more details.
- datacontract/breaking/breaking.py +227 -9
- datacontract/breaking/breaking_rules.py +24 -0
- datacontract/catalog/catalog.py +1 -1
- datacontract/cli.py +99 -32
- datacontract/data_contract.py +26 -4
- datacontract/engines/datacontract/check_that_datacontract_file_exists.py +1 -1
- datacontract/engines/fastjsonschema/check_jsonschema.py +114 -22
- datacontract/engines/soda/check_soda_execute.py +5 -3
- datacontract/engines/soda/connections/duckdb.py +1 -0
- datacontract/engines/soda/connections/kafka.py +12 -12
- datacontract/export/avro_idl_converter.py +1 -2
- datacontract/export/bigquery_converter.py +4 -3
- datacontract/export/data_caterer_converter.py +1 -1
- datacontract/export/dbml_converter.py +2 -4
- datacontract/export/dbt_converter.py +2 -3
- datacontract/export/exporter.py +1 -1
- datacontract/export/exporter_factory.py +3 -2
- datacontract/export/go_converter.py +3 -2
- datacontract/export/great_expectations_converter.py +202 -40
- datacontract/export/html_export.py +1 -1
- datacontract/export/jsonschema_converter.py +3 -2
- datacontract/export/odcs_v2_exporter.py +1 -1
- datacontract/export/odcs_v3_exporter.py +1 -1
- datacontract/export/pandas_type_converter.py +40 -0
- datacontract/export/protobuf_converter.py +1 -1
- datacontract/export/rdf_converter.py +4 -5
- datacontract/export/sodacl_converter.py +6 -2
- datacontract/export/spark_converter.py +7 -6
- datacontract/export/sql_converter.py +1 -2
- datacontract/export/sqlalchemy_converter.py +1 -2
- datacontract/export/terraform_converter.py +1 -1
- datacontract/imports/avro_importer.py +1 -1
- datacontract/imports/bigquery_importer.py +1 -1
- datacontract/imports/dbml_importer.py +2 -2
- datacontract/imports/dbt_importer.py +3 -2
- datacontract/imports/glue_importer.py +5 -3
- datacontract/imports/iceberg_importer.py +5 -6
- datacontract/imports/importer.py +1 -0
- datacontract/imports/importer_factory.py +7 -1
- datacontract/imports/jsonschema_importer.py +3 -2
- datacontract/imports/odcs_v2_importer.py +2 -2
- datacontract/imports/odcs_v3_importer.py +2 -2
- datacontract/imports/parquet_importer.py +81 -0
- datacontract/imports/spark_importer.py +2 -1
- datacontract/imports/sql_importer.py +1 -1
- datacontract/imports/unity_importer.py +3 -3
- datacontract/integration/opentelemetry.py +0 -1
- datacontract/lint/lint.py +2 -1
- datacontract/lint/linters/description_linter.py +1 -0
- datacontract/lint/linters/example_model_linter.py +1 -0
- datacontract/lint/linters/field_pattern_linter.py +1 -0
- datacontract/lint/linters/field_reference_linter.py +1 -0
- datacontract/lint/linters/notice_period_linter.py +1 -0
- datacontract/lint/linters/quality_schema_linter.py +1 -0
- datacontract/lint/linters/valid_constraints_linter.py +1 -0
- datacontract/lint/resolve.py +1 -1
- datacontract/lint/schema.py +1 -1
- datacontract/model/data_contract_specification.py +11 -5
- datacontract/model/run.py +21 -12
- datacontract/templates/index.html +6 -6
- datacontract/web.py +2 -3
- {datacontract_cli-0.10.14.dist-info → datacontract_cli-0.10.15.dist-info}/METADATA +97 -52
- datacontract_cli-0.10.15.dist-info/RECORD +105 -0
- {datacontract_cli-0.10.14.dist-info → datacontract_cli-0.10.15.dist-info}/WHEEL +1 -1
- datacontract_cli-0.10.14.dist-info/RECORD +0 -103
- {datacontract_cli-0.10.14.dist-info → datacontract_cli-0.10.15.dist-info}/LICENSE +0 -0
- {datacontract_cli-0.10.14.dist-info → datacontract_cli-0.10.15.dist-info}/entry_points.txt +0 -0
- {datacontract_cli-0.10.14.dist-info → datacontract_cli-0.10.15.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,218 @@
|
|
|
1
1
|
from datacontract.breaking.breaking_rules import BreakingRules
|
|
2
2
|
from datacontract.model.breaking_change import BreakingChange, Location, Severity
|
|
3
|
-
from datacontract.model.data_contract_specification import Field, Model, Quality
|
|
3
|
+
from datacontract.model.data_contract_specification import Contact, Field, Info, Model, Quality, Terms
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def info_breaking_changes(
|
|
7
|
+
old_info: Info,
|
|
8
|
+
new_info: Info,
|
|
9
|
+
new_path: str,
|
|
10
|
+
include_severities: [Severity],
|
|
11
|
+
) -> list[BreakingChange]:
|
|
12
|
+
results = list[BreakingChange]()
|
|
13
|
+
|
|
14
|
+
composition = ["info"]
|
|
15
|
+
|
|
16
|
+
if old_info and new_info:
|
|
17
|
+
info_definition_fields = vars(new_info) | new_info.model_extra | old_info.model_extra
|
|
18
|
+
|
|
19
|
+
for info_definition_field in info_definition_fields.keys():
|
|
20
|
+
if info_definition_field == "contact":
|
|
21
|
+
continue
|
|
22
|
+
|
|
23
|
+
old_value = getattr(old_info, info_definition_field, None)
|
|
24
|
+
new_value = getattr(new_info, info_definition_field, None)
|
|
25
|
+
|
|
26
|
+
rule_name = None
|
|
27
|
+
description = None
|
|
28
|
+
|
|
29
|
+
if old_value is None and new_value is not None:
|
|
30
|
+
rule_name = f"info_{_camel_to_snake(info_definition_field)}_added"
|
|
31
|
+
description = f"added with value: `{new_value}`"
|
|
32
|
+
|
|
33
|
+
elif old_value is not None and new_value is None:
|
|
34
|
+
rule_name = f"info_{_camel_to_snake(info_definition_field)}_removed"
|
|
35
|
+
description = "removed info property"
|
|
36
|
+
|
|
37
|
+
elif old_value != new_value:
|
|
38
|
+
rule_name = f"info_{_camel_to_snake(info_definition_field)}_updated"
|
|
39
|
+
description = f"changed from `{old_value}` to `{new_value}`"
|
|
40
|
+
|
|
41
|
+
if rule_name is not None:
|
|
42
|
+
severity = _get_rule(rule_name)
|
|
43
|
+
if severity in include_severities:
|
|
44
|
+
results.append(
|
|
45
|
+
BreakingChange(
|
|
46
|
+
description=description,
|
|
47
|
+
check_name=rule_name,
|
|
48
|
+
severity=severity,
|
|
49
|
+
location=Location(path=new_path, composition=composition + [info_definition_field]),
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
results.extend(
|
|
54
|
+
contact_breaking_changes(
|
|
55
|
+
old_contact=getattr(old_info, "contact", None),
|
|
56
|
+
new_contact=getattr(new_info, "contact", None),
|
|
57
|
+
composition=composition + ["contact"],
|
|
58
|
+
new_path=new_path,
|
|
59
|
+
include_severities=include_severities,
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return results
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def contact_breaking_changes(
|
|
67
|
+
old_contact: Contact,
|
|
68
|
+
new_contact: Contact,
|
|
69
|
+
composition: list[str],
|
|
70
|
+
new_path: str,
|
|
71
|
+
include_severities: [Severity],
|
|
72
|
+
) -> list[BreakingChange]:
|
|
73
|
+
results = list[BreakingChange]()
|
|
74
|
+
|
|
75
|
+
if not old_contact and new_contact:
|
|
76
|
+
rule_name = "contact_added"
|
|
77
|
+
severity = _get_rule(rule_name)
|
|
78
|
+
description = "added contact"
|
|
79
|
+
|
|
80
|
+
if severity in include_severities:
|
|
81
|
+
results.append(
|
|
82
|
+
BreakingChange(
|
|
83
|
+
description=description,
|
|
84
|
+
check_name=rule_name,
|
|
85
|
+
severity=severity,
|
|
86
|
+
location=Location(path=new_path, composition=composition),
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
elif old_contact and not new_contact:
|
|
91
|
+
rule_name = "contact_removed"
|
|
92
|
+
severity = _get_rule(rule_name)
|
|
93
|
+
description = "removed contact"
|
|
94
|
+
|
|
95
|
+
if severity in include_severities:
|
|
96
|
+
results.append(
|
|
97
|
+
BreakingChange(
|
|
98
|
+
description=description,
|
|
99
|
+
check_name=rule_name,
|
|
100
|
+
severity=severity,
|
|
101
|
+
location=Location(path=new_path, composition=composition),
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
elif old_contact and new_contact:
|
|
106
|
+
contact_definition_fields = vars(new_contact) | new_contact.model_extra | old_contact.model_extra
|
|
107
|
+
|
|
108
|
+
for contact_definition_field in contact_definition_fields.keys():
|
|
109
|
+
old_value = getattr(old_contact, contact_definition_field, None)
|
|
110
|
+
new_value = getattr(new_contact, contact_definition_field, None)
|
|
111
|
+
|
|
112
|
+
rule_name = None
|
|
113
|
+
description = None
|
|
114
|
+
|
|
115
|
+
if old_value is None and new_value is not None:
|
|
116
|
+
rule_name = f"contact_{_camel_to_snake(contact_definition_field)}_added"
|
|
117
|
+
description = f"added with value: `{new_value}`"
|
|
118
|
+
|
|
119
|
+
elif old_value is not None and new_value is None:
|
|
120
|
+
rule_name = f"contact_{_camel_to_snake(contact_definition_field)}_removed"
|
|
121
|
+
description = "removed contact property"
|
|
122
|
+
|
|
123
|
+
elif old_value != new_value:
|
|
124
|
+
rule_name = f"contact_{_camel_to_snake(contact_definition_field)}_updated"
|
|
125
|
+
description = f"changed from `{old_value}` to `{new_value}`"
|
|
126
|
+
|
|
127
|
+
if rule_name is not None:
|
|
128
|
+
severity = _get_rule(rule_name)
|
|
129
|
+
if severity in include_severities:
|
|
130
|
+
results.append(
|
|
131
|
+
BreakingChange(
|
|
132
|
+
description=description,
|
|
133
|
+
check_name=rule_name,
|
|
134
|
+
severity=severity,
|
|
135
|
+
location=Location(path=new_path, composition=composition + [contact_definition_field]),
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
return results
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def terms_breaking_changes(
|
|
143
|
+
old_terms: Terms,
|
|
144
|
+
new_terms: Terms,
|
|
145
|
+
new_path: str,
|
|
146
|
+
include_severities: [Severity],
|
|
147
|
+
) -> list[BreakingChange]:
|
|
148
|
+
results = list[BreakingChange]()
|
|
149
|
+
|
|
150
|
+
composition = ["terms"]
|
|
151
|
+
|
|
152
|
+
if not old_terms and new_terms:
|
|
153
|
+
rule_name = "terms_added"
|
|
154
|
+
severity = _get_rule(rule_name)
|
|
155
|
+
description = "added terms"
|
|
156
|
+
|
|
157
|
+
if severity in include_severities:
|
|
158
|
+
results.append(
|
|
159
|
+
BreakingChange(
|
|
160
|
+
description=description,
|
|
161
|
+
check_name=rule_name,
|
|
162
|
+
severity=severity,
|
|
163
|
+
location=Location(path=new_path, composition=composition),
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
elif old_terms and not new_terms:
|
|
167
|
+
rule_name = "terms_removed"
|
|
168
|
+
severity = _get_rule(rule_name)
|
|
169
|
+
description = "removed terms"
|
|
170
|
+
|
|
171
|
+
if severity in include_severities:
|
|
172
|
+
results.append(
|
|
173
|
+
BreakingChange(
|
|
174
|
+
description=description,
|
|
175
|
+
check_name=rule_name,
|
|
176
|
+
severity=severity,
|
|
177
|
+
location=Location(path=new_path, composition=composition),
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
if old_terms and new_terms:
|
|
182
|
+
terms_definition_fields = vars(new_terms) | new_terms.model_extra | old_terms.model_extra
|
|
183
|
+
|
|
184
|
+
for terms_definition_field in terms_definition_fields.keys():
|
|
185
|
+
old_value = getattr(old_terms, terms_definition_field, None)
|
|
186
|
+
new_value = getattr(new_terms, terms_definition_field, None)
|
|
187
|
+
|
|
188
|
+
rule_name = None
|
|
189
|
+
description = None
|
|
190
|
+
|
|
191
|
+
if old_value is None and new_value is not None:
|
|
192
|
+
rule_name = f"terms_{_camel_to_snake(terms_definition_field)}_added"
|
|
193
|
+
description = f"added with value: `{new_value}`"
|
|
194
|
+
|
|
195
|
+
elif old_value is not None and new_value is None:
|
|
196
|
+
rule_name = f"terms_{_camel_to_snake(terms_definition_field)}_removed"
|
|
197
|
+
description = "removed info property"
|
|
198
|
+
|
|
199
|
+
elif old_value != new_value:
|
|
200
|
+
rule_name = f"terms_{_camel_to_snake(terms_definition_field)}_updated"
|
|
201
|
+
description = f"changed from `{old_value}` to `{new_value}`"
|
|
202
|
+
|
|
203
|
+
if rule_name is not None:
|
|
204
|
+
severity = _get_rule(rule_name)
|
|
205
|
+
if severity in include_severities:
|
|
206
|
+
results.append(
|
|
207
|
+
BreakingChange(
|
|
208
|
+
description=description,
|
|
209
|
+
check_name=rule_name,
|
|
210
|
+
severity=severity,
|
|
211
|
+
location=Location(path=new_path, composition=composition + [terms_definition_field]),
|
|
212
|
+
)
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
return results
|
|
4
216
|
|
|
5
217
|
|
|
6
218
|
def quality_breaking_changes(
|
|
@@ -129,14 +341,14 @@ def model_breaking_changes(
|
|
|
129
341
|
) -> list[BreakingChange]:
|
|
130
342
|
results = list[BreakingChange]()
|
|
131
343
|
|
|
132
|
-
model_definition_fields = vars(new_model)
|
|
344
|
+
model_definition_fields = vars(new_model) | new_model.model_extra | old_model.model_extra
|
|
133
345
|
|
|
134
346
|
for model_definition_field in model_definition_fields.keys():
|
|
135
347
|
if model_definition_field == "fields":
|
|
136
348
|
continue
|
|
137
349
|
|
|
138
|
-
old_value = getattr(old_model, model_definition_field)
|
|
139
|
-
new_value = getattr(new_model, model_definition_field)
|
|
350
|
+
old_value = getattr(old_model, model_definition_field, None)
|
|
351
|
+
new_value = getattr(new_model, model_definition_field, None)
|
|
140
352
|
|
|
141
353
|
rule_name = None
|
|
142
354
|
description = None
|
|
@@ -237,13 +449,13 @@ def field_breaking_changes(
|
|
|
237
449
|
) -> list[BreakingChange]:
|
|
238
450
|
results = list[BreakingChange]()
|
|
239
451
|
|
|
240
|
-
field_definition_fields = vars(new_field)
|
|
452
|
+
field_definition_fields = vars(new_field) | new_field.model_extra | old_field.model_extra
|
|
241
453
|
for field_definition_field in field_definition_fields.keys():
|
|
242
454
|
if field_definition_field == "ref_obj":
|
|
243
455
|
continue
|
|
244
456
|
|
|
245
|
-
old_value = getattr(old_field, field_definition_field)
|
|
246
|
-
new_value = getattr(new_field, field_definition_field)
|
|
457
|
+
old_value = getattr(old_field, field_definition_field, None)
|
|
458
|
+
new_value = getattr(new_field, field_definition_field, None)
|
|
247
459
|
|
|
248
460
|
if field_definition_field == "fields":
|
|
249
461
|
results.extend(
|
|
@@ -320,9 +532,15 @@ def _get_rule(rule_name) -> Severity:
|
|
|
320
532
|
try:
|
|
321
533
|
return getattr(BreakingRules, rule_name)
|
|
322
534
|
except AttributeError:
|
|
323
|
-
|
|
324
|
-
|
|
535
|
+
try:
|
|
536
|
+
first, *_, last = rule_name.split("_")
|
|
537
|
+
short_rule = "__".join([first, last])
|
|
538
|
+
return getattr(BreakingRules, short_rule)
|
|
539
|
+
except AttributeError:
|
|
540
|
+
print(f"WARNING: Breaking Rule not found for {rule_name}!")
|
|
541
|
+
return Severity.ERROR
|
|
325
542
|
|
|
326
543
|
|
|
327
544
|
def _camel_to_snake(s):
|
|
545
|
+
s = s.replace("-", "_")
|
|
328
546
|
return "".join(["_" + c.lower() if c.isupper() else c for c in s]).lstrip("_")
|
|
@@ -12,6 +12,10 @@ class BreakingRules:
|
|
|
12
12
|
|
|
13
13
|
model_type_updated = Severity.ERROR
|
|
14
14
|
|
|
15
|
+
model__removed = Severity.INFO # To support model extension keys
|
|
16
|
+
model__added = Severity.INFO
|
|
17
|
+
model__updated = Severity.INFO
|
|
18
|
+
|
|
15
19
|
# field rules
|
|
16
20
|
field_added = Severity.INFO
|
|
17
21
|
field_removed = Severity.ERROR
|
|
@@ -34,6 +38,8 @@ class BreakingRules:
|
|
|
34
38
|
|
|
35
39
|
field_required_updated = Severity.ERROR
|
|
36
40
|
|
|
41
|
+
field_primary_added = Severity.WARNING
|
|
42
|
+
field_primary_removed = Severity.WARNING
|
|
37
43
|
field_primary_updated = Severity.WARNING
|
|
38
44
|
|
|
39
45
|
field_references_added = Severity.WARNING
|
|
@@ -94,9 +100,27 @@ class BreakingRules:
|
|
|
94
100
|
field_example_updated = Severity.INFO
|
|
95
101
|
field_example_removed = Severity.INFO
|
|
96
102
|
|
|
103
|
+
field__removed = Severity.INFO # To support field extension keys
|
|
104
|
+
field__added = Severity.INFO
|
|
105
|
+
field__updated = Severity.INFO
|
|
106
|
+
|
|
97
107
|
# quality Rules
|
|
98
108
|
quality_added = Severity.INFO
|
|
99
109
|
quality_removed = Severity.WARNING
|
|
100
110
|
|
|
101
111
|
quality_type_updated = Severity.WARNING
|
|
102
112
|
quality_specification_updated = Severity.WARNING
|
|
113
|
+
|
|
114
|
+
# info rules
|
|
115
|
+
info__added = Severity.INFO # will match `info_<somekey>_added` etc
|
|
116
|
+
info__removed = Severity.INFO
|
|
117
|
+
info__updated = Severity.INFO
|
|
118
|
+
|
|
119
|
+
contact__added = Severity.INFO
|
|
120
|
+
contact__removed = Severity.INFO
|
|
121
|
+
contact__updated = Severity.INFO
|
|
122
|
+
|
|
123
|
+
# terms rules
|
|
124
|
+
terms__added = Severity.INFO
|
|
125
|
+
terms__removed = Severity.INFO
|
|
126
|
+
terms__updated = Severity.INFO
|
datacontract/catalog/catalog.py
CHANGED
|
@@ -3,7 +3,7 @@ from datetime import datetime
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
5
|
import pytz
|
|
6
|
-
from jinja2 import
|
|
6
|
+
from jinja2 import Environment, PackageLoader, select_autoescape
|
|
7
7
|
|
|
8
8
|
from datacontract.data_contract import DataContract
|
|
9
9
|
from datacontract.export.html_export import get_version
|
datacontract/cli.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from importlib import metadata
|
|
2
2
|
from pathlib import Path
|
|
3
|
-
from typing import Iterable, Optional
|
|
4
|
-
from typing import List
|
|
3
|
+
from typing import Iterable, List, Optional
|
|
5
4
|
|
|
6
5
|
import typer
|
|
7
6
|
import uvicorn
|
|
@@ -13,11 +12,16 @@ from typer.core import TyperGroup
|
|
|
13
12
|
from typing_extensions import Annotated
|
|
14
13
|
|
|
15
14
|
from datacontract import web
|
|
16
|
-
from datacontract.catalog.catalog import
|
|
15
|
+
from datacontract.catalog.catalog import create_data_contract_html, create_index_html
|
|
17
16
|
from datacontract.data_contract import DataContract, ExportFormat
|
|
18
17
|
from datacontract.imports.importer import ImportFormat
|
|
19
|
-
from datacontract.init.download_datacontract_file import
|
|
20
|
-
|
|
18
|
+
from datacontract.init.download_datacontract_file import (
|
|
19
|
+
FileExistsException,
|
|
20
|
+
download_datacontract_file,
|
|
21
|
+
)
|
|
22
|
+
from datacontract.integration.datamesh_manager import (
|
|
23
|
+
publish_data_contract_to_datamesh_manager,
|
|
24
|
+
)
|
|
21
25
|
|
|
22
26
|
DEFAULT_DATA_CONTRACT_SCHEMA_URL = "https://datacontract.com/datacontract.schema.json"
|
|
23
27
|
|
|
@@ -46,7 +50,11 @@ def version_callback(value: bool):
|
|
|
46
50
|
def common(
|
|
47
51
|
ctx: typer.Context,
|
|
48
52
|
version: bool = typer.Option(
|
|
49
|
-
None,
|
|
53
|
+
None,
|
|
54
|
+
"--version",
|
|
55
|
+
help="Prints the current version.",
|
|
56
|
+
callback=version_callback,
|
|
57
|
+
is_eager=True,
|
|
50
58
|
),
|
|
51
59
|
):
|
|
52
60
|
"""
|
|
@@ -62,7 +70,8 @@ def common(
|
|
|
62
70
|
@app.command()
|
|
63
71
|
def init(
|
|
64
72
|
location: Annotated[
|
|
65
|
-
str,
|
|
73
|
+
str,
|
|
74
|
+
typer.Argument(help="The location (url or path) of the data contract yaml to create."),
|
|
66
75
|
] = "datacontract.yaml",
|
|
67
76
|
template: Annotated[
|
|
68
77
|
str, typer.Option(help="URL of a template or data contract")
|
|
@@ -84,10 +93,12 @@ def init(
|
|
|
84
93
|
@app.command()
|
|
85
94
|
def lint(
|
|
86
95
|
location: Annotated[
|
|
87
|
-
str,
|
|
96
|
+
str,
|
|
97
|
+
typer.Argument(help="The location (url or path) of the data contract yaml."),
|
|
88
98
|
] = "datacontract.yaml",
|
|
89
99
|
schema: Annotated[
|
|
90
|
-
str,
|
|
100
|
+
str,
|
|
101
|
+
typer.Option(help="The location (url or path) of the Data Contract Specification JSON Schema"),
|
|
91
102
|
] = DEFAULT_DATA_CONTRACT_SCHEMA_URL,
|
|
92
103
|
):
|
|
93
104
|
"""
|
|
@@ -100,10 +111,12 @@ def lint(
|
|
|
100
111
|
@app.command()
|
|
101
112
|
def test(
|
|
102
113
|
location: Annotated[
|
|
103
|
-
str,
|
|
114
|
+
str,
|
|
115
|
+
typer.Argument(help="The location (url or path) of the data contract yaml."),
|
|
104
116
|
] = "datacontract.yaml",
|
|
105
117
|
schema: Annotated[
|
|
106
|
-
str,
|
|
118
|
+
str,
|
|
119
|
+
typer.Option(help="The location (url or path) of the Data Contract Specification JSON Schema"),
|
|
107
120
|
] = DEFAULT_DATA_CONTRACT_SCHEMA_URL,
|
|
108
121
|
server: Annotated[
|
|
109
122
|
str,
|
|
@@ -115,7 +128,8 @@ def test(
|
|
|
115
128
|
),
|
|
116
129
|
] = "all",
|
|
117
130
|
examples: Annotated[
|
|
118
|
-
bool,
|
|
131
|
+
bool,
|
|
132
|
+
typer.Option(help="Run the schema and quality tests on the example data within the data contract."),
|
|
119
133
|
] = None,
|
|
120
134
|
publish: Annotated[str, typer.Option(help="The url to publish the results after the test")] = None,
|
|
121
135
|
publish_to_opentelemetry: Annotated[
|
|
@@ -166,7 +180,10 @@ def export(
|
|
|
166
180
|
# TODO: this should be a subcommand
|
|
167
181
|
rdf_base: Annotated[
|
|
168
182
|
Optional[str],
|
|
169
|
-
typer.Option(
|
|
183
|
+
typer.Option(
|
|
184
|
+
help="[rdf] The base URI used to generate the RDF graph.",
|
|
185
|
+
rich_help_panel="RDF Options",
|
|
186
|
+
),
|
|
170
187
|
] = None,
|
|
171
188
|
# TODO: this should be a subcommand
|
|
172
189
|
sql_server_type: Annotated[
|
|
@@ -177,14 +194,21 @@ def export(
|
|
|
177
194
|
),
|
|
178
195
|
] = "auto",
|
|
179
196
|
location: Annotated[
|
|
180
|
-
str,
|
|
197
|
+
str,
|
|
198
|
+
typer.Argument(help="The location (url or path) of the data contract yaml."),
|
|
181
199
|
] = "datacontract.yaml",
|
|
182
200
|
schema: Annotated[
|
|
183
|
-
str,
|
|
201
|
+
str,
|
|
202
|
+
typer.Option(help="The location (url or path) of the Data Contract Specification JSON Schema"),
|
|
184
203
|
] = DEFAULT_DATA_CONTRACT_SCHEMA_URL,
|
|
204
|
+
# TODO: this should be a subcommand
|
|
205
|
+
engine: Annotated[
|
|
206
|
+
Optional[str],
|
|
207
|
+
typer.Option(help="[engine] The engine used for great expection run."),
|
|
208
|
+
] = None,
|
|
185
209
|
):
|
|
186
210
|
"""
|
|
187
|
-
Convert data contract to a specific format.
|
|
211
|
+
Convert data contract to a specific format. Saves to file specified by `output` option if present, otherwise prints to stdout.
|
|
188
212
|
"""
|
|
189
213
|
# TODO exception handling
|
|
190
214
|
result = DataContract(data_contract_file=location, schema_location=schema, server=server).export(
|
|
@@ -193,6 +217,7 @@ def export(
|
|
|
193
217
|
server=server,
|
|
194
218
|
rdf_base=rdf_base,
|
|
195
219
|
sql_server_type=sql_server_type,
|
|
220
|
+
engine=engine,
|
|
196
221
|
)
|
|
197
222
|
# Don't interpret console markup in output.
|
|
198
223
|
if output is None:
|
|
@@ -206,8 +231,15 @@ def export(
|
|
|
206
231
|
@app.command(name="import")
|
|
207
232
|
def import_(
|
|
208
233
|
format: Annotated[ImportFormat, typer.Option(help="The format of the source file.")],
|
|
234
|
+
output: Annotated[
|
|
235
|
+
Path,
|
|
236
|
+
typer.Option(
|
|
237
|
+
help="Specify the file path where the Data Contract will be saved. If no path is provided, the output will be printed to stdout."
|
|
238
|
+
),
|
|
239
|
+
] = None,
|
|
209
240
|
source: Annotated[
|
|
210
|
-
Optional[str],
|
|
241
|
+
Optional[str],
|
|
242
|
+
typer.Option(help="The path to the file or Glue Database that should be imported."),
|
|
211
243
|
] = None,
|
|
212
244
|
glue_table: Annotated[
|
|
213
245
|
Optional[List[str]],
|
|
@@ -250,7 +282,7 @@ def import_(
|
|
|
250
282
|
] = None,
|
|
251
283
|
):
|
|
252
284
|
"""
|
|
253
|
-
Create a data contract from the given source location.
|
|
285
|
+
Create a data contract from the given source location. Saves to file specified by `output` option if present, otherwise prints to stdout.
|
|
254
286
|
"""
|
|
255
287
|
result = DataContract().import_from_source(
|
|
256
288
|
format=format,
|
|
@@ -265,16 +297,23 @@ def import_(
|
|
|
265
297
|
dbml_table=dbml_table,
|
|
266
298
|
iceberg_table=iceberg_table,
|
|
267
299
|
)
|
|
268
|
-
|
|
300
|
+
if output is None:
|
|
301
|
+
console.print(result.to_yaml())
|
|
302
|
+
else:
|
|
303
|
+
with output.open("w") as f:
|
|
304
|
+
f.write(result.to_yaml())
|
|
305
|
+
console.print(f"Written result to {output}")
|
|
269
306
|
|
|
270
307
|
|
|
271
308
|
@app.command(name="publish")
|
|
272
309
|
def publish(
|
|
273
310
|
location: Annotated[
|
|
274
|
-
str,
|
|
311
|
+
str,
|
|
312
|
+
typer.Argument(help="The location (url or path) of the data contract yaml."),
|
|
275
313
|
] = "datacontract.yaml",
|
|
276
314
|
schema: Annotated[
|
|
277
|
-
str,
|
|
315
|
+
str,
|
|
316
|
+
typer.Option(help="The location (url or path) of the Data Contract Specification JSON Schema"),
|
|
278
317
|
] = DEFAULT_DATA_CONTRACT_SCHEMA_URL,
|
|
279
318
|
):
|
|
280
319
|
"""
|
|
@@ -290,11 +329,15 @@ def publish(
|
|
|
290
329
|
@app.command(name="catalog")
|
|
291
330
|
def catalog(
|
|
292
331
|
files: Annotated[
|
|
293
|
-
Optional[str],
|
|
332
|
+
Optional[str],
|
|
333
|
+
typer.Option(
|
|
334
|
+
help="Glob pattern for the data contract files to include in the catalog. Applies recursively to any subfolders."
|
|
335
|
+
),
|
|
294
336
|
] = "*.yaml",
|
|
295
337
|
output: Annotated[Optional[str], typer.Option(help="Output directory for the catalog html files.")] = "catalog/",
|
|
296
338
|
schema: Annotated[
|
|
297
|
-
str,
|
|
339
|
+
str,
|
|
340
|
+
typer.Option(help="The location (url or path) of the Data Contract Specification JSON Schema"),
|
|
298
341
|
] = DEFAULT_DATA_CONTRACT_SCHEMA_URL,
|
|
299
342
|
):
|
|
300
343
|
"""
|
|
@@ -305,7 +348,7 @@ def catalog(
|
|
|
305
348
|
console.print(f"Created {output}")
|
|
306
349
|
|
|
307
350
|
contracts = []
|
|
308
|
-
for file in Path().
|
|
351
|
+
for file in Path().rglob(files):
|
|
309
352
|
try:
|
|
310
353
|
create_data_contract_html(contracts, file, path, schema)
|
|
311
354
|
except Exception as e:
|
|
@@ -316,8 +359,14 @@ def catalog(
|
|
|
316
359
|
|
|
317
360
|
@app.command()
|
|
318
361
|
def breaking(
|
|
319
|
-
location_old: Annotated[
|
|
320
|
-
|
|
362
|
+
location_old: Annotated[
|
|
363
|
+
str,
|
|
364
|
+
typer.Argument(help="The location (url or path) of the old data contract yaml."),
|
|
365
|
+
],
|
|
366
|
+
location_new: Annotated[
|
|
367
|
+
str,
|
|
368
|
+
typer.Argument(help="The location (url or path) of the new data contract yaml."),
|
|
369
|
+
],
|
|
321
370
|
):
|
|
322
371
|
"""
|
|
323
372
|
Identifies breaking changes between data contracts. Prints to stdout.
|
|
@@ -336,8 +385,14 @@ def breaking(
|
|
|
336
385
|
|
|
337
386
|
@app.command()
|
|
338
387
|
def changelog(
|
|
339
|
-
location_old: Annotated[
|
|
340
|
-
|
|
388
|
+
location_old: Annotated[
|
|
389
|
+
str,
|
|
390
|
+
typer.Argument(help="The location (url or path) of the old data contract yaml."),
|
|
391
|
+
],
|
|
392
|
+
location_new: Annotated[
|
|
393
|
+
str,
|
|
394
|
+
typer.Argument(help="The location (url or path) of the new data contract yaml."),
|
|
395
|
+
],
|
|
341
396
|
):
|
|
342
397
|
"""
|
|
343
398
|
Generate a changelog between data contracts. Prints to stdout.
|
|
@@ -353,8 +408,14 @@ def changelog(
|
|
|
353
408
|
|
|
354
409
|
@app.command()
|
|
355
410
|
def diff(
|
|
356
|
-
location_old: Annotated[
|
|
357
|
-
|
|
411
|
+
location_old: Annotated[
|
|
412
|
+
str,
|
|
413
|
+
typer.Argument(help="The location (url or path) of the old data contract yaml."),
|
|
414
|
+
],
|
|
415
|
+
location_new: Annotated[
|
|
416
|
+
str,
|
|
417
|
+
typer.Argument(help="The location (url or path) of the new data contract yaml."),
|
|
418
|
+
],
|
|
358
419
|
):
|
|
359
420
|
"""
|
|
360
421
|
PLACEHOLDER. Currently works as 'changelog' does.
|
|
@@ -391,7 +452,13 @@ def _handle_result(run):
|
|
|
391
452
|
i = 1
|
|
392
453
|
for check in run.checks:
|
|
393
454
|
if check.result != "passed":
|
|
394
|
-
|
|
455
|
+
field = to_field(run, check)
|
|
456
|
+
if field:
|
|
457
|
+
field = field + " "
|
|
458
|
+
else:
|
|
459
|
+
field = ""
|
|
460
|
+
console.print(f"{i}) {field}{check.name}: {check.reason}")
|
|
461
|
+
i += 1
|
|
395
462
|
raise typer.Exit(code=1)
|
|
396
463
|
|
|
397
464
|
|
|
@@ -401,7 +468,7 @@ def _print_table(run):
|
|
|
401
468
|
table.add_column("Check", max_width=100)
|
|
402
469
|
table.add_column("Field", max_width=32)
|
|
403
470
|
table.add_column("Details", max_width=50)
|
|
404
|
-
for check in run.checks:
|
|
471
|
+
for check in sorted(run.checks, key=lambda c: (c.result or "", c.model or "", c.field or "")):
|
|
405
472
|
table.add_row(with_markup(check.result), check.name, to_field(run, check), check.reason)
|
|
406
473
|
console.print(table)
|
|
407
474
|
|
datacontract/data_contract.py
CHANGED
|
@@ -8,7 +8,12 @@ import yaml
|
|
|
8
8
|
if typing.TYPE_CHECKING:
|
|
9
9
|
from pyspark.sql import SparkSession
|
|
10
10
|
|
|
11
|
-
from datacontract.breaking.breaking import
|
|
11
|
+
from datacontract.breaking.breaking import (
|
|
12
|
+
info_breaking_changes,
|
|
13
|
+
models_breaking_changes,
|
|
14
|
+
quality_breaking_changes,
|
|
15
|
+
terms_breaking_changes,
|
|
16
|
+
)
|
|
12
17
|
from datacontract.engines.datacontract.check_that_datacontract_contains_valid_servers_configuration import (
|
|
13
18
|
check_that_datacontract_contains_valid_server_configuration,
|
|
14
19
|
)
|
|
@@ -17,7 +22,6 @@ from datacontract.engines.soda.check_soda_execute import check_soda_execute
|
|
|
17
22
|
from datacontract.export.exporter import ExportFormat
|
|
18
23
|
from datacontract.export.exporter_factory import exporter_factory
|
|
19
24
|
from datacontract.imports.importer_factory import importer_factory
|
|
20
|
-
|
|
21
25
|
from datacontract.integration.datamesh_manager import publish_test_results_to_datamesh_manager
|
|
22
26
|
from datacontract.integration.opentelemetry import publish_test_results_to_opentelemetry
|
|
23
27
|
from datacontract.lint import resolve
|
|
@@ -28,10 +32,10 @@ from datacontract.lint.linters.field_reference_linter import FieldReferenceLinte
|
|
|
28
32
|
from datacontract.lint.linters.notice_period_linter import NoticePeriodLinter
|
|
29
33
|
from datacontract.lint.linters.quality_schema_linter import QualityUsesSchemaLinter
|
|
30
34
|
from datacontract.lint.linters.valid_constraints_linter import ValidFieldConstraintsLinter
|
|
31
|
-
from datacontract.model.breaking_change import
|
|
35
|
+
from datacontract.model.breaking_change import BreakingChange, BreakingChanges, Severity
|
|
32
36
|
from datacontract.model.data_contract_specification import DataContractSpecification, Server
|
|
33
37
|
from datacontract.model.exceptions import DataContractException
|
|
34
|
-
from datacontract.model.run import
|
|
38
|
+
from datacontract.model.run import Check, Run
|
|
35
39
|
|
|
36
40
|
|
|
37
41
|
class DataContract:
|
|
@@ -276,6 +280,24 @@ class DataContract:
|
|
|
276
280
|
|
|
277
281
|
breaking_changes = list[BreakingChange]()
|
|
278
282
|
|
|
283
|
+
breaking_changes.extend(
|
|
284
|
+
info_breaking_changes(
|
|
285
|
+
old_info=old.info,
|
|
286
|
+
new_info=new.info,
|
|
287
|
+
new_path=other._data_contract_file,
|
|
288
|
+
include_severities=include_severities,
|
|
289
|
+
)
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
breaking_changes.extend(
|
|
293
|
+
terms_breaking_changes(
|
|
294
|
+
old_terms=old.terms,
|
|
295
|
+
new_terms=new.terms,
|
|
296
|
+
new_path=other._data_contract_file,
|
|
297
|
+
include_severities=include_severities,
|
|
298
|
+
)
|
|
299
|
+
)
|
|
300
|
+
|
|
279
301
|
breaking_changes.extend(
|
|
280
302
|
quality_breaking_changes(
|
|
281
303
|
old_quality=old.quality,
|