datacontract-cli 0.9.6.post2__py3-none-any.whl → 0.9.8__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.

Files changed (60) hide show
  1. datacontract/breaking/breaking.py +139 -63
  2. datacontract/breaking/breaking_rules.py +71 -54
  3. datacontract/cli.py +138 -45
  4. datacontract/data_contract.py +316 -78
  5. datacontract/engines/datacontract/check_that_datacontract_contains_valid_servers_configuration.py +5 -1
  6. datacontract/engines/datacontract/check_that_datacontract_file_exists.py +9 -8
  7. datacontract/engines/datacontract/check_that_datacontract_str_is_valid.py +26 -22
  8. datacontract/engines/fastjsonschema/check_jsonschema.py +31 -25
  9. datacontract/engines/fastjsonschema/s3/s3_read_files.py +8 -6
  10. datacontract/engines/soda/check_soda_execute.py +46 -35
  11. datacontract/engines/soda/connections/bigquery.py +5 -3
  12. datacontract/engines/soda/connections/dask.py +0 -1
  13. datacontract/engines/soda/connections/databricks.py +2 -2
  14. datacontract/engines/soda/connections/duckdb.py +4 -4
  15. datacontract/engines/soda/connections/kafka.py +36 -17
  16. datacontract/engines/soda/connections/postgres.py +3 -3
  17. datacontract/engines/soda/connections/snowflake.py +4 -4
  18. datacontract/export/avro_converter.py +3 -7
  19. datacontract/export/avro_idl_converter.py +280 -0
  20. datacontract/export/dbt_converter.py +55 -80
  21. datacontract/export/great_expectations_converter.py +141 -0
  22. datacontract/export/jsonschema_converter.py +3 -1
  23. datacontract/export/odcs_converter.py +10 -12
  24. datacontract/export/protobuf_converter.py +99 -0
  25. datacontract/export/pydantic_converter.py +140 -0
  26. datacontract/export/rdf_converter.py +35 -12
  27. datacontract/export/sodacl_converter.py +24 -24
  28. datacontract/export/sql_converter.py +93 -0
  29. datacontract/export/sql_type_converter.py +131 -0
  30. datacontract/export/terraform_converter.py +71 -0
  31. datacontract/imports/avro_importer.py +106 -0
  32. datacontract/imports/sql_importer.py +0 -2
  33. datacontract/init/download_datacontract_file.py +2 -2
  34. datacontract/integration/publish_datamesh_manager.py +4 -9
  35. datacontract/integration/publish_opentelemetry.py +107 -0
  36. datacontract/lint/files.py +2 -2
  37. datacontract/lint/lint.py +46 -31
  38. datacontract/lint/linters/description_linter.py +34 -0
  39. datacontract/lint/linters/example_model_linter.py +67 -43
  40. datacontract/lint/linters/field_pattern_linter.py +34 -0
  41. datacontract/lint/linters/field_reference_linter.py +38 -0
  42. datacontract/lint/linters/notice_period_linter.py +55 -0
  43. datacontract/lint/linters/primary_field_linter.py +28 -0
  44. datacontract/lint/linters/quality_schema_linter.py +52 -0
  45. datacontract/lint/linters/valid_constraints_linter.py +99 -0
  46. datacontract/lint/resolve.py +53 -8
  47. datacontract/lint/schema.py +2 -3
  48. datacontract/lint/urls.py +4 -5
  49. datacontract/model/breaking_change.py +27 -5
  50. datacontract/model/data_contract_specification.py +45 -25
  51. datacontract/model/exceptions.py +13 -2
  52. datacontract/model/run.py +1 -1
  53. datacontract/web.py +5 -8
  54. {datacontract_cli-0.9.6.post2.dist-info → datacontract_cli-0.9.8.dist-info}/METADATA +207 -35
  55. datacontract_cli-0.9.8.dist-info/RECORD +63 -0
  56. {datacontract_cli-0.9.6.post2.dist-info → datacontract_cli-0.9.8.dist-info}/WHEEL +1 -1
  57. datacontract_cli-0.9.6.post2.dist-info/RECORD +0 -47
  58. {datacontract_cli-0.9.6.post2.dist-info → datacontract_cli-0.9.8.dist-info}/LICENSE +0 -0
  59. {datacontract_cli-0.9.6.post2.dist-info → datacontract_cli-0.9.8.dist-info}/entry_points.txt +0 -0
  60. {datacontract_cli-0.9.6.post2.dist-info → datacontract_cli-0.9.8.dist-info}/top_level.txt +0 -0
datacontract/cli.py CHANGED
@@ -5,7 +5,7 @@ from typing import Iterable, Optional
5
5
  import typer
