datacontract-cli 0.9.7__py3-none-any.whl → 0.9.9__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 (62) hide show
  1. datacontract/breaking/breaking.py +48 -57
  2. datacontract/cli.py +100 -80
  3. datacontract/data_contract.py +178 -128
  4. datacontract/engines/datacontract/check_that_datacontract_contains_valid_servers_configuration.py +5 -1
  5. datacontract/engines/datacontract/check_that_datacontract_file_exists.py +9 -8
  6. datacontract/engines/datacontract/check_that_datacontract_str_is_valid.py +26 -22
  7. datacontract/engines/fastjsonschema/check_jsonschema.py +31 -25
  8. datacontract/engines/fastjsonschema/s3/s3_read_files.py +8 -6
  9. datacontract/engines/soda/check_soda_execute.py +58 -36
  10. datacontract/engines/soda/connections/bigquery.py +5 -3
  11. datacontract/engines/soda/connections/dask.py +0 -1
  12. datacontract/engines/soda/connections/databricks.py +2 -2
  13. datacontract/engines/soda/connections/duckdb.py +25 -8
  14. datacontract/engines/soda/connections/kafka.py +36 -17
  15. datacontract/engines/soda/connections/postgres.py +3 -3
  16. datacontract/engines/soda/connections/snowflake.py +4 -4
  17. datacontract/export/avro_converter.py +9 -11
  18. datacontract/export/avro_idl_converter.py +65 -42
  19. datacontract/export/csv_type_converter.py +36 -0
  20. datacontract/export/dbt_converter.py +43 -32
  21. datacontract/export/great_expectations_converter.py +141 -0
  22. datacontract/export/html_export.py +46 -0
  23. datacontract/export/jsonschema_converter.py +3 -1
  24. datacontract/export/odcs_converter.py +5 -7
  25. datacontract/export/protobuf_converter.py +12 -10
  26. datacontract/export/pydantic_converter.py +131 -0
  27. datacontract/export/rdf_converter.py +34 -11
  28. datacontract/export/sodacl_converter.py +118 -21
  29. datacontract/export/sql_converter.py +30 -8
  30. datacontract/export/sql_type_converter.py +44 -4
  31. datacontract/export/terraform_converter.py +4 -3
  32. datacontract/imports/avro_importer.py +65 -18
  33. datacontract/imports/sql_importer.py +0 -2
  34. datacontract/init/download_datacontract_file.py +2 -2
  35. datacontract/integration/publish_datamesh_manager.py +6 -12
  36. datacontract/integration/publish_opentelemetry.py +30 -16
  37. datacontract/lint/files.py +2 -2
  38. datacontract/lint/lint.py +26 -31
  39. datacontract/lint/linters/description_linter.py +12 -21
  40. datacontract/lint/linters/example_model_linter.py +28 -29
  41. datacontract/lint/linters/field_pattern_linter.py +8 -8
  42. datacontract/lint/linters/field_reference_linter.py +11 -10
  43. datacontract/lint/linters/notice_period_linter.py +18 -22
  44. datacontract/lint/linters/quality_schema_linter.py +16 -20
  45. datacontract/lint/linters/valid_constraints_linter.py +42 -37
  46. datacontract/lint/resolve.py +50 -14
  47. datacontract/lint/schema.py +2 -3
  48. datacontract/lint/urls.py +4 -5
  49. datacontract/model/breaking_change.py +2 -1
  50. datacontract/model/data_contract_specification.py +8 -7
  51. datacontract/model/exceptions.py +13 -2
  52. datacontract/model/run.py +3 -2
  53. datacontract/web.py +3 -7
  54. datacontract_cli-0.9.9.dist-info/METADATA +951 -0
  55. datacontract_cli-0.9.9.dist-info/RECORD +64 -0
  56. datacontract/lint/linters/primary_field_linter.py +0 -30
  57. datacontract_cli-0.9.7.dist-info/METADATA +0 -603
  58. datacontract_cli-0.9.7.dist-info/RECORD +0 -61
  59. {datacontract_cli-0.9.7.dist-info → datacontract_cli-0.9.9.dist-info}/LICENSE +0 -0
  60. {datacontract_cli-0.9.7.dist-info → datacontract_cli-0.9.9.dist-info}/WHEEL +0 -0
  61. {datacontract_cli-0.9.7.dist-info → datacontract_cli-0.9.9.dist-info}/entry_points.txt +0 -0
  62. {datacontract_cli-0.9.7.dist-info → datacontract_cli-0.9.9.dist-info}/top_level.txt +0 -0
