datacontract-cli 0.10.13__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 +104 -32
- datacontract/data_contract.py +35 -5
- 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 +38 -17
- datacontract/export/avro_converter.py +8 -1
- datacontract/export/avro_idl_converter.py +2 -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/dcs_exporter.py +6 -0
- datacontract/export/exporter.py +5 -2
- datacontract/export/exporter_factory.py +16 -3
- 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_converter.py → odcs_v2_exporter.py} +5 -5
- datacontract/export/odcs_v3_exporter.py +294 -0
- 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 +86 -2
- datacontract/export/spark_converter.py +10 -7
- datacontract/export/sql_converter.py +1 -2
- datacontract/export/sql_type_converter.py +55 -11
- 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 +161 -0
- datacontract/imports/importer.py +2 -0
- datacontract/imports/importer_factory.py +12 -1
- datacontract/imports/jsonschema_importer.py +3 -2
- datacontract/imports/odcs_importer.py +25 -168
- datacontract/imports/odcs_v2_importer.py +177 -0
- datacontract/imports/odcs_v3_importer.py +309 -0
- 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/datamesh_manager.py +1 -1
- 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 +14 -9
- datacontract/lint/resources.py +21 -0
- datacontract/lint/schema.py +1 -1
- datacontract/lint/urls.py +4 -2
- datacontract/model/data_contract_specification.py +83 -13
- datacontract/model/odcs.py +11 -0
- datacontract/model/run.py +21 -12
- datacontract/templates/index.html +6 -6
- datacontract/web.py +2 -3
- {datacontract_cli-0.10.13.dist-info → datacontract_cli-0.10.15.dist-info}/METADATA +176 -93
- datacontract_cli-0.10.15.dist-info/RECORD +105 -0
- {datacontract_cli-0.10.13.dist-info → datacontract_cli-0.10.15.dist-info}/WHEEL +1 -1
- datacontract/engines/datacontract/check_that_datacontract_str_is_valid.py +0 -48
- datacontract_cli-0.10.13.dist-info/RECORD +0 -97
- {datacontract_cli-0.10.13.dist-info → datacontract_cli-0.10.15.dist-info}/LICENSE +0 -0
- {datacontract_cli-0.10.13.dist-info → datacontract_cli-0.10.15.dist-info}/entry_points.txt +0 -0
- {datacontract_cli-0.10.13.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]],
|
|
@@ -244,9 +276,13 @@ def import_(
|
|
|
244
276
|
help="List of table names to import from the DBML file (repeat for multiple table names, leave empty for all tables in the file)."
|
|
245
277
|
),
|
|
246
278
|
] = None,
|
|
279
|
+
iceberg_table: Annotated[
|
|
280
|
+
Optional[str],
|
|
281
|
+
typer.Option(help="Table name to assign to the model created from the Iceberg schema."),
|
|
282
|
+
] = None,
|
|
247
283
|
):
|
|
248
284
|
"""
|
|
249
|
-
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.
|
|
250
286
|
"""
|
|
251
287
|
result = DataContract().import_from_source(
|
|
252
288
|
format=format,
|
|
@@ -259,17 +295,25 @@ def import_(
|
|
|
259
295
|
dbt_model=dbt_model,
|
|
260
296
|
dbml_schema=dbml_schema,
|
|
261
297
|
dbml_table=dbml_table,
|
|
298
|
+
iceberg_table=iceberg_table,
|
|
262
299
|
)
|
|
263
|
-
|
|
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}")
|
|
264
306
|
|
|
265
307
|
|
|
266
308
|
@app.command(name="publish")
|
|
267
309
|
def publish(
|
|
268
310
|
location: Annotated[
|
|
269
|
-
str,
|
|
311
|
+
str,
|
|
312
|
+
typer.Argument(help="The location (url or path) of the data contract yaml."),
|
|
270
313
|
] = "datacontract.yaml",
|
|
271
314
|
schema: Annotated[
|
|
272
|
-
str,
|
|
315
|
+
str,
|
|
316
|
+
typer.Option(help="The location (url or path) of the Data Contract Specification JSON Schema"),
|
|
273
317
|
] = DEFAULT_DATA_CONTRACT_SCHEMA_URL,
|
|
274
318
|
):
|
|
275
319
|
"""
|
|
@@ -285,11 +329,15 @@ def publish(
|
|
|
285
329
|
@app.command(name="catalog")
|
|
286
330
|
def catalog(
|
|
287
331
|
files: Annotated[
|
|
288
|
-
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
|
+
),
|
|
289
336
|
] = "*.yaml",
|
|
290
337
|
output: Annotated[Optional[str], typer.Option(help="Output directory for the catalog html files.")] = "catalog/",
|
|
291
338
|
schema: Annotated[
|
|
292
|
-
str,
|
|
339
|
+
str,
|
|
340
|
+
typer.Option(help="The location (url or path) of the Data Contract Specification JSON Schema"),
|
|
293
341
|
] = DEFAULT_DATA_CONTRACT_SCHEMA_URL,
|
|
294
342
|
):
|
|
295
343
|
"""
|
|
@@ -300,7 +348,7 @@ def catalog(
|
|
|
300
348
|
console.print(f"Created {output}")
|
|
301
349
|
|
|
302
350
|
contracts = []
|
|
303
|
-
for file in Path().
|
|
351
|
+
for file in Path().rglob(files):
|
|
304
352
|
try:
|
|
305
353
|
create_data_contract_html(contracts, file, path, schema)
|
|
306
354
|
except Exception as e:
|
|
@@ -311,8 +359,14 @@ def catalog(
|
|
|
311
359
|
|
|
312
360
|
@app.command()
|
|
313
361
|
def breaking(
|
|
314
|
-
location_old: Annotated[
|
|
315
|
-
|
|
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
|
+
],
|
|
316
370
|
):
|
|
317
371
|
"""
|
|
318
372
|
Identifies breaking changes between data contracts. Prints to stdout.
|
|
@@ -331,8 +385,14 @@ def breaking(
|
|
|
331
385
|
|
|
332
386
|
@app.command()
|
|
333
387
|
def changelog(
|
|
334
|
-
location_old: Annotated[
|
|
335
|
-
|
|
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
|
+
],
|
|
336
396
|
):
|
|
337
397
|
"""
|
|
338
398
|
Generate a changelog between data contracts. Prints to stdout.
|
|
@@ -348,8 +408,14 @@ def changelog(
|
|
|
348
408
|
|
|
349
409
|
@app.command()
|
|
350
410
|
def diff(
|
|
351
|
-
location_old: Annotated[
|
|
352
|
-
|
|
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
|
+
],
|
|
353
419
|
):
|
|
354
420
|
"""
|
|
355
421
|
PLACEHOLDER. Currently works as 'changelog' does.
|
|
@@ -386,7 +452,13 @@ def _handle_result(run):
|
|
|
386
452
|
i = 1
|
|
387
453
|
for check in run.checks:
|
|
388
454
|
if check.result != "passed":
|
|
389
|
-
|
|
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
|
|
390
462
|
raise typer.Exit(code=1)
|
|
391
463
|
|
|
392
464
|
|
|
@@ -396,7 +468,7 @@ def _print_table(run):
|
|
|
396
468
|
table.add_column("Check", max_width=100)
|
|
397
469
|
table.add_column("Field", max_width=32)
|
|
398
470
|
table.add_column("Details", max_width=50)
|
|
399
|
-
for check in run.checks:
|
|
471
|
+
for check in sorted(run.checks, key=lambda c: (c.result or "", c.model or "", c.field or "")):
|
|
400
472
|
table.add_row(with_markup(check.result), check.name, to_field(run, check), check.reason)
|
|
401
473
|
console.print(table)
|
|
402
474
|
|