6
6
  from click import Context
7
7
  from rich import box
8
- from rich import print
8
+ from rich.console import Console
9
9
  from rich.table import Table
10
10
  from typer.core import TyperGroup
11
11
  from typing_extensions import Annotated
@@ -14,6 +14,7 @@ from datacontract.data_contract import DataContract
14
14
  from datacontract.init.download_datacontract_file import \
15
15
  download_datacontract_file, FileExistsException
16
16
 
17
+ console = Console()
17
18
 
18
19
  class OrderedCommands(TyperGroup):
19
20
  def list_commands(self, ctx: Context) -> Iterable[str]:
@@ -29,15 +30,16 @@ app = typer.Typer(
29
30
 
30
31
  def version_callback(value: bool):
31
32
  if value:
32
- print(metadata.version("datacontract-cli"))
33
+ console.print(metadata.version("datacontract-cli"))
33
34
  raise typer.Exit()
34
35
 
35
36
 
36
37
  @app.callback()
37
38
  def common(
38
39
  ctx: typer.Context,
39
- version: bool = typer.Option(None, "--version", help="Prints the current version.", callback=version_callback,
40
- is_eager=True),
40
+ version: bool = typer.Option(
41
+ None, "--version", help="Prints the current version.", callback=version_callback, is_eager=True
42
+ ),
41
43
  ):
42
44
  """
43
45
  The datacontract CLI is an open source command-line tool for working with Data Contracts (https://datacontract.com).
@@ -51,10 +53,12 @@ def common(
51
53
 
52
54
  @app.command()
53
55
  def init(
54
- location: Annotated[str, typer.Argument(
55
- help="The location (url or path) of the data contract yaml to create.")] = "datacontract.yaml",
56
- template: Annotated[str, typer.Option(
57
- help="URL of a template or data contract")] = "https://datacontract.com/datacontract.init.yaml",
56
+ location: Annotated[
57
+ str, typer.Argument(help="The location (url or path) of the data contract yaml to create.")
58
+ ] = "datacontract.yaml",
59
+ template: Annotated[
60
+ str, typer.Option(help="URL of a template or data contract")
61
+ ] = "https://datacontract.com/datacontract.init.yaml",
58
62
  overwrite: Annotated[bool, typer.Option(help="Replace the existing datacontract.yaml")] = False,
59
63
  ):
60
64
  """
@@ -63,19 +67,20 @@ def init(
63
67
  try:
64
68
  download_datacontract_file(location, template, overwrite)
65
69
  except FileExistsException:
66
- print("File already exists, use --overwrite to overwrite")
70
+ console.print("File already exists, use --overwrite to overwrite")
67
71
  raise typer.Exit(code=1)
68
72
  else:
69
- print("📄 data contract written to " + location)
73
+ console.print("📄 data contract written to " + location)
70
74
 
71
75
 
72
76
  @app.command()
73
77
  def lint(
74
78
  location: Annotated[
75
- str, typer.Argument(help="The location (url or path) of the data contract yaml.")] = "datacontract.yaml",
79
+ str, typer.Argument(help="The location (url or path) of the data contract yaml.")
80
+ ] = "datacontract.yaml",
76
81
  schema: Annotated[
77
- str, typer.Option(
78
- help="The location (url or path) of the Data Contract Specification JSON Schema")] = "https://datacontract.com/datacontract.schema.json",
82
+ str, typer.Option(help="The location (url or path) of the Data Contract Specification JSON Schema")
83
+ ] = "https://datacontract.com/datacontract.schema.json",
79
84
  ):
