datacontract-cli 0.10.0__py3-none-any.whl → 0.10.37__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.
Files changed (136) hide show
  1. datacontract/__init__.py +13 -0
  2. datacontract/api.py +260 -0
  3. datacontract/breaking/breaking.py +242 -12
  4. datacontract/breaking/breaking_rules.py +37 -1
  5. datacontract/catalog/catalog.py +80 -0
  6. datacontract/cli.py +387 -117
  7. datacontract/data_contract.py +216 -353
  8. datacontract/engines/data_contract_checks.py +1041 -0
  9. datacontract/engines/data_contract_test.py +113 -0
  10. datacontract/engines/datacontract/check_that_datacontract_contains_valid_servers_configuration.py +2 -3
  11. datacontract/engines/datacontract/check_that_datacontract_file_exists.py +1 -1
  12. datacontract/engines/fastjsonschema/check_jsonschema.py +176 -42
  13. datacontract/engines/fastjsonschema/s3/s3_read_files.py +16 -1
  14. datacontract/engines/soda/check_soda_execute.py +100 -56
  15. datacontract/engines/soda/connections/athena.py +79 -0
  16. datacontract/engines/soda/connections/bigquery.py +8 -1
  17. datacontract/engines/soda/connections/databricks.py +12 -3
  18. datacontract/engines/soda/connections/duckdb_connection.py +241 -0
  19. datacontract/engines/soda/connections/kafka.py +206 -113
  20. datacontract/engines/soda/connections/snowflake.py +8 -5
  21. datacontract/engines/soda/connections/sqlserver.py +43 -0
  22. datacontract/engines/soda/connections/trino.py +26 -0
  23. datacontract/export/avro_converter.py +72 -8
  24. datacontract/export/avro_idl_converter.py +31 -25
  25. datacontract/export/bigquery_converter.py +130 -0
  26. datacontract/export/custom_converter.py +40 -0
  27. datacontract/export/data_caterer_converter.py +161 -0
  28. datacontract/export/dbml_converter.py +148 -0
  29. datacontract/export/dbt_converter.py +141 -54
  30. datacontract/export/dcs_exporter.py +6 -0
  31. datacontract/export/dqx_converter.py +126 -0
  32. datacontract/export/duckdb_type_converter.py +57 -0
  33. datacontract/export/excel_exporter.py +923 -0
  34. datacontract/export/exporter.py +100 -0
  35. datacontract/export/exporter_factory.py +216 -0
  36. datacontract/export/go_converter.py +105 -0
  37. datacontract/export/great_expectations_converter.py +257 -36
  38. datacontract/export/html_exporter.py +86 -0
  39. datacontract/export/iceberg_converter.py +188 -0
  40. datacontract/export/jsonschema_converter.py +71 -16
  41. datacontract/export/markdown_converter.py +337 -0
  42. datacontract/export/mermaid_exporter.py +110 -0
  43. datacontract/export/odcs_v3_exporter.py +375 -0
  44. datacontract/export/pandas_type_converter.py +40 -0
  45. datacontract/export/protobuf_converter.py +168 -68
  46. datacontract/export/pydantic_converter.py +6 -0
  47. datacontract/export/rdf_converter.py +13 -6
  48. datacontract/export/sodacl_converter.py +36 -188
  49. datacontract/export/spark_converter.py +245 -0
  50. datacontract/export/sql_converter.py +37 -3
  51. datacontract/export/sql_type_converter.py +269 -8
  52. datacontract/export/sqlalchemy_converter.py +170 -0
  53. datacontract/export/terraform_converter.py +7 -2
  54. datacontract/imports/avro_importer.py +246 -26
  55. datacontract/imports/bigquery_importer.py +221 -0
  56. datacontract/imports/csv_importer.py +143 -0
  57. datacontract/imports/dbml_importer.py +112 -0
  58. datacontract/imports/dbt_importer.py +240 -0
  59. datacontract/imports/excel_importer.py +1111 -0
  60. datacontract/imports/glue_importer.py +288 -0
  61. datacontract/imports/iceberg_importer.py +172 -0
  62. datacontract/imports/importer.py +51 -0
  63. datacontract/imports/importer_factory.py +128 -0
  64. datacontract/imports/json_importer.py +325 -0
  65. datacontract/imports/jsonschema_importer.py +146 -0
  66. datacontract/imports/odcs_importer.py +60 -0
  67. datacontract/imports/odcs_v3_importer.py +516 -0
  68. datacontract/imports/parquet_importer.py +81 -0
  69. datacontract/imports/protobuf_importer.py +264 -0
  70. datacontract/imports/spark_importer.py +262 -0
  71. datacontract/imports/sql_importer.py +274 -35
  72. datacontract/imports/unity_importer.py +219 -0
  73. datacontract/init/init_template.py +20 -0
  74. datacontract/integration/datamesh_manager.py +86 -0
  75. datacontract/lint/resolve.py +271 -49
  76. datacontract/lint/resources.py +21 -0
  77. datacontract/lint/schema.py +53 -17
  78. datacontract/lint/urls.py +32 -12
  79. datacontract/model/data_contract_specification/__init__.py +1 -0
  80. datacontract/model/exceptions.py +4 -1
  81. datacontract/model/odcs.py +24 -0
  82. datacontract/model/run.py +49 -29
  83. datacontract/output/__init__.py +0 -0
  84. datacontract/output/junit_test_results.py +135 -0
  85. datacontract/output/output_format.py +10 -0
  86. datacontract/output/test_results_writer.py +79 -0
  87. datacontract/py.typed +0 -0
  88. datacontract/schemas/datacontract-1.1.0.init.yaml +91 -0
  89. datacontract/schemas/datacontract-1.1.0.schema.json +1975 -0
  90. datacontract/schemas/datacontract-1.2.0.init.yaml +91 -0
  91. datacontract/schemas/datacontract-1.2.0.schema.json +2029 -0
  92. datacontract/schemas/datacontract-1.2.1.init.yaml +91 -0
  93. datacontract/schemas/datacontract-1.2.1.schema.json +2058 -0
  94. datacontract/schemas/odcs-3.0.1.schema.json +2634 -0
  95. datacontract/schemas/odcs-3.0.2.schema.json +2382 -0
  96. datacontract/templates/datacontract.html +139 -294
  97. datacontract/templates/datacontract_odcs.html +685 -0
  98. datacontract/templates/index.html +236 -0
  99. datacontract/templates/partials/datacontract_information.html +86 -0
  100. datacontract/templates/partials/datacontract_servicelevels.html +253 -0
  101. datacontract/templates/partials/datacontract_terms.html +51 -0
  102. datacontract/templates/partials/definition.html +25 -0
  103. datacontract/templates/partials/example.html +27 -0
  104. datacontract/templates/partials/model_field.html +144 -0
  105. datacontract/templates/partials/quality.html +49 -0
  106. datacontract/templates/partials/server.html +211 -0
  107. datacontract/templates/style/output.css +491 -72
  108. datacontract_cli-0.10.37.dist-info/METADATA +2235 -0
  109. datacontract_cli-0.10.37.dist-info/RECORD +119 -0
  110. {datacontract_cli-0.10.0.dist-info → datacontract_cli-0.10.37.dist-info}/WHEEL +1 -1
  111. {datacontract_cli-0.10.0.dist-info → datacontract_cli-0.10.37.dist-info/licenses}/LICENSE +1 -1
  112. datacontract/engines/datacontract/check_that_datacontract_str_is_valid.py +0 -48
  113. datacontract/engines/soda/connections/dask.py +0 -28
  114. datacontract/engines/soda/connections/duckdb.py +0 -76
  115. datacontract/export/csv_type_converter.py +0 -36
  116. datacontract/export/html_export.py +0 -66
  117. datacontract/export/odcs_converter.py +0 -102
  118. datacontract/init/download_datacontract_file.py +0 -17
  119. datacontract/integration/publish_datamesh_manager.py +0 -33
  120. datacontract/integration/publish_opentelemetry.py +0 -107
  121. datacontract/lint/lint.py +0 -141
  122. datacontract/lint/linters/description_linter.py +0 -34
  123. datacontract/lint/linters/example_model_linter.py +0 -91
  124. datacontract/lint/linters/field_pattern_linter.py +0 -34
  125. datacontract/lint/linters/field_reference_linter.py +0 -38
  126. datacontract/lint/linters/notice_period_linter.py +0 -55
  127. datacontract/lint/linters/quality_schema_linter.py +0 -52
  128. datacontract/lint/linters/valid_constraints_linter.py +0 -99
  129. datacontract/model/data_contract_specification.py +0 -141
  130. datacontract/web.py +0 -14
  131. datacontract_cli-0.10.0.dist-info/METADATA +0 -951
  132. datacontract_cli-0.10.0.dist-info/RECORD +0 -66
  133. /datacontract/{model → breaking}/breaking_change.py +0 -0
  134. /datacontract/{lint/linters → export}/__init__.py +0 -0
  135. {datacontract_cli-0.10.0.dist-info → datacontract_cli-0.10.37.dist-info}/entry_points.txt +0 -0
  136. {datacontract_cli-0.10.0.dist-info → datacontract_cli-0.10.37.dist-info}/top_level.txt +0 -0