@@ -6,19 +6,19 @@ from datacontract.model.exceptions import DataContractException
6
6
 
7
7
 
8
8
  def import_avro(data_contract_specification: DataContractSpecification, source: str) -> DataContractSpecification:
9
-
10
9
  if data_contract_specification.models is None:
11
10
  data_contract_specification.models = {}
12
11
 
13
12
  try:
14
- avro_schema = avro.schema.parse(open(source, "rb").read())
13
+ with open(source, "r") as file:
14
+ avro_schema = avro.schema.parse(file.read())
15
15
  except Exception as e:
16
16
  raise DataContractException(
17
17
  type="schema",
18
18
  name="Parse avro schema",
19
19
  reason=f"Failed to parse avro schema from {source}",
20
20
  engine="datacontract",
21
- original_exception=e
21
+ original_exception=e,
22
22
  )
23
23
 
24
24
  # type record is being used for both the table and the object types in data contract
@@ -26,30 +26,83 @@ def import_avro(data_contract_specification: DataContractSpecification, source:
26
26
  fields = import_record_fields(avro_schema.fields)
27
27
 
28
28
  data_contract_specification.models[avro_schema.name] = Model(
29
- type="table",
30
29
  fields=fields,
31
- description=avro_schema.doc,
32
30
  )
33
31
 
32
+ if avro_schema.get_prop("doc") is not None:
33
+ data_contract_specification.models[avro_schema.name].description = avro_schema.get_prop("doc")
34
+
35
+ if avro_schema.get_prop("namespace") is not None:
36
+ data_contract_specification.models[avro_schema.name].namespace = avro_schema.get_prop("namespace")
37
+
34
38
  return data_contract_specification
35
39
 
36
40
 
37
41
  def import_record_fields(record_fields):
38
-
39
42
  imported_fields = {}
40
43
  for field in record_fields:
44
+ imported_fields[field.name] = Field()
45
+ imported_fields[field.name].required = True
46
+ imported_fields[field.name].description = field.doc
47
+ for prop in field.other_props:
48
+ imported_fields[field.name].__setattr__(prop, field.other_props[prop])
49
+
41
50
  if field.type.type == "record":
42
- imported_fields[field.name] = Field()
43
51
  imported_fields[field.name].type = "object"
44
52
  imported_fields[field.name].description = field.type.doc
45
53
  imported_fields[field.name].fields = import_record_fields(field.type.fields)
46
- else:
47
- imported_fields[field.name] = Field()
54
+ elif field.type.type == "union":
55
+ imported_fields[field.name].required = False
56
+ type = import_type_of_optional_field(field)
57
+ imported_fields[field.name].type = type
58
+ if type == "record":
59
+ imported_fields[field.name].fields = import_record_fields(get_record_from_union_field(field).fields)
60
+ elif field.type.type == "array":
61
+ imported_fields[field.name].type = "array"
62
+ imported_fields[field.name].items = import_avro_array_items(field.type)
63
+ else: # primitive type
48
64
  imported_fields[field.name].type = map_type_from_avro(field.type.type)
49
- imported_fields[field.name].description = field.doc
65
+
50
66
  return imported_fields
51
67
 
52
68
 
69
+ def import_avro_array_items(array_schema):
70
+ items = Field()
71
+ for prop in array_schema.other_props:
72
+ items.__setattr__(prop, array_schema.other_props[prop])
73
+
74
+ if array_schema.items.type == "record":
75
+ items.type = "object"
76
+ items.fields = import_record_fields(array_schema.items.fields)
77
+ elif array_schema.items.type == "array":
78
+ items.type = "array"
79
+ items.items = import_avro_array_items(array_schema.items)
80
+ else: # primitive type
81
+ items.type = map_type_from_avro(array_schema.items.type)
82
+
83
+ return items
84
+
85
+
86
+ def import_type_of_optional_field(field):
87
+ for field_type in field.type.schemas:
88
+ if field_type.type != "null":
89
+ return map_type_from_avro(field_type.type)
90
+ raise DataContractException(
91
+ type="schema",
92
+ result="failed",
93
+ name="Map avro type to data contract type",
94
+ reason="Could not import optional field: union type does not contain a non-null type",
95
+ engine="datacontract",
96
+ )
97
+
98
+
99
+ def get_record_from_union_field(field):
100
+ for field_type in field.type.schemas:
101
+ if field_type.type == "record":
102
+ return field_type
103
+ return None
104
+
105
+
53
106
  def map_type_from_avro(avro_type_str: str):
54
107
  # TODO: ambiguous mapping in the export
55
108
  if avro_type_str == "null":
@@ -66,14 +119,8 @@ def map_type_from_avro(avro_type_str: str):
66
119
  return "long"
67
120
  elif avro_type_str == "boolean":
68
121
  return "boolean"
69
- elif avro_type_str == "array":
70
- raise DataContractException(
71
- type="schema",
72
- result="failed",
73
- name="Map avro type to data contract type",
74
- reason=f"Array type not supported",
75
- engine="datacontract",
76
- )
122
+ elif avro_type_str == "record":
123
+ return "record"
77
124
  else:
78
125
  raise DataContractException(
79
126
  type="schema",
@@ -5,12 +5,10 @@ from datacontract.model.data_contract_specification import \
5
5
 
6
6
 
7
7
  def import_sql(data_contract_specification: DataContractSpecification, format: str, source: str):
8
-
9
8
  ddl = parse_from_file(source, group_by_type=True)
10
9
  tables = ddl["tables"]
11
10
 
12
11
  for table in tables:
13
-
14
12
  if data_contract_specification.models is None:
15
13
  data_contract_specification.models = {}
16
14
 
@@ -9,9 +9,9 @@ def download_datacontract_file(file_path: str, from_url: str, overwrite_file: bo
9
9
 
10
10
  with requests.get(from_url) as response:
11
11
  response.raise_for_status()
12
- with open(file_path, 'w') as f:
12
+ with open(file_path, "w") as f:
13
13
  f.write(response.text)
14
14
 
15
15
 
16
16
  class FileExistsException(Exception):
17
- pass
17
+ pass
@@ -1,19 +1,17 @@
1
- import logging
2
1
  import os
3
2
 
4
3
  import requests
5
4
 
6
- from datacontract.model.run import \
7
- Run
5
+ from datacontract.model.run import Run
8
6
 
9
7
 
10
8
  def publish_datamesh_manager(run: Run, publish_url: str):
11
9
  try:
12
10
  if publish_url is None:
13
- url = f"https://api.datamesh-manager.com/api/runs"
11
+ url = "https://api.datamesh-manager.com/api/runs"
14
12
  else:
15
13
  url = publish_url
16
- datamesh_manager_api_key = os.getenv('DATAMESH_MANAGER_API_KEY')
14
+ datamesh_manager_api_key = os.getenv("DATAMESH_MANAGER_API_KEY")
17
15
 
18
16
  if run.dataContractId is None:
19
17
  raise Exception("Cannot publish run results, as data contract ID is unknown")
@@ -21,10 +19,7 @@ def publish_datamesh_manager(run: Run, publish_url: str):
21
19
  if datamesh_manager_api_key is None:
22
20
  raise Exception("Cannot publish run results, as DATAMESH_MANAGER_API_KEY is not set")
23
21
 
24
- headers = {
25
- 'Content-Type': 'application/json',
26
- 'x-api-key': datamesh_manager_api_key
27
- }
22
+ headers = {"Content-Type": "application/json", "x-api-key": datamesh_manager_api_key}
28
23
  request_body = run.model_dump_json()
29
24
  # print("Request Body:", request_body)
30
25
  response = requests.post(url, data=request_body, headers=headers)
@@ -33,7 +28,6 @@ def publish_datamesh_manager(run: Run, publish_url: str):
33
28
  if response.status_code != 200:
34
29
  run.log_error(f"Error publishing test results to Data Mesh Manager: {response.text}")
35
30
  return
36
- logging.info("Published test results to %s", url)
31
+ run.log_info(f"Published test results to {url}")
37
32
  except Exception as e:
38
- logging.error(f"Failed publishing test results. Error: {str(e)}")
39
-
33
+ run.log_error(f"Failed publishing test results. Error: {str(e)}")
@@ -1,21 +1,24 @@
1
1
  import logging
2
+ import math
2
3
  import os
3
4
  from importlib import metadata
4
- from uuid import uuid4
5
- import math
6
5
 
7
- from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
8
- from opentelemetry.metrics import Observation
9
-
10
- from datacontract.model.run import \
11
- Run
12
6
  from opentelemetry import metrics
7
+ from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import \
8
+ OTLPMetricExporter as OTLPgRPCMetricExporter
9
+ from opentelemetry.exporter.otlp.proto.http.metric_exporter import \
10
+ OTLPMetricExporter
11
+ from opentelemetry.metrics import Observation
13
12
  from opentelemetry.sdk.metrics import MeterProvider
14
- from opentelemetry.sdk.metrics.export import ConsoleMetricExporter, PeriodicExportingMetricReader
13
+ from opentelemetry.sdk.metrics.export import ConsoleMetricExporter, \
14
+ PeriodicExportingMetricReader
15
15
 
16
- # Publishes metrics of a test run.
17
- # Metric contains the values:
18
- # 0 == test run passed,
16
+ from datacontract.model.run import Run
17
+
18
+
19
+ # Publishes metrics of a test run.
20
+ # Metric contains the values:
21
+ # 0 == test run passed,
19
22
  # 1 == test run has warnings
20
23
  # 2 == test run failed
21
24
  # 3 == test run not possible due to an error
@@ -26,13 +29,14 @@ from opentelemetry.sdk.metrics.export import ConsoleMetricExporter, PeriodicExpo
26
29
  # OTEL_SERVICE_NAME=datacontract-cli
27
30
  # OTEL_EXPORTER_OTLP_ENDPOINT=https://YOUR_ID.apm.westeurope.azure.elastic-cloud.com:443
28
31
  # OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer%20secret (Optional, when using SaaS Products)
29
- # OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf (Optional, because it is the default value)
32
+ # OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf and OTEL_EXPORTER_OTLP_PROTOCOL=grpc
30
33
  #
31
34
  # Current limitations:
32
35
  # - no gRPC support
33
36
  # - currently, only ConsoleExporter and OTLP Exporter
34
37
  # - Metrics only, no logs yet (but loosely planned)
35
38
 
39
+
36
40
  def publish_opentelemetry(run: Run):
37
41
  try:
38
42
  if run.dataContractId is None:
@@ -48,7 +52,8 @@ def publish_opentelemetry(run: Run):
48
52
  name="datacontract.cli.test",
49
53
  callbacks=[lambda x: _to_observation_callback(run)],
50
54
  unit="result",
51
- description="The overall result of the data contract test run")
55
+ description="The overall result of the data contract test run",
56
+ )
52
57
 
53
58
  telemetry.publish()
54
59
  except Exception as e:
@@ -80,10 +85,19 @@ def _to_observation(run):
80
85
 
81
86
  class Telemetry:
82
87
  def __init__(self):
83
- self.exporter = ConsoleMetricExporter()
84
- self.remote_exporter = OTLPMetricExporter()
88
+ protocol = os.getenv("OTEL_EXPORTER_OTLP_PROTOCOL")
89
+
90
+ # lower to allow grpc, GRPC and alike values.
91
+ if protocol and protocol.lower() == "grpc":
92
+ self.remote_exporter = OTLPgRPCMetricExporter()
93
+ else:
94
+ # Fallback to default OTEL http/protobuf which is used when the variable is not set.
95
+ # This Exporter also works for http/json.
96
+ self.remote_exporter = OTLPMetricExporter()
97
+
98
+ self.console_exporter = ConsoleMetricExporter()
85
99
  # using math.inf so it does not collect periodically. we do this in collect ourselves, one-time.
86
- self.reader = PeriodicExportingMetricReader(self.exporter, export_interval_millis=math.inf)
100
+ self.reader = PeriodicExportingMetricReader(self.console_exporter, export_interval_millis=math.inf)
87
101
  self.remote_reader = PeriodicExportingMetricReader(self.remote_exporter, export_interval_millis=math.inf)
88
102
  provider = MeterProvider(metric_readers=[self.reader, self.remote_reader])
89
103
  metrics.set_meter_provider(provider)
@@ -10,8 +10,8 @@ def read_file(path):
10
10
  name=f"Reading data contract from {path}",
11
11
  reason=f"The file '{path}' does not exist.",
12
12
  engine="datacontract",
13
- result="error"
13
+ result="error",
14
14
  )