80
85
  """
81
86
  Validate that the datacontract.yaml is correctly formatted.
@@ -87,30 +92,46 @@ def lint(
87
92
  @app.command()
88
93
  def test(
89
94
  location: Annotated[
90
- str, typer.Argument(help="The location (url or path) of the data contract yaml.")] = "datacontract.yaml",
95
+ str, typer.Argument(help="The location (url or path) of the data contract yaml.")
96
+ ] = "datacontract.yaml",
91
97
  schema: Annotated[
92
- str, typer.Option(
93
- help="The location (url or path) of the Data Contract Specification JSON Schema")] = "https://datacontract.com/datacontract.schema.json",
94
- server: Annotated[str, typer.Option(
95
- help="The server configuration to run the schema and quality tests. "
96
- "Use the key of the server object in the data contract yaml file "
97
- "to refer to a server, e.g., `production`, or `all` for all "
98
- "servers (default).")] = "all",
99
- examples: Annotated[bool, typer.Option(
100
- help="Run the schema and quality tests on the example data within the data contract.")] = None,
101
- publish: Annotated[str, typer.Option(
102
- help="The url to publish the results after the test")] = None,
103
- logs: Annotated[bool, typer.Option(
104
- help="Print logs")] = False,
98
+ str, typer.Option(help="The location (url or path) of the Data Contract Specification JSON Schema")
99
+ ] = "https://datacontract.com/datacontract.schema.json",
100
+ server: Annotated[
101
+ str,
102
+ typer.Option(
103
+ help="The server configuration to run the schema and quality tests. "
104
+ "Use the key of the server object in the data contract yaml file "
105
+ "to refer to a server, e.g., `production`, or `all` for all "
106
+ "servers (default)."
107
+ ),
108
+ ] = "all",
109
+ examples: Annotated[
110
+ bool, typer.Option(help="Run the schema and quality tests on the example data within the data contract.")
111
+ ] = None,
112
+ publish: Annotated[str, typer.Option(help="The url to publish the results after the test")] = None,
113
+ publish_to_opentelemetry: Annotated[
114
+ bool,
115
+ typer.Option(
116
+ help="Publish the results to opentelemetry. Use environment variables to configure the OTLP endpoint, headers, etc."
117
+ ),
118
+ ] = False,
119
+ logs: Annotated[bool, typer.Option(help="Print logs")] = False,
105
120
  ):
106
121
  """
107
122
  Run schema and quality tests on configured servers.
108
123
  """
109
- print(f"Testing {location}")
124
+ console.print(f"Testing {location}")
110
125
  if server == "all":
111
126
  server = None
112
- run = DataContract(data_contract_file=location, schema_location=schema, publish_url=publish, server=server,
113
- examples=examples).test()
127
+ run = DataContract(
128
+ data_contract_file=location,
129
+ schema_location=schema,
130
+ publish_url=publish,
131
+ publish_to_opentelemetry=publish_to_opentelemetry,
132
+ server=server,
133
+ examples=examples,
134
+ ).test()
114
135
  if logs:
115
136
  _print_logs(run)
116
137
  _handle_result(run)
@@ -118,6 +139,7 @@ def test(
118
139
 
119
140
  class ExportFormat(str, Enum):
120
141
  jsonschema = "jsonschema"
142
+ pydantic_model = "pydantic-model"
121
143
  sodacl = "sodacl"
122
144
  dbt = "dbt"
123
145
  dbt_sources = "dbt-sources"
@@ -125,26 +147,58 @@ class ExportFormat(str, Enum):
125
147
  odcs = "odcs"
126
148
  rdf = "rdf"
127
149
  avro = "avro"
150
+ protobuf = "protobuf"
151
+ great_expectations = "great-expectations"
152
+ terraform = "terraform"
153
+ avro_idl = "avro-idl"
154
+ sql = "sql"
155
+ sql_query = "sql-query"
128
156
 
129
157
 
130
158
  @app.command()
131
159
  def export(
132
160
  format: Annotated[ExportFormat, typer.Option(help="The export format.")],
133
161
  server: Annotated[str, typer.Option(help="The server name to export.")] = None,
134
- rdf_base: Annotated[Optional[str], typer.Option(help="[rdf] The base URI used to generate the RDF graph.")] = None,
162
+ model: Annotated[
163
+ str,
164
+ typer.Option(
165
+ help="Use the key of the model in the data contract yaml file "
166
+ "to refer to a model, e.g., `orders`, or `all` for all "
167
+ "models (default)."
168
+ ),
169
+ ] = "all",
170
+ rdf_base: Annotated[
171
+ Optional[str],
172
+ typer.Option(help="[rdf] The base URI used to generate the RDF graph.", rich_help_panel="RDF Options"),
173
+ ] = None,
174
+ sql_server_type: Annotated[
175
+ Optional[str],
176
+ typer.Option(
177
+ help="[sql] The server type to determine the sql dialect. By default, it uses 'auto' to automatically detect the sql dialect via the specified servers in the data contract.",
178
+ rich_help_panel="SQL Options",
179
+ ),
180
+ ] = "auto",
135
181
  location: Annotated[
136
- str, typer.Argument(help="The location (url or path) of the data contract yaml.")] = "datacontract.yaml",
182
+ str, typer.Argument(help="The location (url or path) of the data contract yaml.")
183
+ ] = "datacontract.yaml",
137
184
  ):
138
185
  """