datacontract/lint/lint.py DELETED
@@ -1,141 +0,0 @@
1
- import abc
2
- from dataclasses import dataclass, field
3
- from enum import Enum
4
- from typing import Sequence, Any, cast
5
-
6
- from datacontract.model.run import Check
7
- from ..model.data_contract_specification import DataContractSpecification
8
-
9
- """This module contains linter definitions for linting a data contract.
10
-
11
- Lints are quality checks that can succeed, fail, or warn. They are
12
- distinct from checks such as "valid yaml" or "file not found", which
13
- will cause the processing of the data contract to stop. Lints can be
14
- ignored, and are high-level requirements on the format of a data
15
- contract."""
16
-
17
-
18
- class LintSeverity(Enum):
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.
23
- """
24
-
25
- ERROR = 2
26
- WARNING = 1
27
-
28
-
29
- @dataclass
30
- class LinterMessage:
31
- """A single linter message with attached severity and optional "model" that
32
- caused the message.
33
-
34
- Attributes:
35
- outcome: The outcome of the linting, either ERROR or WARNING. Linting outcomes with level WARNING are discarded for now.
36
- message: A message describing the error or warning in more detail.
37
- model: The model that caused the lint to fail. Is optional.
38
-
39
- """
40
-
41
- outcome: LintSeverity
42
- message: str
43
- model: Any = None
44
-
45
- @classmethod
46
- def error(cls, message: str, model=None):
47
- return LinterMessage(LintSeverity.ERROR, message, model)
48
-
49
- @classmethod
50
- def warning(cls, message: str, model=None):
51
- return LinterMessage(LintSeverity.WARNING, message, model)
52
-
53
-
54
- @dataclass
55
- class LinterResult:
56
- """Result of linting a contract. Contains multiple LinterResults from
57
- the same linter or lint phase.
58
-
59
- Attributes:
60
- linter: The linter that produced these results
61
- results: A list of linting results. Multiple identical linting
62
- results can be present in the list. An empty list means that
63
- the linter ran without producing warnings or errors.
64
- """
65
-
66
- results: Sequence[LinterMessage] = field(default_factory=list)
67
-
68
- @classmethod
69
- def erroneous(cls, message, model=None):
70
- return cls([LinterMessage.error(message, model)])
71
-
72
- @classmethod
73
- def cautious(cls, message, model=None):
74
- return cls([LinterMessage.warning(message, model)])
75
-
76
- def with_warning(self, message, model=None):
77
- result = LinterMessage.warning(message, model)
78
- return LinterResult(cast(list[LinterMessage], self.results) + [result])
79
-
80
- def with_error(self, message, model=None):
81
- result = LinterMessage.error(message, model)
82
- return LinterResult(cast(list[LinterMessage], self.results) + [result])
83
-
84
- def has_errors(self) -> bool:
85
- return any(map(lambda result: result.outcome == LintSeverity.ERROR, self.results))
86
-
87
- def has_warnings(self) -> bool:
88
- return any(map(lambda result: result.outcome == LintSeverity.WARNING, self.results))
89
-
90
- def error_results(self) -> Sequence[LinterMessage]:
91
- return [result for result in self.results if result.outcome == LintSeverity.ERROR]
92
-
93
- def warning_results(self) -> Sequence[LinterMessage]:
94
- return [result for result in self.results if result.outcome == LintSeverity.WARNING]
95
-
96
- def no_errors_or_warnings(self) -> bool:
97
- return len(self.results) == 0
98
-
99
- def combine(self, other: "LinterResult") -> "LinterResult":
100
- return LinterResult(cast(list[Any], self.results) + cast(list[Any], other.results))
101
-
102
-
103
- class Linter(abc.ABC):
104
- @property
105
- @abc.abstractmethod
106
- def name(self) -> str:
107
- """Human-readable name of the linter."""
108
- pass
109
-
110
- @property
111
- @abc.abstractmethod
112
- def id(self) -> str:
113
- """A linter ID for configuration (i.e. enabling and disabling)."""
114
- pass
115
-
116
- @abc.abstractmethod
117
- def lint_implementation(self, contract: DataContractSpecification) -> LinterResult:
118
- pass
119
-
120
- def lint(self, contract: DataContractSpecification) -> list[Check]:
121
- """Call with a data contract to get a list of check results from the linter."""
122
- result = self.lint_implementation(contract)
123
- checks = []
124
- if not result.error_results():
125
- checks.append(Check(type="lint", name=f"Linter '{self.name}'", result="passed", engine="datacontract"))
126
- else:
127
- # All linter messages are treated as warnings. Severity is
128
- # currently ignored, but could be used in filtering in the future
129
- # Linter messages with level WARNING are currently ignored, but might
130
- # be logged or printed in the future.
131
- for lint_error in result.error_results():
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
- )
141
- return checks
@@ -1,34 +0,0 @@
1
- from datacontract.model.data_contract_specification import DataContractSpecification
2
- from ..lint import Linter, LinterResult
3
-
4
-
5
- class DescriptionLinter(Linter):
6
- """Check for a description on contracts, models, model fields, definitions and examples."""
7
-
8
- @property
9
- def name(self) -> str:
10
- return "Objects have descriptions"
11
-
12
- @property
13
- def id(self) -> str:
14
- return "description"
15
-
16
- def lint_implementation(self, contract: DataContractSpecification) -> LinterResult:
17
- result = LinterResult()
18
- if not contract.info or not contract.info.description:
19
- result = result.with_error("Contract has empty description.")
20
- for model_name, model in contract.models.items():
21
- if not model.description:
22
- result = result.with_error(f"Model '{model_name}' has empty description.")
23
- for field_name, field in model.fields.items():
24
- if not field.description:
25
- result = result.with_error(
26
- f"Field '{field_name}' in model '{model_name}'" f" has empty description."
27
- )
28
- for definition_name, definition in contract.definitions.items():
29
- if not definition.description:
30
- result = result.with_error(f"Definition '{definition_name}' has empty description.")
31
- for index, example in enumerate(contract.examples):
32
- if not example.description:
33
- result = result.with_error(f"Example {index + 1} has empty description.")
34
- return result
@@ -1,91 +0,0 @@
1
- import csv
2
- import io
3
- import json
4
-
5
- import yaml
6
-
7
- from datacontract.model.data_contract_specification import \
8
- DataContractSpecification, Example
9
- from ..lint import Linter, LinterResult
10
-
11
-
12
- class ExampleModelLinter(Linter):
13
- @property
14
- def name(self) -> str:
15
- return "Example(s) match model"
16
-
17
- @property
18
- def id(self) -> str:
19
- return "example-model"
20
-
21
- @staticmethod
22
- def get_example_headers(example: Example) -> list[str]:
23
- if isinstance(example.data, str):
24
- match example.type:
25
- case "csv":
26
- dialect = csv.Sniffer().sniff(example.data)
27
- data = io.StringIO(example.data)
28
- reader = csv.reader(data, dialect=dialect)
29
- return next(reader)
30
- case "yaml":
31
- data = yaml.safe_load(example.data)
32
- return data.keys()
33
- case "json":
34
- data = json.loads(example.data)
35
- return data.keys()
36
- case _:
37
- # This is checked in lint_implementation, so shouldn't happen.
38
- raise NotImplementedError(f"Unknown type {example.type}")
39
- else:
40
- # Checked in lint_implementation, shouldn't happen.
41
- raise NotImplementedError("Can't lint object examples.")
42
-
43
- def lint_implementation(self, contract: DataContractSpecification) -> LinterResult:
44
- """Check whether the example(s) headers match the model.
45
-
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.
49
- """
50
- result = LinterResult()
51
- examples = contract.examples
52
- models = contract.models
53
- examples_with_model = []
54
- for index, example in enumerate(examples):
55
- if example.model not in models:
56
- result = result.with_error(f"Example {index + 1} has non-existent model '{example.model}'")
57
- else:
58
- examples_with_model.append((index, example, models.get(example.model)))
59
- for index, example, model in examples_with_model:
60
- if example.type == "custom":
61
- result = result.with_warning(
62
- f"Example {index + 1} has type" ' "custom", cannot check model' " conformance"
63
- )
64
- elif not isinstance(example.data, str):
65
- result = result.with_warning(
66
- f"Example {index + 1} is not a " "string example, can only lint string examples for now."
67
- )
68
- elif model.type == "object":
69
- result = result.with_warning(
70
- f"Example {index + 1} uses a "
71
- f"model '{example.model}' with type 'object'. Linting is "
72
- "currently only supported for 'table' models"
73
- )
74
- else:
75
- if example.type in ("csv", "yaml", "json"):
76
- headers = self.get_example_headers(example)
77
- for example_header in headers:
78
- if example_header not in model.fields:
79
- result = result.with_error(
80
- f"Example {index + 1} has field '{example_header}'"
81
- f" that's not contained in model '{example.model}'"
82
- )
83
- for field_name, field_value in model.fields.items():
84
- if field_name not in headers and field_value.required:
85
- result = result.with_error(
86
- f"Example {index + 1} is missing field '{field_name}'"
87
- f" required by model '{example.model}'"
88
- )
89
- else:
90
- result = result.with_error(f"Example {index + 1} has unknown type" f"{example.type}")
91
- return result
@@ -1,34 +0,0 @@
1
- import re
2
-
3
- from datacontract.model.data_contract_specification import \
4
- DataContractSpecification
5
- from ..lint import Linter, LinterResult
6
-
7
-
8
- class FieldPatternLinter(Linter):
9
- """Checks that all patterns defined for fields are correct Python regex
10
- syntax.
11
-
12
- """
13
-
14
- @property
15
- def name(self):
16
- return "Field pattern is correct regex"
17
-
18
- @property
19
- def id(self) -> str:
20
- return "field-pattern"
21
-
22
- def lint_implementation(self, contract: DataContractSpecification) -> LinterResult:
23
- result = LinterResult()
24
- for model_name, model in contract.models.items():
25
- for field_name, field in model.fields.items():
26
- if field.pattern:
27
- try:
28
- re.compile(field.pattern)
29
- except re.error as e:
30
- result = result.with_error(
31
- f"Failed to compile pattern regex '{field.pattern}' for "
32
- f"field '{field_name}' in model '{model_name}': {e.msg}"
33
- )
34
- return result
@@ -1,38 +0,0 @@
1
- from datacontract.model.data_contract_specification import DataContractSpecification
2
- from ..lint import Linter, LinterResult
3
-
4
-
5
- class FieldReferenceLinter(Linter):
6
- """Checks that all references definitions in fields refer to existing
7
- fields.
8
-
9
- """
10
-
11
- @property
12
- def name(self):
13
- return "Field references existing field"
14
-
15
- @property
16
- def id(self) -> str:
17
- return "field-reference"
18
-
19
- def lint_implementation(self, contract: DataContractSpecification) -> LinterResult:
20
- result = LinterResult()
21
- for model_name, model in contract.models.items():
22
- for field_name, field in model.fields.items():
23
- if field.references:
24
- (ref_model, ref_field) = field.references.split(".", maxsplit=2)
25
- if ref_model not in contract.models:
26
- result = result.with_error(
27
- f"Field '{field_name}' in model '{model_name}'"
28
- f" references non-existing model '{ref_model}'."
29
- )
30
- else:
31
- ref_model_obj = contract.models[ref_model]
32
- if ref_field not in ref_model_obj.fields:
33
- result = result.with_error(
34
- f"Field '{field_name}' in model '{model_name}'"
35
- f" references non-existing field '{ref_field}'"
36
- f" in model '{ref_model}'."
37
- )
38
- return result
@@ -1,55 +0,0 @@
1
- import re
2
-
3
- from datacontract.model.data_contract_specification import \
4
- DataContractSpecification
5
- from ..lint import Linter, LinterResult
6
-
7
-
8
- class NoticePeriodLinter(Linter):
9
- @property
10
- def name(self) -> str:
11
- return "noticePeriod in ISO8601 format"
12
-
13
- @property
14
- def id(self) -> str:
15
- return "notice-period"
16
-
17
- # Regex matching the "simple" ISO8601 duration format
18
- simple = re.compile(
19
- r"""P # Introduces period
20
- (:?[0-9\.,]+Y)? # Number of years
21
- (:?[0-9\.,]+M)? # Number of months
22
- (:?[0-9\.,]+W)? # Number of weeks
23
- (:?[0-9\.,]+D)? # Number of days
24
- (:? # Time part (optional)
25
- T # Always starts with T
26
- (:?[0-9\.,]+H)? # Number of hours
27
- (:?[0-9\.,]+M)? # Number of minutes
28
- (:?[0-9\.,]+S)? # Number of seconds
29
- )?
30
- """,
31
- re.VERBOSE,
32
- )
33
- datetime_basic = re.compile(r"P\d{8}T\d{6}")
34
- datetime_extended = re.compile(r"P\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}")
35
-
36
- def lint_implementation(self, contract: DataContractSpecification) -> LinterResult:
37
- """Check whether the notice period is specified using ISO8601 duration syntax."""
38
- if not contract.terms:
39
- return LinterResult.cautious("No terms defined.")
40
- period = contract.terms.noticePeriod
41
- if not period:
42
- return LinterResult.cautious("No notice period defined.")
43
- if not period.startswith("P"):
44
- return LinterResult.erroneous(f"Notice period '{period}' is not a valid" "ISO8601 duration.")
45
- if period == "P":
46
- return LinterResult.erroneous(
47
- "Notice period 'P' is not a valid" "ISO8601 duration, requires at least one" "duration to be specified."
48
- )
49
- if (
50
- not self.simple.fullmatch(period)
51
- and not self.datetime_basic.fullmatch(period)
52
- and not self.datetime_extended.fullmatch(period)
53
- ):
54
- return LinterResult.erroneous(f"Notice period '{period}' is not a valid ISO8601 duration.")
55
- return LinterResult()
@@ -1,52 +0,0 @@
1
- import yaml
2
-
3
- from datacontract.model.data_contract_specification import \
4
- DataContractSpecification, Model
5
- from ..lint import Linter, LinterResult
6
-
7
-
8
- class QualityUsesSchemaLinter(Linter):
9
- @property
10
- def name(self) -> str:
11
- return "Quality check(s) use model"
12
-
13
- @property
14
- def id(self) -> str:
15
- return "quality-schema"
16
-
17
- def lint_sodacl(self, check, models: dict[str, Model]) -> LinterResult:
18
- result = LinterResult()
19
- for sodacl_check in check.keys():
20
- table_name = sodacl_check[len("checks for ") :]
21
- if table_name not in models:
22
- result = result.with_error(f"Quality check on unknown model '{table_name}'")
23
- return result
24
-
25
- def lint_montecarlo(self, check, models: dict[str, Model]) -> LinterResult:
26
- return LinterResult().with_warning("Linting montecarlo checks is not currently implemented")
27
-
28
- def lint_great_expectations(self, check, models: dict[str, Model]) -> LinterResult:
29
- return LinterResult().with_warning("Linting great expectations checks is not currently implemented")
30
-
31
- def lint_implementation(self, contract: DataContractSpecification) -> LinterResult:
32
- result = LinterResult()
33
- models = contract.models
34
- check = contract.quality
35
- if not check:
36
- return LinterResult()
37
- if not check.specification:
38
- return LinterResult.cautious("Quality check without specification.")
39
- if isinstance(check.specification, str):
40
- check_specification = yaml.safe_load(check.specification)
41
- else:
42
- check_specification = check.specification
43
- match check.type:
44
- case "SodaCL":
45
- result = result.combine(self.lint_sodacl(check_specification, models))
46
- case "montecarlo":
47
- result = result.combine(self.lint_montecarlo(check_specification, models))
48
- case "great-expectations":
49
- result = result.combine(self.lint_great_expectations(check_specification, models))
50
- case _:
51
- result = result.with_warning("Can't lint quality check " f"with type '{check.type}'")
52
- return result
@@ -1,99 +0,0 @@
1
- from datacontract.model.data_contract_specification import DataContractSpecification, Field
2
- from ..lint import Linter, LinterResult
3
-
4
-
5
- class ValidFieldConstraintsLinter(Linter):
6
- """Check validity of field constraints.
7
-
8
- More precisely, check that only numeric constraints are specified on
9
- fields of numeric type and string constraints on fields of string type.
10
- Additionally, the linter checks that defined constraints make sense.
11
- Minimum values should not be greater than maximum values, exclusive and
12
- non-exclusive minimum and maximum should not be combined and string
13
- pattern and format should not be combined.
14
-
15
- """
16
-
17
- valid_types_for_constraint = {
18
- "pattern": set(["string", "text", "varchar"]),
19
- "format": set(["string", "text", "varchar"]),
20
- "minLength": set(["string", "text", "varchar"]),
21
- "maxLength": set(["string", "text", "varchar"]),
22
- "minimum": set(["int", "integer", "number", "decimal", "numeric", "long", "bigint", "float", "double"]),
23
- "exclusiveMinimum": set(
24
- ["int", "integer", "number", "decimal", "numeric", "long", "bigint", "float", "double"]
25
- ),
26
- "maximum": set(["int", "integer", "number", "decimal", "numeric", "long", "bigint", "float", "double"]),
27
- "exclusiveMaximum": set(
28
- ["int", "integer", "number", "decimal", "numeric", "long", "bigint", "float", "double"]
29
- ),
30
- }
31
-
32
- def check_minimum_maximum(self, field: Field, field_name: str, model_name: str) -> LinterResult:
33
- (min, max, xmin, xmax) = (field.minimum, field.maximum, field.exclusiveMinimum, field.exclusiveMaximum)
34
- match (
35
- "minimum" in field.model_fields_set,
36
- "maximum" in field.model_fields_set,
37
- "exclusiveMinimum" in field.model_fields_set,
38
- "exclusiveMaximum" in field.model_fields_set,
39
- ):
40
- case (True, True, _, _) if min > max:
41
- return LinterResult.erroneous(
42
- f"Minimum {min} is greater than maximum {max} on " f"field '{field_name}' in model '{model_name}'."
43
- )
44
- case (_, _, True, True) if xmin >= xmax:
45
- return LinterResult.erroneous(
46
- f"Exclusive minimum {xmin} is greater than exclusive"
47
- f" maximum {xmax} on field '{field_name}' in model '{model_name}'."
48
- )
49
- case (True, True, True, True):
50
- return LinterResult.erroneous(
51
- f"Both exclusive and non-exclusive minimum and maximum are "
52
- f"defined on field '{field_name}' in model '{model_name}'."
53
- )
54
- case (True, _, True, _):
55
- return LinterResult.erroneous(
56
- f"Both exclusive and non-exclusive minimum are "
57
- f"defined on field '{field_name}' in model '{model_name}'."
58
- )
59
- case (_, True, _, True):
60
- return LinterResult.erroneous(
61
- f"Both exclusive and non-exclusive maximum are "
62
- f"defined on field '{field_name}' in model '{model_name}'."
63
- )
64
- return LinterResult()
65
-
66
- def check_string_constraints(self, field: Field, field_name: str, model_name: str) -> LinterResult:
67
- result = LinterResult()
68
- if field.minLength and field.maxLength and field.minLength > field.maxLength:
69
- result = result.with_error(
70
- f"Minimum length is greater that maximum length on" f" field '{field_name}' in model '{model_name}'."
71
- )
72
- if field.pattern and field.format:
73
- result = result.with_error(
74
- f"Both a pattern and a format are defined for field" f" '{field_name}' in model '{model_name}'."
75
- )
76
- return result
77
-
78
- @property
79
- def name(self):
80
- return "Fields use valid constraints"
81
-
82
- @property
83
- def id(self):
84
- return "field-constraints"
85
-
86
- def lint_implementation(self, contract: DataContractSpecification) -> LinterResult:
87
- result = LinterResult()
88
- for model_name, model in contract.models.items():
89
- for field_name, field in model.fields.items():
90
- for _property, allowed_types in self.valid_types_for_constraint.items():
91
- if _property in field.model_fields_set and field.type not in allowed_types:
92
- result = result.with_error(
93
- f"Forbidden constraint '{_property}' defined on field "
94
- f"'{field_name}' in model '{model_name}'. Field type "
95
- f"is '{field.type}'."
96
- )
97
- result = result.combine(self.check_minimum_maximum(field, field_name, model_name))
98
- result = result.combine(self.check_string_constraints(field, field_name, model_name))
99
- return result