datacontract-cli 0.10.8__py3-none-any.whl → 0.10.10__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 (42) hide show
  1. datacontract/catalog/catalog.py +4 -2
  2. datacontract/cli.py +36 -18
  3. datacontract/data_contract.py +13 -53
  4. datacontract/engines/soda/check_soda_execute.py +10 -2
  5. datacontract/engines/soda/connections/duckdb.py +32 -12
  6. datacontract/engines/soda/connections/trino.py +26 -0
  7. datacontract/export/avro_converter.py +1 -1
  8. datacontract/export/exporter.py +3 -2
  9. datacontract/export/exporter_factory.py +132 -39
  10. datacontract/export/jsonschema_converter.py +7 -7
  11. datacontract/export/sodacl_converter.py +17 -12
  12. datacontract/export/spark_converter.py +211 -0
  13. datacontract/export/sql_type_converter.py +28 -0
  14. datacontract/imports/avro_importer.py +149 -7
  15. datacontract/imports/bigquery_importer.py +17 -0
  16. datacontract/imports/dbt_importer.py +117 -0
  17. datacontract/imports/glue_importer.py +116 -33
  18. datacontract/imports/importer.py +34 -0
  19. datacontract/imports/importer_factory.py +90 -0
  20. datacontract/imports/jsonschema_importer.py +14 -3
  21. datacontract/imports/odcs_importer.py +8 -0
  22. datacontract/imports/spark_importer.py +134 -0
  23. datacontract/imports/sql_importer.py +8 -0
  24. datacontract/imports/unity_importer.py +23 -9
  25. datacontract/integration/publish_datamesh_manager.py +10 -5
  26. datacontract/lint/resolve.py +87 -21
  27. datacontract/lint/schema.py +24 -4
  28. datacontract/model/data_contract_specification.py +37 -4
  29. datacontract/templates/datacontract.html +18 -3
  30. datacontract/templates/index.html +1 -1
  31. datacontract/templates/partials/datacontract_information.html +20 -0
  32. datacontract/templates/partials/datacontract_terms.html +7 -0
  33. datacontract/templates/partials/definition.html +9 -1
  34. datacontract/templates/partials/model_field.html +23 -6
  35. datacontract/templates/partials/server.html +49 -16
  36. datacontract/templates/style/output.css +42 -0
  37. {datacontract_cli-0.10.8.dist-info → datacontract_cli-0.10.10.dist-info}/METADATA +310 -122
  38. {datacontract_cli-0.10.8.dist-info → datacontract_cli-0.10.10.dist-info}/RECORD +42 -36
  39. {datacontract_cli-0.10.8.dist-info → datacontract_cli-0.10.10.dist-info}/WHEEL +1 -1
  40. {datacontract_cli-0.10.8.dist-info → datacontract_cli-0.10.10.dist-info}/LICENSE +0 -0
  41. {datacontract_cli-0.10.8.dist-info → datacontract_cli-0.10.10.dist-info}/entry_points.txt +0 -0
  42. {datacontract_cli-0.10.8.dist-info → datacontract_cli-0.10.10.dist-info}/top_level.txt +0 -0
@@ -25,7 +25,7 @@ def resolve_data_contract(
25
25
  data_contract_location, schema_location, inline_definitions, inline_quality
26
26
  )
27
27
  elif data_contract_str is not None:
28
- return resolve_data_contract_from_str(data_contract_str, schema_location, inline_definitions, inline_quality)
28
+ return _resolve_data_contract_from_str(data_contract_str, schema_location, inline_definitions, inline_quality)
29
29
  elif data_contract is not None:
30
30
  return data_contract
31
31
  else:
