deriva-ml 1.17.10__py3-none-any.whl → 1.17.12__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.
- deriva_ml/__init__.py +69 -1
- deriva_ml/asset/__init__.py +17 -0
- deriva_ml/asset/asset.py +357 -0
- deriva_ml/asset/aux_classes.py +100 -0
- deriva_ml/bump_version.py +254 -11
- deriva_ml/catalog/__init__.py +31 -0
- deriva_ml/catalog/clone.py +1939 -0
- deriva_ml/catalog/localize.py +426 -0
- deriva_ml/core/__init__.py +29 -0
- deriva_ml/core/base.py +845 -1067
- deriva_ml/core/config.py +169 -21
- deriva_ml/core/constants.py +120 -19
- deriva_ml/core/definitions.py +123 -13
- deriva_ml/core/enums.py +47 -73
- deriva_ml/core/ermrest.py +226 -193
- deriva_ml/core/exceptions.py +297 -14
- deriva_ml/core/filespec.py +99 -28
- deriva_ml/core/logging_config.py +225 -0
- deriva_ml/core/mixins/__init__.py +42 -0
- deriva_ml/core/mixins/annotation.py +915 -0
- deriva_ml/core/mixins/asset.py +384 -0
- deriva_ml/core/mixins/dataset.py +237 -0
- deriva_ml/core/mixins/execution.py +408 -0
- deriva_ml/core/mixins/feature.py +365 -0
- deriva_ml/core/mixins/file.py +263 -0
- deriva_ml/core/mixins/path_builder.py +145 -0
- deriva_ml/core/mixins/rid_resolution.py +204 -0
- deriva_ml/core/mixins/vocabulary.py +400 -0
- deriva_ml/core/mixins/workflow.py +322 -0
- deriva_ml/core/validation.py +389 -0
- deriva_ml/dataset/__init__.py +2 -1
- deriva_ml/dataset/aux_classes.py +20 -4
- deriva_ml/dataset/catalog_graph.py +575 -0
- deriva_ml/dataset/dataset.py +1242 -1008
- deriva_ml/dataset/dataset_bag.py +1311 -182
- deriva_ml/dataset/history.py +27 -14
- deriva_ml/dataset/upload.py +225 -38
- deriva_ml/demo_catalog.py +126 -110
- deriva_ml/execution/__init__.py +46 -2
- deriva_ml/execution/base_config.py +639 -0
- deriva_ml/execution/execution.py +543 -242
- deriva_ml/execution/execution_configuration.py +26 -11
- deriva_ml/execution/execution_record.py +592 -0
- deriva_ml/execution/find_caller.py +298 -0
- deriva_ml/execution/model_protocol.py +175 -0
- deriva_ml/execution/multirun_config.py +153 -0
- deriva_ml/execution/runner.py +595 -0
- deriva_ml/execution/workflow.py +223 -34
- deriva_ml/experiment/__init__.py +8 -0
- deriva_ml/experiment/experiment.py +411 -0
- deriva_ml/feature.py +6 -1
- deriva_ml/install_kernel.py +143 -6
- deriva_ml/interfaces.py +862 -0
- deriva_ml/model/__init__.py +99 -0
- deriva_ml/model/annotations.py +1278 -0
- deriva_ml/model/catalog.py +286 -60
- deriva_ml/model/database.py +144 -649
- deriva_ml/model/deriva_ml_database.py +308 -0
- deriva_ml/model/handles.py +14 -0
- deriva_ml/run_model.py +319 -0
- deriva_ml/run_notebook.py +507 -38
- deriva_ml/schema/__init__.py +18 -2
- deriva_ml/schema/annotations.py +62 -33
- deriva_ml/schema/create_schema.py +169 -69
- deriva_ml/schema/validation.py +601 -0
- {deriva_ml-1.17.10.dist-info → deriva_ml-1.17.12.dist-info}/METADATA +4 -4
- deriva_ml-1.17.12.dist-info/RECORD +77 -0
- {deriva_ml-1.17.10.dist-info → deriva_ml-1.17.12.dist-info}/WHEEL +1 -1
- {deriva_ml-1.17.10.dist-info → deriva_ml-1.17.12.dist-info}/entry_points.txt +1 -0
- deriva_ml/protocols/dataset.py +0 -19
- deriva_ml/test.py +0 -94
- deriva_ml-1.17.10.dist-info/RECORD +0 -45
- {deriva_ml-1.17.10.dist-info → deriva_ml-1.17.12.dist-info}/licenses/LICENSE +0 -0
- {deriva_ml-1.17.10.dist-info → deriva_ml-1.17.12.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,915 @@
|
|
|
1
|
+
"""Annotation management mixin for DerivaML.
|
|
2
|
+
|
|
3
|
+
This module provides the AnnotationMixin class which handles
|
|
4
|
+
Deriva catalog annotation operations for controlling how data
|
|
5
|
+
is displayed in the Chaise web interface.
|
|
6
|
+
|
|
7
|
+
Annotation Tags:
|
|
8
|
+
- display: tag:isrd.isi.edu,2015:display
|
|
9
|
+
- visible-columns: tag:isrd.isi.edu,2016:visible-columns
|
|
10
|
+
- visible-foreign-keys: tag:isrd.isi.edu,2016:visible-foreign-keys
|
|
11
|
+
- table-display: tag:isrd.isi.edu,2016:table-display
|
|
12
|
+
- column-display: tag:isrd.isi.edu,2016:column-display
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
18
|
+
|
|
19
|
+
# Deriva imports - use importlib to avoid shadowing by local 'deriva.py' files
|
|
20
|
+
import importlib
|
|
21
|
+
_ermrest_model = importlib.import_module("deriva.core.ermrest_model")
|
|
22
|
+
Column = _ermrest_model.Column
|
|
23
|
+
Table = _ermrest_model.Table
|
|
24
|
+
|
|
25
|
+
from pydantic import ConfigDict, validate_call
|
|
26
|
+
|
|
27
|
+
from deriva_ml.core.exceptions import DerivaMLException
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from deriva_ml.model.catalog import DerivaModel
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Annotation tag URIs
|
|
34
|
+
DISPLAY_TAG = "tag:isrd.isi.edu,2015:display"
|
|
35
|
+
VISIBLE_COLUMNS_TAG = "tag:isrd.isi.edu,2016:visible-columns"
|
|
36
|
+
VISIBLE_FOREIGN_KEYS_TAG = "tag:isrd.isi.edu,2016:visible-foreign-keys"
|
|
37
|
+
TABLE_DISPLAY_TAG = "tag:isrd.isi.edu,2016:table-display"
|
|
38
|
+
COLUMN_DISPLAY_TAG = "tag:isrd.isi.edu,2016:column-display"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class AnnotationMixin:
|
|
42
|
+
"""Mixin providing annotation management operations.
|
|
43
|
+
|
|
44
|
+
This mixin requires the host class to have:
|
|
45
|
+
- model: DerivaModel instance
|
|
46
|
+
- pathBuilder(): method returning catalog path builder
|
|
47
|
+
|
|
48
|
+
Methods:
|
|
49
|
+
get_table_annotations: Get all display-related annotations for a table
|
|
50
|
+
get_column_annotations: Get all display-related annotations for a column
|
|
51
|
+
set_display_annotation: Set display annotation on table or column
|
|
52
|
+
set_visible_columns: Set visible-columns annotation on a table
|
|
53
|
+
set_visible_foreign_keys: Set visible-foreign-keys annotation on a table
|
|
54
|
+
set_table_display: Set table-display annotation on a table
|
|
55
|
+
set_column_display: Set column-display annotation on a column
|
|
56
|
+
list_foreign_keys: List all foreign keys related to a table
|
|
57
|
+
add_visible_column: Add a column to visible-columns list
|
|
58
|
+
remove_visible_column: Remove a column from visible-columns list
|
|
59
|
+
reorder_visible_columns: Reorder columns in visible-columns list
|
|
60
|
+
add_visible_foreign_key: Add a foreign key to visible-foreign-keys list
|
|
61
|
+
remove_visible_foreign_key: Remove a foreign key from visible-foreign-keys list
|
|
62
|
+
reorder_visible_foreign_keys: Reorder foreign keys in visible-foreign-keys list
|
|
63
|
+
apply_annotations: Apply staged annotation changes to the catalog
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
# Type hints for IDE support - actual attributes/methods from host class
|
|
67
|
+
model: "DerivaModel"
|
|
68
|
+
pathBuilder: Callable[[], Any]
|
|
69
|
+
|
|
70
|
+
# =========================================================================
|
|
71
|
+
# Core Annotation Operations
|
|
72
|
+
# =========================================================================
|
|
73
|
+
|
|
74
|
+
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
|
75
|
+
def get_table_annotations(self, table: str | Table) -> dict[str, Any]:
|
|
76
|
+
"""Get all display-related annotations for a table.
|
|
77
|
+
|
|
78
|
+
Returns the current values of display, visible-columns, visible-foreign-keys,
|
|
79
|
+
and table-display annotations for the specified table.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
table: Table name or Table object.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Dictionary with keys: table, schema, display, visible_columns,
|
|
86
|
+
visible_foreign_keys, table_display. Missing annotations are None.
|
|
87
|
+
|
|
88
|
+
Example:
|
|
89
|
+
>>> annotations = ml.get_table_annotations("Image")
|
|
90
|
+
>>> print(annotations["visible_columns"])
|
|
91
|
+
"""
|
|
92
|
+
table_obj = self.model.name_to_table(table)
|
|
93
|
+
return {
|
|
94
|
+
"table": table_obj.name,
|
|
95
|
+
"schema": table_obj.schema.name,
|
|
96
|
+
"display": table_obj.annotations.get(DISPLAY_TAG),
|
|
97
|
+
"visible_columns": table_obj.annotations.get(VISIBLE_COLUMNS_TAG),
|
|
98
|
+
"visible_foreign_keys": table_obj.annotations.get(VISIBLE_FOREIGN_KEYS_TAG),
|
|
99
|
+
"table_display": table_obj.annotations.get(TABLE_DISPLAY_TAG),
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
|
103
|
+
def get_column_annotations(self, table: str | Table, column_name: str) -> dict[str, Any]:
|
|
104
|
+
"""Get all display-related annotations for a column.
|
|
105
|
+
|
|
106
|
+
Returns the current values of display and column-display annotations
|
|
107
|
+
for the specified column.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
table: Table name or Table object containing the column.
|
|
111
|
+
column_name: Name of the column.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Dictionary with keys: table, column, display, column_display.
|
|
115
|
+
Missing annotations are None.
|
|
116
|
+
|
|
117
|
+
Example:
|
|
118
|
+
>>> annotations = ml.get_column_annotations("Image", "Filename")
|
|
119
|
+
>>> print(annotations["display"])
|
|
120
|
+
"""
|
|
121
|
+
table_obj = self.model.name_to_table(table)
|
|
122
|
+
column = table_obj.columns[column_name]
|
|
123
|
+
return {
|
|
124
|
+
"table": table_obj.name,
|
|
125
|
+
"column": column.name,
|
|
126
|
+
"display": column.annotations.get(DISPLAY_TAG),
|
|
127
|
+
"column_display": column.annotations.get(COLUMN_DISPLAY_TAG),
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
|
131
|
+
def set_display_annotation(
|
|
132
|
+
self,
|
|
133
|
+
table: str | Table,
|
|
134
|
+
annotation: dict[str, Any] | None,
|
|
135
|
+
column_name: str | None = None,
|
|
136
|
+
) -> str:
|
|
137
|
+
"""Set the display annotation on a table or column.
|
|
138
|
+
|
|
139
|
+
The display annotation controls basic naming and display options.
|
|
140
|
+
Changes are staged locally until apply_annotations() is called.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
table: Table name or Table object.
|
|
144
|
+
annotation: The display annotation value. Set to None to remove.
|
|
145
|
+
column_name: If provided, sets annotation on the column; otherwise on the table.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Target identifier (table name or table.column).
|
|
149
|
+
|
|
150
|
+
Example:
|
|
151
|
+
>>> ml.set_display_annotation("Image", {"name": "Images"})
|
|
152
|
+
>>> ml.set_display_annotation("Image", {"name": "File Name"}, column_name="Filename")
|
|
153
|
+
>>> ml.apply_annotations() # Commit changes
|
|
154
|
+
"""
|
|
155
|
+
table_obj = self.model.name_to_table(table)
|
|
156
|
+
|
|
157
|
+
if column_name:
|
|
158
|
+
column = table_obj.columns[column_name]
|
|
159
|
+
if annotation is None:
|
|
160
|
+
column.annotations.pop(DISPLAY_TAG, None)
|
|
161
|
+
else:
|
|
162
|
+
column.annotations[DISPLAY_TAG] = annotation
|
|
163
|
+
return f"{table_obj.name}.{column_name}"
|
|
164
|
+
else:
|
|
165
|
+
if annotation is None:
|
|
166
|
+
table_obj.annotations.pop(DISPLAY_TAG, None)
|
|
167
|
+
else:
|
|
168
|
+
table_obj.annotations[DISPLAY_TAG] = annotation
|
|
169
|
+
return table_obj.name
|
|
170
|
+
|
|
171
|
+
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
|
172
|
+
def set_visible_columns(
|
|
173
|
+
self,
|
|
174
|
+
table: str | Table,
|
|
175
|
+
annotation: dict[str, Any] | None,
|
|
176
|
+
) -> str:
|
|
177
|
+
"""Set the visible-columns annotation on a table.
|
|
178
|
+
|
|
179
|
+
Controls which columns appear in different UI contexts and their order.
|
|
180
|
+
Changes are staged locally until apply_annotations() is called.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
table: Table name or Table object.
|
|
184
|
+
annotation: The visible-columns annotation value. Set to None to remove.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Table name.
|
|
188
|
+
|
|
189
|
+
Example:
|
|
190
|
+
>>> ml.set_visible_columns("Image", {
|
|
191
|
+
... "compact": ["RID", "Filename", "Subject"],
|
|
192
|
+
... "detailed": ["RID", "Filename", "Subject", "Description"]
|
|
193
|
+
... })
|
|
194
|
+
>>> ml.apply_annotations()
|
|
195
|
+
"""
|
|
196
|
+
table_obj = self.model.name_to_table(table)
|
|
197
|
+
|
|
198
|
+
if annotation is None:
|
|
199
|
+
table_obj.annotations.pop(VISIBLE_COLUMNS_TAG, None)
|
|
200
|
+
else:
|
|
201
|
+
table_obj.annotations[VISIBLE_COLUMNS_TAG] = annotation
|
|
202
|
+
|
|
203
|
+
return table_obj.name
|
|
204
|
+
|
|
205
|
+
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
|
206
|
+
def set_visible_foreign_keys(
|
|
207
|
+
self,
|
|
208
|
+
table: str | Table,
|
|
209
|
+
annotation: dict[str, Any] | None,
|
|
210
|
+
) -> str:
|
|
211
|
+
"""Set the visible-foreign-keys annotation on a table.
|
|
212
|
+
|
|
213
|
+
Controls which related tables (via inbound foreign keys) appear in
|
|
214
|
+
different UI contexts and their order.
|
|
215
|
+
Changes are staged locally until apply_annotations() is called.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
table: Table name or Table object.
|
|
219
|
+
annotation: The visible-foreign-keys annotation value. Set to None to remove.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Table name.
|
|
223
|
+
|
|
224
|
+
Example:
|
|
225
|
+
>>> ml.set_visible_foreign_keys("Subject", {
|
|
226
|
+
... "detailed": [
|
|
227
|
+
... ["domain", "Image_Subject_fkey"],
|
|
228
|
+
... ["domain", "Diagnosis_Subject_fkey"]
|
|
229
|
+
... ]
|
|
230
|
+
... })
|
|
231
|
+
>>> ml.apply_annotations()
|
|
232
|
+
"""
|
|
233
|
+
table_obj = self.model.name_to_table(table)
|
|
234
|
+
|
|
235
|
+
if annotation is None:
|
|
236
|
+
table_obj.annotations.pop(VISIBLE_FOREIGN_KEYS_TAG, None)
|
|
237
|
+
else:
|
|
238
|
+
table_obj.annotations[VISIBLE_FOREIGN_KEYS_TAG] = annotation
|
|
239
|
+
|
|
240
|
+
return table_obj.name
|
|
241
|
+
|
|
242
|
+
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
|
243
|
+
def set_table_display(
|
|
244
|
+
self,
|
|
245
|
+
table: str | Table,
|
|
246
|
+
annotation: dict[str, Any] | None,
|
|
247
|
+
) -> str:
|
|
248
|
+
"""Set the table-display annotation on a table.
|
|
249
|
+
|
|
250
|
+
Controls table-level display options like row naming patterns,
|
|
251
|
+
page size, and row ordering.
|
|
252
|
+
Changes are staged locally until apply_annotations() is called.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
table: Table name or Table object.
|
|
256
|
+
annotation: The table-display annotation value. Set to None to remove.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
Table name.
|
|
260
|
+
|
|
261
|
+
Example:
|
|
262
|
+
>>> ml.set_table_display("Subject", {
|
|
263
|
+
... "row_name": {
|
|
264
|
+
... "row_markdown_pattern": "{{{Name}}} ({{{Species}}})"
|
|
265
|
+
... }
|
|
266
|
+
... })
|
|
267
|
+
>>> ml.apply_annotations()
|
|
268
|
+
"""
|
|
269
|
+
table_obj = self.model.name_to_table(table)
|
|
270
|
+
|
|
271
|
+
if annotation is None:
|
|
272
|
+
table_obj.annotations.pop(TABLE_DISPLAY_TAG, None)
|
|
273
|
+
else:
|
|
274
|
+
table_obj.annotations[TABLE_DISPLAY_TAG] = annotation
|
|
275
|
+
|
|
276
|
+
return table_obj.name
|
|
277
|
+
|
|
278
|
+
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
|
279
|
+
def set_column_display(
|
|
280
|
+
self,
|
|
281
|
+
table: str | Table,
|
|
282
|
+
column_name: str,
|
|
283
|
+
annotation: dict[str, Any] | None,
|
|
284
|
+
) -> str:
|
|
285
|
+
"""Set the column-display annotation on a column.
|
|
286
|
+
|
|
287
|
+
Controls how a column's values are rendered, including custom
|
|
288
|
+
formatting and markdown patterns.
|
|
289
|
+
Changes are staged locally until apply_annotations() is called.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
table: Table name or Table object containing the column.
|
|
293
|
+
column_name: Name of the column.
|
|
294
|
+
annotation: The column-display annotation value. Set to None to remove.
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
Column identifier (table.column).
|
|
298
|
+
|
|
299
|
+
Example:
|
|
300
|
+
>>> ml.set_column_display("Measurement", "Value", {
|
|
301
|
+
... "*": {"pre_format": {"format": "%.2f"}}
|
|
302
|
+
... })
|
|
303
|
+
>>> ml.apply_annotations()
|
|
304
|
+
"""
|
|
305
|
+
table_obj = self.model.name_to_table(table)
|
|
306
|
+
column = table_obj.columns[column_name]
|
|
307
|
+
|
|
308
|
+
if annotation is None:
|
|
309
|
+
column.annotations.pop(COLUMN_DISPLAY_TAG, None)
|
|
310
|
+
else:
|
|
311
|
+
column.annotations[COLUMN_DISPLAY_TAG] = annotation
|
|
312
|
+
|
|
313
|
+
return f"{table_obj.name}.{column_name}"
|
|
314
|
+
|
|
315
|
+
def apply_annotations(self) -> None:
|
|
316
|
+
"""Apply all staged annotation changes to the catalog.
|
|
317
|
+
|
|
318
|
+
Commits any annotation changes made via set_display_annotation,
|
|
319
|
+
set_visible_columns, set_visible_foreign_keys, set_table_display,
|
|
320
|
+
or set_column_display to the remote catalog.
|
|
321
|
+
|
|
322
|
+
Example:
|
|
323
|
+
>>> ml.set_display_annotation("Image", {"name": "Images"})
|
|
324
|
+
>>> ml.set_visible_columns("Image", {"compact": ["RID", "Filename"]})
|
|
325
|
+
>>> ml.apply_annotations() # Commit all changes
|
|
326
|
+
"""
|
|
327
|
+
self.model.apply()
|
|
328
|
+
|
|
329
|
+
# =========================================================================
|
|
330
|
+
# Foreign Key Information
|
|
331
|
+
# =========================================================================
|
|
332
|
+
|
|
333
|
+
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
|
334
|
+
def list_foreign_keys(self, table: str | Table) -> dict[str, Any]:
|
|
335
|
+
"""List all foreign keys related to a table.
|
|
336
|
+
|
|
337
|
+
Returns both outbound foreign keys (from this table to others) and
|
|
338
|
+
inbound foreign keys (from other tables to this one). Useful for
|
|
339
|
+
determining valid constraint names for visible-columns and
|
|
340
|
+
visible-foreign-keys annotations.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
table: Table name or Table object.
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
Dictionary with:
|
|
347
|
+
- table: Table name
|
|
348
|
+
- outbound: List of outbound foreign keys
|
|
349
|
+
- inbound: List of inbound foreign keys
|
|
350
|
+
Each foreign key contains constraint_name, from_table, from_columns,
|
|
351
|
+
to_table, to_columns.
|
|
352
|
+
|
|
353
|
+
Example:
|
|
354
|
+
>>> fkeys = ml.list_foreign_keys("Image")
|
|
355
|
+
>>> for fk in fkeys["outbound"]:
|
|
356
|
+
... print(f"{fk['constraint_name']} -> {fk['to_table']}")
|
|
357
|
+
"""
|
|
358
|
+
table_obj = self.model.name_to_table(table)
|
|
359
|
+
|
|
360
|
+
outbound = []
|
|
361
|
+
for fkey in table_obj.foreign_keys:
|
|
362
|
+
outbound.append({
|
|
363
|
+
"constraint_name": [fkey.constraint_schema.name, fkey.constraint_name],
|
|
364
|
+
"from_table": table_obj.name,
|
|
365
|
+
"from_columns": [col.name for col in fkey.columns],
|
|
366
|
+
"to_table": fkey.pk_table.name,
|
|
367
|
+
"to_columns": [col.name for col in fkey.referenced_columns],
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
inbound = []
|
|
371
|
+
for fkey in table_obj.referenced_by:
|
|
372
|
+
inbound.append({
|
|
373
|
+
"constraint_name": [fkey.constraint_schema.name, fkey.constraint_name],
|
|
374
|
+
"from_table": fkey.table.name,
|
|
375
|
+
"from_columns": [col.name for col in fkey.columns],
|
|
376
|
+
"to_table": table_obj.name,
|
|
377
|
+
"to_columns": [col.name for col in fkey.referenced_columns],
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
"table": table_obj.name,
|
|
382
|
+
"outbound": outbound,
|
|
383
|
+
"inbound": inbound,
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
# =========================================================================
|
|
387
|
+
# Visible Columns Convenience Methods
|
|
388
|
+
# =========================================================================
|
|
389
|
+
|
|
390
|
+
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
|
391
|
+
def add_visible_column(
|
|
392
|
+
self,
|
|
393
|
+
table: str | Table,
|
|
394
|
+
context: str,
|
|
395
|
+
column: str | list[str] | dict[str, Any],
|
|
396
|
+
position: int | None = None,
|
|
397
|
+
) -> list[Any]:
|
|
398
|
+
"""Add a column to the visible-columns list for a specific context.
|
|
399
|
+
|
|
400
|
+
Convenience method for adding columns without replacing the entire
|
|
401
|
+
visible-columns annotation. Changes are staged until apply_annotations()
|
|
402
|
+
is called.
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
table: Table name or Table object.
|
|
406
|
+
context: The context to modify (e.g., "compact", "detailed", "entry").
|
|
407
|
+
column: Column to add. Can be:
|
|
408
|
+
- String: column name (e.g., "Filename")
|
|
409
|
+
- List: foreign key reference (e.g., ["schema", "fkey_name"])
|
|
410
|
+
- Dict: pseudo-column definition
|
|
411
|
+
position: Position to insert at (0-indexed). If None, appends to end.
|
|
412
|
+
|
|
413
|
+
Returns:
|
|
414
|
+
The updated column list for the context.
|
|
415
|
+
|
|
416
|
+
Raises:
|
|
417
|
+
DerivaMLException: If context references another context.
|
|
418
|
+
|
|
419
|
+
Example:
|
|
420
|
+
>>> ml.add_visible_column("Image", "compact", "Description")
|
|
421
|
+
>>> ml.add_visible_column("Image", "detailed", ["domain", "Image_Subject_fkey"], 1)
|
|
422
|
+
>>> ml.apply_annotations()
|
|
423
|
+
"""
|
|
424
|
+
table_obj = self.model.name_to_table(table)
|
|
425
|
+
|
|
426
|
+
# Get or create visible_columns annotation
|
|
427
|
+
visible_cols = table_obj.annotations.get(VISIBLE_COLUMNS_TAG, {})
|
|
428
|
+
if visible_cols is None:
|
|
429
|
+
visible_cols = {}
|
|
430
|
+
|
|
431
|
+
# Get or create the context list
|
|
432
|
+
context_list = visible_cols.get(context, [])
|
|
433
|
+
if isinstance(context_list, str):
|
|
434
|
+
raise DerivaMLException(
|
|
435
|
+
f"Context '{context}' references another context '{context_list}'. "
|
|
436
|
+
"Set it explicitly first with set_visible_columns()."
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
# Make a copy to avoid modifying in place
|
|
440
|
+
context_list = list(context_list)
|
|
441
|
+
|
|
442
|
+
# Insert at position or append
|
|
443
|
+
if position is not None:
|
|
444
|
+
context_list.insert(position, column)
|
|
445
|
+
else:
|
|
446
|
+
context_list.append(column)
|
|
447
|
+
|
|
448
|
+
# Update the annotation
|
|
449
|
+
visible_cols[context] = context_list
|
|
450
|
+
table_obj.annotations[VISIBLE_COLUMNS_TAG] = visible_cols
|
|
451
|
+
|
|
452
|
+
return context_list
|
|
453
|
+
|
|
454
|
+
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
|
455
|
+
def remove_visible_column(
|
|
456
|
+
self,
|
|
457
|
+
table: str | Table,
|
|
458
|
+
context: str,
|
|
459
|
+
column: str | list[str] | int,
|
|
460
|
+
) -> list[Any]:
|
|
461
|
+
"""Remove a column from the visible-columns list for a specific context.
|
|
462
|
+
|
|
463
|
+
Convenience method for removing columns without replacing the entire
|
|
464
|
+
visible-columns annotation. Changes are staged until apply_annotations()
|
|
465
|
+
is called.
|
|
466
|
+
|
|
467
|
+
Args:
|
|
468
|
+
table: Table name or Table object.
|
|
469
|
+
context: The context to modify (e.g., "compact", "detailed").
|
|
470
|
+
column: Column to remove. Can be:
|
|
471
|
+
- String: column name to find and remove
|
|
472
|
+
- List: foreign key reference [schema, constraint] to find and remove
|
|
473
|
+
- Integer: index position to remove (0-indexed)
|
|
474
|
+
|
|
475
|
+
Returns:
|
|
476
|
+
The updated column list for the context.
|
|
477
|
+
|
|
478
|
+
Raises:
|
|
479
|
+
DerivaMLException: If annotation or context doesn't exist, or column not found.
|
|
480
|
+
|
|
481
|
+
Example:
|
|
482
|
+
>>> ml.remove_visible_column("Image", "compact", "Description")
|
|
483
|
+
>>> ml.remove_visible_column("Image", "compact", 0) # Remove first column
|
|
484
|
+
>>> ml.apply_annotations()
|
|
485
|
+
"""
|
|
486
|
+
table_obj = self.model.name_to_table(table)
|
|
487
|
+
|
|
488
|
+
# Get visible_columns annotation
|
|
489
|
+
visible_cols = table_obj.annotations.get(VISIBLE_COLUMNS_TAG, {})
|
|
490
|
+
if not visible_cols:
|
|
491
|
+
raise DerivaMLException(f"Table '{table_obj.name}' has no visible-columns annotation.")
|
|
492
|
+
|
|
493
|
+
# Get the context list
|
|
494
|
+
context_list = visible_cols.get(context)
|
|
495
|
+
if context_list is None:
|
|
496
|
+
raise DerivaMLException(f"Context '{context}' not found in visible-columns annotation.")
|
|
497
|
+
if isinstance(context_list, str):
|
|
498
|
+
raise DerivaMLException(
|
|
499
|
+
f"Context '{context}' references another context '{context_list}'. "
|
|
500
|
+
"Set it explicitly first with set_visible_columns()."
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
# Make a copy
|
|
504
|
+
context_list = list(context_list)
|
|
505
|
+
removed = None
|
|
506
|
+
|
|
507
|
+
# Remove by index or by value
|
|
508
|
+
if isinstance(column, int):
|
|
509
|
+
if 0 <= column < len(context_list):
|
|
510
|
+
removed = context_list.pop(column)
|
|
511
|
+
else:
|
|
512
|
+
raise DerivaMLException(
|
|
513
|
+
f"Index {column} out of range (list has {len(context_list)} items)."
|
|
514
|
+
)
|
|
515
|
+
else:
|
|
516
|
+
# Find and remove the column
|
|
517
|
+
for i, item in enumerate(context_list):
|
|
518
|
+
if item == column:
|
|
519
|
+
removed = context_list.pop(i)
|
|
520
|
+
break
|
|
521
|
+
# Also check if it's a pseudo-column with matching source
|
|
522
|
+
if isinstance(item, dict) and isinstance(column, str):
|
|
523
|
+
if item.get("source") == column:
|
|
524
|
+
removed = context_list.pop(i)
|
|
525
|
+
break
|
|
526
|
+
|
|
527
|
+
if removed is None:
|
|
528
|
+
raise DerivaMLException(f"Column {column!r} not found in context '{context}'.")
|
|
529
|
+
|
|
530
|
+
# Update the annotation
|
|
531
|
+
visible_cols[context] = context_list
|
|
532
|
+
table_obj.annotations[VISIBLE_COLUMNS_TAG] = visible_cols
|
|
533
|
+
|
|
534
|
+
return context_list
|
|
535
|
+
|
|
536
|
+
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
|
537
|
+
def reorder_visible_columns(
|
|
538
|
+
self,
|
|
539
|
+
table: str | Table,
|
|
540
|
+
context: str,
|
|
541
|
+
new_order: list[int] | list[str | list[str] | dict[str, Any]],
|
|
542
|
+
) -> list[Any]:
|
|
543
|
+
"""Reorder columns in the visible-columns list for a specific context.
|
|
544
|
+
|
|
545
|
+
Convenience method for reordering columns without manually reconstructing
|
|
546
|
+
the list. Changes are staged until apply_annotations() is called.
|
|
547
|
+
|
|
548
|
+
Args:
|
|
549
|
+
table: Table name or Table object.
|
|
550
|
+
context: The context to modify (e.g., "compact", "detailed").
|
|
551
|
+
new_order: The new order specification. Can be:
|
|
552
|
+
- List of indices: [2, 0, 1, 3] reorders by current positions
|
|
553
|
+
- List of column specs: ["Name", "RID", ...] specifies exact order
|
|
554
|
+
|
|
555
|
+
Returns:
|
|
556
|
+
The reordered column list.
|
|
557
|
+
|
|
558
|
+
Raises:
|
|
559
|
+
DerivaMLException: If annotation or context doesn't exist, or invalid order.
|
|
560
|
+
|
|
561
|
+
Example:
|
|
562
|
+
>>> ml.reorder_visible_columns("Image", "compact", [2, 0, 1, 3, 4])
|
|
563
|
+
>>> ml.reorder_visible_columns("Image", "compact", ["Filename", "Subject", "RID"])
|
|
564
|
+
>>> ml.apply_annotations()
|
|
565
|
+
"""
|
|
566
|
+
table_obj = self.model.name_to_table(table)
|
|
567
|
+
|
|
568
|
+
# Get visible_columns annotation
|
|
569
|
+
visible_cols = table_obj.annotations.get(VISIBLE_COLUMNS_TAG, {})
|
|
570
|
+
if not visible_cols:
|
|
571
|
+
raise DerivaMLException(f"Table '{table_obj.name}' has no visible-columns annotation.")
|
|
572
|
+
|
|
573
|
+
# Get the context list
|
|
574
|
+
context_list = visible_cols.get(context)
|
|
575
|
+
if context_list is None:
|
|
576
|
+
raise DerivaMLException(f"Context '{context}' not found in visible-columns annotation.")
|
|
577
|
+
if isinstance(context_list, str):
|
|
578
|
+
raise DerivaMLException(
|
|
579
|
+
f"Context '{context}' references another context '{context_list}'. "
|
|
580
|
+
"Set it explicitly first with set_visible_columns()."
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
original_list = list(context_list)
|
|
584
|
+
|
|
585
|
+
# Determine if new_order is indices or column specs
|
|
586
|
+
if new_order and isinstance(new_order[0], int):
|
|
587
|
+
# Reorder by indices
|
|
588
|
+
if len(new_order) != len(original_list):
|
|
589
|
+
raise DerivaMLException(
|
|
590
|
+
f"Index list length ({len(new_order)}) must match "
|
|
591
|
+
f"current list length ({len(original_list)})."
|
|
592
|
+
)
|
|
593
|
+
if set(new_order) != set(range(len(original_list))):
|
|
594
|
+
raise DerivaMLException("Index list must contain each index exactly once.")
|
|
595
|
+
new_list = [original_list[i] for i in new_order]
|
|
596
|
+
else:
|
|
597
|
+
# new_order is the exact new column list
|
|
598
|
+
new_list = list(new_order)
|
|
599
|
+
|
|
600
|
+
# Update the annotation
|
|
601
|
+
visible_cols[context] = new_list
|
|
602
|
+
table_obj.annotations[VISIBLE_COLUMNS_TAG] = visible_cols
|
|
603
|
+
|
|
604
|
+
return new_list
|
|
605
|
+
|
|
606
|
+
# =========================================================================
|
|
607
|
+
# Visible Foreign Keys Convenience Methods
|
|
608
|
+
# =========================================================================
|
|
609
|
+
|
|
610
|
+
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
|
611
|
+
def add_visible_foreign_key(
|
|
612
|
+
self,
|
|
613
|
+
table: str | Table,
|
|
614
|
+
context: str,
|
|
615
|
+
foreign_key: list[str] | dict[str, Any],
|
|
616
|
+
position: int | None = None,
|
|
617
|
+
) -> list[Any]:
|
|
618
|
+
"""Add a foreign key to the visible-foreign-keys list for a specific context.
|
|
619
|
+
|
|
620
|
+
Convenience method for adding related tables without replacing the entire
|
|
621
|
+
visible-foreign-keys annotation. Changes are staged until apply_annotations()
|
|
622
|
+
is called.
|
|
623
|
+
|
|
624
|
+
Args:
|
|
625
|
+
table: Table name or Table object.
|
|
626
|
+
context: The context to modify (typically "detailed" or "*").
|
|
627
|
+
foreign_key: Foreign key to add. Can be:
|
|
628
|
+
- List: inbound foreign key reference (e.g., ["schema", "Other_Table_fkey"])
|
|
629
|
+
- Dict: pseudo-column definition for complex relationships
|
|
630
|
+
position: Position to insert at (0-indexed). If None, appends to end.
|
|
631
|
+
|
|
632
|
+
Returns:
|
|
633
|
+
The updated foreign key list for the context.
|
|
634
|
+
|
|
635
|
+
Raises:
|
|
636
|
+
DerivaMLException: If context references another context.
|
|
637
|
+
|
|
638
|
+
Example:
|
|
639
|
+
>>> ml.add_visible_foreign_key("Subject", "detailed", ["domain", "Image_Subject_fkey"])
|
|
640
|
+
>>> ml.apply_annotations()
|
|
641
|
+
"""
|
|
642
|
+
table_obj = self.model.name_to_table(table)
|
|
643
|
+
|
|
644
|
+
# Get or create visible_foreign_keys annotation
|
|
645
|
+
visible_fkeys = table_obj.annotations.get(VISIBLE_FOREIGN_KEYS_TAG, {})
|
|
646
|
+
if visible_fkeys is None:
|
|
647
|
+
visible_fkeys = {}
|
|
648
|
+
|
|
649
|
+
# Get or create the context list
|
|
650
|
+
context_list = visible_fkeys.get(context, [])
|
|
651
|
+
if isinstance(context_list, str):
|
|
652
|
+
raise DerivaMLException(
|
|
653
|
+
f"Context '{context}' references another context '{context_list}'. "
|
|
654
|
+
"Set it explicitly first with set_visible_foreign_keys()."
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
# Make a copy to avoid modifying in place
|
|
658
|
+
context_list = list(context_list)
|
|
659
|
+
|
|
660
|
+
# Insert at position or append
|
|
661
|
+
if position is not None:
|
|
662
|
+
context_list.insert(position, foreign_key)
|
|
663
|
+
else:
|
|
664
|
+
context_list.append(foreign_key)
|
|
665
|
+
|
|
666
|
+
# Update the annotation
|
|
667
|
+
visible_fkeys[context] = context_list
|
|
668
|
+
table_obj.annotations[VISIBLE_FOREIGN_KEYS_TAG] = visible_fkeys
|
|
669
|
+
|
|
670
|
+
return context_list
|
|
671
|
+
|
|
672
|
+
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
|
673
|
+
def remove_visible_foreign_key(
|
|
674
|
+
self,
|
|
675
|
+
table: str | Table,
|
|
676
|
+
context: str,
|
|
677
|
+
foreign_key: list[str] | int,
|
|
678
|
+
) -> list[Any]:
|
|
679
|
+
"""Remove a foreign key from the visible-foreign-keys list for a specific context.
|
|
680
|
+
|
|
681
|
+
Convenience method for removing related tables without replacing the entire
|
|
682
|
+
visible-foreign-keys annotation. Changes are staged until apply_annotations()
|
|
683
|
+
is called.
|
|
684
|
+
|
|
685
|
+
Args:
|
|
686
|
+
table: Table name or Table object.
|
|
687
|
+
context: The context to modify (e.g., "detailed", "*").
|
|
688
|
+
foreign_key: Foreign key to remove. Can be:
|
|
689
|
+
- List: foreign key reference [schema, constraint] to find and remove
|
|
690
|
+
- Integer: index position to remove (0-indexed)
|
|
691
|
+
|
|
692
|
+
Returns:
|
|
693
|
+
The updated foreign key list for the context.
|
|
694
|
+
|
|
695
|
+
Raises:
|
|
696
|
+
DerivaMLException: If annotation or context doesn't exist, or foreign key not found.
|
|
697
|
+
|
|
698
|
+
Example:
|
|
699
|
+
>>> ml.remove_visible_foreign_key("Subject", "detailed", ["domain", "Image_Subject_fkey"])
|
|
700
|
+
>>> ml.remove_visible_foreign_key("Subject", "detailed", 0) # Remove first
|
|
701
|
+
>>> ml.apply_annotations()
|
|
702
|
+
"""
|
|
703
|
+
table_obj = self.model.name_to_table(table)
|
|
704
|
+
|
|
705
|
+
# Get visible_foreign_keys annotation
|
|
706
|
+
visible_fkeys = table_obj.annotations.get(VISIBLE_FOREIGN_KEYS_TAG, {})
|
|
707
|
+
if not visible_fkeys:
|
|
708
|
+
raise DerivaMLException(
|
|
709
|
+
f"Table '{table_obj.name}' has no visible-foreign-keys annotation."
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
# Get the context list
|
|
713
|
+
context_list = visible_fkeys.get(context)
|
|
714
|
+
if context_list is None:
|
|
715
|
+
raise DerivaMLException(
|
|
716
|
+
f"Context '{context}' not found in visible-foreign-keys annotation."
|
|
717
|
+
)
|
|
718
|
+
if isinstance(context_list, str):
|
|
719
|
+
raise DerivaMLException(
|
|
720
|
+
f"Context '{context}' references another context '{context_list}'. "
|
|
721
|
+
"Set it explicitly first with set_visible_foreign_keys()."
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
# Make a copy
|
|
725
|
+
context_list = list(context_list)
|
|
726
|
+
removed = None
|
|
727
|
+
|
|
728
|
+
# Remove by index or by value
|
|
729
|
+
if isinstance(foreign_key, int):
|
|
730
|
+
if 0 <= foreign_key < len(context_list):
|
|
731
|
+
removed = context_list.pop(foreign_key)
|
|
732
|
+
else:
|
|
733
|
+
raise DerivaMLException(
|
|
734
|
+
f"Index {foreign_key} out of range (list has {len(context_list)} items)."
|
|
735
|
+
)
|
|
736
|
+
else:
|
|
737
|
+
# Find and remove the foreign key
|
|
738
|
+
for i, item in enumerate(context_list):
|
|
739
|
+
if item == foreign_key:
|
|
740
|
+
removed = context_list.pop(i)
|
|
741
|
+
break
|
|
742
|
+
|
|
743
|
+
if removed is None:
|
|
744
|
+
raise DerivaMLException(
|
|
745
|
+
f"Foreign key {foreign_key!r} not found in context '{context}'."
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
# Update the annotation
|
|
749
|
+
visible_fkeys[context] = context_list
|
|
750
|
+
table_obj.annotations[VISIBLE_FOREIGN_KEYS_TAG] = visible_fkeys
|
|
751
|
+
|
|
752
|
+
return context_list
|
|
753
|
+
|
|
754
|
+
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
|
755
|
+
def reorder_visible_foreign_keys(
|
|
756
|
+
self,
|
|
757
|
+
table: str | Table,
|
|
758
|
+
context: str,
|
|
759
|
+
new_order: list[int] | list[list[str] | dict[str, Any]],
|
|
760
|
+
) -> list[Any]:
|
|
761
|
+
"""Reorder foreign keys in the visible-foreign-keys list for a specific context.
|
|
762
|
+
|
|
763
|
+
Convenience method for reordering related tables without manually
|
|
764
|
+
reconstructing the list. Changes are staged until apply_annotations()
|
|
765
|
+
is called.
|
|
766
|
+
|
|
767
|
+
Args:
|
|
768
|
+
table: Table name or Table object.
|
|
769
|
+
context: The context to modify (e.g., "detailed", "*").
|
|
770
|
+
new_order: The new order specification. Can be:
|
|
771
|
+
- List of indices: [2, 0, 1] reorders by current positions
|
|
772
|
+
- List of foreign key refs: [["schema", "fkey1"], ...] specifies exact order
|
|
773
|
+
|
|
774
|
+
Returns:
|
|
775
|
+
The reordered foreign key list.
|
|
776
|
+
|
|
777
|
+
Raises:
|
|
778
|
+
DerivaMLException: If annotation or context doesn't exist, or invalid order.
|
|
779
|
+
|
|
780
|
+
Example:
|
|
781
|
+
>>> ml.reorder_visible_foreign_keys("Subject", "detailed", [2, 0, 1])
|
|
782
|
+
>>> ml.apply_annotations()
|
|
783
|
+
"""
|
|
784
|
+
table_obj = self.model.name_to_table(table)
|
|
785
|
+
|
|
786
|
+
# Get visible_foreign_keys annotation
|
|
787
|
+
visible_fkeys = table_obj.annotations.get(VISIBLE_FOREIGN_KEYS_TAG, {})
|
|
788
|
+
if not visible_fkeys:
|
|
789
|
+
raise DerivaMLException(
|
|
790
|
+
f"Table '{table_obj.name}' has no visible-foreign-keys annotation."
|
|
791
|
+
)
|
|
792
|
+
|
|
793
|
+
# Get the context list
|
|
794
|
+
context_list = visible_fkeys.get(context)
|
|
795
|
+
if context_list is None:
|
|
796
|
+
raise DerivaMLException(
|
|
797
|
+
f"Context '{context}' not found in visible-foreign-keys annotation."
|
|
798
|
+
)
|
|
799
|
+
if isinstance(context_list, str):
|
|
800
|
+
raise DerivaMLException(
|
|
801
|
+
f"Context '{context}' references another context '{context_list}'. "
|
|
802
|
+
"Set it explicitly first with set_visible_foreign_keys()."
|
|
803
|
+
)
|
|
804
|
+
|
|
805
|
+
original_list = list(context_list)
|
|
806
|
+
|
|
807
|
+
# Determine if new_order is indices or foreign key specs
|
|
808
|
+
if new_order and isinstance(new_order[0], int):
|
|
809
|
+
# Reorder by indices
|
|
810
|
+
if len(new_order) != len(original_list):
|
|
811
|
+
raise DerivaMLException(
|
|
812
|
+
f"Index list length ({len(new_order)}) must match "
|
|
813
|
+
f"current list length ({len(original_list)})."
|
|
814
|
+
)
|
|
815
|
+
if set(new_order) != set(range(len(original_list))):
|
|
816
|
+
raise DerivaMLException("Index list must contain each index exactly once.")
|
|
817
|
+
new_list = [original_list[i] for i in new_order]
|
|
818
|
+
else:
|
|
819
|
+
# new_order is the exact new foreign key list
|
|
820
|
+
new_list = list(new_order)
|
|
821
|
+
|
|
822
|
+
# Update the annotation
|
|
823
|
+
visible_fkeys[context] = new_list
|
|
824
|
+
table_obj.annotations[VISIBLE_FOREIGN_KEYS_TAG] = visible_fkeys
|
|
825
|
+
|
|
826
|
+
return new_list
|
|
827
|
+
|
|
828
|
+
# =========================================================================
|
|
829
|
+
# Template Helpers
|
|
830
|
+
# =========================================================================
|
|
831
|
+
|
|
832
|
+
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
|
833
|
+
def get_handlebars_template_variables(self, table: str | Table) -> dict[str, Any]:
|
|
834
|
+
"""Get all available template variables for a table.
|
|
835
|
+
|
|
836
|
+
Returns the columns, foreign keys, and special variables that can be
|
|
837
|
+
used in Handlebars templates (row_markdown_pattern, markdown_pattern, etc.)
|
|
838
|
+
for the specified table.
|
|
839
|
+
|
|
840
|
+
Args:
|
|
841
|
+
table: Table name or Table object.
|
|
842
|
+
|
|
843
|
+
Returns:
|
|
844
|
+
Dictionary with columns, foreign_keys, special_variables, and helper_examples.
|
|
845
|
+
|
|
846
|
+
Example:
|
|
847
|
+
>>> vars = ml.get_handlebars_template_variables("Image")
|
|
848
|
+
>>> for col in vars["columns"]:
|
|
849
|
+
... print(f"{col['name']}: {col['template']}")
|
|
850
|
+
"""
|
|
851
|
+
table_obj = self.model.name_to_table(table)
|
|
852
|
+
|
|
853
|
+
# Get columns
|
|
854
|
+
columns = []
|
|
855
|
+
for col in table_obj.columns:
|
|
856
|
+
columns.append({
|
|
857
|
+
"name": col.name,
|
|
858
|
+
"type": str(col.type.typename),
|
|
859
|
+
"template": "{{{" + col.name + "}}}",
|
|
860
|
+
"row_template": "{{{_row." + col.name + "}}}",
|
|
861
|
+
})
|
|
862
|
+
|
|
863
|
+
# Get foreign keys (outbound)
|
|
864
|
+
foreign_keys = []
|
|
865
|
+
for fkey in table_obj.foreign_keys:
|
|
866
|
+
schema_name = fkey.constraint_schema.name
|
|
867
|
+
constraint_name = fkey.constraint_name
|
|
868
|
+
fk_path = f"$fkeys.{schema_name}.{constraint_name}"
|
|
869
|
+
|
|
870
|
+
# Get columns from referenced table
|
|
871
|
+
ref_columns = [col.name for col in fkey.pk_table.columns]
|
|
872
|
+
|
|
873
|
+
foreign_keys.append({
|
|
874
|
+
"constraint": [schema_name, constraint_name],
|
|
875
|
+
"from_columns": [col.name for col in fkey.columns],
|
|
876
|
+
"to_table": fkey.pk_table.name,
|
|
877
|
+
"to_columns": ref_columns,
|
|
878
|
+
"values_template": "{{{" + fk_path + ".values.COLUMN}}}",
|
|
879
|
+
"row_name_template": "{{{" + fk_path + ".rowName}}}",
|
|
880
|
+
"example_column_templates": [
|
|
881
|
+
"{{{" + fk_path + ".values." + c + "}}}"
|
|
882
|
+
for c in ref_columns[:3] # Show first 3 as examples
|
|
883
|
+
]
|
|
884
|
+
})
|
|
885
|
+
|
|
886
|
+
return {
|
|
887
|
+
"table": table_obj.name,
|
|
888
|
+
"columns": columns,
|
|
889
|
+
"foreign_keys": foreign_keys,
|
|
890
|
+
"special_variables": {
|
|
891
|
+
"_value": {
|
|
892
|
+
"description": "Current column value (in column_display)",
|
|
893
|
+
"template": "{{{_value}}}"
|
|
894
|
+
},
|
|
895
|
+
"_row": {
|
|
896
|
+
"description": "Object with all row columns",
|
|
897
|
+
"template": "{{{_row.column_name}}}"
|
|
898
|
+
},
|
|
899
|
+
"$catalog.id": {
|
|
900
|
+
"description": "Catalog ID",
|
|
901
|
+
"template": "{{{$catalog.id}}}"
|
|
902
|
+
},
|
|
903
|
+
"$catalog.snapshot": {
|
|
904
|
+
"description": "Current snapshot ID",
|
|
905
|
+
"template": "{{{$catalog.snapshot}}}"
|
|
906
|
+
},
|
|
907
|
+
},
|
|
908
|
+
"helper_examples": {
|
|
909
|
+
"conditional": "{{#if column}}...{{else}}...{{/if}}",
|
|
910
|
+
"iteration": "{{#each array}}{{{this}}}{{/each}}",
|
|
911
|
+
"comparison": "{{#ifCond val1 '==' val2}}...{{/ifCond}}",
|
|
912
|
+
"date_format": "{{formatDate RCT 'YYYY-MM-DD'}}",
|
|
913
|
+
"json_output": "{{toJSON object}}"
|
|
914
|
+
}
|
|
915
|
+
}
|