datacontract-cli 0.10.23__py3-none-any.whl → 0.10.25__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 (43) hide show
  1. datacontract/__init__.py +13 -0
  2. datacontract/api.py +3 -3
  3. datacontract/catalog/catalog.py +2 -2
  4. datacontract/cli.py +1 -1
  5. datacontract/data_contract.py +5 -3
  6. datacontract/engines/data_contract_test.py +13 -4
  7. datacontract/engines/fastjsonschema/s3/s3_read_files.py +3 -2
  8. datacontract/engines/soda/check_soda_execute.py +16 -3
  9. datacontract/engines/soda/connections/duckdb_connection.py +61 -5
  10. datacontract/engines/soda/connections/kafka.py +3 -2
  11. datacontract/export/avro_converter.py +8 -1
  12. datacontract/export/bigquery_converter.py +1 -1
  13. datacontract/export/duckdb_type_converter.py +57 -0
  14. datacontract/export/great_expectations_converter.py +49 -2
  15. datacontract/export/odcs_v3_exporter.py +162 -136
  16. datacontract/export/protobuf_converter.py +163 -69
  17. datacontract/export/spark_converter.py +1 -1
  18. datacontract/imports/avro_importer.py +30 -5
  19. datacontract/imports/csv_importer.py +111 -57
  20. datacontract/imports/excel_importer.py +850 -0
  21. datacontract/imports/importer.py +5 -2
  22. datacontract/imports/importer_factory.py +10 -0
  23. datacontract/imports/odcs_v3_importer.py +226 -127
  24. datacontract/imports/protobuf_importer.py +264 -0
  25. datacontract/lint/linters/description_linter.py +1 -3
  26. datacontract/lint/linters/field_reference_linter.py +1 -2
  27. datacontract/lint/linters/notice_period_linter.py +2 -2
  28. datacontract/lint/linters/valid_constraints_linter.py +3 -3
  29. datacontract/lint/resolve.py +23 -8
  30. datacontract/model/data_contract_specification/__init__.py +1 -0
  31. datacontract/model/run.py +3 -0
  32. datacontract/output/__init__.py +0 -0
  33. datacontract/templates/datacontract.html +2 -1
  34. datacontract/templates/index.html +2 -1
  35. {datacontract_cli-0.10.23.dist-info → datacontract_cli-0.10.25.dist-info}/METADATA +305 -195
  36. {datacontract_cli-0.10.23.dist-info → datacontract_cli-0.10.25.dist-info}/RECORD +40 -38
  37. {datacontract_cli-0.10.23.dist-info → datacontract_cli-0.10.25.dist-info}/WHEEL +1 -1
  38. datacontract/export/csv_type_converter.py +0 -36
  39. datacontract/lint/linters/quality_schema_linter.py +0 -52
  40. datacontract/model/data_contract_specification.py +0 -327
  41. {datacontract_cli-0.10.23.dist-info → datacontract_cli-0.10.25.dist-info}/entry_points.txt +0 -0
  42. {datacontract_cli-0.10.23.dist-info → datacontract_cli-0.10.25.dist-info/licenses}/LICENSE +0 -0
  43. {datacontract_cli-0.10.23.dist-info → datacontract_cli-0.10.25.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,8 @@
1
1
  from abc import ABC, abstractmethod
2
2
  from enum import Enum
3
3
 
4
- from datacontract.model.data_contract_specification import DataContractSpecification
4
+ from datacontract_specification.model import DataContractSpecification
5
+ from open_data_contract_standard.model import OpenDataContractStandard
5
6
 
6
7
 
7
8
  class Importer(ABC):
@@ -14,7 +15,7 @@ class Importer(ABC):
14
15
  data_contract_specification: DataContractSpecification,
15
16
  source: str,
16
17
  import_args: dict,
17
- ) -> DataContractSpecification:
18
+ ) -> DataContractSpecification | OpenDataContractStandard:
18
19
  pass
19
20
 
20
21
 
@@ -32,6 +33,8 @@ class ImportFormat(str, Enum):
32
33
  iceberg = "iceberg"
33
34
  parquet = "parquet"
34
35
  csv = "csv"
36
+ protobuf = "protobuf"
37
+ excel = "excel"
35
38
 
36
39
  @classmethod
37
40
  def get_supported_formats(cls):