@@ -45,7 +45,7 @@ def resolve_data_contract_from_location(
45
45
  data_contract_str = fetch_resource(location)
46
46
  else:
47
47
  data_contract_str = read_file(location)
48
- return resolve_data_contract_from_str(data_contract_str, schema_location, inline_definitions, inline_quality)
48
+ return _resolve_data_contract_from_str(data_contract_str, schema_location, inline_definitions, inline_quality)
49
49
 
50
50
 
51
51
  def inline_definitions_into_data_contract(spec: DataContractSpecification):
@@ -55,23 +55,53 @@ def inline_definitions_into_data_contract(spec: DataContractSpecification):
55
55
  if not field.ref and not field.ref_obj:
56
56
  continue
57
57
 
58
- definition = resolve_definition_ref(field.ref, spec.definitions)
58
+ definition = _resolve_definition_ref(field.ref, spec)
59
59
  field.ref_obj = definition
60
60
 
61
61
  for field_name in field.model_fields.keys():
62
62
  if field_name in definition.model_fields_set and field_name not in field.model_fields_set:
63
63
  setattr(field, field_name, getattr(definition, field_name))
64
+ # extras
65
+ for extra_field_name, extra_field_value in definition.model_extra.items():
66
+ if extra_field_name not in field.model_extra.keys():
67
+ setattr(field, extra_field_name, extra_field_value)
64
68
 
65
69
 
66
- def resolve_definition_ref(ref, definitions) -> Definition:
67
- if ref.startswith("http://") or ref.startswith("https://"):
68
- definition_str = fetch_resource(ref)
69
- definition_dict = to_yaml(definition_str)
70
- return Definition(**definition_dict)
70
+ def _resolve_definition_ref(ref, spec) -> Definition:
71
+ logging.info(f"Resolving definition ref {ref}")
71
72
 
72
- elif ref.startswith("#/definitions/"):
73
- definition_name = ref.split("#/definitions/")[1]
74
- return definitions[definition_name]
73
+ if "#" in ref:
74
+ path, definition_path = ref.split("#")
75
+ else:
76
+ path, definition_path = ref, None
77
+
78
+ if path.startswith("http://") or path.startswith("https://"):
79
+ logging.info(f"Resolving definition url {path}")
80
+
81
+ definition_str = fetch_resource(path)
82
+ definition_dict = _to_yaml(definition_str)
83
+ definition = Definition(**definition_dict)
84
+ if definition_path is not None:
85
+ return _find_by_path_in_definition(definition_path, definition)
86
+ else:
87
+ return definition
88
+ elif path.startswith("file://"):
89
+ logging.info(f"Resolving definition file path {path}")
90
+
91
+ path = path.replace("file://", "")
92
+ definition_str = _fetch_file(path)
93
+ definition_dict = _to_yaml(definition_str)
94
+ definition = Definition(**definition_dict)
95
+ if definition_path is not None:
96
+ return _find_by_path_in_definition(definition_path, definition)
97
+ else:
98
+ return definition
99
+ elif ref.startswith("#"):
100
+ logging.info(f"Resolving definition local path {path}")
101
+
102
+ definition_path = ref[1:]
103
+
104
+ return _find_by_path_in_spec(definition_path, spec)
75
105
  else:
76
106
  raise DataContractException(
77
107
  type="lint",
@@ -82,7 +112,43 @@ def resolve_definition_ref(ref, definitions) -> Definition:
82
112
  )
83
113
 
84
114
 
85
- def resolve_quality_ref(quality: Quality):
115
+ def _find_by_path_in_spec(definition_path: str, spec: DataContractSpecification):
116
+ path_elements = definition_path.split("/")
117
+ definition = spec.definitions[path_elements[2]]
118
+ definition = _find_subfield_in_definition(definition, path_elements[3:])
119
+ return definition
120
+
121
+
122
+ def _find_by_path_in_definition(definition_path: str, definition: Definition):
123
+ if definition_path == "" or definition_path == "/":
124
+ return definition
125
+
126
+ path_elements = definition_path.split("/")
127
+ return _find_subfield_in_definition(definition, path_elements[1:])
128
+
129
+
130
+ def _find_subfield_in_definition(definition: Definition, path_elements):
131
+ while len(path_elements) > 0 and path_elements[0] == "fields":
132
+ definition = definition.fields[path_elements[1]]
133
+ path_elements = path_elements[2:]
134
+
135
+ return definition
136
+
137
+
138
+ def _fetch_file(path) -> str:
139
+ if not os.path.exists(path):
140
+ raise DataContractException(
141
+ type="export",
142
+ result="failed",
143
+ name="Check that data contract definition is valid",
144
+ reason=f"Cannot resolve reference {path}",
145
+ engine="datacontract",
146
+ )
147
+ with open(path, "r") as file:
148
+ return file.read()
149
+
150
+
151
+ def _resolve_quality_ref(quality: Quality):
86
152
  """
87
153
  Return the content of a ref file path
88
154
  @param quality data contract quality specification
@@ -91,13 +157,13 @@ def resolve_quality_ref(quality: Quality):
91
157
  specification = quality.specification
92
158
  if quality.type == "great-expectations":
93
159
  for model, model_quality in specification.items():
94
- specification[model] = get_quality_ref_file(model_quality)
160
+ specification[model] = _get_quality_ref_file(model_quality)
95
161
  else:
96
162
  if "$ref" in specification:
97
- quality.specification = get_quality_ref_file(specification)
163
+ quality.specification = _get_quality_ref_file(specification)
98
164
 
99
165
 
100
- def get_quality_ref_file(quality_spec: str | object) -> str | object:
166
+ def _get_quality_ref_file(quality_spec: str | object) -> str | object:
101
167
  """
102
168
  Get the file associated with a quality reference
103
169
  @param quality_spec quality specification
@@ -118,23 +184,23 @@ def get_quality_ref_file(quality_spec: str | object) -> str | object:
118
184
  return quality_spec
119
185
 
120
186
 
121
- def resolve_data_contract_from_str(
187
+ def _resolve_data_contract_from_str(
122
188
  data_contract_str, schema_location: str = None, inline_definitions: bool = False, inline_quality: bool = False
123
189
  ) -> DataContractSpecification:
124
- data_contract_yaml_dict = to_yaml(data_contract_str)
125
- validate(data_contract_yaml_dict, schema_location)
190
+ data_contract_yaml_dict = _to_yaml(data_contract_str)
191
+ _validate(data_contract_yaml_dict, schema_location)
126
192
 
127
193
  spec = DataContractSpecification(**data_contract_yaml_dict)
128
194
 
129
195
  if inline_definitions:
130
196
  inline_definitions_into_data_contract(spec)
131
197
  if spec.quality and inline_quality:
132
- resolve_quality_ref(spec.quality)
198
+ _resolve_quality_ref(spec.quality)
133
199
 
134
200
  return spec
135
201
 
136
202
 
137
- def to_yaml(data_contract_str):
203
+ def _to_yaml(data_contract_str):
138
204
  try:
139
205
  yaml_dict = yaml.safe_load(data_contract_str)
140
206
  return yaml_dict
@@ -149,7 +215,7 @@ def to_yaml(data_contract_str):
149
215
  )
150
216
 
151
217
 
152
- def validate(data_contract_yaml, schema_location: str = None):
218
+ def _validate(data_contract_yaml, schema_location: str = None):
153
219
  schema = fetch_schema(schema_location)
154
220
  try:
155
221
  fastjsonschema.validate(schema, data_contract_yaml)
@@ -1,18 +1,37 @@
1
1
  import json
2
2
  import os
3
+ from typing import Dict, Any
3
4
 
4
5
  import requests
5
6
 
6
7
  from datacontract.model.exceptions import DataContractException
7
8
 
8
9
 
9
- def fetch_schema(location: str = None):
10
+ def fetch_schema(location: str = None) -> Dict[str, Any]:
11
+ """
12
+ Fetch and return a JSON schema from a given location.
13
+
14
+ This function retrieves a JSON schema either from a URL or a local file path.
15
+ If no location is provided, it defaults to the DataContract schema URL.
16
+
17
+ Args:
18
+ location: The URL or file path of the schema.
19
+
20
+ Returns:
21
+ The JSON schema as a dictionary.
22
+
23
+ Raises:
24
+ DataContractException: If the specified local file does not exist.
25
+ requests.RequestException: If there's an error fetching the schema from a URL.
26
+ json.JSONDecodeError: If there's an error decoding the JSON schema.
27
+
28
+ """
10
29
  if location is None:
11
30
  location = "https://datacontract.com/datacontract.schema.json"
12
31
 
13
32
  if location.startswith("http://") or location.startswith("https://"):
14
33
  response = requests.get(location)
15
- return response.json()
34
+ schema = response.json()
16
35
  else:
17
36
  if not os.path.exists(location):
18
37
  raise DataContractException(
@@ -23,5 +42,6 @@ def fetch_schema(location: str = None):
23
42
  result="error",
24
43
  )
25
44
  with open(location, "r") as file:
26
- file_content = file.read()
27
- return json.loads(file_content)
45
+ schema = json.load(file)
46
+
47
+ return schema
@@ -10,9 +10,15 @@ class Contact(pyd.BaseModel):
10
10
  url: str = None
11
11
  email: str = None
12
12
 
13
+ model_config = pyd.ConfigDict(
14
+ extra="allow",
15
+ )
16
+
13
17
 
14
18
  class Server(pyd.BaseModel):
15
19
  type: str = None
20
+ description: str = None
21
+ environment: str = None
16
22
  format: str = None
17
23
  project: str = None
18
24
  dataset: str = None
@@ -33,6 +39,10 @@ class Server(pyd.BaseModel):
33
39
  outputPortId: str = None
34
40
  driver: str = None
35
41
 
42
+ model_config = pyd.ConfigDict(
43
+ extra="allow",
44
+ )
45
+
36
46
 
37
47
  class Terms(pyd.BaseModel):
38
48
  usage: str = None
@@ -41,6 +51,10 @@ class Terms(pyd.BaseModel):
41
51
  noticePeriod: str = None
42
52
  description: str = None
43
53
 
54
+ model_config = pyd.ConfigDict(
55
+ extra="allow",
56
+ )
57
+
44
58
 
45
59
  class Definition(pyd.BaseModel):
46
60
  domain: str = None
@@ -59,9 +73,15 @@ class Definition(pyd.BaseModel):
59
73
  exclusiveMaximum: int = None
60
74
  pii: bool = None
61
75
  classification: str = None
76
+ fields: Dict[str, "Definition"] = {}
62
77
  tags: List[str] = []
78
+ links: Dict[str, str] = {}
63
79
  example: str = None
64
80
 
81
+ model_config = pyd.ConfigDict(
82
+ extra="allow",
83
+ )
84
+
65
85
 
66
86
  class Field(pyd.BaseModel):
67
87
  ref: str = pyd.Field(default=None, alias="$ref")
@@ -85,19 +105,26 @@ class Field(pyd.BaseModel):
85
105
  exclusiveMaximum: int = None
86
106
  enum: List[str] = []
87
107
  tags: List[str] = []
108
+ links: Dict[str, str] = {}
88
109
  fields: Dict[str, "Field"] = {}
89
110
  items: "Field" = None
111
+ keys: "Field" = None
112
+ values: "Field" = None
90
113
  precision: int = None
91
114
  scale: int = None
92
115
  example: str = None
93
116
  config: Dict[str, Any] = None
94
117
 
118
+ model_config = pyd.ConfigDict(
119
+ extra="allow",
120
+ )
121
+
95
122
 
96
123
  class Model(pyd.BaseModel):
97
- description: str = None
98
- type: str = None
99
- namespace: str = None
100
- title: str = None
124
+ description: Optional[str] = None
125
+ type: Optional[str] = None
126
+ namespace: Optional[str] = None
127
+ title: Optional[str] = None
101
128
  fields: Dict[str, Field] = {}
102
129
  config: Dict[str, Any] = None
103
130
 
@@ -110,6 +137,10 @@ class Info(pyd.BaseModel):
110
137
  owner: str = None
111
138
  contact: Contact = None
112
139
 
140
+ model_config = pyd.ConfigDict(
141
+ extra="allow",
142
+ )
143
+
113
144
 
114
145
  class Example(pyd.BaseModel):
115
146
  type: str = None
@@ -191,6 +222,8 @@ class DataContractSpecification(pyd.BaseModel):
191
222
  examples: List[Example] = []
192
223
  quality: Quality = None
193
224
  servicelevels: Optional[ServiceLevel] = None
225
+ links: Dict[str, str] = {}
226
+ tags: List[str] = []
194
227
 
195
228
  @classmethod
196
229
  def from_file(cls, file):
@@ -41,6 +41,21 @@
41
41
  <div class="mt-1 flex flex-col sm:mt-0 sm:flex-row sm:flex-wrap sm:space-x-6">
42
42
  {{ datacontract.id }}
43
43
  </div>
44
+ <div class="mt-1 flex flex-col sm:mt-0 sm:flex-row sm:flex-wrap sm:space-x-6">
45
+ {% if datacontract.tags %}
46
+ <div class="mt-2 flex items-center text-sm text-gray-500 whitespace-nowrap">
47
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400">
48
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 005.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 009.568 3z" />
49
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 6h.008v.008H6V6z" />
50
+ </svg>
51
+ {% for tag in datacontract.tags %}
52
+ <span class="inline-flex items-center rounded-full bg-indigo-100 px-2.5 py-0.5 text-xs font-medium text-gray-800 mr-1">
53
+ <span>{{ tag }}</span>
54
+ </span>
55
+ {% endfor %}
56
+ </div>
57
+ {% endif %}
58
+ </div>
44
59
  </div>
45
60
  <div class="mt-5 flex lg:mt-0 lg:ml-4 gap-3 items-center">
46
61
  <button
@@ -129,14 +144,14 @@
129
144
  </div>
130
145
  {% endfor %}
131
146
  </section>
132
-
147
+
133
148
  {% if datacontract.definitions %}
134
149
  <section id="definitions">
135
150
  <div class="px-4 sm:px-0">
136
151
  <h1 class="text-base font-semibold leading-6 text-gray-900">Definitions</h1>
137
152
  <p class="text-sm text-gray-500">Domain specific definitions in the data contract</p>
138
153
  </div>
139
-
154
+
140
155
  {% for definition_name, definition in datacontract.definitions.items() %}
141
156
  {{ render_partial('partials/definition.html', definition_name = definition_name, definition = definition)}}
142
157
  {% endfor %}
@@ -235,7 +250,7 @@
235
250
  </div>
236
251
  <div class="mt-8 md:order-1 md:mt-0">
237
252
  <p class="text-center leading-5 text-gray-400">
238
- Supported with ❤️ by <a href="https://datamesh-manager.com" class="text-gray-400 hover:text-gray-500">Data Mesh Manager</a>
253
+ Supported with ❤️ by <a href="https://datacontract-manager.com" class="text-gray-400 hover:text-gray-500">Data Contract Manager</a>
239
254
  </p>
240
255
  </div>
241
256
  </div>
@@ -190,7 +190,7 @@
190
190
  </div>
191
191
  <div class="mt-8 md:order-1 md:mt-0">
192
192
  <p class="text-center leading-5 text-gray-400">
193
- Supported with ❤️ by <a href="https://datamesh-manager.com" class="text-gray-400 hover:text-gray-500">Data Mesh Manager</a>
193
+ Supported with ❤️ by <a href="https://datacontract-manager.com" class="text-gray-400 hover:text-gray-500">Data Contract Manager</a>
194
194
  </p>
195
195
  </div>
196
196
  </div>
@@ -6,6 +6,19 @@
6
6
 
7
7
  <div class="px-4 py-5 sm:px-6">
8
8
 
9
+ {% if datacontract.links %}
10
+ <div class="flex flex-wrap gap-3">
11
+ {% for name, href in datacontract.links.items() %}
12
+ <a href="{{ href }}" class="flex flex-col text-center rounded-md bg-white px-2 py-2 text-sm font-medium text-gray-500 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 mb-6" target="_blank" style="min-width: 100px">
13
+ <div class="mx-auto w-8 h-8 my-2">
14
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M23.91,5.29a3.13,3.13,0,0,0-.72-1.23.35.35,0,0,0-.49,0,.34.34,0,0,0,0,.48,2.66,2.66,0,0,1,.54,1,1.56,1.56,0,0,1-.1,1,19.42,19.42,0,0,1-3.49,3.64,9.62,9.62,0,0,1-2.74,1.8,5.51,5.51,0,0,1-1.71.45.91.91,0,0,1-.73-.3c-.2-.22-.52-.52-.82-.83.66-.65,1.32-1.35,2.08-2.09a.3.3,0,0,0,0-.43.31.31,0,0,0-.43,0c-.84.6-1.6,1.14-2.32,1.69a7.18,7.18,0,0,1-.32-.81,5.23,5.23,0,0,1-.23-1,3.23,3.23,0,0,1,.94-2.5,31.64,31.64,0,0,1,3.65-3,5.24,5.24,0,0,1,1.63-1,2.56,2.56,0,0,1,1.77,0,2.08,2.08,0,0,1,.65.41,2.31,2.31,0,0,1,.49.61.3.3,0,0,0,.54-.26A2.78,2.78,0,0,0,21.52,2a2.91,2.91,0,0,0-.84-.57,3.23,3.23,0,0,0-1.81-.18,5.64,5.64,0,0,0-2.38,1.13,30.94,30.94,0,0,0-3.91,3,4.26,4.26,0,0,0-1.29,3.34,6.18,6.18,0,0,0,.27,1.23A11.36,11.36,0,0,0,12,11.06l0,.07-1.21,1c-.76.67-1.5,1.37-2.38,2.07a.34.34,0,0,0-.09.48.35.35,0,0,0,.48.09c1-.58,1.81-1.14,2.62-1.74.36-.26.71-.54,1-.83l.24-.21.82.82a2,2,0,0,0,1.6.67,6.72,6.72,0,0,0,2-.57,10.52,10.52,0,0,0,3-2.1,19.88,19.88,0,0,0,3.58-4.07A2.3,2.3,0,0,0,23.91,5.29Z" fill="#191919" fill-rule="evenodd"></path><path d="M11.86,14.07a.34.34,0,0,0,0,.48.81.81,0,0,1,.29.57,1.28,1.28,0,0,1-.27.65,29.38,29.38,0,0,1-3.6,3.65,9.07,9.07,0,0,1-2.73,1.79,5.43,5.43,0,0,1-1.71.45,1,1,0,0,1-.73-.31c-.22-.25-.6-.61-.93-1a2.71,2.71,0,0,1-.43-.56,9.71,9.71,0,0,1-.39-1,4.53,4.53,0,0,1-.22-1,3.21,3.21,0,0,1,1-2.51,30.47,30.47,0,0,1,3.66-3,5.14,5.14,0,0,1,1.65-1,2.62,2.62,0,0,1,1.79,0,.3.3,0,0,0,.4-.16.31.31,0,0,0-.17-.4,3.16,3.16,0,0,0-1.79-.19A5.63,5.63,0,0,0,5.27,11.7a30.41,30.41,0,0,0-3.93,2.94A4.21,4.21,0,0,0,0,18a6,6,0,0,0,.19,1A9.53,9.53,0,0,0,.7,20.3a3.53,3.53,0,0,0,.47.69c.36.41.83.84,1.09,1.13a2,2,0,0,0,1.59.68,6.21,6.21,0,0,0,2.05-.56,10,10,0,0,0,3-2.1,30.27,30.27,0,0,0,3.6-4,2,2,0,0,0,.36-1.07,1.46,1.46,0,0,0-.53-1A.35.35,0,0,0,11.86,14.07Z" fill="#191919" fill-rule="evenodd"></path><path d="M6.43,15.15l1.16-.79A.31.31,0,0,0,7.73,14a.3.3,0,0,0-.41-.13L6,14.49a7.39,7.39,0,0,0-2.3,1.83,2.35,2.35,0,0,0-.38,2.26,1,1,0,0,0,1,.68,2.41,2.41,0,0,0,1.17-.43,9.29,9.29,0,0,0,.91-.75c.66-.6,1.25-1.26,1.86-1.85a.35.35,0,0,0,0-.49.36.36,0,0,0-.49,0c-.57.45-1.13.95-1.73,1.42a10.58,10.58,0,0,1-1.17.81c-1.23.74-.36-.89-.3-1A7.43,7.43,0,0,1,6.43,15.15Z" fill="#0c6fff" fill-rule="evenodd"></path><path d="M19,7a6.13,6.13,0,0,1-1.26,1.28c-.21.15-.5.38-.78.56a1.9,1.9,0,0,1-.49.25.34.34,0,0,0-.25.41.34.34,0,0,0,.41.26,2.47,2.47,0,0,0,.55-.2c.35-.18.73-.44,1-.61a6.9,6.9,0,0,0,1.57-1.33A8.69,8.69,0,0,0,20.9,6a3.7,3.7,0,0,0,.43-1.13A1,1,0,0,0,21,4a1.36,1.36,0,0,0-.89-.39,3,3,0,0,0-1.08.17,6.4,6.4,0,0,0-1.63.82,15.32,15.32,0,0,0-2,1.7,4.54,4.54,0,0,0-.65.74,2.69,2.69,0,0,0-.31.61,3.5,3.5,0,0,0-.07.9.33.33,0,0,0,.17.24A.31.31,0,0,0,15,8.63c.18-.26-.17-.6.47-1.3A5.64,5.64,0,0,1,16,6.88a19,19,0,0,1,2-1.42,5.88,5.88,0,0,1,1.39-.61A1.87,1.87,0,0,1,20,4.73c.54,0,.12.31-.08.76A7.29,7.29,0,0,1,19,7Z" fill="#0c6fff" fill-rule="evenodd"></path></g></svg>
15
+ </div>
16
+ <div>{{ name }}</div>
17
+ </a>
18
+ {% endfor %}
19
+ </div>
20
+ {% endif %}
21
+
9
22
  <dl class="grid grid-cols-1 gap-x-4 gap-y-6 sm:grid-cols-2">
10
23
  <div class="sm:col-span-1">
11
24
  <dt class="text-sm font-medium text-gray-500">Title</dt>
@@ -42,6 +55,13 @@
42
55
  </div>
43
56
  {% endif %}
44
57
 
58
+ {% for key, value in datacontract.info.model_extra.items() %}
59
+ <div class="sm:col-span-1">
60
+ <dt class="text-sm font-medium text-gray-500">{{ key }}</dt>
61
+ <dd class="mt-1 text-sm text-gray-900">{{ value }}</dd>
62
+ </div>
63
+ {% endfor %}
64
+
45
65
  {% if datacontract.info.contact %}
46
66
  <div class="sm:col-span-1">
47
67
  <dt class="text-sm font-medium text-gray-500">Contact</dt>
@@ -39,6 +39,13 @@
39
39
  </div>
40
40
  {% endif %}
41
41
 
42
+ {% for key, value in datacontract.terms.model_extra.items() %}
43
+ <div class="sm:col-span-1">
44
+ <dt class="text-sm font-medium text-gray-500">{{ key }}</dt>
45
+ <dd class="mt-1 text-sm text-gray-900">{{ value }}</dd>
46
+ </div>
47
+ {% endfor %}
48
+
42
49
  </dl>
43
50
  </div>
44
51
  </div>
@@ -14,7 +14,7 @@
14
14
  </tr>
15
15
  </thead>
16
16
  <tbody class="divide-y divide-gray-200 bg-white">
17
- <tr>
17
+ <tr id="/definitions/{{ definition_name }}">
18
18
  <td class="whitespace-nowrap py-2 pl-4 pr-2 text-sm font-medium text-gray-900 sm:pl-6 w-2/12">
19
19
  <div class="py-2 text-sm">
20
20
  {% if definition.title %}
@@ -87,6 +87,14 @@
87
87
  {% if definition.pii %}
88
88
  <span class="inline-flex items-center rounded-md bg-yellow-50 px-1 py-1 text-xs font-medium text-yellow-600 ring-1 ring-inset ring-yellow-500/10 mr-1 mt-1">PII</span>
89
89
  {% endif %}
90
+ {% for key, value in definition.model_extra.items() %}
91
+ <span class="inline-flex items-center rounded-md bg-gray-50 px-1 py-1 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10 mr-1 mt-1">{{ key }}:{{ value }}</span>
92
+ {% endfor %}
93
+ {% if definition.links %}
94
+ {% for name,href in definition.links.items() %}
95
+ <a href="{{ href }}" class="inline-flex items-center px-1 py-1 mr-1 mt-1 text-sky-500 hover:text-gray-700 text-xs font-semibold">{{ name }}</a>
96
+ {% endfor %}
97
+ {% endif %}
90
98
  </div>
91
99
  </td>
92
100
 
@@ -14,9 +14,11 @@
14
14
  {% endif %}
15
15
  <div class="py-2 text-sm">
16
16
  {% if field.title %}
17
- <div>{{ field.title }}</div>
17
+ <span>{{ field.title }}</span><br>
18
18
  {% endif %}
19
- <div class="font-mono">{{ field_name }}</div>
19
+ <span class="font-mono flex">{{ field_name }}{% if field.ref %} <a href="{{ field.ref }}">
20
+ <svg title="Definition" class="mr-1.5 h-5 w-5 flex-shrink-0" xmlns="http://www.w3.org/2000/svg" viewBox="-0.75 -0.75 24 24"><defs></defs><path d="M3.046875 5.15625h16.40625s0.9375 0 0.9375 0.9375v10.3125s0 0.9375 -0.9375 0.9375H3.046875s-0.9375 0 -0.9375 -0.9375v-10.3125s0 -0.9375 0.9375 -0.9375" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"></path><path d="m12.568125 10.3125 4.6875 0" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"></path><path d="m12.568125 13.125 4.6875 0" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"></path><path d="M5.068124999999999 8.4375h4.6875v4.6875h-4.6875Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"></path></svg>
21
+ </a>{% endif %}</span>
20
22
  </div>
21
23
  </td>
22
24
  <td class="whitespace-nowrap px-1 py-2 text-sm text-gray-500 w-1/12">
@@ -83,15 +85,30 @@
83
85
  {% if field.pii %}
84
86
  <span class="inline-flex items-center rounded-md bg-yellow-50 px-1 py-1 text-xs font-medium text-yellow-600 ring-1 ring-inset ring-yellow-500/10 mr-1 mt-1">PII</span>
85
87
  {% endif %}
88
+ {% for key, value in field.model_extra.items() %}
89
+ <span class="inline-flex items-center rounded-md bg-gray-50 px-1 py-1 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10 mr-1 mt-1">{{ key }}:{{ value }}</span>
90
+ {% endfor %}
91
+ {% if field.links %}
92
+ {% for name,href in field.links.items() %}
93
+ <a href="{{ href }}" class="inline-flex items-center px-1 py-1 mr-1 mt-1 text-sky-500 hover:text-gray-700 text-xs font-semibold">{{ name }}</a>
94
+ {% endfor %}
95
+ {% endif %}
86
96
  </div>
87
97
  </td>
88
98
  </tr>
89
99
 
100
+ {% macro render_nested_partial(field_name, field, level) %}
101
+ {{ render_partial('partials/model_field.html', nested=True, field_name=field_name, field=field, level=level + 1) }}
102
+ <!-- Mark the end of the contained fields -->
103
+ <tr style="--tw-divide-y-reverse: 2"></tr>
104
+ {% endmacro %}
105
+
90
106
  {% if field.fields %}
91
107
  {% for field_name, field in field.fields.items() %}
92
- {{ render_partial('partials/model_field.html', nested = True, field_name=field_name, field = field, level = level + 1) }}
108
+ {{ render_nested_partial(field_name, field, level) }}
93
109
  {% endfor %}
94
- <!-- Mark the end of the contained fields -->
95
- <tr style="--tw-divide-y-reverse: 2">
96
- </tr>
110
+ {% endif %}
111
+
112
+ {% if field.items %}
113
+ {{ render_nested_partial("item", field.items, level) }}
97
114
  {% endif %}