datacontract-cli 0.10.8__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.

Files changed (36) hide show
  1. datacontract/catalog/catalog.py +4 -2
  2. datacontract/cli.py +29 -18
  3. datacontract/data_contract.py +9 -51
  4. datacontract/engines/soda/check_soda_execute.py +5 -0
  5. datacontract/engines/soda/connections/duckdb.py +28 -12
  6. datacontract/engines/soda/connections/trino.py +26 -0
  7. datacontract/export/exporter.py +3 -2
  8. datacontract/export/exporter_factory.py +132 -39
  9. datacontract/export/jsonschema_converter.py +7 -7
  10. datacontract/export/sodacl_converter.py +16 -11
  11. datacontract/export/spark_converter.py +211 -0
  12. datacontract/export/sql_type_converter.py +28 -0
  13. datacontract/imports/avro_importer.py +8 -0
  14. datacontract/imports/bigquery_importer.py +17 -0
  15. datacontract/imports/glue_importer.py +115 -32
  16. datacontract/imports/importer.py +29 -0
  17. datacontract/imports/importer_factory.py +72 -0
  18. datacontract/imports/jsonschema_importer.py +8 -0
  19. datacontract/imports/odcs_importer.py +8 -0
  20. datacontract/imports/sql_importer.py +8 -0
  21. datacontract/imports/unity_importer.py +23 -9
  22. datacontract/lint/resolve.py +22 -1
  23. datacontract/model/data_contract_specification.py +34 -4
  24. datacontract/templates/datacontract.html +17 -2
  25. datacontract/templates/partials/datacontract_information.html +20 -0
  26. datacontract/templates/partials/datacontract_terms.html +7 -0
  27. datacontract/templates/partials/definition.html +9 -1
  28. datacontract/templates/partials/model_field.html +23 -6
  29. datacontract/templates/partials/server.html +49 -16
  30. datacontract/templates/style/output.css +42 -0
  31. {datacontract_cli-0.10.8.dist-info → datacontract_cli-0.10.9.dist-info}/METADATA +203 -28
  32. {datacontract_cli-0.10.8.dist-info → datacontract_cli-0.10.9.dist-info}/RECORD +36 -32
  33. {datacontract_cli-0.10.8.dist-info → datacontract_cli-0.10.9.dist-info}/WHEEL +1 -1
  34. {datacontract_cli-0.10.8.dist-info → datacontract_cli-0.10.9.dist-info}/LICENSE +0 -0
  35. {datacontract_cli-0.10.8.dist-info → datacontract_cli-0.10.9.dist-info}/entry_points.txt +0 -0
  36. {datacontract_cli-0.10.8.dist-info → datacontract_cli-0.10.9.dist-info}/top_level.txt +0 -0
@@ -3,9 +3,24 @@ import requests
3
3
  import os
4
4
  import typing
5
5
 
6
+ from datacontract.imports.importer import Importer
6
7
  from datacontract.model.data_contract_specification import DataContractSpecification, Model, Field
7
8
  from datacontract.model.exceptions import DataContractException
8
9
 
10
+
11
+ class UnityImporter(Importer):
12
+ def import_source(
13
+ self, data_contract_specification: DataContractSpecification, source: str, import_args: dict
14
+ ) -> dict:
15
+ if source is not None:
16
+ data_contract_specification = import_unity_from_json(data_contract_specification, source)
17
+ else:
18
+ data_contract_specification = import_unity_from_api(
19
+ data_contract_specification, import_args.get("unity_table_full_name")
20
+ )
21
+ return data_contract_specification
22
+
23
+
9
24
  def import_unity_from_json(
10
25
  data_contract_specification: DataContractSpecification, source: str
11
26
  ) -> DataContractSpecification:
@@ -22,23 +37,21 @@ def import_unity_from_json(
22
37
  )
23
38
  return convert_unity_schema(data_contract_specification, unity_schema)
24
39
 
40
+
25
41
  def import_unity_from_api(
26
- data_contract_specification: DataContractSpecification,
27
- unity_table_full_name: typing.Optional[str] = None
42
+ data_contract_specification: DataContractSpecification, unity_table_full_name: typing.Optional[str] = None
28
43
  ) -> DataContractSpecification:
29
- databricks_instance = os.getenv('DATABRICKS_IMPORT_INSTANCE')
30
- access_token = os.getenv('DATABRICKS_IMPORT_ACCESS_TOKEN')
44
+ databricks_instance = os.getenv("DATABRICKS_IMPORT_INSTANCE")
45
+ access_token = os.getenv("DATABRICKS_IMPORT_ACCESS_TOKEN")
31
46
 