@@ -109,3 +109,13 @@ importer_factory.register_lazy_importer(
109
109
  module_path="datacontract.imports.csv_importer",
110
110
  class_name="CsvImporter",
111
111
  )
112
+ importer_factory.register_lazy_importer(
113
+ name=ImportFormat.protobuf,
114
+ module_path="datacontract.imports.protobuf_importer",
115
+ class_name="ProtoBufImporter",
116
+ )
117
+ importer_factory.register_lazy_importer(
118
+ name=ImportFormat.excel,
119
+ module_path="datacontract.imports.excel_importer",
120
+ class_name="ExcelImporter",
121
+ )
@@ -1,9 +1,11 @@
1
1
  import datetime
2
2
  import logging
3
+ import re
3
4
  from typing import Any, Dict, List
4
5
  from venv import logger
5
6
 
6
- import yaml
7
+ from datacontract_specification.model import Quality
8
+ from open_data_contract_standard.model import CustomProperty, OpenDataContractStandard, SchemaProperty
7
9
 
8
10
  from datacontract.imports.importer import Importer
9
11
  from datacontract.lint.resources import read_resource
@@ -14,9 +16,9 @@ from datacontract.model.data_contract_specification import (
14
16
  Field,
15
17
  Info,
16
18
  Model,
17
- Quality,
18
19
  Retention,
19
20
  Server,
21
+ ServerRole,
20
22
  ServiceLevel,
21
23
  Terms,
22
24
  )
@@ -39,7 +41,7 @@ def import_odcs_v3_from_str(
39
41
  data_contract_specification: DataContractSpecification, source_str: str
40
42
  ) -> DataContractSpecification:
41
43
  try:
42
- odcs_contract = yaml.safe_load(source_str)
44
+ odcs = OpenDataContractStandard.from_string(source_str)
43
45
  except Exception as e:
44
46
  raise DataContractException(
45
47
  type="schema",
@@ -49,129 +51,140 @@ def import_odcs_v3_from_str(
49
51
  original_exception=e,
50
52
  )
51
53
 
52
- data_contract_specification.id = odcs_contract["id"]
53
- data_contract_specification.info = import_info(odcs_contract)
54
- data_contract_specification.servers = import_servers(odcs_contract)
55
- data_contract_specification.terms = import_terms(odcs_contract)
56
- data_contract_specification.servicelevels = import_servicelevels(odcs_contract)
57
- data_contract_specification.models = import_models(odcs_contract)
58
- data_contract_specification.tags = import_tags(odcs_contract)
54
+ return import_from_odcs_model(data_contract_specification, odcs)
59
55
 
56
+
57
+ def import_from_odcs_model(data_contract_specification, odcs):
58
+ data_contract_specification.id = odcs.id
59
+ data_contract_specification.info = import_info(odcs)
60
+ data_contract_specification.servers = import_servers(odcs)
61
+ data_contract_specification.terms = import_terms(odcs)
62
+ data_contract_specification.servicelevels = import_servicelevels(odcs)
63
+ data_contract_specification.models = import_models(odcs)
64
+ data_contract_specification.tags = import_tags(odcs)
60
65
  return data_contract_specification
61
66
 
62
67
 
63
- def import_info(odcs_contract: Dict[str, Any]) -> Info:
68
+ def import_info(odcs: Any) -> Info:
64
69
  info = Info()
65
70
 
66
- info.title = odcs_contract.get("name") if odcs_contract.get("name") is not None else ""
71
+ info.title = odcs.name if odcs.name is not None else ""
67
72
 
68
- if odcs_contract.get("version") is not None:
69
- info.version = odcs_contract.get("version")
73
+ if odcs.version is not None:
74
+ info.version = odcs.version
70
75
 
71
76
  # odcs.description.purpose => datacontract.description
72
- if odcs_contract.get("description") is not None and odcs_contract.get("description").get("purpose") is not None:
73
- info.description = odcs_contract.get("description").get("purpose")
77
+ if odcs.description is not None and odcs.description.purpose is not None:
78
+ info.description = odcs.description.purpose
74
79
 
75
80
  # odcs.domain => datacontract.owner
76
- if odcs_contract.get("domain") is not None:
77
- info.owner = odcs_contract.get("domain")
81
+ owner = get_owner(odcs.customProperties)
82
+ if owner is not None:
83
+ info.owner = owner
78
84
 
79
85
  # add dataProduct as custom property
80
- if odcs_contract.get("dataProduct") is not None:
81
- info.dataProduct = odcs_contract.get("dataProduct")
86
+ if odcs.dataProduct is not None:
87
+ info.dataProduct = odcs.dataProduct
82
88
 
83
89
  # add tenant as custom property
84
- if odcs_contract.get("tenant") is not None:
85
- info.tenant = odcs_contract.get("tenant")
90
+ if odcs.tenant is not None:
91
+ info.tenant = odcs.tenant
86
92
 
87
93
  return info
88
94
 
89
95
 
90
- def import_servers(odcs_contract: Dict[str, Any]) -> Dict[str, Server] | None:
91
- if odcs_contract.get("servers") is None:
96
+ def import_server_roles(roles: List[Dict]) -> List[ServerRole] | None:
97
+ if roles is None:
98
+ return None
99
+ result = []
100
+ for role in roles:
101
+ server_role = ServerRole()
102
+ server_role.name = role.role
103
+ server_role.description = role.description
104
+ result.append(server_role)
105
+
106
+
107
+ def import_servers(odcs: OpenDataContractStandard) -> Dict[str, Server] | None:
108
+ if odcs.servers is None:
92
109
  return None
93
110
  servers = {}
94
- for odcs_server in odcs_contract.get("servers"):
95
- server_name = odcs_server.get("server")
111
+ for odcs_server in odcs.servers:
112
+ server_name = odcs_server.server
96
113
  if server_name is None:
97
114
  logger.warning("Server name is missing, skipping server")
98
115
  continue
99
116
 
100
117
  server = Server()
101
- server.type = odcs_server.get("type")
102
- server.description = odcs_server.get("description")
103
- server.environment = odcs_server.get("environment")
104
- server.format = odcs_server.get("format")
105
- server.project = odcs_server.get("project")
106
- server.dataset = odcs_server.get("dataset")
107
- server.path = odcs_server.get("path")
108
- server.delimiter = odcs_server.get("delimiter")
109
- server.endpointUrl = odcs_server.get("endpointUrl")
110
- server.location = odcs_server.get("location")
111
- server.account = odcs_server.get("account")
112
- server.database = odcs_server.get("database")
113
- server.schema_ = odcs_server.get("schema")
114
- server.host = odcs_server.get("host")
115
- server.port = odcs_server.get("port")
116
- server.catalog = odcs_server.get("catalog")
117
- server.topic = odcs_server.get("topic")
118
- server.http_path = odcs_server.get("http_path")
119
- server.token = odcs_server.get("token")
120
- server.dataProductId = odcs_server.get("dataProductId")
121
- server.outputPortId = odcs_server.get("outputPortId")
122
- server.driver = odcs_server.get("driver")
123
- server.roles = odcs_server.get("roles")
124
-
118
+ server.type = odcs_server.type
119
+ server.description = odcs_server.description
120
+ server.environment = odcs_server.environment
121
+ server.format = odcs_server.format
122
+ server.project = odcs_server.project
123
+ server.dataset = odcs_server.dataset
124
+ server.path = odcs_server.path
125
+ server.delimiter = odcs_server.delimiter
126
+ server.endpointUrl = odcs_server.endpointUrl
127
+ server.location = odcs_server.location
128
+ server.account = odcs_server.account
129
+ server.database = odcs_server.database
130
+ server.schema_ = odcs_server.schema_
131
+ server.host = odcs_server.host
132
+ server.port = odcs_server.port
133
+ server.catalog = odcs_server.catalog
134
+ server.topic = getattr(odcs_server, "topic", None)
135
+ server.http_path = getattr(odcs_server, "http_path", None)
136
+ server.token = getattr(odcs_server, "token", None)
137
+ server.driver = getattr(odcs_server, "driver", None)
138
+ server.roles = import_server_roles(odcs_server.roles)
139
+ server.storageAccount = (
140
+ re.search(r"(?:@|://)([^.]+)\.", odcs_server.location, re.IGNORECASE) if server.type == "azure" else None
141
+ )
125
142
  servers[server_name] = server
126
143
  return servers
127
144
 
128
145
 
129
- def import_terms(odcs_contract: Dict[str, Any]) -> Terms | None:
130
- if odcs_contract.get("description") is None:
146
+ def import_terms(odcs: Any) -> Terms | None:
147
+ if odcs.description is None:
131
148
  return None
132
- if (
133
- odcs_contract.get("description").get("usage") is not None
134
- or odcs_contract.get("description").get("limitations") is not None
135
- or odcs_contract.get("price") is not None
136
- ):
149
+ if odcs.description.usage is not None or odcs.description.limitations is not None or odcs.price is not None:
137
150
  terms = Terms()
138
- if odcs_contract.get("description").get("usage") is not None:
139
- terms.usage = odcs_contract.get("description").get("usage")
140
- if odcs_contract.get("description").get("limitations") is not None:
141
- terms.limitations = odcs_contract.get("description").get("limitations")
142
- if odcs_contract.get("price") is not None:
143
- terms.billing = f"{odcs_contract.get('price').get('priceAmount')} {odcs_contract.get('price').get('priceCurrency')} / {odcs_contract.get('price').get('priceUnit')}"
151
+ if odcs.description.usage is not None:
152
+ terms.usage = odcs.description.usage
153
+ if odcs.description.limitations is not None:
154
+ terms.limitations = odcs.description.limitations
155
+ if odcs.price is not None:
156
+ terms.billing = f"{odcs.price.priceAmount} {odcs.price.priceCurrency} / {odcs.price.priceUnit}"
144
157
 
145
158
  return terms
146
159
  else:
147
160
  return None
148
161
 
149
162
 
150
- def import_servicelevels(odcs_contract: Dict[str, Any]) -> ServiceLevel:
163
+ def import_servicelevels(odcs: Any) -> ServiceLevel:
151
164
  # find the two properties we can map (based on the examples)
152
- sla_properties = odcs_contract.get("slaProperties") if odcs_contract.get("slaProperties") is not None else []
153
- availability = next((p for p in sla_properties if p["property"] == "generalAvailability"), None)
154
- retention = next((p for p in sla_properties if p["property"] == "retention"), None)
165
+ sla_properties = odcs.slaProperties if odcs.slaProperties is not None else []
166
+ availability = next((p for p in sla_properties if p.property == "generalAvailability"), None)
167
+ retention = next((p for p in sla_properties if p.property == "retention"), None)
155
168
 
156
169
  if availability is not None or retention is not None:
157
170
  servicelevel = ServiceLevel()
158
171
 
159
172
  if availability is not None:
160
- value = availability.get("value")
173
+ value = availability.value
161
174
  if isinstance(value, datetime.datetime):
162
175
  value = value.isoformat()
163
176
  servicelevel.availability = Availability(description=value)
164
177
 
165
178
  if retention is not None:
166
- servicelevel.retention = Retention(period=f"{retention.get('value')}{retention.get('unit')}")
179
+ servicelevel.retention = Retention(period=f"{retention.value}{retention.unit}")
167
180
 
168
181
  return servicelevel
169
182
  else:
170
183
  return None
171
184
 
172
185
 
173
- def get_server_type(odcs_contract: Dict[str, Any]) -> str | None:
174
- servers = import_servers(odcs_contract)
186
+ def get_server_type(odcs: OpenDataContractStandard) -> str | None:
187
+ servers = import_servers(odcs)
175
188
  if servers is None or len(servers) == 0:
176
189
  return None
177
190
  # get first server from map
@@ -179,49 +192,106 @@ def get_server_type(odcs_contract: Dict[str, Any]) -> str | None:
179
192
  return server.type
180
193
 
181
194
 
182
- def import_models(odcs_contract: Dict[str, Any]) -> Dict[str, Model]:
183
- custom_type_mappings = get_custom_type_mappings(odcs_contract.get("customProperties"))
195
+ def import_models(odcs: Any) -> Dict[str, Model]:
196
+ custom_type_mappings = get_custom_type_mappings(odcs.customProperties)
184
197
 
185
- odcs_schemas = odcs_contract.get("schema") if odcs_contract.get("schema") is not None else []
198
+ odcs_schemas = odcs.schema_ if odcs.schema_ is not None else []
186
199
  result = {}
187
200
 
188
201
  for odcs_schema in odcs_schemas:
189
- schema_name = odcs_schema.get("name")
190
- schema_physical_name = odcs_schema.get("physicalName")
191
- schema_description = odcs_schema.get("description") if odcs_schema.get("description") is not None else ""
202
+ schema_name = odcs_schema.name
203
+ schema_physical_name = odcs_schema.physicalName
204
+ schema_description = odcs_schema.description if odcs_schema.description is not None else ""
192
205
  model_name = schema_physical_name if schema_physical_name is not None else schema_name
193
- model = Model(description=" ".join(schema_description.splitlines()), type="table")
194
- model.fields = import_fields(
195
- odcs_schema.get("properties"), custom_type_mappings, server_type=get_server_type(odcs_contract)
196
- )
197
- if odcs_schema.get("quality") is not None:
198
- # convert dict to pydantic model
199
-
200
- model.quality = [Quality.model_validate(q) for q in odcs_schema.get("quality")]
206
+ model = Model(description=" ".join(schema_description.splitlines()) if schema_description else "", type="table")
207
+ model.fields = import_fields(odcs_schema.properties, custom_type_mappings, server_type=get_server_type(odcs))
208
+ if odcs_schema.quality is not None:
209
+ model.quality = convert_quality_list(odcs_schema.quality)
201
210
  model.title = schema_name
202
- if odcs_schema.get("dataGranularityDescription") is not None:
203
- model.config = {"dataGranularityDescription": odcs_schema.get("dataGranularityDescription")}
211
+ if odcs_schema.dataGranularityDescription is not None:
212
+ model.config = {"dataGranularityDescription": odcs_schema.dataGranularityDescription}
204
213
  result[model_name] = model
205
214
 
206
215
  return result
207
216
 
208
217
 
209
- def import_field_config(odcs_property: Dict[str, Any], server_type=None) -> Dict[str, Any]:
218
+ def convert_quality_list(odcs_quality_list):
219
+ """Convert a list of ODCS DataQuality objects to datacontract Quality objects"""
220
+ quality_list = []
221
+
222
+ if odcs_quality_list is not None:
223
+ for odcs_quality in odcs_quality_list:
224
+ quality = Quality(type=odcs_quality.type)
225
+
226
+ if odcs_quality.description is not None:
227
+ quality.description = odcs_quality.description
228
+ if odcs_quality.query is not None:
229
+ quality.query = odcs_quality.query
230
+ if odcs_quality.mustBe is not None:
231
+ quality.mustBe = odcs_quality.mustBe
232
+ if odcs_quality.mustNotBe is not None:
233
+ quality.mustNotBe = odcs_quality.mustNotBe
234
+ if odcs_quality.mustBeGreaterThan is not None:
235
+ quality.mustBeGreaterThan = odcs_quality.mustBeGreaterThan
236
+ if odcs_quality.mustBeGreaterOrEqualTo is not None:
237
+ quality.mustBeGreaterThanOrEqualTo = odcs_quality.mustBeGreaterOrEqualTo
238
+ if odcs_quality.mustBeLessThan is not None:
239
+ quality.mustBeLessThan = odcs_quality.mustBeLessThan
240
+ if odcs_quality.mustBeLessOrEqualTo is not None:
241
+ quality.mustBeLessThanOrEqualTo = odcs_quality.mustBeLessOrEqualTo
242
+ if odcs_quality.mustBeBetween is not None:
243
+ quality.mustBeBetween = odcs_quality.mustBeBetween
244
+ if odcs_quality.mustNotBeBetween is not None:
245
+ quality.mustNotBeBetween = odcs_quality.mustNotBeBetween
246
+ if odcs_quality.engine is not None:
247
+ quality.engine = odcs_quality.engine
248
+ if odcs_quality.implementation is not None:
249
+ quality.implementation = odcs_quality.implementation
250
+ if odcs_quality.businessImpact is not None:
251
+ quality.model_extra["businessImpact"] = odcs_quality.businessImpact
252
+ if odcs_quality.dimension is not None:
253
+ quality.model_extra["dimension"] = odcs_quality.dimension
254
+ if odcs_quality.rule is not None:
255
+ quality.model_extra["rule"] = odcs_quality.rule
256
+ if odcs_quality.schedule is not None:
257
+ quality.model_extra["schedule"] = odcs_quality.schedule
258
+ if odcs_quality.scheduler is not None:
259
+ quality.model_extra["scheduler"] = odcs_quality.scheduler
260
+ if odcs_quality.severity is not None:
261
+ quality.model_extra["severity"] = odcs_quality.severity
262
+ if odcs_quality.method is not None:
263
+ quality.model_extra["method"] = odcs_quality.method
264
+ if odcs_quality.customProperties is not None:
265
+ quality.model_extra["customProperties"] = []
266
+ for item in odcs_quality.customProperties:
267
+ quality.model_extra["customProperties"].append(
268
+ {
269
+ "property": item.property,
270
+ "value": item.value,
271
+ }
272
+ )
273
+
274
+ quality_list.append(quality)
275
+
276
+ return quality_list
277
+
278
+
279
+ def import_field_config(odcs_property: SchemaProperty, server_type=None) -> Dict[str, Any]:
210
280
  config = {}
211
- if odcs_property.get("criticalDataElement") is not None:
212
- config["criticalDataElement"] = odcs_property.get("criticalDataElement")
213
- if odcs_property.get("encryptedName") is not None:
214
- config["encryptedName"] = odcs_property.get("encryptedName")
215
- if odcs_property.get("partitionKeyPosition") is not None:
216
- config["partitionKeyPosition"] = odcs_property.get("partitionKeyPosition")
217
- if odcs_property.get("partitioned") is not None:
218
- config["partitioned"] = odcs_property.get("partitioned")
219
-
220
- if odcs_property.get("customProperties") is not None and isinstance(odcs_property.get("customProperties"), list):
221
- for item in odcs_property.get("customProperties"):
222
- config[item["property"]] = item["value"]
223
-
224
- physical_type = odcs_property.get("physicalType")
281
+ if odcs_property.criticalDataElement is not None:
282
+ config["criticalDataElement"] = odcs_property.criticalDataElement
283
+ if odcs_property.encryptedName is not None:
284
+ config["encryptedName"] = odcs_property.encryptedName
285
+ if odcs_property.partitionKeyPosition is not None:
286
+ config["partitionKeyPosition"] = odcs_property.partitionKeyPosition
287
+ if odcs_property.partitioned is not None:
288
+ config["partitioned"] = odcs_property.partitioned
289
+
290
+ if odcs_property.customProperties is not None:
291
+ for item in odcs_property.customProperties:
292
+ config[item.property] = item.value
293
+
294
+ physical_type = odcs_property.physicalType
225
295
  if physical_type is not None:
226
296
  if server_type == "postgres" or server_type == "postgresql":
227
297
  config["postgresType"] = physical_type
@@ -241,13 +311,13 @@ def import_field_config(odcs_property: Dict[str, Any], server_type=None) -> Dict
241
311
  return config
242
312
 
243
313
 
244
- def has_composite_primary_key(odcs_properties) -> bool:
245
- primary_keys = [prop for prop in odcs_properties if prop.get("primaryKey") is not None and prop.get("primaryKey")]
314
+ def has_composite_primary_key(odcs_properties: List[SchemaProperty]) -> bool:
315
+ primary_keys = [prop for prop in odcs_properties if prop.primaryKey is not None and prop.primaryKey]
246
316
  return len(primary_keys) > 1
247
317
 
248
318
 
249
319
  def import_fields(
250
- odcs_properties: Dict[str, Any], custom_type_mappings: Dict[str, str], server_type
320
+ odcs_properties: List[SchemaProperty], custom_type_mappings: Dict[str, str], server_type
251
321
  ) -> Dict[str, Field]:
252
322
  logger = logging.getLogger(__name__)
253
323
  result = {}
@@ -256,31 +326,51 @@ def import_fields(
256
326
  return result
257
327
 
258
328
  for odcs_property in odcs_properties:
259
- mapped_type = map_type(odcs_property.get("logicalType"), custom_type_mappings)
329
+ mapped_type = map_type(odcs_property.logicalType, custom_type_mappings)
260
330
  if mapped_type is not None:
261
- property_name = odcs_property["name"]
262
- description = odcs_property.get("description") if odcs_property.get("description") is not None else None
331
+ property_name = odcs_property.name
332
+ description = odcs_property.description if odcs_property.description is not None else None
263
333
  field = Field(
264
334
  description=" ".join(description.splitlines()) if description is not None else None,
265
335
  type=mapped_type,
266
- title=odcs_property.get("businessName"),
267
- required=not odcs_property.get("nullable") if odcs_property.get("nullable") is not None else False,
268
- primaryKey=odcs_property.get("primaryKey")
269
- if not has_composite_primary_key(odcs_properties) and odcs_property.get("primaryKey") is not None
336
+ title=odcs_property.businessName,
337
+ required=odcs_property.required if odcs_property.required is not None else None,
338
+ primaryKey=odcs_property.primaryKey
339
+ if not has_composite_primary_key(odcs_properties) and odcs_property.primaryKey is not None
270
340
  else False,
271
- unique=odcs_property.get("unique"),
272
- examples=odcs_property.get("examples") if odcs_property.get("examples") is not None else None,
273
- classification=odcs_property.get("classification")
274
- if odcs_property.get("classification") is not None
275
- else "",
276
- tags=odcs_property.get("tags") if odcs_property.get("tags") is not None else None,
277
- quality=odcs_property.get("quality") if odcs_property.get("quality") is not None else [],
341
+ unique=odcs_property.unique if odcs_property.unique else None,
342
+ examples=odcs_property.examples if odcs_property.examples is not None else None,
343
+ classification=odcs_property.classification if odcs_property.classification is not None else None,
344
+ tags=odcs_property.tags if odcs_property.tags is not None else None,
345
+ quality=convert_quality_list(odcs_property.quality),
346
+ fields=import_fields(odcs_property.properties, custom_type_mappings, server_type)
347
+ if odcs_property.properties is not None
348
+ else {},
278
349
  config=import_field_config(odcs_property, server_type),
350
+ format=getattr(odcs_property, "format", None),
279
351
  )
352
+ # mapped_type is array
353
+ if field.type == "array" and odcs_property.items is not None:
354
+ # nested array object
355
+ if odcs_property.items.logicalType == "object":
356
+ field.items = Field(
357
+ type="object",
358
+ fields=import_fields(odcs_property.items.properties, custom_type_mappings, server_type),
359
+ )
360
+ # array of simple type
361
+ elif odcs_property.items.logicalType is not None:
362
+ field.items = Field(type=odcs_property.items.logicalType)
363
+
364
+ # enum from quality validValues as enum
365
+ if field.type == "string":
366
+ for q in field.quality:
367
+ if hasattr(q, "validValues"):
368
+ field.enum = q.validValues
369
+
280
370
  result[property_name] = field
281
371
  else:
282
372
  logger.info(
283
- f"Can't map {odcs_property.get('column')} to the Datacontract Mapping types, as there is no equivalent or special mapping. Consider introducing a customProperty 'dc_mapping_{odcs_property.get('logicalName')}' that defines your expected type as the 'value'"
373
+ f"Can't map {odcs_property.name} to the Datacontract Mapping types, as there is no equivalent or special mapping. Consider introducing a customProperty 'dc_mapping_{odcs_property.logicalType}' that defines your expected type as the 'value'"
284
374
  )
285
375
 
286
376
  return result
@@ -298,19 +388,28 @@ def map_type(odcs_type: str, custom_mappings: Dict[str, str]) -> str | None:
298
388
  return None
299
389
 
300
390
 
301
- def get_custom_type_mappings(odcs_custom_properties: List[Any]) -> Dict[str, str]:
391
+ def get_custom_type_mappings(odcs_custom_properties: List[CustomProperty]) -> Dict[str, str]:
302
392
  result = {}
303
393
  if odcs_custom_properties is not None:
304
394
  for prop in odcs_custom_properties:
305
- if prop["property"].startswith("dc_mapping_"):
306
- odcs_type_name = prop["property"].substring(11)
307
- datacontract_type = prop["value"]
395
+ if prop.property.startswith("dc_mapping_"):
396
+ odcs_type_name = prop.property[11:] # Changed substring to slice
397
+ datacontract_type = prop.value
308
398
  result[odcs_type_name] = datacontract_type
309
399
 
310
400
  return result
311
401
 
312
402
 
313
- def import_tags(odcs_contract) -> List[str] | None:
314
- if odcs_contract.get("tags") is None:
403
+ def get_owner(odcs_custom_properties: List[CustomProperty]) -> str | None:
404
+ if odcs_custom_properties is not None:
405
+ for prop in odcs_custom_properties:
406
+ if prop.property == "owner":
407
+ return prop.value
408
+
409
+ return None
410
+
411
+
412
+ def import_tags(odcs: OpenDataContractStandard) -> List[str] | None:
413
+ if odcs.tags is None:
315
414
  return None
316
- return odcs_contract.get("tags")
415
+ return odcs.tags