15
- with open(path, 'r') as file:
15
+ with open(path, "r") as file:
16
16
  file_content = file.read()
17
17
  return file_content
datacontract/lint/lint.py CHANGED
@@ -1,10 +1,10 @@
1
- from enum import Enum
1
+ import abc
2
2
  from dataclasses import dataclass, field
3
+ from enum import Enum
3
4
  from typing import Sequence, Any, cast
4
- import abc
5
5
 
6
- from ..model.data_contract_specification import DataContractSpecification
7
6
  from datacontract.model.run import Check
7
+ from ..model.data_contract_specification import DataContractSpecification
8
8
 
9
9
  """This module contains linter definitions for linting a data contract.
10
10
 
@@ -17,10 +17,11 @@ contract."""
17
17
 
18
18
  class LintSeverity(Enum):
19
19
  """The severity of a lint message. Generally, lint messages should be
20
- emitted with a severity of ERROR. WARNING should be used when the linter
21
- cannot determine a lint result, for example, when an unsupported model
22
- type is used.
20
+ emitted with a severity of ERROR. WARNING should be used when the linter
21
+ cannot determine a lint result, for example, when an unsupported model
22
+ type is used.
23
23
  """
24
+
24
25
  ERROR = 2
25
26
  WARNING = 1
26
27
 