32
47
  if not databricks_instance or not access_token:
33
48
  print("Missing environment variables for Databricks instance or access token.")
34
49
  print("Both, $DATABRICKS_IMPORT_INSTANCE and $DATABRICKS_IMPORT_ACCESS_TOKEN must be set.")
35
50
  exit(1) # Exit if variables are not set
36
51
 
37
- api_url = f'{databricks_instance}/api/2.1/unity-catalog/tables/{unity_table_full_name}'
52
+ api_url = f"{databricks_instance}/api/2.1/unity-catalog/tables/{unity_table_full_name}"
38
53
 
39
- headers = {
40
- 'Authorization': f'Bearer {access_token}'
41
- }
54
+ headers = {"Authorization": f"Bearer {access_token}"}
42
55
  response = requests.get(api_url, headers=headers)
43
56
 
44
57
  if response.status_code != 200:
@@ -46,13 +59,14 @@ def import_unity_from_api(
46
59
  type="schema",
47
60
  name="Retrieve unity catalog schema",
48
61
  reason=f"Failed to retrieve unity catalog schema from databricks instance: {response.status_code} {response.text}",
49
- engine="datacontract"
62
+ engine="datacontract",
50
63
  )
51
64
 
52
65
  convert_unity_schema(data_contract_specification, response.json())
53
66
 
54
67
  return data_contract_specification
55
68
 
69
+
56
70
  def convert_unity_schema(
57
71
  data_contract_specification: DataContractSpecification, unity_schema: dict
58
72
  ) -> DataContractSpecification:
@@ -61,6 +61,10 @@ def inline_definitions_into_data_contract(spec: DataContractSpecification):
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
70
  def resolve_definition_ref(ref, definitions) -> Definition:
@@ -68,7 +72,11 @@ def resolve_definition_ref(ref, definitions) -> Definition:
68
72
  definition_str = fetch_resource(ref)
69
73
  definition_dict = to_yaml(definition_str)
70
74
  return Definition(**definition_dict)
71
-
75
+ elif ref.startswith("file://"):
76
+ path = ref.replace("file://", "")
77
+ definition_str = fetch_file(path)
78
+ definition_dict = to_yaml(definition_str)
79
+ return Definition(**definition_dict)
72
80
  elif ref.startswith("#/definitions/"):
73
81
  definition_name = ref.split("#/definitions/")[1]
74
82
  return definitions[definition_name]
@@ -82,6 +90,19 @@ def resolve_definition_ref(ref, definitions) -> Definition:
82
90
  )
83
91
 
84
92
 
93
+ def fetch_file(path) -> str:
94
+ if not os.path.exists(path):
95
+ raise DataContractException(
96
+ type="export",
97
+ result="failed",
98
+ name="Check that data contract definition is valid",
99
+ reason=f"Cannot resolve reference {path}",
100
+ engine="datacontract",
101
+ )
102
+ with open(path, "r") as file:
103
+ return file.read()
104
+
105
+
85
106
  def resolve_quality_ref(quality: Quality):
