datacontract-cli 0.10.7__py3-none-any.whl → 0.10.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.
- datacontract/catalog/catalog.py +4 -2
- datacontract/cli.py +44 -15
- datacontract/data_contract.py +52 -206
- datacontract/engines/fastjsonschema/s3/s3_read_files.py +13 -1
- datacontract/engines/soda/check_soda_execute.py +9 -2
- datacontract/engines/soda/connections/bigquery.py +8 -1
- datacontract/engines/soda/connections/duckdb.py +28 -12
- datacontract/engines/soda/connections/trino.py +26 -0
- datacontract/export/__init__.py +0 -0
- datacontract/export/avro_converter.py +15 -3
- datacontract/export/avro_idl_converter.py +29 -22
- datacontract/export/bigquery_converter.py +15 -0
- datacontract/export/dbml_converter.py +9 -0
- datacontract/export/dbt_converter.py +26 -1
- datacontract/export/exporter.py +88 -0
- datacontract/export/exporter_factory.py +145 -0
- datacontract/export/go_converter.py +6 -0
- datacontract/export/great_expectations_converter.py +10 -0
- datacontract/export/html_export.py +6 -0
- datacontract/export/jsonschema_converter.py +31 -23
- datacontract/export/odcs_converter.py +24 -1
- datacontract/export/protobuf_converter.py +6 -0
- datacontract/export/pydantic_converter.py +6 -0
- datacontract/export/rdf_converter.py +9 -0
- datacontract/export/sodacl_converter.py +23 -12
- datacontract/export/spark_converter.py +211 -0
- datacontract/export/sql_converter.py +32 -2
- datacontract/export/sql_type_converter.py +32 -5
- datacontract/export/terraform_converter.py +6 -0
- datacontract/imports/avro_importer.py +8 -0
- datacontract/imports/bigquery_importer.py +47 -4
- datacontract/imports/glue_importer.py +122 -30
- datacontract/imports/importer.py +29 -0
- datacontract/imports/importer_factory.py +72 -0
- datacontract/imports/jsonschema_importer.py +8 -0
- datacontract/imports/odcs_importer.py +200 -0
- datacontract/imports/sql_importer.py +8 -0
- datacontract/imports/unity_importer.py +152 -0
- datacontract/lint/resolve.py +22 -1
- datacontract/model/data_contract_specification.py +36 -4
- datacontract/templates/datacontract.html +17 -2
- datacontract/templates/partials/datacontract_information.html +20 -0
- datacontract/templates/partials/datacontract_terms.html +7 -0
- datacontract/templates/partials/definition.html +9 -1
- datacontract/templates/partials/model_field.html +23 -6
- datacontract/templates/partials/server.html +113 -48
- datacontract/templates/style/output.css +51 -0
- datacontract/web.py +17 -0
- {datacontract_cli-0.10.7.dist-info → datacontract_cli-0.10.9.dist-info}/METADATA +298 -59
- datacontract_cli-0.10.9.dist-info/RECORD +93 -0
- {datacontract_cli-0.10.7.dist-info → datacontract_cli-0.10.9.dist-info}/WHEEL +1 -1
- datacontract_cli-0.10.7.dist-info/RECORD +0 -84
- {datacontract_cli-0.10.7.dist-info → datacontract_cli-0.10.9.dist-info}/LICENSE +0 -0
- {datacontract_cli-0.10.7.dist-info → datacontract_cli-0.10.9.dist-info}/entry_points.txt +0 -0
- {datacontract_cli-0.10.7.dist-info → datacontract_cli-0.10.9.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import boto3
|
|
2
2
|
from typing import List
|
|
3
3
|
|
|
4
|
+
from datacontract.imports.importer import Importer
|
|
4
5
|
from datacontract.model.data_contract_specification import (
|
|
5
6
|
DataContractSpecification,
|
|
6
7
|
Model,
|
|
@@ -9,7 +10,14 @@ from datacontract.model.data_contract_specification import (
|
|
|
9
10
|
)
|
|
10
11
|
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
class GlueImporter(Importer):
|
|
14
|
+
def import_source(
|
|
15
|
+
self, data_contract_specification: DataContractSpecification, source: str, import_args: dict
|
|
16
|
+
) -> dict:
|
|
17
|
+
return import_glue(data_contract_specification, source, import_args.get("glue_tables"))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_glue_database(database_name: str):
|
|
13
21
|
"""Get the details Glue database.
|
|
14
22
|
|
|
15
23
|
Args:
|
|
@@ -18,31 +26,32 @@ def get_glue_database(datebase_name: str):
|
|
|
18
26
|
Returns:
|
|
19
27
|
set: catalogid and locationUri
|
|
20
28
|
"""
|
|
21
|
-
|
|
22
29
|
glue = boto3.client("glue")
|
|
23
30
|
try:
|
|
24
|
-
response = glue.get_database(Name=
|
|
31
|
+
response = glue.get_database(Name=database_name)
|
|
25
32
|
except glue.exceptions.EntityNotFoundException:
|
|
26
|
-
print(f"Database not found {
|
|
33
|
+
print(f"Database not found {database_name}.")
|
|
27
34
|
return (None, None)
|
|
28
35
|
except Exception as e:
|
|
29
36
|
# todo catch all
|
|
30
37
|
print(f"Error: {e}")
|
|
31
38
|
return (None, None)
|
|
32
39
|
|
|
33
|
-
return (
|
|
40
|
+
return (
|
|
41
|
+
response["Database"]["CatalogId"],
|
|
42
|
+
response["Database"].get("LocationUri", "None"),
|
|
43
|
+
)
|
|
34
44
|
|
|
35
45
|
|
|
36
46
|
def get_glue_tables(database_name: str) -> List[str]:
|
|
37
47
|
"""Get the list of tables in a Glue database.
|
|
38
48
|
|
|
39
49
|
Args:
|
|
40
|
-
database_name (str):
|
|
50
|
+
database_name (str): Glue database to request.
|
|
41
51
|
|
|
42
52
|
Returns:
|
|
43
|
-
List[
|
|
53
|
+
List[str]: List of table names
|
|
44
54
|
"""
|
|
45
|
-
|
|
46
55
|
glue = boto3.client("glue")
|
|
47
56
|
|
|
48
57
|
# Set the paginator
|
|
@@ -107,9 +116,21 @@ def get_glue_table_schema(database_name: str, table_name: str):
|
|
|
107
116
|
return table_schema
|
|
108
117
|
|
|
109
118
|
|
|
110
|
-
def import_glue(
|
|
111
|
-
|
|
119
|
+
def import_glue(
|
|
120
|
+
data_contract_specification: DataContractSpecification,
|
|
121
|
+
source: str,
|
|
122
|
+
table_names: List[str],
|
|
123
|
+
):
|
|
124
|
+
"""Import the schema of a Glue database.
|
|
112
125
|
|
|
126
|
+
Args:
|
|
127
|
+
data_contract_specification (DataContractSpecification): The data contract specification to update.
|
|
128
|
+
source (str): The name of the Glue database.
|
|
129
|
+
table_names (List[str]): List of table names to import. If None, all tables in the database are imported.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
DataContractSpecification: The updated data contract specification.
|
|
133
|
+
"""
|
|
113
134
|
catalogid, location_uri = get_glue_database(source)
|
|
114
135
|
|
|
115
136
|
# something went wrong
|
|
@@ -131,17 +152,21 @@ def import_glue(data_contract_specification: DataContractSpecification, source:
|
|
|
131
152
|
|
|
132
153
|
fields = {}
|
|
133
154
|
for column in table_schema:
|
|
134
|
-
field =
|
|
135
|
-
field.type = map_type_from_sql(column["Type"])
|
|
155
|
+
field = create_typed_field(column["Type"])
|
|
136
156
|
|
|
137
157
|
# hive partitons are required, but are not primary keys
|
|
138
158
|
if column.get("Hive"):
|
|
139
159
|
field.required = True
|
|
140
160
|
|
|
141
161
|
field.description = column.get("Comment")
|
|
142
|
-
|
|
143
162
|
fields[column["Name"]] = field
|
|
144
163
|
|
|
164
|
+
if "decimal" in column["Type"]:
|
|
165
|
+
# Extract precision and scale from the string
|
|
166
|
+
perc_scale = column["Type"][8:-1].split(",")
|
|
167
|
+
field.precision = int(perc_scale[0])
|
|
168
|
+
field.scale = int(perc_scale[1])
|
|
169
|
+
|
|
145
170
|
data_contract_specification.models[table_name] = Model(
|
|
146
171
|
type="table",
|
|
147
172
|
fields=fields,
|
|
@@ -150,35 +175,102 @@ def import_glue(data_contract_specification: DataContractSpecification, source:
|
|
|
150
175
|
return data_contract_specification
|
|
151
176
|
|
|
152
177
|
|
|
153
|
-
def
|
|
178
|
+
def create_typed_field(dtype: str) -> Field:
|
|
179
|
+
"""Create a typed field based on the given data type.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
dtype (str): The data type of the field.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Field: The created field with the appropriate type.
|
|
186
|
+
"""
|
|
187
|
+
field = Field()
|
|
188
|
+
dtype = dtype.strip().lower().replace(" ", "")
|
|
189
|
+
if dtype.startswith(("array", "struct")):
|
|
190
|
+
orig_dtype: str = dtype
|
|
191
|
+
if dtype.startswith("array"):
|
|
192
|
+
field.type = "array"
|
|
193
|
+
field.items = create_typed_field(orig_dtype[6:-1])
|
|
194
|
+
elif dtype.startswith("struct"):
|
|
195
|
+
field.type = "struct"
|
|
196
|
+
for f in split_struct(orig_dtype[7:-1]):
|
|
197
|
+
field.fields[f.split(":", 1)[0].strip()] = create_typed_field(f.split(":", 1)[1])
|
|
198
|
+
else:
|
|
199
|
+
field.type = map_type_from_sql(dtype)
|
|
200
|
+
return field
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def split_fields(s: str):
|
|
204
|
+
"""Split a string of fields considering nested structures.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
s (str): The string to split.
|
|
208
|
+
|
|
209
|
+
Yields:
|
|
210
|
+
str: The next field in the string.
|
|
211
|
+
"""
|
|
212
|
+
counter: int = 0
|
|
213
|
+
last: int = 0
|
|
214
|
+
for i, x in enumerate(s):
|
|
215
|
+
if x in ("<", "("):
|
|
216
|
+
counter += 1
|
|
217
|
+
elif x in (">", ")"):
|
|
218
|
+
counter -= 1
|
|
219
|
+
elif x == "," and counter == 0:
|
|
220
|
+
yield s[last:i]
|
|
221
|
+
last = i + 1
|
|
222
|
+
yield s[last:]
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def split_struct(s: str) -> List[str]:
|
|
226
|
+
"""Split a struct string into individual fields.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
s (str): The struct string to split.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
List[str]: List of individual fields in the struct.
|
|
233
|
+
"""
|
|
234
|
+
return list(split_fields(s=s))
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def map_type_from_sql(sql_type: str) -> str:
|
|
238
|
+
"""Map an SQL type to a corresponding field type.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
sql_type (str): The SQL type to map.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
str: The corresponding field type.
|
|
245
|
+
"""
|
|
154
246
|
if sql_type is None:
|
|
155
247
|
return None
|
|
156
248
|
|
|
157
|
-
|
|
249
|
+
sql_type = sql_type.lower()
|
|
250
|
+
if sql_type.startswith("varchar"):
|
|
158
251
|
return "varchar"
|
|
159
|
-
if sql_type.
|
|
252
|
+
if sql_type.startswith("string"):
|
|
160
253
|
return "string"
|
|
161
|
-
if sql_type.
|
|
254
|
+
if sql_type.startswith("text"):
|
|
162
255
|
return "text"
|
|
163
|
-
|
|
256
|
+
if sql_type.startswith("byte"):
|
|
164
257
|
return "byte"
|
|
165
|
-
|
|
258
|
+
if sql_type.startswith("short"):
|
|
166
259
|
return "short"
|
|
167
|
-
|
|
260
|
+
if sql_type.startswith("integer") or sql_type.startswith("int"):
|
|
168
261
|
return "integer"
|
|
169
|
-
|
|
262
|
+
if sql_type.startswith("long") or sql_type.startswith("bigint"):
|
|
170
263
|
return "long"
|
|
171
|
-
|
|
172
|
-
return "long"
|
|
173
|
-
elif sql_type.lower().startswith("float"):
|
|
264
|
+
if sql_type.startswith("float"):
|
|
174
265
|
return "float"
|
|
175
|
-
|
|
266
|
+
if sql_type.startswith("double"):
|
|
176
267
|
return "double"
|
|
177
|
-
|
|
268
|
+
if sql_type.startswith("boolean"):
|
|
178
269
|
return "boolean"
|
|
179
|
-
|
|
270
|
+
if sql_type.startswith("timestamp"):
|
|
180
271
|
return "timestamp"
|
|
181
|
-
|
|
272
|
+
if sql_type.startswith("date"):
|
|
182
273
|
return "date"
|
|
183
|
-
|
|
184
|
-
return "
|
|
274
|
+
if sql_type.startswith("decimal"):
|
|
275
|
+
return "decimal"
|
|
276
|
+
return "variant"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
from datacontract.model.data_contract_specification import DataContractSpecification
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Importer(ABC):
|
|
8
|
+
def __init__(self, import_format) -> None:
|
|
9
|
+
self.import_format = import_format
|
|
10
|
+
|
|
11
|
+
@abstractmethod
|
|
12
|
+
def import_source(
|
|
13
|
+
self, data_contract_specification: DataContractSpecification, source: str, import_args: dict
|
|
14
|
+
) -> dict:
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ImportFormat(str, Enum):
|
|
19
|
+
sql = "sql"
|
|
20
|
+
avro = "avro"
|
|
21
|
+
glue = "glue"
|
|
22
|
+
jsonschema = "jsonschema"
|
|
23
|
+
bigquery = "bigquery"
|
|
24
|
+
odcs = "odcs"
|
|
25
|
+
unity = "unity"
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def get_suported_formats(cls):
|
|
29
|
+
return list(map(lambda c: c.value, cls))
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import importlib.util
|
|
2
|
+
import sys
|
|
3
|
+
from datacontract.imports.importer import ImportFormat, Importer
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ImporterFactory:
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self.dict_importer = {}
|
|
9
|
+
self.dict_lazy_importer = {}
|
|
10
|
+
|
|
11
|
+
def register_importer(self, name, importer: Importer):
|
|
12
|
+
self.dict_importer.update({name: importer})
|
|
13
|
+
|
|
14
|
+
def register_lazy_importer(self, name: str, module_path: str, class_name: str):
|
|
15
|
+
self.dict_lazy_importer.update({name: (module_path, class_name)})
|
|
16
|
+
|
|
17
|
+
def create(self, name) -> Importer:
|
|
18
|
+
importers = self.dict_importer.copy()
|
|
19
|
+
importers.update(self.dict_lazy_importer.copy())
|
|
20
|
+
if name not in importers.keys():
|
|
21
|
+
raise ValueError(f"The '{name}' format is not suportted.")
|
|
22
|
+
importer_class = importers[name]
|
|
23
|
+
if type(importers[name]) is tuple:
|
|
24
|
+
importer_class = load_module_class(module_path=importers[name][0], class_name=importers[name][1])
|
|
25
|
+
if not importer_class:
|
|
26
|
+
raise ValueError(f"Module {name} could not be loaded.")
|
|
27
|
+
return importer_class(name)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def import_module(module_path):
|
|
31
|
+
if importlib.util.find_spec(module_path) is not None:
|
|
32
|
+
try:
|
|
33
|
+
module = importlib.import_module(module_path)
|
|
34
|
+
except ModuleNotFoundError:
|
|
35
|
+
return None
|
|
36
|
+
sys.modules[module_path] = module
|
|
37
|
+
return module
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def load_module_class(module_path, class_name):
|
|
41
|
+
module = import_module(module_path)
|
|
42
|
+
if not module:
|
|
43
|
+
return None
|
|
44
|
+
return getattr(module, class_name)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
importer_factory = ImporterFactory()
|
|
48
|
+
importer_factory.register_lazy_importer(
|
|
49
|
+
name=ImportFormat.avro, module_path="datacontract.imports.avro_importer", class_name="AvroImporter"
|
|
50
|
+
)
|
|
51
|
+
importer_factory.register_lazy_importer(
|
|
52
|
+
name=ImportFormat.bigquery,
|
|
53
|
+
module_path="datacontract.imports.bigquery_importer",
|
|
54
|
+
class_name="BigQueryImporter",
|
|
55
|
+
)
|
|
56
|
+
importer_factory.register_lazy_importer(
|
|
57
|
+
name=ImportFormat.glue, module_path="datacontract.imports.glue_importer", class_name="GlueImporter"
|
|
58
|
+
)
|
|
59
|
+
importer_factory.register_lazy_importer(
|
|
60
|
+
name=ImportFormat.jsonschema,
|
|
61
|
+
module_path="datacontract.imports.jsonschema_importer",
|
|
62
|
+
class_name="JsonSchemaImporter",
|
|
63
|
+
)
|
|
64
|
+
importer_factory.register_lazy_importer(
|
|
65
|
+
name=ImportFormat.odcs, module_path="datacontract.imports.odcs_importer", class_name="OdcsImporter"
|
|
66
|
+
)
|
|
67
|
+
importer_factory.register_lazy_importer(
|
|
68
|
+
name=ImportFormat.sql, module_path="datacontract.imports.sql_importer", class_name="SqlImporter"
|
|
69
|
+
)
|
|
70
|
+
importer_factory.register_lazy_importer(
|
|
71
|
+
name=ImportFormat.unity, module_path="datacontract.imports.unity_importer", class_name="UnityImporter"
|
|
72
|
+
)
|
|
@@ -2,10 +2,18 @@ import json
|
|
|
2
2
|
|
|
3
3
|
import fastjsonschema
|
|
4
4
|
|
|
5
|
+
from datacontract.imports.importer import Importer
|
|
5
6
|
from datacontract.model.data_contract_specification import DataContractSpecification, Model, Field, Definition
|
|
6
7
|
from datacontract.model.exceptions import DataContractException
|
|
7
8
|
|
|
8
9
|
|
|
10
|
+
class JsonSchemaImporter(Importer):
|
|
11
|
+
def import_source(
|
|
12
|
+
self, data_contract_specification: DataContractSpecification, source: str, import_args: dict
|
|
13
|
+
) -> dict:
|
|
14
|
+
return import_jsonschema(data_contract_specification, source)
|
|
15
|
+
|
|
16
|
+
|
|
9
17
|
def convert_json_schema_properties(properties, is_definition=False):
|
|
10
18
|
fields = {}
|
|
11
19
|
for field_name, field_schema in properties.items():
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Any, Dict, List
|
|
4
|
+
import yaml
|
|
5
|
+
from datacontract.imports.importer import Importer
|
|
6
|
+
from datacontract.model.data_contract_specification import (
|
|
7
|
+
Availability,
|
|
8
|
+
Contact,
|
|
9
|
+
DataContractSpecification,
|
|
10
|
+
Info,
|
|
11
|
+
Model,
|
|
12
|
+
Field,
|
|
13
|
+
Retention,
|
|
14
|
+
ServiceLevel,
|
|
15
|
+
Terms,
|
|
16
|
+
)
|
|
17
|
+
from datacontract.model.exceptions import DataContractException
|
|
18
|
+
|
|
19
|
+
DATACONTRACT_TYPES = [
|
|
20
|
+
"string",
|
|
21
|
+
"text",
|
|
22
|
+
"varchar",
|
|
23
|
+
"number",
|
|
24
|
+
"decimal",
|
|
25
|
+
"numeric",
|
|
26
|
+
"int",
|
|
27
|
+
"integer",
|
|
28
|
+
"long",
|
|
29
|
+
"bigint",
|
|
30
|
+
"float",
|
|
31
|
+
"double",
|
|
32
|
+
"boolean",
|
|
33
|
+
"timestamp",
|
|
34
|
+
"timestamp_tz",
|
|
35
|
+
"timestamp_ntz",
|
|
36
|
+
"date",
|
|
37
|
+
"array",
|
|
38
|
+
"bytes",
|
|
39
|
+
"object",
|
|
40
|
+
"record",
|
|
41
|
+
"struct",
|
|
42
|
+
"null",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class OdcsImporter(Importer):
|
|
47
|
+
def import_source(
|
|
48
|
+
self, data_contract_specification: DataContractSpecification, source: str, import_args: dict
|
|
49
|
+
) -> dict:
|
|
50
|
+
return import_odcs(data_contract_specification, source)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def import_odcs(data_contract_specification: DataContractSpecification, source: str) -> DataContractSpecification:
|
|
54
|
+
try:
|
|
55
|
+
with open(source, "r") as file:
|
|
56
|
+
odcs_contract = yaml.safe_load(file.read())
|
|
57
|
+
|
|
58
|
+
except Exception as e:
|
|
59
|
+
raise DataContractException(
|
|
60
|
+
type="schema",
|
|
61
|
+
name="Parse ODCS contract",
|
|
62
|
+
reason=f"Failed to parse odcs contract from {source}",
|
|
63
|
+
engine="datacontract",
|
|
64
|
+
original_exception=e,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
data_contract_specification.id = odcs_contract["uuid"]
|
|
68
|
+
data_contract_specification.info = import_info(odcs_contract)
|
|
69
|
+
data_contract_specification.terms = import_terms(odcs_contract)
|
|
70
|
+
data_contract_specification.servicelevels = import_servicelevels(odcs_contract)
|
|
71
|
+
data_contract_specification.models = import_models(odcs_contract)
|
|
72
|
+
|
|
73
|
+
return data_contract_specification
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def import_info(odcs_contract: Dict[str, Any]) -> Info:
|
|
77
|
+
info = Info(title=odcs_contract.get("quantumName"), version=odcs_contract.get("version"))
|
|
78
|
+
|
|
79
|
+
if odcs_contract.get("description").get("purpose") is not None:
|
|
80
|
+
info.description = odcs_contract.get("description").get("purpose")
|
|
81
|
+
|
|
82
|
+
if odcs_contract.get("datasetDomain") is not None:
|
|
83
|
+
info.owner = odcs_contract.get("datasetDomain")
|
|
84
|
+
|
|
85
|
+
if odcs_contract.get("productDl") is not None or odcs_contract.get("productFeedbackUrl") is not None:
|
|
86
|
+
contact = Contact()
|
|
87
|
+
if odcs_contract.get("productDl") is not None:
|
|
88
|
+
contact.name = odcs_contract.get("productDl")
|
|
89
|
+
if odcs_contract.get("productFeedbackUrl") is not None:
|
|
90
|
+
contact.url = odcs_contract.get("productFeedbackUrl")
|
|
91
|
+
|
|
92
|
+
info.contact = contact
|
|
93
|
+
|
|
94
|
+
return info
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def import_terms(odcs_contract: Dict[str, Any]) -> Terms | None:
|
|
98
|
+
if (
|
|
99
|
+
odcs_contract.get("description").get("usage") is not None
|
|
100
|
+
or odcs_contract.get("description").get("limitations") is not None
|
|
101
|
+
or odcs_contract.get("price") is not None
|
|
102
|
+
):
|
|
103
|
+
terms = Terms()
|
|
104
|
+
if odcs_contract.get("description").get("usage") is not None:
|
|
105
|
+
terms.usage = odcs_contract.get("description").get("usage")
|
|
106
|
+
if odcs_contract.get("description").get("limitations") is not None:
|
|
107
|
+
terms.limitations = odcs_contract.get("description").get("limitations")
|
|
108
|
+
if odcs_contract.get("price") is not None:
|
|
109
|
+
terms.billing = f"{odcs_contract.get('price').get('priceAmount')} {odcs_contract.get('price').get('priceCurrency')} / {odcs_contract.get('price').get('priceUnit')}"
|
|
110
|
+
|
|
111
|
+
return terms
|
|
112
|
+
else:
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def import_servicelevels(odcs_contract: Dict[str, Any]) -> ServiceLevel:
|
|
117
|
+
# find the two properties we can map (based on the examples)
|
|
118
|
+
sla_properties = odcs_contract.get("slaProperties") if odcs_contract.get("slaProperties") is not None else []
|
|
119
|
+
availability = next((p for p in sla_properties if p["property"] == "generalAvailability"), None)
|
|
120
|
+
retention = next((p for p in sla_properties if p["property"] == "retention"), None)
|
|
121
|
+
|
|
122
|
+
if availability is not None or retention is not None:
|
|
123
|
+
servicelevel = ServiceLevel()
|
|
124
|
+
|
|
125
|
+
if availability is not None:
|
|
126
|
+
value = availability.get("value")
|
|
127
|
+
if isinstance(value, datetime.datetime):
|
|
128
|
+
value = value.isoformat()
|
|
129
|
+
servicelevel.availability = Availability(description=value)
|
|
130
|
+
|
|
131
|
+
if retention is not None:
|
|
132
|
+
servicelevel.retention = Retention(period=f"{retention.get('value')}{retention.get('unit')}")
|
|
133
|
+
|
|
134
|
+
return servicelevel
|
|
135
|
+
else:
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def import_models(odcs_contract: Dict[str, Any]) -> Dict[str, Model]:
|
|
140
|
+
custom_type_mappings = get_custom_type_mappings(odcs_contract.get("customProperties"))
|
|
141
|
+
|
|
142
|
+
odcs_tables = odcs_contract.get("dataset") if odcs_contract.get("dataset") is not None else []
|
|
143
|
+
result = {}
|
|
144
|
+
|
|
145
|
+
for table in odcs_tables:
|
|
146
|
+
description = table.get("description") if table.get("description") is not None else ""
|
|
147
|
+
model = Model(description=" ".join(description.splitlines()), type="table")
|
|
148
|
+
model.fields = import_fields(table.get("columns"), custom_type_mappings)
|
|
149
|
+
result[table.get("table")] = model
|
|
150
|
+
|
|
151
|
+
return result
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def import_fields(odcs_columns: Dict[str, Any], custom_type_mappings: Dict[str, str]) -> Dict[str, Field]:
|
|
155
|
+
logger = logging.getLogger(__name__)
|
|
156
|
+
result = {}
|
|
157
|
+
|
|
158
|
+
for column in odcs_columns:
|
|
159
|
+
mapped_type = map_type(column.get("logicalType"), custom_type_mappings)
|
|
160
|
+
if mapped_type is not None:
|
|
161
|
+
description = column.get("description") if column.get("description") is not None else ""
|
|
162
|
+
field = Field(
|
|
163
|
+
description=" ".join(description.splitlines()),
|
|
164
|
+
type=mapped_type,
|
|
165
|
+
title=column.get("businessName") if column.get("businessName") is not None else "",
|
|
166
|
+
required=not column.get("isNullable") if column.get("isNullable") is not None else False,
|
|
167
|
+
primary=column.get("isPrimary") if column.get("isPrimary") is not None else False,
|
|
168
|
+
unique=column.get("isUnique") if column.get("isUnique") is not None else False,
|
|
169
|
+
classification=column.get("classification") if column.get("classification") is not None else "",
|
|
170
|
+
tags=column.get("tags") if column.get("tags") is not None else [],
|
|
171
|
+
)
|
|
172
|
+
result[column["column"]] = field
|
|
173
|
+
else:
|
|
174
|
+
logger.info(
|
|
175
|
+
f"Can't properly map {column.get('column')} to the Datacontract Mapping types, as there is no equivalent or special mapping. Consider introducing a customProperty 'dc_mapping_{column.get('logicalName')}' that defines your expected type as the 'value'"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
return result
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def map_type(odcs_type: str, custom_mappings: Dict[str, str]) -> str | None:
|
|
182
|
+
t = odcs_type.lower()
|
|
183
|
+
if t in DATACONTRACT_TYPES:
|
|
184
|
+
return t
|
|
185
|
+
elif custom_mappings.get(t) is not None:
|
|
186
|
+
return custom_mappings.get(t)
|
|
187
|
+
else:
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def get_custom_type_mappings(odcs_custom_properties: List[Any]) -> Dict[str, str]:
|
|
192
|
+
result = {}
|
|
193
|
+
if odcs_custom_properties is not None:
|
|
194
|
+
for prop in odcs_custom_properties:
|
|
195
|
+
if prop["property"].startswith("dc_mapping_"):
|
|
196
|
+
odcs_type_name = prop["property"].substring(11)
|
|
197
|
+
datacontract_type = prop["value"]
|
|
198
|
+
result[odcs_type_name] = datacontract_type
|
|
199
|
+
|
|
200
|
+
return result
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
from simple_ddl_parser import parse_from_file
|
|
2
2
|
|
|
3
|
+
from datacontract.imports.importer import Importer
|
|
3
4
|
from datacontract.model.data_contract_specification import DataContractSpecification, Model, Field
|
|
4
5
|
|
|
5
6
|
|
|
7
|
+
class SqlImporter(Importer):
|
|
8
|
+
def import_source(
|
|
9
|
+
self, data_contract_specification: DataContractSpecification, source: str, import_args: dict
|
|
10
|
+
) -> dict:
|
|
11
|
+
return import_sql(data_contract_specification, self.import_format, source)
|
|
12
|
+
|
|
13
|
+
|
|
6
14
|
def import_sql(data_contract_specification: DataContractSpecification, format: str, source: str):
|
|
7
15
|
ddl = parse_from_file(source, group_by_type=True)
|
|
8
16
|
tables = ddl["tables"]
|