@@ -36,6 +37,7 @@ class LinterMessage:
36
37
  model: The model that caused the lint to fail. Is optional.
37
38
 
38
39
  """
40
+
39
41
  outcome: LintSeverity
40
42
  message: str
41
43
  model: Any = None
@@ -60,6 +62,7 @@ class LinterResult:
60
62
  results can be present in the list. An empty list means that
61
63
  the linter ran without producing warnings or errors.
62
64
  """
65
+
63
66
  results: Sequence[LinterMessage] = field(default_factory=list)
64
67
 
65
68
  @classmethod
@@ -72,34 +75,29 @@ class LinterResult:
72
75
 
73
76
  def with_warning(self, message, model=None):
74
77
  result = LinterMessage.warning(message, model)
75
- return LinterResult(cast(list[LinterMessage],self.results) + [result])
78
+ return LinterResult(cast(list[LinterMessage], self.results) + [result])
76
79
 
77
80
  def with_error(self, message, model=None):
78
81
  result = LinterMessage.error(message, model)
79
82
  return LinterResult(cast(list[LinterMessage], self.results) + [result])
80
83
 
81
84
  def has_errors(self) -> bool:
82
- return any(map(lambda result: result.outcome == LintSeverity.ERROR,
83
- self.results))
85
+ return any(map(lambda result: result.outcome == LintSeverity.ERROR, self.results))
84
86
 