86
107
  """
87
108
  Return the content of a ref file path
@@ -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
@@ -60,8 +74,13 @@ class Definition(pyd.BaseModel):
60
74
  pii: bool = None
61
75
  classification: str = None
62
76
  tags: List[str] = []
77
+ links: Dict[str, str] = {}
63
78
  example: str = None
64
79
 
80
+ model_config = pyd.ConfigDict(
81
+ extra="allow",
82
+ )
83
+
65
84
 
66
85
  class Field(pyd.BaseModel):
67
86
  ref: str = pyd.Field(default=None, alias="$ref")
@@ -85,6 +104,7 @@ class Field(pyd.BaseModel):
85
104
  exclusiveMaximum: int = None
86
105
  enum: List[str] = []
87
106
  tags: List[str] = []
107
+ links: Dict[str, str] = {}
88
108
  fields: Dict[str, "Field"] = {}
89
109
  items: "Field" = None
90
110
  precision: int = None
@@ -92,12 +112,16 @@ class Field(pyd.BaseModel):
92
112
  example: str = None
93
113
  config: Dict[str, Any] = None
94
114
 
115
+ model_config = pyd.ConfigDict(
116
+ extra="allow",
117
+ )
118
+
95
119
 
96
120
  class Model(pyd.BaseModel):
97
- description: str = None
98
- type: str = None
99
- namespace: str = None
100
- title: str = None
121
+ description: Optional[str] = None
122
+ type: Optional[str] = None
123
+ namespace: Optional[str] = None
124
+ title: Optional[str] = None
101
125
  fields: Dict[str, Field] = {}
102
126
  config: Dict[str, Any] = None
103
127
 
@@ -110,6 +134,10 @@ class Info(pyd.BaseModel):
110
134
  owner: str = None
111
135
  contact: Contact = None
112
136
 
137
+ model_config = pyd.ConfigDict(
138
+ extra="allow",
139
+ )
140
+
113
141
 
114
142
  class Example(pyd.BaseModel):
115
143
  type: str = None
@@ -191,6 +219,8 @@ class DataContractSpecification(pyd.BaseModel):
191
219
  examples: List[Example] = []
192
220
  quality: Quality = None
193
221
  servicelevels: Optional[ServiceLevel] = None
222
+ links: Dict[str, str] = {}
223
+ tags: List[str] = []
194
224
 
195
225
  @classmethod
196
226
  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 %}
@@ -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 %}
@@ -1,6 +1,6 @@
1
1
  <li class="relative flex gap-x-6 px-4 py-5 sm:px-6">
2
2
  <div class="flex items-center gap-x-4">
3
- <div class="hidden sm:flex sm:flex-col">
3
+ <div class="sm:flex sm:flex-col">
4
4
  <div class="flex flex-col">
5
5
  <dt class="text-sm font-medium text-gray-500">Server</dt>
6
6
  <dd class="mt-1 text-sm text-gray-900">{{server_name}}</dd>
@@ -8,9 +8,20 @@
8
8
  </div>
9
9
  </div>
10
10
 
11
+ {% if server.environment %}
12
+ <div class="flex items-center gap-x-4">
13
+ <div class="sm:flex sm:flex-col">
14
+ <div class="flex flex-col">
15
+ <dt class="text-sm font-medium text-gray-500">Environment</dt>
16
+ <dd class="mt-1 text-sm text-gray-900">{{server.environment}}</dd>
17
+ </div>
18
+ </div>
19
+ </div>
20
+ {% endif %}
21
+
11
22
  {% if server.type %}
12
23
  <div class="flex items-center gap-x-4">
13
- <div class="hidden sm:flex sm:flex-col">
24
+ <div class="sm:flex sm:flex-col">
14
25
  <div class="flex flex-col">
15
26
  <dt class="text-sm font-medium text-gray-500">Type</dt>
16
27
  <dd class="mt-1 text-sm text-gray-900">{{server.type}}</dd>
@@ -21,7 +32,7 @@
21
32
 
22
33
  {% if server.project %}
23
34
  <div class="flex items-center gap-x-4">
24
- <div class="hidden sm:flex sm:flex-col">
35
+ <div class="sm:flex sm:flex-col">
25
36
  <div class="flex flex-col">
26
37
  <dt class="text-sm font-medium text-gray-500">Project</dt>
27
38
  <dd class="mt-1 text-sm text-gray-900">{{server.project}}</dd>
@@ -32,7 +43,7 @@
32
43
 
33
44
  {% if server.dataset %}
34
45
  <div class="flex items-center gap-x-4">
35
- <div class="hidden sm:flex sm:flex-col">
46
+ <div class="sm:flex sm:flex-col">
36
47
  <div class="flex flex-col">
37
48
  <dt class="text-sm font-medium text-gray-500">Dataset</dt>
38
49
  <dd class="mt-1 text-sm text-gray-900">{{server.dataset}}</dd>
@@ -43,7 +54,7 @@
43
54
 
44
55
  {% if server.location %}
45
56
  <div class="flex items-center gap-x-4">
46
- <div class="hidden sm:flex sm:flex-col">
57
+ <div class="sm:flex sm:flex-col">
47
58
  <div class="flex flex-col">
48
59
  <dt class="text-sm font-medium text-gray-500">Location</dt>
49
60
  <dd class="mt-1 text-sm text-gray-900">{{server.location}}</dd>
@@ -54,7 +65,7 @@
54
65
 
55
66
  {% if server.endpointUrl %}
56
67
  <div class="flex items-center gap-x-4">
57
- <div class="hidden sm:flex sm:flex-col">
68
+ <div class="sm:flex sm:flex-col">
58
69
  <div class="flex flex-col">
59
70
  <dt class="text-sm font-medium text-gray-500">Endpoint URL</dt>
60
71
  <dd class="mt-1 text-sm text-gray-900">{{server.endpointUrl}}</dd>
@@ -65,7 +76,7 @@
65
76
 
66
77
  {% if server.account %}
67
78
  <div class="flex items-center gap-x-4">
68
- <div class="hidden sm:flex sm:flex-col">
79
+ <div class="sm:flex sm:flex-col">
69
80
  <div class="flex flex-col">
70
81
  <dt class="text-sm font-medium text-gray-500">Account</dt>
71
82
  <dd class="mt-1 text-sm text-gray-900">{{server.account}}</dd>
@@ -76,7 +87,7 @@
76
87
 
77
88
  {% if server.host %}
78
89
  <div class="flex items-center gap-x-4">
79
- <div class="hidden sm:flex sm:flex-col">
90
+ <div class="sm:flex sm:flex-col">
80
91
  <div class="flex flex-col">
81
92
  <dt class="text-sm font-medium text-gray-500">Host</dt>
82
93
  <dd class="mt-1 text-sm text-gray-900">{{server.host}}</dd>
@@ -87,7 +98,7 @@
87
98
 
88
99
  {% if server.port %}
89
100
  <div class="flex items-center gap-x-4">
90
- <div class="hidden sm:flex sm:flex-col">
101
+ <div class="sm:flex sm:flex-col">
91
102
  <div class="flex flex-col">
92
103
  <dt class="text-sm font-medium text-gray-500">Port</dt>
93
104
  <dd class="mt-1 text-sm text-gray-900">{{server.port}}</dd>
@@ -98,7 +109,7 @@
98
109
 
99
110
  {% if server.catalog %}
100
111
  <div class="flex items-center gap-x-4">
101
- <div class="hidden sm:flex sm:flex-col">
112
+ <div class="sm:flex sm:flex-col">
102
113
  <div class="flex flex-col">
103
114
  <dt class="text-sm font-medium text-gray-500">Catalog</dt>
104
115
  <dd class="mt-1 text-sm text-gray-900">{{server.catalog}}</dd>
@@ -109,7 +120,7 @@
109
120
 
110
121
  {% if server.database %}
111
122
  <div class="flex items-center gap-x-4">
112
- <div class="hidden sm:flex sm:flex-col">
123
+ <div class="sm:flex sm:flex-col">
113
124
  <div class="flex flex-col">
114
125
  <dt class="text-sm font-medium text-gray-500">Database</dt>
115
126
  <dd class="mt-1 text-sm text-gray-900">{{server.database}}</dd>
@@ -120,7 +131,7 @@
120
131
 
121
132
  {% if server.schema_ %}
122
133
  <div class="flex items-center gap-x-4">
123
- <div class="hidden sm:flex sm:flex-col">
134
+ <div class="sm:flex sm:flex-col">
124
135
  <div class="flex flex-col">
125
136
  <dt class="text-sm font-medium text-gray-500">Schema</dt>
126
137
  <dd class="mt-1 text-sm text-gray-900">{{server.schema_}}</dd>
@@ -131,7 +142,7 @@
131
142
 
132
143
  {% if server.topic %}
133
144
  <div class="flex items-center gap-x-4">
134
- <div class="hidden sm:flex sm:flex-col">
145
+ <div class="sm:flex sm:flex-col">
135
146
  <div class="flex flex-col">
136
147
  <dt class="text-sm font-medium text-gray-500">Topic</dt>
137
148
  <dd class="mt-1 text-sm text-gray-900">{{server.topic}}</dd>
@@ -142,7 +153,7 @@
142
153
 
143
154
  {% if server.path %}
144
155
  <div class="flex items-center gap-x-4">
145
- <div class="hidden sm:flex sm:flex-col">
156
+ <div class="sm:flex sm:flex-col">
146
157
  <div class="flex flex-col">
147
158
  <dt class="text-sm font-medium text-gray-500">Path</dt>
148
159
  <dd class="mt-1 text-sm text-gray-900">{{server.path}}</dd>
@@ -153,7 +164,7 @@
153
164
 
154
165
  {% if server.format %}
155
166
  <div class="flex items-center gap-x-4">
156
- <div class="hidden sm:flex sm:flex-col">
167
+ <div class="sm:flex sm:flex-col">
157
168
  <div class="flex flex-col">
158
169
  <dt class="text-sm font-medium text-gray-500">Format</dt>
159
170
  <dd class="mt-1 text-sm text-gray-900">{{server.format}}</dd>
@@ -164,7 +175,7 @@
164
175
 
165
176
  {% if server.delimiter %}
166
177
  <div class="flex items-center gap-x-4">
167
- <div class="hidden sm:flex sm:flex-col">
178
+ <div class="sm:flex sm:flex-col">
168
179
  <div class="flex flex-col">
169
180
  <dt class="text-sm font-medium text-gray-500">Delimiter</dt>
170
181
  <dd class="mt-1 text-sm text-gray-900">{{server.delimiter}}</dd>
@@ -173,4 +184,26 @@
173
184
  </div>
174
185
  {% endif %}
175
186
 
187
+ {% if server.description %}
188
+ <div class="flex items-center gap-x-4">
189
+ <div class="sm:flex sm:flex-col">
190
+ <div class="flex flex-col">
191
+ <dt class="text-sm font-medium text-gray-500">Description</dt>
192
+ <dd class="mt-1 text-sm text-gray-900">{{server.description}}</dd>
193
+ </div>
194
+ </div>
195
+ </div>
196
+ {% endif %}
197
+
198
+ {% for key, value in server.model_extra.items() %}
199
+ <div class="flex items-center gap-x-4">
200
+ <div class="sm:flex sm:flex-col">
201
+ <div class="flex flex-col">
202
+ <dt class="text-sm font-medium text-gray-500">{{ key }}</dt>
203
+ <dd class="mt-1 text-sm text-gray-900">{{ value }}</dd>
204
+ </div>
205
+ </div>
206
+ </div>
207
+ {% endfor %}
208
+
176
209
  </li>
@@ -630,6 +630,11 @@ video {
630
630
  margin-right: auto;
631
631
  }
632
632
 
633
+ .my-2 {
634
+ margin-top: 0.5rem;
635
+ margin-bottom: 0.5rem;
636
+ }
637
+
633
638
  .-ml-0 {
634
639
  margin-left: -0px;
635
640
  }
@@ -642,6 +647,10 @@ video {
642
647
  margin-bottom: 0.75rem;
643
648
  }
644
649
 
650
+ .mb-6 {
651
+ margin-bottom: 1.5rem;
652
+ }
653
+
645
654
  .mr-1 {
646
655
  margin-right: 0.25rem;
647
656
  }
@@ -741,6 +750,10 @@ video {
741
750
  height: 1.5rem;
742
751
  }
743
752
 
753
+ .h-8 {
754
+ height: 2rem;
755
+ }
756
+
744
757
  .h-full {
745
758
  height: 100%;
746
759
  }
@@ -781,6 +794,10 @@ video {
781
794
  width: 58.333333%;
782
795
  }
783
796
 
797
+ .w-8 {
798
+ width: 2rem;
799
+ }
800
+
784
801
  .w-9\/12 {
785
802
  width: 75%;
786
803
  }
@@ -988,6 +1005,11 @@ video {
988
1005
  background-color: rgb(107 114 128 / var(--tw-bg-opacity));
989
1006
  }
990
1007
 
1008
+ .bg-indigo-100 {
1009
+ --tw-bg-opacity: 1;
1010
+ background-color: rgb(224 231 255 / var(--tw-bg-opacity));
1011
+ }
1012
+
991
1013
  .bg-indigo-600 {
992
1014
  --tw-bg-opacity: 1;
993
1015
  background-color: rgb(79 70 229 / var(--tw-bg-opacity));
@@ -1030,6 +1052,11 @@ video {
1030
1052
  padding-right: 0.5rem;
1031
1053
  }
1032
1054
 
1055
+ .px-2\.5 {
1056
+ padding-left: 0.625rem;
1057
+ padding-right: 0.625rem;
1058
+ }
1059
+
1033
1060
  .px-3 {
1034
1061
  padding-left: 0.75rem;
1035
1062
  padding-right: 0.75rem;
@@ -1045,6 +1072,16 @@ video {
1045
1072
  padding-right: 1.5rem;
1046
1073
  }
1047
1074
 
1075
+ .py-0 {
1076
+ padding-top: 0px;
1077
+ padding-bottom: 0px;
1078
+ }
1079
+
1080
+ .py-0\.5 {
1081
+ padding-top: 0.125rem;
1082
+ padding-bottom: 0.125rem;
1083
+ }
1084
+
1048
1085
  .py-1 {
1049
1086
  padding-top: 0.25rem;
1050
1087
  padding-bottom: 0.25rem;
@@ -1198,6 +1235,11 @@ video {
1198
1235
  color: rgb(75 85 99 / var(--tw-text-opacity));
1199
1236
  }
1200
1237
 
1238
+ .text-gray-800 {
1239
+ --tw-text-opacity: 1;
1240
+ color: rgb(31 41 55 / var(--tw-text-opacity));
1241
+ }
1242
+
1201
1243
  .text-gray-900 {
1202
1244
  --tw-text-opacity: 1;
1203
1245
  color: rgb(17 24 39 / var(--tw-text-opacity));