139
- Convert data contract to a specific format. Prints to stdout.
186
+ Convert data contract to a specific format. console.prints to stdout.
140
187
  """
141
188
  # TODO exception handling
142
- result = DataContract(data_contract_file=location, server=server).export(format, rdf_base)
143
- print(result)
189
+ result = DataContract(data_contract_file=location, server=server).export(
190
+ export_format=format,
191
+ model=model,
192
+ rdf_base=rdf_base,
193
+ sql_server_type=sql_server_type,
194
+ )
195
+ # Don't interpret console markup in output.
196
+ console.print(result, markup=False)
144
197
 
145
198
 
146
199
  class ImportFormat(str, Enum):
147
200
  sql = "sql"
201
+ avro = "avro"
148
202
 
149
203
 
150
204
  @app.command(name="import")
@@ -156,7 +210,7 @@ def import_(
156
210
  Create a data contract from the given source file. Prints to stdout.
157
211
  """
158
212
  result = DataContract().import_from_source(format, source)
159
- print(result.to_yaml())
213
+ console.print(result.to_yaml())
160
214
 
161
215
 
162
216
  @app.command()
@@ -169,23 +223,62 @@ def breaking(
169
223
  """
170
224
 
171
225
  # TODO exception handling
172
- result = DataContract(data_contract_file=location_old).breaking(DataContract(data_contract_file=location_new))
173
- print(str(result))
226
+ result = DataContract(data_contract_file=location_old, inline_definitions=True).breaking(
227
+ DataContract(data_contract_file=location_new, inline_definitions=True)
228
+ )
229
+
230
+ console.print(result.breaking_str())
231
+
174
232
  if not result.passed_checks():
175
233
  raise typer.Exit(code=1)
176
234
 
177
235
 
236
+ @app.command()
237
+ def changelog(
238
+ location_old: Annotated[str, typer.Argument(help="The location (url or path) of the old data contract yaml.")],
239
+ location_new: Annotated[str, typer.Argument(help="The location (url or path) of the new data contract yaml.")],
240
+ ):
241
+ """
242
+ Generate a changelog between data contracts. Prints to stdout.
243
+ """
244
+
245
+ # TODO exception handling
246
+ result = DataContract(data_contract_file=location_old, inline_definitions=True).changelog(
247
+ DataContract(data_contract_file=location_new, inline_definitions=True)
248
+ )
249
+
250
+ console.print(result.changelog_str())
251
+
252
+
253
+ @app.command()
254
+ def diff(
255
+ location_old: Annotated[str, typer.Argument(help="The location (url or path) of the old data contract yaml.")],
256
+ location_new: Annotated[str, typer.Argument(help="The location (url or path) of the new data contract yaml.")],
257
+ ):
258
+ """
259
+ PLACEHOLDER. Currently works as 'changelog' does.
260
+ """
261
+
262
+ # TODO change to diff output, not the changelog entries
263
+ result = DataContract(data_contract_file=location_old, inline_definitions=True).changelog(
264
+ DataContract(data_contract_file=location_new, inline_definitions=True)
265
+ )
266
+
267
+ console.print(result.changelog_str())
268
+
269
+
178
270
  def _handle_result(run):
179
271
  _print_table(run)
180
272
  if run.result == "passed":
181
- print(
182
- f"🟢 data contract is valid. Run {len(run.checks)} checks. Took {(run.timestampEnd - run.timestampStart).total_seconds()} seconds.")
273
+ console.print(
274
+ f"🟢 data contract is valid. Run {len(run.checks)} checks. Took {(run.timestampEnd - run.timestampStart).total_seconds()} seconds."
275
+ )
183
276
  else:
184
- print("🔴 data contract is invalid, found the following errors:")
277
+ console.print("🔴 data contract is invalid, found the following errors:")
185
278
  i = 1
186
279
  for check in run.checks:
187
280
  if check.result != "passed":
188
- print(str(++i) + ") " + check.reason)
281
+ console.print(str(++i) + ") " + check.reason)
189
282
  raise typer.Exit(code=1)
190
283
 
191
284
 
@@ -197,7 +290,7 @@ def _print_table(run):
197
290
  table.add_column("Details", max_width=50)
198
291
  for check in run.checks:
199
292
  table.add_row(with_markup(check.result), check.name, to_field(run, check), check.reason)
200
- print(table)
293
+ console.print(table)
201
294
 
202
295
 
203
296
  def to_field(run, check):
@@ -211,9 +304,9 @@ def to_field(run, check):
211
304
 
212
305
 
213
306
  def _print_logs(run):
214
- print("\nLogs:")
307
+ console.print("\nLogs:")
215
308
  for log in run.logs:
216
- print(log.timestamp.strftime("%y-%m-%d %H:%M:%S"), log.level.ljust(5), log.message)
309
+ console.print(log.timestamp.strftime("%y-%m-%d %H:%M:%S"), log.level.ljust(5), log.message)
217
310
 
218
311
 
219
312
  def with_markup(result):