85
87
  def has_warnings(self) -> bool:
86
- return any(map(lambda result: result.outcome == LintSeverity.WARNING,
87
- self.results))
88
+ return any(map(lambda result: result.outcome == LintSeverity.WARNING, self.results))
88
89
 
89
90
  def error_results(self) -> Sequence[LinterMessage]:
90
- return [result for result in self.results
91
- if result.outcome == LintSeverity.ERROR]
91
+ return [result for result in self.results if result.outcome == LintSeverity.ERROR]
92
92
 
93
93
  def warning_results(self) -> Sequence[LinterMessage]:
94
- return [result for result in self.results
95
- if result.outcome == LintSeverity.WARNING]
94
+ return [result for result in self.results if result.outcome == LintSeverity.WARNING]
96
95
 
97
96
  def no_errors_or_warnings(self) -> bool:
98
97
  return len(self.results) == 0
99
98
 
100
- def combine(self, other: 'LinterResult') -> 'LinterResult':
101
- return LinterResult(cast(list[Any], self.results) +
102
- cast(list[Any], other.results))
99
+ def combine(self, other: "LinterResult") -> "LinterResult":
100
+ return LinterResult(cast(list[Any], self.results) + cast(list[Any], other.results))
103
101
 
104
102
 
105
103
  class Linter(abc.ABC):
