datacontract-cli 0.10.23__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 (80) hide show
  1. datacontract/__init__.py +13 -0
  2. datacontract/api.py +12 -5
  3. datacontract/catalog/catalog.py +5 -3
  4. datacontract/cli.py +116 -10
  5. datacontract/data_contract.py +143 -65
  6. datacontract/engines/data_contract_checks.py +366 -60
  7. datacontract/engines/data_contract_test.py +50 -4
  8. datacontract/engines/fastjsonschema/check_jsonschema.py +37 -19
  9. datacontract/engines/fastjsonschema/s3/s3_read_files.py +3 -2
  10. datacontract/engines/soda/check_soda_execute.py +22 -3
  11. datacontract/engines/soda/connections/athena.py +79 -0
  12. datacontract/engines/soda/connections/duckdb_connection.py +65 -6
  13. datacontract/engines/soda/connections/kafka.py +4 -2
  14. datacontract/export/avro_converter.py +20 -3
  15. datacontract/export/bigquery_converter.py +1 -1
  16. datacontract/export/dbt_converter.py +36 -7
  17. datacontract/export/dqx_converter.py +126 -0
  18. datacontract/export/duckdb_type_converter.py +57 -0
  19. datacontract/export/excel_exporter.py +923 -0
  20. datacontract/export/exporter.py +3 -0
  21. datacontract/export/exporter_factory.py +17 -1
  22. datacontract/export/great_expectations_converter.py +55 -5
  23. datacontract/export/{html_export.py → html_exporter.py} +31 -20
  24. datacontract/export/markdown_converter.py +134 -5
  25. datacontract/export/mermaid_exporter.py +110 -0
  26. datacontract/export/odcs_v3_exporter.py +187 -145
  27. datacontract/export/protobuf_converter.py +163 -69
  28. datacontract/export/rdf_converter.py +2 -2
  29. datacontract/export/sodacl_converter.py +9 -1
  30. datacontract/export/spark_converter.py +31 -4
  31. datacontract/export/sql_converter.py +6 -2
  32. datacontract/export/sql_type_converter.py +20 -8
  33. datacontract/imports/avro_importer.py +63 -12
  34. datacontract/imports/csv_importer.py +111 -57
  35. datacontract/imports/excel_importer.py +1111 -0
  36. datacontract/imports/importer.py +16 -3
  37. datacontract/imports/importer_factory.py +17 -0
  38. datacontract/imports/json_importer.py +325 -0
  39. datacontract/imports/odcs_importer.py +2 -2
  40. datacontract/imports/odcs_v3_importer.py +351 -151
  41. datacontract/imports/protobuf_importer.py +264 -0
  42. datacontract/imports/spark_importer.py +117 -13
  43. datacontract/imports/sql_importer.py +32 -16
  44. datacontract/imports/unity_importer.py +84 -38
  45. datacontract/init/init_template.py +1 -1
  46. datacontract/integration/datamesh_manager.py +16 -2
  47. datacontract/lint/resolve.py +112 -23
  48. datacontract/lint/schema.py +24 -15
  49. datacontract/model/data_contract_specification/__init__.py +1 -0
  50. datacontract/model/odcs.py +13 -0
  51. datacontract/model/run.py +3 -0
  52. datacontract/output/junit_test_results.py +3 -3
  53. datacontract/schemas/datacontract-1.1.0.init.yaml +1 -1
  54. datacontract/schemas/datacontract-1.2.0.init.yaml +91 -0
  55. datacontract/schemas/datacontract-1.2.0.schema.json +2029 -0
  56. datacontract/schemas/datacontract-1.2.1.init.yaml +91 -0
  57. datacontract/schemas/datacontract-1.2.1.schema.json +2058 -0
  58. datacontract/schemas/odcs-3.0.2.schema.json +2382 -0
  59. datacontract/templates/datacontract.html +54 -3
  60. datacontract/templates/datacontract_odcs.html +685 -0
  61. datacontract/templates/index.html +5 -2
  62. datacontract/templates/partials/server.html +2 -0
  63. datacontract/templates/style/output.css +319 -145
  64. {datacontract_cli-0.10.23.dist-info → datacontract_cli-0.10.37.dist-info}/METADATA +656 -431
  65. datacontract_cli-0.10.37.dist-info/RECORD +119 -0
  66. {datacontract_cli-0.10.23.dist-info → datacontract_cli-0.10.37.dist-info}/WHEEL +1 -1
  67. {datacontract_cli-0.10.23.dist-info → datacontract_cli-0.10.37.dist-info/licenses}/LICENSE +1 -1
  68. datacontract/export/csv_type_converter.py +0 -36
  69. datacontract/lint/lint.py +0 -142
  70. datacontract/lint/linters/description_linter.py +0 -35
  71. datacontract/lint/linters/field_pattern_linter.py +0 -34
  72. datacontract/lint/linters/field_reference_linter.py +0 -48
  73. datacontract/lint/linters/notice_period_linter.py +0 -55
  74. datacontract/lint/linters/quality_schema_linter.py +0 -52
  75. datacontract/lint/linters/valid_constraints_linter.py +0 -100
  76. datacontract/model/data_contract_specification.py +0 -327
  77. datacontract_cli-0.10.23.dist-info/RECORD +0 -113
  78. /datacontract/{lint/linters → output}/__init__.py +0 -0
  79. {datacontract_cli-0.10.23.dist-info → datacontract_cli-0.10.37.dist-info}/entry_points.txt +0 -0
  80. {datacontract_cli-0.10.23.dist-info → datacontract_cli-0.10.37.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,17 @@
1
- from typing import Dict
2
-
3
- import yaml
1
+ from typing import Any, Dict
2
+
3
+ from open_data_contract_standard.model import (
4
+ CustomProperty,
5
+ DataQuality,
6
+ Description,
7
+ OpenDataContractStandard,
8
+ Role,
9
+ SchemaObject,
10
+ SchemaProperty,
11
+ Server,
12
+ ServiceLevelAgreementProperty,
13
+ Support,
14
+ )
4
15
 
5
16
  from datacontract.export.exporter import Exporter
6
17
  from datacontract.model.data_contract_specification import DataContractSpecification, Field, Model
@@ -12,154 +23,146 @@ class OdcsV3Exporter(Exporter):
12
23
 
13
24
 
14
25
  def to_odcs_v3_yaml(data_contract_spec: DataContractSpecification) -> str:
15
- odcs = {
16
- "apiVersion": "v3.0.0",
17
- "kind": "DataContract",
18
- "id": data_contract_spec.id,
19
- "name": data_contract_spec.info.title,
20
- "version": data_contract_spec.info.version,
21
- "domain": data_contract_spec.info.owner,
22
- "status": to_status(data_contract_spec.info.status),
23
- }
26
+ result = to_odcs_v3(data_contract_spec)
27
+
28
+ return result.to_yaml()
29
+
24
30
 
31
+ def to_odcs_v3(data_contract_spec: DataContractSpecification) -> OpenDataContractStandard:
32
+ result = OpenDataContractStandard(
33
+ apiVersion="v3.0.1",
34
+ kind="DataContract",
35
+ id=data_contract_spec.id,
36
+ name=data_contract_spec.info.title,
37
+ version=data_contract_spec.info.version,
38
+ status=to_status(data_contract_spec.info.status),
39
+ )
25
40
  if data_contract_spec.terms is not None:
26
- odcs["description"] = {
27
- "purpose": data_contract_spec.terms.description.strip()
41
+ result.description = Description(
42
+ purpose=data_contract_spec.terms.description.strip()
28
43
  if data_contract_spec.terms.description is not None
29
44
  else None,
30
- "usage": data_contract_spec.terms.usage.strip() if data_contract_spec.terms.usage is not None else None,
31
- "limitations": data_contract_spec.terms.limitations.strip()
45
+ usage=data_contract_spec.terms.usage.strip() if data_contract_spec.terms.usage is not None else None,
46
+ limitations=data_contract_spec.terms.limitations.strip()
32
47
  if data_contract_spec.terms.limitations is not None
33
48
  else None,
34
- }
35
-
36
- odcs["schema"] = []
49
+ )
50
+ result.schema_ = []
37
51
  for model_key, model_value in data_contract_spec.models.items():
38
52
  odcs_schema = to_odcs_schema(model_key, model_value)
39
- odcs["schema"].append(odcs_schema)
40
-
53
+ result.schema_.append(odcs_schema)
41
54
  if data_contract_spec.servicelevels is not None:
42
55
  slas = []
43
56
  if data_contract_spec.servicelevels.availability is not None:
44
57
  slas.append(
45
- {
46
- "property": "generalAvailability",
47
- "value": data_contract_spec.servicelevels.availability.description,
48
- }
58
+ ServiceLevelAgreementProperty(
59
+ property="generalAvailability", value=data_contract_spec.servicelevels.availability.description
60
+ )
49
61
  )
50
62
  if data_contract_spec.servicelevels.retention is not None:
51
- slas.append({"property": "retention", "value": data_contract_spec.servicelevels.retention.period})
63
+ slas.append(
64
+ ServiceLevelAgreementProperty(
65
+ property="retention", value=data_contract_spec.servicelevels.retention.period
66
+ )
67
+ )
52
68
 
53
69
  if len(slas) > 0:
54
- odcs["slaProperties"] = slas
55
-
70
+ result.slaProperties = slas
56
71
  if data_contract_spec.info.contact is not None:
57
72
  support = []
58
73
  if data_contract_spec.info.contact.email is not None:
59
- support.append(
60
- {
61
- "channel": "email",
62
- "url": "mailto:" + data_contract_spec.info.contact.email,
63
- }
64
- )
74
+ support.append(Support(channel="email", url="mailto:" + data_contract_spec.info.contact.email))
65
75
  if data_contract_spec.info.contact.url is not None:
66
- support.append(
67
- {
68
- "channel": "other",
69
- "url": data_contract_spec.info.contact.url,
70
- }
71
- )
76
+ support.append(Support(channel="other", url=data_contract_spec.info.contact.url))
72
77
  if len(support) > 0:
73
- odcs["support"] = support
74
-
78
+ result.support = support
75
79
  if data_contract_spec.servers is not None and len(data_contract_spec.servers) > 0:
76
80
  servers = []
77
81
 
78
82
  for server_key, server_value in data_contract_spec.servers.items():
79
- server_dict = {}
80
- server_dict["server"] = server_key
81
- if server_value.type is not None:
82
- server_dict["type"] = server_value.type
83
+ server = Server(server=server_key, type=server_value.type or "")
84
+
85
+ # Set all the attributes that are not None
83
86
  if server_value.environment is not None:
84
- server_dict["environment"] = server_value.environment
87
+ server.environment = server_value.environment
85
88
  if server_value.account is not None:
86
- server_dict["account"] = server_value.account
89
+ server.account = server_value.account
87
90
  if server_value.database is not None:
88
- server_dict["database"] = server_value.database
91
+ server.database = server_value.database
89
92
  if server_value.schema_ is not None:
90
- server_dict["schema"] = server_value.schema_
93
+ server.schema_ = server_value.schema_
91
94
  if server_value.format is not None:
92
- server_dict["format"] = server_value.format
95
+ server.format = server_value.format
93
96
  if server_value.project is not None:
94
- server_dict["project"] = server_value.project
97
+ server.project = server_value.project
95
98
  if server_value.dataset is not None:
96
- server_dict["dataset"] = server_value.dataset
99
+ server.dataset = server_value.dataset
97
100
  if server_value.path is not None:
98
- server_dict["path"] = server_value.path
101
+ server.path = server_value.path
99
102
  if server_value.delimiter is not None:
100
- server_dict["delimiter"] = server_value.delimiter
103
+ server.delimiter = server_value.delimiter
101
104
  if server_value.endpointUrl is not None:
102
- server_dict["endpointUrl"] = server_value.endpointUrl
105
+ server.endpointUrl = server_value.endpointUrl
103
106
  if server_value.location is not None:
104
- server_dict["location"] = server_value.location
107
+ server.location = server_value.location
105
108
  if server_value.host is not None:
106
- server_dict["host"] = server_value.host
109
+ server.host = server_value.host
107
110
  if server_value.port is not None:
108
- server_dict["port"] = server_value.port
111
+ server.port = server_value.port
109
112
  if server_value.catalog is not None:
110
- server_dict["catalog"] = server_value.catalog
113
+ server.catalog = server_value.catalog
111
114
  if server_value.topic is not None:
112
- server_dict["topic"] = server_value.topic
115
+ server.topic = server_value.topic
113
116
  if server_value.http_path is not None:
114
- server_dict["http_path"] = server_value.http_path
117
+ server.http_path = server_value.http_path
115
118
  if server_value.token is not None:
116
- server_dict["token"] = server_value.token
119
+ server.token = server_value.token
117
120
  if server_value.driver is not None:
118
- server_dict["driver"] = server_value.driver
121
+ server.driver = server_value.driver
122
+
119
123
  if server_value.roles is not None:
120
- server_dict["roles"] = [
121
- {"name": role.name, "description": role.description} for role in server_value.roles
122
- ]
123
- servers.append(server_dict)
124
+ server.roles = [Role(role=role.name, description=role.description) for role in server_value.roles]
124
125
 
125
- if len(servers) > 0:
126
- odcs["servers"] = servers
126
+ servers.append(server)
127
127
 
128
- odcs["customProperties"] = []
128
+ if len(servers) > 0:
129
+ result.servers = servers
130
+ custom_properties = []
131
+ if data_contract_spec.info.owner is not None:
132
+ custom_properties.append(CustomProperty(property="owner", value=data_contract_spec.info.owner))
129
133
  if data_contract_spec.info.model_extra is not None:
130
134
  for key, value in data_contract_spec.info.model_extra.items():
131
- odcs["customProperties"].append({"property": key, "value": value})
132
- if len(odcs["customProperties"]) == 0:
133
- del odcs["customProperties"]
135
+ custom_properties.append(CustomProperty(property=key, value=value))
136
+ if len(custom_properties) > 0:
137
+ result.customProperties = custom_properties
138
+ return result
134
139
 
135
- return yaml.dump(odcs, indent=2, sort_keys=False, allow_unicode=True)
136
140
 
141
+ def to_odcs_schema(model_key, model_value: Model) -> SchemaObject:
142
+ schema_obj = SchemaObject(
143
+ name=model_key, physicalName=model_key, logicalType="object", physicalType=model_value.type
144
+ )
137
145
 
138
- def to_odcs_schema(model_key, model_value: Model) -> dict:
139
- odcs_table = {
140
- "name": model_key,
141
- "physicalName": model_key,
142
- "logicalType": "object",
143
- "physicalType": model_value.type,
144
- }
145
146
  if model_value.description is not None:
146
- odcs_table["description"] = model_value.description
147
+ schema_obj.description = model_value.description
148
+
147
149
  properties = to_properties(model_value.fields)
148
150
  if properties:
149
- odcs_table["properties"] = properties
151
+ schema_obj.properties = properties
150
152
 
151
153
  model_quality = to_odcs_quality_list(model_value.quality)
152
154
  if len(model_quality) > 0:
153
- odcs_table["quality"] = model_quality
155
+ schema_obj.quality = model_quality
154
156
 
155
- odcs_table["customProperties"] = []
157
+ custom_properties = []
156
158
  if model_value.model_extra is not None:
157
159
  for key, value in model_value.model_extra.items():
158
- odcs_table["customProperties"].append({"property": key, "value": value})
159
- if len(odcs_table["customProperties"]) == 0:
160
- del odcs_table["customProperties"]
160
+ custom_properties.append(CustomProperty(property=key, value=value))
161
+
162
+ if len(custom_properties) > 0:
163
+ schema_obj.customProperties = custom_properties
161
164
 
162
- return odcs_table
165
+ return schema_obj
163
166
 
164
167
 
165
168
  def to_properties(fields: Dict[str, Field]) -> list:
@@ -197,82 +200,119 @@ def to_logical_type(type: str) -> str | None:
197
200
  return "array"
198
201
  if type.lower() in ["array"]:
199
202
  return "array"
203
+ if type.lower() in ["variant"]:
204
+ return "variant"
200
205
  if type.lower() in ["null"]:
201
206
  return None
202
207
  return None
203
208
 
204
209
 
205
- def to_physical_type(type: str) -> str | None:
206
- # TODO: to we need to do a server mapping here?
207
- return type
210
+ def to_physical_type(config: Dict[str, Any]) -> str | None:
211
+ if config is None:
212
+ return None
213
+ if "postgresType" in config:
214
+ return config["postgresType"]
215
+ elif "bigqueryType" in config:
216
+ return config["bigqueryType"]
217
+ elif "snowflakeType" in config:
218
+ return config["snowflakeType"]
219
+ elif "redshiftType" in config:
220
+ return config["redshiftType"]
221
+ elif "sqlserverType" in config:
222
+ return config["sqlserverType"]
223
+ elif "databricksType" in config:
224
+ return config["databricksType"]
225
+ elif "physicalType" in config:
226
+ return config["physicalType"]
227
+ return None
208
228
 
209
229
 
210
- def to_property(field_name: str, field: Field) -> dict:
211
- property = {"name": field_name}
230
+ def to_property(field_name: str, field: Field) -> SchemaProperty:
231
+ property = SchemaProperty(name=field_name)
232
+
233
+ if field.fields:
234
+ properties = []
235
+ for field_name_, field_ in field.fields.items():
236
+ property_ = to_property(field_name_, field_)
237
+ properties.append(property_)
238
+ property.properties = properties
239
+
240
+ if field.items:
241
+ items = to_property(field_name, field.items)
242
+ items.name = None # Clear the name for items
243
+ property.items = items
244
+
212
245
  if field.title is not None:
213
- property["businessName"] = field.title
246
+ property.businessName = field.title
247
+
214
248
  if field.type is not None:
215
- property["logicalType"] = to_logical_type(field.type)
216
- property["physicalType"] = to_physical_type(field.type)
249
+ property.logicalType = to_logical_type(field.type)
250
+ property.physicalType = to_physical_type(field.config) or field.type
251
+
217
252
  if field.description is not None:
218
- property["description"] = field.description
253
+ property.description = field.description
254
+
219
255
  if field.required is not None:
220
- property["nullable"] = not field.required
256
+ property.required = field.required
257
+
221
258
  if field.unique is not None:
222
- property["unique"] = field.unique
259
+ property.unique = field.unique
260
+
223
261
  if field.classification is not None:
224
- property["classification"] = field.classification
262
+ property.classification = field.classification
263
+
225
264
  if field.examples is not None:
226
- property["examples"] = field.examples
265
+ property.examples = field.examples.copy()
266
+
227
267
  if field.example is not None:
228
- property["examples"] = [field.example]
268
+ property.examples = [field.example]
269
+
229
270
  if field.primaryKey is not None and field.primaryKey:
230
- property["primaryKey"] = field.primaryKey
231
- property["primaryKeyPosition"] = 1
271
+ property.primaryKey = field.primaryKey
272
+ property.primaryKeyPosition = 1
273
+
232
274
  if field.primary is not None and field.primary:
233
- property["primaryKey"] = field.primary
234
- property["primaryKeyPosition"] = 1
275
+ property.primaryKey = field.primary
276
+ property.primaryKeyPosition = 1
235
277
 
236
- property["customProperties"] = []
278
+ custom_properties = []
237
279
  if field.model_extra is not None:
238
280
  for key, value in field.model_extra.items():
239
- property["customProperties"].append({"property": key, "value": value})
281
+ custom_properties.append(CustomProperty(property=key, value=value))
282
+
240
283
  if field.pii is not None:
241
- property["customProperties"].append({"property": "pii", "value": field.pii})
242
- if property.get("customProperties") is not None and len(property["customProperties"]) == 0:
243
- del property["customProperties"]
284
+ custom_properties.append(CustomProperty(property="pii", value=field.pii))
285
+
286
+ if len(custom_properties) > 0:
287
+ property.customProperties = custom_properties
244
288
 
245
- property["tags"] = []
246
- if field.tags is not None:
247
- property["tags"].extend(field.tags)
248
- if not property["tags"]:
249
- del property["tags"]
289
+ if field.tags is not None and len(field.tags) > 0:
290
+ property.tags = field.tags
250
291
 
251
- property["logicalTypeOptions"] = {}
292
+ logical_type_options = {}
252
293
  if field.minLength is not None:
253
- property["logicalTypeOptions"]["minLength"] = field.minLength
294
+ logical_type_options["minLength"] = field.minLength
254
295
  if field.maxLength is not None:
255
- property["logicalTypeOptions"]["maxLength"] = field.maxLength
296
+ logical_type_options["maxLength"] = field.maxLength
256
297
  if field.pattern is not None:
257
- property["logicalTypeOptions"]["pattern"] = field.pattern
298
+ logical_type_options["pattern"] = field.pattern
258
299
  if field.minimum is not None:
259
- property["logicalTypeOptions"]["minimum"] = field.minimum
300
+ logical_type_options["minimum"] = field.minimum
260
301
  if field.maximum is not None:
261
- property["logicalTypeOptions"]["maximum"] = field.maximum
302
+ logical_type_options["maximum"] = field.maximum
262
303
  if field.exclusiveMinimum is not None:
263
- property["logicalTypeOptions"]["exclusiveMinimum"] = field.exclusiveMinimum
304
+ logical_type_options["exclusiveMinimum"] = field.exclusiveMinimum
264
305
  if field.exclusiveMaximum is not None:
265
- property["logicalTypeOptions"]["exclusiveMaximum"] = field.exclusiveMaximum
266
- if property["logicalTypeOptions"] == {}:
267
- del property["logicalTypeOptions"]
306
+ logical_type_options["exclusiveMaximum"] = field.exclusiveMaximum
307
+
308
+ if logical_type_options:
309
+ property.logicalTypeOptions = logical_type_options
268
310
 
269
311
  if field.quality is not None:
270
312
  quality_list = field.quality
271
313
  quality_property = to_odcs_quality_list(quality_list)
272
314
  if len(quality_property) > 0:
273
- property["quality"] = quality_property
274
-
275
- # todo enum
315
+ property.quality = quality_property
276
316
 
277
317
  return property
278
318
 
@@ -285,33 +325,35 @@ def to_odcs_quality_list(quality_list):
285
325
 
286
326
 
287
327
  def to_odcs_quality(quality):
288
- quality_dict = {"type": quality.type}
328
+ quality_obj = DataQuality(type=quality.type)
329
+
289
330
  if quality.description is not None:
290
- quality_dict["description"] = quality.description
331
+ quality_obj.description = quality.description
291
332
  if quality.query is not None:
292
- quality_dict["query"] = quality.query
333
+ quality_obj.query = quality.query
293
334
  # dialect is not supported in v3.0.0
294
335
  if quality.mustBe is not None:
295
- quality_dict["mustBe"] = quality.mustBe
336
+ quality_obj.mustBe = quality.mustBe
296
337
  if quality.mustNotBe is not None:
297
- quality_dict["mustNotBe"] = quality.mustNotBe
338
+ quality_obj.mustNotBe = quality.mustNotBe
298
339
  if quality.mustBeGreaterThan is not None:
299
- quality_dict["mustBeGreaterThan"] = quality.mustBeGreaterThan
340
+ quality_obj.mustBeGreaterThan = quality.mustBeGreaterThan
300
341
  if quality.mustBeGreaterThanOrEqualTo is not None:
301
- quality_dict["mustBeGreaterThanOrEqualTo"] = quality.mustBeGreaterThanOrEqualTo
342
+ quality_obj.mustBeGreaterOrEqualTo = quality.mustBeGreaterThanOrEqualTo
302
343
  if quality.mustBeLessThan is not None:
303
- quality_dict["mustBeLessThan"] = quality.mustBeLessThan
344
+ quality_obj.mustBeLessThan = quality.mustBeLessThan
304
345
  if quality.mustBeLessThanOrEqualTo is not None:
305
- quality_dict["mustBeLessThanOrEqualTo"] = quality.mustBeLessThanOrEqualTo
346
+ quality_obj.mustBeLessOrEqualTo = quality.mustBeLessThanOrEqualTo
306
347
  if quality.mustBeBetween is not None:
307
- quality_dict["mustBeBetween"] = quality.mustBeBetween
348
+ quality_obj.mustBeBetween = quality.mustBeBetween
308
349
  if quality.mustNotBeBetween is not None:
309
- quality_dict["mustNotBeBetween"] = quality.mustNotBeBetween
350
+ quality_obj.mustNotBeBetween = quality.mustNotBeBetween
310
351
  if quality.engine is not None:
311
- quality_dict["engine"] = quality.engine
352
+ quality_obj.engine = quality.engine
312
353
  if quality.implementation is not None:
313
- quality_dict["implementation"] = quality.implementation
314
- return quality_dict
354
+ quality_obj.implementation = quality.implementation
355
+
356
+ return quality_obj
315
357
 
316
358
 
317
359
  def to_status(status):