@@ -124,23 +122,20 @@ class Linter(abc.ABC):
124
122
  result = self.lint_implementation(contract)
125
123
  checks = []
126
124
  if not result.error_results():
127
- checks.append(Check(
128
- type="lint",
129
- name=f"Linter '{self.name}'",
130
- result="passed",
131
- engine="datacontract"
132
- ))
125
+ checks.append(Check(type="lint", name=f"Linter '{self.name}'", result="passed", engine="datacontract"))
133
126
  else:
134
127
  # All linter messages are treated as warnings. Severity is
135
128
  # currently ignored, but could be used in filtering in the future
136
129
  # Linter messages with level WARNING are currently ignored, but might
137
130
  # be logged or printed in the future.
138
131
  for lint_error in result.error_results():
139
- checks.append(Check(
140
- type="lint",
141
- name=f"Linter '{self.name}'",
142
- result="warning",
143
- engine="datacontract",
144
- reason=lint_error.message
145
- ))
132
+ checks.append(
133
+ Check(
134
+ type="lint",
135
+ name=f"Linter '{self.name}'",
136
+ result="warning",
137
+ engine="datacontract",
138
+ reason=lint_error.message,
139
+ )
140
+ )
146
141
  return checks
@@ -1,6 +1,5 @@
1
+ from datacontract.model.data_contract_specification import DataContractSpecification
1
2
  from ..lint import Linter, LinterResult
2
- from datacontract.model.data_contract_specification import\
3
- DataContractSpecification, Model
4
3
 
5
4
 
6
5
  class DescriptionLinter(Linter):
@@ -14,30 +13,22 @@ class DescriptionLinter(Linter):
14
13
  def id(self) -> str:
15
14
  return "description"
16
15
 
17
- def lint_implementation(
18
- self,
19
- contract: DataContractSpecification
20
- ) -> LinterResult:
16
+ def lint_implementation(self, contract: DataContractSpecification) -> LinterResult:
21
17
  result = LinterResult()
22
18
  if not contract.info or not contract.info.description:
23
- result = result.with_error(
24
- f"Contract has empty description.")
25
- for (model_name, model) in contract.models.items():
19
+ result = result.with_error("Contract has empty description.")
20
+ for model_name, model in contract.models.items():
26
21
  if not model.description:
27
- result = result.with_error(
28
- f"Model '{model_name}' has empty description."
29
- )
30
- for (field_name, field) in model.fields.items():
22
+ result = result.with_error(f"Model '{model_name}' has empty description.")
23
+ for field_name, field in model.fields.items():
31
24
  if not field.description:
32
25
  result = result.with_error(
33
- f"Field '{field_name}' in model '{model_name}'"
34
- f" has empty description.")
35
- for (definition_name, definition) in contract.definitions.items():
26
+ f"Field '{field_name}' in model '{model_name}'" f" has empty description."
27
+ )
28
+ for definition_name, definition in contract.definitions.items():
36
29
  if not definition.description:
37
- result = result.with_error(
38
- f"Definition '{definition_name}' has empty description.")
39
- for (index, example) in enumerate(contract.examples):
30
+ result = result.with_error(f"Definition '{definition_name}' has empty description.")
31
+ for index, example in enumerate(contract.examples):
40
32
  if not example.description:
41
- result = result.with_error(
42
- f"Example {index + 1} has empty description.")
33
+ result = result.with_error(f"Example {index + 1} has empty description.")
43
34
  return result
@@ -1,14 +1,15 @@
1
1
  import csv
2
- import yaml
3
- import json
4
2
  import io
3
+ import json
4
+
5
+ import yaml
5
6
 
7
+ from datacontract.model.data_contract_specification import \
8
+ DataContractSpecification, Example
6
9
  from ..lint import Linter, LinterResult
7
- from datacontract.model.data_contract_specification import DataContractSpecification, Example
8
10
 
9
11
 
10
12
  class ExampleModelLinter(Linter):
11
-
12
13
  @property
13
14
  def name(self) -> str:
14
15
  return "Example(s) match model"
@@ -37,42 +38,39 @@ class ExampleModelLinter(Linter):
37
38
  raise NotImplementedError(f"Unknown type {example.type}")
38
39
  else:
39
40
  # Checked in lint_implementation, shouldn't happen.
40
- raise NotImplementedError("Can't lint object examples.")
41
+ raise NotImplementedError("Can't lint object examples.")
41
42
 
42
- def lint_implementation(
43
- self,
44
- contract: DataContractSpecification
45
- ) -> LinterResult:
43
+ def lint_implementation(self, contract: DataContractSpecification) -> LinterResult:
46
44
  """Check whether the example(s) headers match the model.
47
45
 
48
- This linter checks whether the example's fields match the model
49
- fields, and whether all required fields of the model are present in
50
- the example.
46
+ This linter checks whether the example's fields match the model
47
+ fields, and whether all required fields of the model are present in
48
+ the example.
51
49
  """
52
50
  result = LinterResult()
53
51
  examples = contract.examples
54
52
  models = contract.models
55
53
  examples_with_model = []
56
- for (index, example) in enumerate(examples):
54
+ for index, example in enumerate(examples):
57
55
  if example.model not in models:
58
- result = result.with_error(
59
- f"Example {index + 1} has non-existent model '{example.model}'")
56
+ result = result.with_error(f"Example {index + 1} has non-existent model '{example.model}'")
60
57
  else:
61
- examples_with_model.append(
62
- (index, example, models.get(example.model)))
63
- for (index, example, model) in examples_with_model:
58
+ examples_with_model.append((index, example, models.get(example.model)))
59
+ for index, example, model in examples_with_model:
64
60
  if example.type == "custom":
65
- result = result.with_warning(f"Example {index + 1} has type"
66
- " \"custom\", cannot check model"
67
- " conformance")
61
+ result = result.with_warning(
62
+ f"Example {index + 1} has type" ' "custom", cannot check model' " conformance"
63
+ )
68
64
  elif not isinstance(example.data, str):
69
- result = result.with_warning(f"Example {index + 1} is not a "
70
- "string example, can only lint string examples for now.")
65
+ result = result.with_warning(
66
+ f"Example {index + 1} is not a " "string example, can only lint string examples for now."
67
+ )
71
68
  elif model.type == "object":
72
69
  result = result.with_warning(
73
70
  f"Example {index + 1} uses a "
74
71
  f"model '{example.model}' with type 'object'. Linting is "
75
- "currently only supported for 'table' models")
72
+ "currently only supported for 'table' models"
73
+ )
76
74
  else:
77
75
  if example.type in ("csv", "yaml", "json"):
78
76
  headers = self.get_example_headers(example)
@@ -80,13 +78,14 @@ class ExampleModelLinter(Linter):
80
78
  if example_header not in model.fields:
81
79
  result = result.with_error(
82
80
  f"Example {index + 1} has field '{example_header}'"
83
- f" that's not contained in model '{example.model}'")
84
- for (field_name, field_value) in model.fields.items():
81
+ f" that's not contained in model '{example.model}'"
82
+ )
83
+ for field_name, field_value in model.fields.items():
85
84
  if field_name not in headers and field_value.required:
86
85
  result = result.with_error(
87
86
  f"Example {index + 1} is missing field '{field_name}'"
88
- f" required by model '{example.model}'")
87
+ f" required by model '{example.model}'"
88
+ )
89
89
  else:
90
- result = result.with_error(f"Example {index + 1} has unknown type"
91
- f"{example.type}")
90
+ result = result.with_error(f"Example {index + 1} has unknown type" f"{example.type}")
92
91
  return result
@@ -1,13 +1,16 @@
1
1
  import re
2
2
 
3
+ from datacontract.model.data_contract_specification import \
4
+ DataContractSpecification
3
5
  from ..lint import Linter, LinterResult
4
- from datacontract.model.data_contract_specification import DataContractSpecification
6
+
5
7
 
6
8
  class FieldPatternLinter(Linter):
7
9
  """Checks that all patterns defined for fields are correct Python regex
8
- syntax.
10
+ syntax.
9
11
 
10
12
  """
13
+
11
14
  @property
12
15
  def name(self):
13
16
  return "Field pattern is correct regex"
@@ -16,13 +19,10 @@ class FieldPatternLinter(Linter):
16
19
  def id(self) -> str:
17
20
  return "field-pattern"
18
21
 
19
- def lint_implementation(
20
- self,
21
- contract: DataContractSpecification
22
- ) -> LinterResult:
22
+ def lint_implementation(self, contract: DataContractSpecification) -> LinterResult:
23
23
  result = LinterResult()
24
- for (model_name, model) in contract.models.items():
25
- for (field_name, field) in model.fields.items():
24
+ for model_name, model in contract.models.items():
25
+ for field_name, field in model.fields.items():
26
26
  if field.pattern:
27
27
  try:
28
28
  re.compile(field.pattern)
@@ -1,11 +1,13 @@
1
- from ..lint import Linter, LinterResult
2
1
  from datacontract.model.data_contract_specification import DataContractSpecification
2
+ from ..lint import Linter, LinterResult
3
+
3
4
 
4
5
  class FieldReferenceLinter(Linter):
5
6
  """Checks that all references definitions in fields refer to existing
6
- fields.
7
+ fields.
7
8
 
8
9
  """
10
+
9
11
  @property
10
12
  def name(self):
11
13
  return "Field references existing field"
@@ -14,24 +16,23 @@ class FieldReferenceLinter(Linter):
14
16
  def id(self) -> str:
15
17
  return "field-reference"
16
18
 
17
- def lint_implementation(
18
- self,
19
- contract: DataContractSpecification
20
- ) -> LinterResult:
19
+ def lint_implementation(self, contract: DataContractSpecification) -> LinterResult:
21
20
  result = LinterResult()
22
- for (model_name, model) in contract.models.items():
23
- for (field_name, field) in model.fields.items():
21
+ for model_name, model in contract.models.items():
22
+ for field_name, field in model.fields.items():
24
23
  if field.references:
25
24
  (ref_model, ref_field) = field.references.split(".", maxsplit=2)
26
25
  if ref_model not in contract.models:
27
26
  result = result.with_error(
28
27
  f"Field '{field_name}' in model '{model_name}'"
29
- f" references non-existing model '{ref_model}'.")
28
+ f" references non-existing model '{ref_model}'."
29
+ )
30
30
  else:
31
31
  ref_model_obj = contract.models[ref_model]
32
32
  if ref_field not in ref_model_obj.fields:
33
33
  result = result.with_error(
34
34
  f"Field '{field_name}' in model '{model_name}'"
35
35
  f" references non-existing field '{ref_field}'"
36
- f" in model '{ref_model}'.")
36
+ f" in model '{ref_model}'."
37
+ )
37
38
  return result