simstack 0.2.0__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.
- simstack/__init__.py +0 -0
- simstack/_version.py +34 -0
- simstack/core/__init__.py +0 -0
- simstack/core/artifacts.py +447 -0
- simstack/core/asnyc_helper.py +14 -0
- simstack/core/config_file.py +60 -0
- simstack/core/context.py +167 -0
- simstack/core/data.py +139 -0
- simstack/core/definitions.py +21 -0
- simstack/core/engine.py +116 -0
- simstack/core/find_simstack_modules.py +43 -0
- simstack/core/hash.py +177 -0
- simstack/core/model_table.py +199 -0
- simstack/core/node.py +911 -0
- simstack/core/node_runner.py +254 -0
- simstack/core/node_table.py +380 -0
- simstack/core/recompute_artifacts.py +120 -0
- simstack/core/reset_db.py +12 -0
- simstack/core/resources.py +101 -0
- simstack/core/run_node.py +69 -0
- simstack/core/runner.py +435 -0
- simstack/core/simstack_result.py +41 -0
- simstack/core/submit_node.py +143 -0
- simstack/core/task_id.py +22 -0
- simstack/data/__init__.py +0 -0
- simstack/methods/__init__.py +0 -0
- simstack/methods/get_file.py +10 -0
- simstack/methods/multiple_executor.py +307 -0
- simstack/models/__init__.py +37 -0
- simstack/models/array_list.py +48 -0
- simstack/models/array_storage.py +54 -0
- simstack/models/artifact_models.py +127 -0
- simstack/models/base_types.py +208 -0
- simstack/models/charts_artifact.py +411 -0
- simstack/models/dataset.py +432 -0
- simstack/models/dataset_metadata.py +303 -0
- simstack/models/file_instance.py +77 -0
- simstack/models/file_list.py +321 -0
- simstack/models/files.py +229 -0
- simstack/models/log_entry_model.py +37 -0
- simstack/models/models.py +40 -0
- simstack/models/node_registry.py +105 -0
- simstack/models/pandas_model.py +210 -0
- simstack/models/parameters.py +388 -0
- simstack/models/pickle_models.py +311 -0
- simstack/models/runner_model.py +39 -0
- simstack/models/simple_table.py +32 -0
- simstack/models/simstack_model.py +97 -0
- simstack/models/slurm_info.py +17 -0
- simstack/models/table_artifact.py +87 -0
- simstack/util/__init__.py +0 -0
- simstack/util/async_zip_utils.py +18 -0
- simstack/util/cleaned_json_schema.py +20 -0
- simstack/util/config_reader.py +273 -0
- simstack/util/custom_model_dump.py +75 -0
- simstack/util/db.py +457 -0
- simstack/util/db_logger.py +117 -0
- simstack/util/default_from_dict.py +57 -0
- simstack/util/directory_iterator.py +216 -0
- simstack/util/file_hashing.py +68 -0
- simstack/util/files.py +396 -0
- simstack/util/find_python_files.py +33 -0
- simstack/util/generate_ui_schema.py +80 -0
- simstack/util/geometry_utils.py +118 -0
- simstack/util/get_module_path.py +26 -0
- simstack/util/git_pull.py +65 -0
- simstack/util/git_repository_status.py +93 -0
- simstack/util/import_module.py +65 -0
- simstack/util/importer.py +163 -0
- simstack/util/make_graph.py +107 -0
- simstack/util/make_table.py +827 -0
- simstack/util/minimal_route_finder.py +72 -0
- simstack/util/mock_db.py +144 -0
- simstack/util/module_path_checker.py +38 -0
- simstack/util/path_manager.py +234 -0
- simstack/util/project_root_finder.py +36 -0
- simstack/util/queue_watcher.py +262 -0
- simstack/util/route_finder.py +74 -0
- simstack/util/runner_util.py +162 -0
- simstack/util/safe_code_executor.py +194 -0
- simstack/util/sample_config.py +98 -0
- simstack/util/sensitive_content_filter.py +48 -0
- simstack/util/setup_logging.py +49 -0
- simstack/util/submit_to_watchdog.py +106 -0
- simstack/util/ui_tools.py +213 -0
- simstack-0.2.0.dist-info/METADATA +361 -0
- simstack-0.2.0.dist-info/RECORD +91 -0
- simstack-0.2.0.dist-info/WHEEL +5 -0
- simstack-0.2.0.dist-info/entry_points.txt +10 -0
- simstack-0.2.0.dist-info/licenses/LICENSE +21 -0
- simstack-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,827 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Union, Dict, Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def make_table_entries_helper(
|
|
6
|
+
model_instance,
|
|
7
|
+
table_name=None,
|
|
8
|
+
max_recursion_level=1,
|
|
9
|
+
drop_id=True,
|
|
10
|
+
current_level=0,
|
|
11
|
+
visited=None,
|
|
12
|
+
field_prefix="",
|
|
13
|
+
):
|
|
14
|
+
"""
|
|
15
|
+
Create table data for AG Grid from a model instance.
|
|
16
|
+
Handles datetime objects properly and can optionally drop ID fields.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
model_instance: The model instance to process
|
|
20
|
+
table_name: Optional name for the table (default: None, uses class name)
|
|
21
|
+
max_recursion_level: Maximum depth for processing nested models (default: 1)
|
|
22
|
+
drop_id: Whether to drop the ID field (default: True)
|
|
23
|
+
current_level: Current recursion level (default: 0)
|
|
24
|
+
visited: Set of objects already visited to prevent infinite recursion (default: None)
|
|
25
|
+
field_prefix: Prefix for field paths in nested structures (default: "")
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
dict: Dictionary with 'tableName' and 'tableData' (rows data for AG Grid)
|
|
29
|
+
"""
|
|
30
|
+
# Initialize the visited set to track objects for preventing infinite recursion
|
|
31
|
+
if visited is None:
|
|
32
|
+
visited = set()
|
|
33
|
+
|
|
34
|
+
# If the instance has already been visited, stop recursion
|
|
35
|
+
if id(model_instance) in visited:
|
|
36
|
+
return {"tableData": {}}
|
|
37
|
+
|
|
38
|
+
# Mark this instance as visited
|
|
39
|
+
visited.add(id(model_instance))
|
|
40
|
+
|
|
41
|
+
# Get model class and fields
|
|
42
|
+
model_class = type(model_instance)
|
|
43
|
+
|
|
44
|
+
if hasattr(model_instance, "make_table_entries"):
|
|
45
|
+
field_prefix = f"{field_prefix}." if field_prefix else ""
|
|
46
|
+
field_prefix = (
|
|
47
|
+
f"{field_prefix}{model_instance.__class__.__name__}"
|
|
48
|
+
if field_prefix
|
|
49
|
+
else model_instance.__class__.__name__
|
|
50
|
+
)
|
|
51
|
+
return model_instance.make_table_entries(
|
|
52
|
+
max_recursion_level=1,
|
|
53
|
+
drop_id=True,
|
|
54
|
+
current_level=0,
|
|
55
|
+
visited=None,
|
|
56
|
+
field_prefix="",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
model_fields = getattr(model_instance, "model_fields", {})
|
|
60
|
+
|
|
61
|
+
# Use class name if table_name not provided
|
|
62
|
+
if table_name is None:
|
|
63
|
+
table_name = model_class.__name__
|
|
64
|
+
|
|
65
|
+
# Initialize empty summary dictionary
|
|
66
|
+
summary = {}
|
|
67
|
+
|
|
68
|
+
# Process each field
|
|
69
|
+
for field_name, field_type in model_fields.items():
|
|
70
|
+
# Skip ID fields if drop_id is True
|
|
71
|
+
if drop_id and (field_name == "id" or field_name == "_id"):
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
# Calculate the full field path for AG Grid
|
|
75
|
+
field_path = f"{field_prefix}.{field_name}" if field_prefix else field_name
|
|
76
|
+
|
|
77
|
+
# Get field value
|
|
78
|
+
field_value = getattr(model_instance, field_name, None)
|
|
79
|
+
|
|
80
|
+
# If the field value is None, add it to summary
|
|
81
|
+
if field_value is None:
|
|
82
|
+
summary[field_name] = None
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
# Handle datetime objects for AG Grid
|
|
86
|
+
if isinstance(field_value, datetime):
|
|
87
|
+
summary[field_name] = field_value.isoformat()
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
# Check if the field is a nested model
|
|
91
|
+
is_nested_model = hasattr(field_value, "model_fields")
|
|
92
|
+
|
|
93
|
+
# If the recursion level is 0, exclude all nested objects
|
|
94
|
+
if max_recursion_level == 0 and is_nested_model:
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
# Handle nested model if it exists and we haven't reached max recursion
|
|
98
|
+
if is_nested_model and current_level < max_recursion_level:
|
|
99
|
+
# Check if the object has its own make_table method
|
|
100
|
+
if hasattr(field_value, "make_table_entries"):
|
|
101
|
+
nested_result = field_value.make_table_entries(
|
|
102
|
+
max_recursion_level=max_recursion_level,
|
|
103
|
+
current_level=current_level + 1,
|
|
104
|
+
visited=visited,
|
|
105
|
+
field_prefix=f"{field_path}.",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
if isinstance(nested_result, dict) and "tableData" in nested_result:
|
|
109
|
+
nested_summary = nested_result["tableData"]
|
|
110
|
+
else:
|
|
111
|
+
# If make_table doesn't return expected format
|
|
112
|
+
nested_summary = nested_result
|
|
113
|
+
else:
|
|
114
|
+
# Process the nested model recursively
|
|
115
|
+
nested_result = make_table_entries_helper(
|
|
116
|
+
field_value,
|
|
117
|
+
f"{table_name}_{field_name}",
|
|
118
|
+
max_recursion_level,
|
|
119
|
+
drop_id,
|
|
120
|
+
current_level + 1,
|
|
121
|
+
visited,
|
|
122
|
+
f"{field_path}.",
|
|
123
|
+
)
|
|
124
|
+
nested_summary = nested_result
|
|
125
|
+
|
|
126
|
+
# Add the nested object as a sub-dictionary
|
|
127
|
+
summary[field_name] = nested_summary
|
|
128
|
+
|
|
129
|
+
elif field_type == "self" and current_level < max_recursion_level:
|
|
130
|
+
# Handle self-referential field, but only go one level deep to avoid cycles
|
|
131
|
+
raise ValueError(
|
|
132
|
+
f"Self-referential field '{field_name}' is not supported in make_table_entries."
|
|
133
|
+
)
|
|
134
|
+
if field_value is not None and id(field_value) not in visited:
|
|
135
|
+
# Process only the direct fields of the self-reference
|
|
136
|
+
nested_result = make_table_entries_helper(
|
|
137
|
+
field_value,
|
|
138
|
+
f"{table_name}_{field_name}",
|
|
139
|
+
0, # Only process direct fields, no further recursion
|
|
140
|
+
drop_id,
|
|
141
|
+
current_level + 1,
|
|
142
|
+
visited,
|
|
143
|
+
f"{field_path}.",
|
|
144
|
+
)
|
|
145
|
+
nested_summary = nested_result["tableData"]
|
|
146
|
+
summary[field_name] = nested_summary
|
|
147
|
+
else:
|
|
148
|
+
# Handle lists/arrays
|
|
149
|
+
if isinstance(field_value, list):
|
|
150
|
+
processed_list = []
|
|
151
|
+
for item in field_value:
|
|
152
|
+
if isinstance(item, datetime):
|
|
153
|
+
processed_list.append(item.isoformat())
|
|
154
|
+
elif (
|
|
155
|
+
hasattr(item, "model_fields")
|
|
156
|
+
and current_level < max_recursion_level
|
|
157
|
+
):
|
|
158
|
+
# Handle nested models in lists
|
|
159
|
+
if hasattr(item, "make_table"):
|
|
160
|
+
nested_result = item.make_table_entries(
|
|
161
|
+
max_recursion_level=0,
|
|
162
|
+
current_level=current_level + 1,
|
|
163
|
+
visited=visited.copy(),
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
if (
|
|
167
|
+
isinstance(nested_result, dict)
|
|
168
|
+
and "tableData" in nested_result
|
|
169
|
+
):
|
|
170
|
+
processed_list.append(nested_result["tableData"])
|
|
171
|
+
else:
|
|
172
|
+
processed_list.append(nested_result)
|
|
173
|
+
else:
|
|
174
|
+
nested_result = make_table_entries_helper(
|
|
175
|
+
item,
|
|
176
|
+
f"{table_name}_{field_name}_item",
|
|
177
|
+
0,
|
|
178
|
+
drop_id,
|
|
179
|
+
current_level + 1,
|
|
180
|
+
visited.copy(),
|
|
181
|
+
"",
|
|
182
|
+
)
|
|
183
|
+
processed_list.append(nested_result["tableData"])
|
|
184
|
+
else:
|
|
185
|
+
processed_list.append(item)
|
|
186
|
+
summary[field_name] = processed_list
|
|
187
|
+
else:
|
|
188
|
+
# This is a simple field, add it directly
|
|
189
|
+
summary[field_name] = field_value
|
|
190
|
+
|
|
191
|
+
return summary
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def is_pydantic_model(obj):
|
|
195
|
+
"""
|
|
196
|
+
Check if an object is a Pydantic model class.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
obj: The object to check
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
bool: True if the object is a Pydantic model class, False otherwise
|
|
203
|
+
"""
|
|
204
|
+
# For Pydantic V1
|
|
205
|
+
if hasattr(obj, "__fields__"):
|
|
206
|
+
return True
|
|
207
|
+
|
|
208
|
+
# For Pydantic V2
|
|
209
|
+
if hasattr(obj, "model_fields"):
|
|
210
|
+
return True
|
|
211
|
+
|
|
212
|
+
# Check for BaseModel inheritance (works for both V1 and V2)
|
|
213
|
+
if isinstance(obj, type):
|
|
214
|
+
from inspect import getmro
|
|
215
|
+
|
|
216
|
+
for base in getmro(obj):
|
|
217
|
+
if base.__name__ == "BaseModel":
|
|
218
|
+
return True
|
|
219
|
+
|
|
220
|
+
# Handle typing objects like Optional[Model]
|
|
221
|
+
if hasattr(obj, "__origin__") and hasattr(obj, "__args__"):
|
|
222
|
+
# Check if it's a Union type (which includes Optional)
|
|
223
|
+
from typing import _SpecialGenericAlias, _GenericAlias
|
|
224
|
+
|
|
225
|
+
if (
|
|
226
|
+
isinstance(obj, (_SpecialGenericAlias, _GenericAlias))
|
|
227
|
+
and obj.__origin__ is Union
|
|
228
|
+
):
|
|
229
|
+
return any(is_pydantic_model(arg) for arg in obj.__args__)
|
|
230
|
+
|
|
231
|
+
return False
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def make_column_defs_helper(
|
|
235
|
+
model_class,
|
|
236
|
+
table_name=None,
|
|
237
|
+
max_recursion_level=1,
|
|
238
|
+
drop_id=True,
|
|
239
|
+
current_level=0,
|
|
240
|
+
visited=None,
|
|
241
|
+
field_prefix="",
|
|
242
|
+
):
|
|
243
|
+
"""
|
|
244
|
+
Create column definitions for AG Grid based on a model class.
|
|
245
|
+
Uses the same logic as table data generation but for model class.
|
|
246
|
+
This is a helper function mapped to make_column_defs in simstack_model.
|
|
247
|
+
The idea is that most classes can call the helper function but some classes might
|
|
248
|
+
want to override it to return specific column definitions.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
model_class: The model class to process
|
|
252
|
+
table_name: Optional name for the table (default: None, uses class name)
|
|
253
|
+
max_recursion_level: Maximum depth for processing nested models (default: 1)
|
|
254
|
+
drop_id: Whether to drop the ID field (default: True)
|
|
255
|
+
current_level: Current recursion level (default: 0)
|
|
256
|
+
visited: Set of objects already visited to prevent infinite recursion (default: None)
|
|
257
|
+
field_prefix: Prefix for field paths in nested structures (default: "")
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
list: Column definitions for AG Grid
|
|
261
|
+
"""
|
|
262
|
+
from typing import get_origin, get_args, List, Union
|
|
263
|
+
|
|
264
|
+
# Initialize a visited set to track classes for preventing infinite recursion
|
|
265
|
+
if visited is None:
|
|
266
|
+
visited = set()
|
|
267
|
+
|
|
268
|
+
# If the class has already been visited, stop recursion
|
|
269
|
+
if id(model_class) in visited:
|
|
270
|
+
return []
|
|
271
|
+
|
|
272
|
+
# Mark this class as visited
|
|
273
|
+
visited.add(id(model_class))
|
|
274
|
+
|
|
275
|
+
if hasattr(model_class, "make_column_defs"):
|
|
276
|
+
return model_class.make_column_defs(
|
|
277
|
+
model_class,
|
|
278
|
+
table_name=None,
|
|
279
|
+
max_recursion_level=1,
|
|
280
|
+
drop_id=True,
|
|
281
|
+
current_level=0,
|
|
282
|
+
visited=None,
|
|
283
|
+
field_prefix="",
|
|
284
|
+
)
|
|
285
|
+
# Get model fields
|
|
286
|
+
model_fields = getattr(
|
|
287
|
+
model_class, "model_fields", getattr(model_class, "__fields__", {})
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# Use class name if table_name not provided
|
|
291
|
+
if table_name is None:
|
|
292
|
+
table_name = model_class.__name__
|
|
293
|
+
|
|
294
|
+
# Initialize column definitions
|
|
295
|
+
column_defs = []
|
|
296
|
+
|
|
297
|
+
# Process each field
|
|
298
|
+
for field_name, field_info in model_fields.items():
|
|
299
|
+
# Skip ID fields if drop_id is True
|
|
300
|
+
if drop_id and (field_name == "id" or field_name == "_id"):
|
|
301
|
+
continue
|
|
302
|
+
|
|
303
|
+
# Calculate the full field path for AG Grid
|
|
304
|
+
field_path = f"{field_prefix}{field_name}" if field_prefix else field_name
|
|
305
|
+
|
|
306
|
+
# Extract field type (different in V1 and V2)
|
|
307
|
+
if hasattr(field_info, "annotation"):
|
|
308
|
+
# Pydantic V2
|
|
309
|
+
field_type = field_info.annotation
|
|
310
|
+
elif hasattr(field_info, "type_"):
|
|
311
|
+
# Pydantic V1
|
|
312
|
+
field_type = field_info.type_
|
|
313
|
+
else:
|
|
314
|
+
# Fallback
|
|
315
|
+
field_type = None
|
|
316
|
+
|
|
317
|
+
# Check if the field type is a Pydantic model
|
|
318
|
+
is_nested_model = is_pydantic_model(field_type)
|
|
319
|
+
|
|
320
|
+
# Extract actual model class for nested types (like Optional[Model])
|
|
321
|
+
if is_nested_model and hasattr(field_type, "__args__"):
|
|
322
|
+
args = get_args(field_type)
|
|
323
|
+
for arg in args:
|
|
324
|
+
if is_pydantic_model(arg):
|
|
325
|
+
field_type = arg
|
|
326
|
+
break
|
|
327
|
+
|
|
328
|
+
# Create column definition based on the field type
|
|
329
|
+
column_def = create_column_def(field_name, field_type, None, field_path)
|
|
330
|
+
|
|
331
|
+
# Set object type for nested models
|
|
332
|
+
if is_nested_model:
|
|
333
|
+
column_def["type"] = "objectColumn"
|
|
334
|
+
|
|
335
|
+
# If recursion level is 0, exclude all nested objects but include column def for the field itself
|
|
336
|
+
if max_recursion_level == 0 and is_nested_model:
|
|
337
|
+
column_defs.append(column_def)
|
|
338
|
+
continue
|
|
339
|
+
|
|
340
|
+
# Handle nested model class if it exists and we haven't reached max recursion
|
|
341
|
+
if is_nested_model and current_level < max_recursion_level:
|
|
342
|
+
# Check if the class has its own make_columns method
|
|
343
|
+
if hasattr(field_type, "make_column_defs"):
|
|
344
|
+
nested_columns = field_type.make_column_defs(
|
|
345
|
+
max_recursion_level=max_recursion_level,
|
|
346
|
+
current_level=current_level + 1,
|
|
347
|
+
visited=visited.copy(),
|
|
348
|
+
)
|
|
349
|
+
else:
|
|
350
|
+
# Process the nested model class recursively
|
|
351
|
+
nested_columns = make_column_defs_helper(
|
|
352
|
+
field_type,
|
|
353
|
+
f"{table_name}_{field_name}",
|
|
354
|
+
max_recursion_level,
|
|
355
|
+
drop_id,
|
|
356
|
+
current_level + 1,
|
|
357
|
+
visited.copy(),
|
|
358
|
+
f"{field_path}.",
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
# Create a column group for nested fields if we have columns
|
|
362
|
+
if nested_columns:
|
|
363
|
+
group_column = {
|
|
364
|
+
"headerName": field_name.capitalize(),
|
|
365
|
+
"marryChildren": True,
|
|
366
|
+
"children": nested_columns,
|
|
367
|
+
}
|
|
368
|
+
column_defs.append(group_column)
|
|
369
|
+
else:
|
|
370
|
+
column_defs.append(column_def)
|
|
371
|
+
elif field_type == "self" and current_level < max_recursion_level:
|
|
372
|
+
# Handle self-referential field type
|
|
373
|
+
# Create a new visited set for this branch to avoid affecting other branches
|
|
374
|
+
self_visited = visited.copy()
|
|
375
|
+
if model_class not in self_visited:
|
|
376
|
+
# Process only the direct fields of the self-reference
|
|
377
|
+
nested_columns = make_column_defs_helper(
|
|
378
|
+
model_class,
|
|
379
|
+
f"{table_name}_{field_name}",
|
|
380
|
+
0, # Only process direct fields, no further recursion
|
|
381
|
+
drop_id,
|
|
382
|
+
current_level + 1,
|
|
383
|
+
self_visited,
|
|
384
|
+
f"{field_path}.",
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
# Create a column group for self-referential fields
|
|
388
|
+
if nested_columns:
|
|
389
|
+
group_column = {
|
|
390
|
+
"headerName": field_name.capitalize(),
|
|
391
|
+
"marryChildren": True,
|
|
392
|
+
"children": nested_columns,
|
|
393
|
+
}
|
|
394
|
+
column_defs.append(group_column)
|
|
395
|
+
else:
|
|
396
|
+
column_defs.append(column_def)
|
|
397
|
+
else:
|
|
398
|
+
column_defs.append(column_def)
|
|
399
|
+
else:
|
|
400
|
+
# Handle lists/arrays
|
|
401
|
+
is_list_type = False
|
|
402
|
+
inner_type = None
|
|
403
|
+
|
|
404
|
+
# Check for various list types using get_origin and get_args
|
|
405
|
+
origin = get_origin(field_type)
|
|
406
|
+
if origin is list or origin is List:
|
|
407
|
+
is_list_type = True
|
|
408
|
+
args = get_args(field_type)
|
|
409
|
+
if args:
|
|
410
|
+
inner_type = args[0]
|
|
411
|
+
# Handle Optional[List[...]]
|
|
412
|
+
elif origin is Union:
|
|
413
|
+
args = get_args(field_type)
|
|
414
|
+
for arg in args:
|
|
415
|
+
arg_origin = get_origin(arg)
|
|
416
|
+
if arg_origin is list or arg_origin is List:
|
|
417
|
+
is_list_type = True
|
|
418
|
+
arg_args = get_args(arg)
|
|
419
|
+
if arg_args:
|
|
420
|
+
inner_type = arg_args[0]
|
|
421
|
+
break
|
|
422
|
+
|
|
423
|
+
if is_list_type:
|
|
424
|
+
# For lists, we add a special column definition
|
|
425
|
+
list_column_def = {
|
|
426
|
+
"headerName": field_name.capitalize(),
|
|
427
|
+
"field": field_path,
|
|
428
|
+
"cellRenderer": "agArrayCellRenderer",
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
# If the list contains models, we might want to indicate that
|
|
432
|
+
if inner_type and is_pydantic_model(inner_type):
|
|
433
|
+
list_column_def["type"] = "modelArrayColumn"
|
|
434
|
+
|
|
435
|
+
column_defs.append(list_column_def)
|
|
436
|
+
else:
|
|
437
|
+
# This is a simple field, add column definition directly
|
|
438
|
+
column_defs.append(column_def)
|
|
439
|
+
|
|
440
|
+
return column_defs
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def make_column_defs_instance(
|
|
444
|
+
model_instance,
|
|
445
|
+
table_name=None,
|
|
446
|
+
max_recursion_level=1,
|
|
447
|
+
drop_id=True,
|
|
448
|
+
current_level=0,
|
|
449
|
+
visited=None,
|
|
450
|
+
field_prefix="",
|
|
451
|
+
):
|
|
452
|
+
"""
|
|
453
|
+
Create column definitions for AG Grid based on a model instance.
|
|
454
|
+
Uses the same logic as table data generation but works on an actual instance.
|
|
455
|
+
This allows for dynamic column generation based on actual data values.
|
|
456
|
+
|
|
457
|
+
Args:
|
|
458
|
+
model_instance: The model instance to process
|
|
459
|
+
table_name: Optional name for the table (default: None, uses class name)
|
|
460
|
+
max_recursion_level: Maximum depth for processing nested models (default: 1)
|
|
461
|
+
drop_id: Whether to drop the ID field (default: True)
|
|
462
|
+
current_level: Current recursion level (default: 0)
|
|
463
|
+
visited: Set of objects already visited to prevent infinite recursion (default: None)
|
|
464
|
+
field_prefix: Prefix for field paths in nested structures (default: "")
|
|
465
|
+
|
|
466
|
+
Returns:
|
|
467
|
+
list: Column definitions for AG Grid
|
|
468
|
+
"""
|
|
469
|
+
from typing import get_origin, get_args, List, Union
|
|
470
|
+
|
|
471
|
+
# Initialize a visited set to track instances for preventing infinite recursion
|
|
472
|
+
if visited is None:
|
|
473
|
+
visited = set()
|
|
474
|
+
|
|
475
|
+
# If the instance has already been visited, stop recursion
|
|
476
|
+
instance_id = id(model_instance)
|
|
477
|
+
if instance_id in visited:
|
|
478
|
+
return []
|
|
479
|
+
|
|
480
|
+
# Mark this instance as visited
|
|
481
|
+
visited.add(instance_id)
|
|
482
|
+
|
|
483
|
+
# Check if the instance has a custom make_column_defs method
|
|
484
|
+
if hasattr(model_instance, "make_column_defs_instance") and callable(
|
|
485
|
+
getattr(model_instance, "make_column_defs_instance")
|
|
486
|
+
):
|
|
487
|
+
return model_instance.make_column_defs_instance(
|
|
488
|
+
table_name=table_name,
|
|
489
|
+
max_recursion_level=max_recursion_level,
|
|
490
|
+
drop_id=drop_id,
|
|
491
|
+
current_level=current_level,
|
|
492
|
+
visited=visited.copy(),
|
|
493
|
+
field_prefix=field_prefix,
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
# Get model class and fields
|
|
497
|
+
model_class = type(model_instance)
|
|
498
|
+
model_fields = getattr(
|
|
499
|
+
model_class, "model_fields", getattr(model_class, "__fields__", {})
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
# Use class name if table_name not provided
|
|
503
|
+
if table_name is None:
|
|
504
|
+
table_name = model_class.__name__
|
|
505
|
+
|
|
506
|
+
# Initialize column definitions
|
|
507
|
+
column_defs = []
|
|
508
|
+
|
|
509
|
+
# Process each field
|
|
510
|
+
for field_name, field_info in model_fields.items():
|
|
511
|
+
# Skip ID fields if drop_id is True
|
|
512
|
+
if drop_id and (field_name == "id" or field_name == "_id"):
|
|
513
|
+
continue
|
|
514
|
+
|
|
515
|
+
# Calculate the full field path for AG Grid
|
|
516
|
+
field_path = f"{field_prefix}.{field_name}" if field_prefix else field_name
|
|
517
|
+
|
|
518
|
+
# Get the actual field value from the instance
|
|
519
|
+
try:
|
|
520
|
+
field_value = getattr(model_instance, field_name, None)
|
|
521
|
+
except AttributeError:
|
|
522
|
+
field_value = None
|
|
523
|
+
|
|
524
|
+
# Extract field type (different in V1 and V2)
|
|
525
|
+
if hasattr(field_info, "annotation"):
|
|
526
|
+
# Pydantic V2
|
|
527
|
+
field_type = field_info.annotation
|
|
528
|
+
elif hasattr(field_info, "type_"):
|
|
529
|
+
# Pydantic V1
|
|
530
|
+
field_type = field_info.type_
|
|
531
|
+
else:
|
|
532
|
+
# Fallback - infer from actual value
|
|
533
|
+
field_type = type(field_value) if field_value is not None else str
|
|
534
|
+
|
|
535
|
+
# Check if the field type is a Pydantic model
|
|
536
|
+
is_nested_model = is_pydantic_model(field_type)
|
|
537
|
+
|
|
538
|
+
# Extract actual model class for nested types (like Optional[Model])
|
|
539
|
+
if is_nested_model and hasattr(field_type, "__args__"):
|
|
540
|
+
args = get_args(field_type)
|
|
541
|
+
for arg in args:
|
|
542
|
+
if is_pydantic_model(arg):
|
|
543
|
+
field_type = arg
|
|
544
|
+
break
|
|
545
|
+
|
|
546
|
+
# For instances, we can also infer type from actual value
|
|
547
|
+
if field_value is not None and is_pydantic_model(type(field_value)):
|
|
548
|
+
field_type = type(field_value)
|
|
549
|
+
is_nested_model = True
|
|
550
|
+
|
|
551
|
+
# Create column definition based on the field type and actual value
|
|
552
|
+
column_def = create_column_def(field_name, field_type, field_value, field_path)
|
|
553
|
+
|
|
554
|
+
# Set object type for nested models
|
|
555
|
+
if is_nested_model:
|
|
556
|
+
column_def["type"] = "objectColumn"
|
|
557
|
+
|
|
558
|
+
# If recursion level is 0, exclude all nested objects but include column def for the field itself
|
|
559
|
+
if max_recursion_level == 0 and is_nested_model:
|
|
560
|
+
column_defs.append(column_def)
|
|
561
|
+
continue
|
|
562
|
+
|
|
563
|
+
# Handle nested model instance if it exists and we haven't reached max recursion
|
|
564
|
+
if (
|
|
565
|
+
is_nested_model
|
|
566
|
+
and field_value is not None
|
|
567
|
+
and current_level < max_recursion_level
|
|
568
|
+
):
|
|
569
|
+
# Check if the instance has its own make_column_defs method
|
|
570
|
+
if hasattr(field_value, "make_column_defs_instance") and callable(
|
|
571
|
+
getattr(field_value, "make_column_defs_instance")
|
|
572
|
+
):
|
|
573
|
+
nested_columns = field_value.make_column_defs_instance(
|
|
574
|
+
max_recursion_level=max_recursion_level,
|
|
575
|
+
current_level=current_level + 1,
|
|
576
|
+
visited=visited.copy(),
|
|
577
|
+
field_prefix=f"{field_path}." if field_path else "",
|
|
578
|
+
)
|
|
579
|
+
else:
|
|
580
|
+
# Process the nested model instance recursively
|
|
581
|
+
nested_columns = make_column_defs_instance(
|
|
582
|
+
field_value,
|
|
583
|
+
f"{table_name}_{field_name}",
|
|
584
|
+
max_recursion_level,
|
|
585
|
+
drop_id,
|
|
586
|
+
current_level + 1,
|
|
587
|
+
visited.copy(),
|
|
588
|
+
f"{field_path}.",
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
# Create a column group for nested fields if we have columns
|
|
592
|
+
if nested_columns:
|
|
593
|
+
group_column = {
|
|
594
|
+
"headerName": field_name.capitalize(),
|
|
595
|
+
"marryChildren": True,
|
|
596
|
+
"children": nested_columns,
|
|
597
|
+
}
|
|
598
|
+
column_defs.append(group_column)
|
|
599
|
+
else:
|
|
600
|
+
column_defs.append(column_def)
|
|
601
|
+
elif field_type == "self" and current_level < max_recursion_level:
|
|
602
|
+
# Handle self-referential field type
|
|
603
|
+
if field_value is not None:
|
|
604
|
+
# Create a new visited set for this branch to avoid affecting other branches
|
|
605
|
+
self_visited = visited.copy()
|
|
606
|
+
if instance_id not in self_visited:
|
|
607
|
+
# Process only the direct fields of the self-reference
|
|
608
|
+
nested_columns = make_column_defs_instance(
|
|
609
|
+
field_value,
|
|
610
|
+
f"{table_name}_{field_name}",
|
|
611
|
+
0, # Only process direct fields, no further recursion
|
|
612
|
+
drop_id,
|
|
613
|
+
current_level + 1,
|
|
614
|
+
self_visited,
|
|
615
|
+
f"{field_path}.",
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
# Create a column group for self-referential fields
|
|
619
|
+
if nested_columns:
|
|
620
|
+
group_column = {
|
|
621
|
+
"headerName": field_name.capitalize(),
|
|
622
|
+
"marryChildren": True,
|
|
623
|
+
"children": nested_columns,
|
|
624
|
+
}
|
|
625
|
+
column_defs.append(group_column)
|
|
626
|
+
else:
|
|
627
|
+
column_defs.append(column_def)
|
|
628
|
+
else:
|
|
629
|
+
column_defs.append(column_def)
|
|
630
|
+
else:
|
|
631
|
+
column_defs.append(column_def)
|
|
632
|
+
else:
|
|
633
|
+
# Handle lists/arrays
|
|
634
|
+
is_list_type = False
|
|
635
|
+
inner_type = None
|
|
636
|
+
|
|
637
|
+
# Check for various list types using get_origin and get_args
|
|
638
|
+
origin = get_origin(field_type)
|
|
639
|
+
if origin is list or origin is List:
|
|
640
|
+
is_list_type = True
|
|
641
|
+
args = get_args(field_type)
|
|
642
|
+
if args:
|
|
643
|
+
inner_type = args[0]
|
|
644
|
+
# Handle Optional[List[...]]
|
|
645
|
+
elif origin is Union:
|
|
646
|
+
args = get_args(field_type)
|
|
647
|
+
for arg in args:
|
|
648
|
+
arg_origin = get_origin(arg)
|
|
649
|
+
if arg_origin is list or arg_origin is List:
|
|
650
|
+
is_list_type = True
|
|
651
|
+
arg_args = get_args(arg)
|
|
652
|
+
if arg_args:
|
|
653
|
+
inner_type = arg_args[0]
|
|
654
|
+
break
|
|
655
|
+
|
|
656
|
+
# Also check if the actual field value is a list
|
|
657
|
+
if field_value is not None and isinstance(field_value, list):
|
|
658
|
+
is_list_type = True
|
|
659
|
+
# Try to infer inner type from first non-None element
|
|
660
|
+
if field_value and inner_type is None:
|
|
661
|
+
for item in field_value:
|
|
662
|
+
if item is not None:
|
|
663
|
+
inner_type = type(item)
|
|
664
|
+
break
|
|
665
|
+
|
|
666
|
+
if is_list_type:
|
|
667
|
+
# For lists, we add a special column definition
|
|
668
|
+
list_column_def = {
|
|
669
|
+
"headerName": field_name.capitalize(),
|
|
670
|
+
"field": field_path,
|
|
671
|
+
"cellRenderer": "agArrayCellRenderer",
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
# If the list contains models, we might want to indicate that
|
|
675
|
+
if inner_type and is_pydantic_model(inner_type):
|
|
676
|
+
list_column_def["type"] = "modelArrayColumn"
|
|
677
|
+
|
|
678
|
+
# If we have actual list data, we can provide more specific info
|
|
679
|
+
if field_value is not None and isinstance(field_value, list):
|
|
680
|
+
list_column_def["listLength"] = len(field_value)
|
|
681
|
+
if field_value:
|
|
682
|
+
# Add info about the first item for reference
|
|
683
|
+
first_item = field_value[0]
|
|
684
|
+
if is_pydantic_model(type(first_item)):
|
|
685
|
+
list_column_def["itemType"] = type(first_item).__name__
|
|
686
|
+
|
|
687
|
+
column_defs.append(list_column_def)
|
|
688
|
+
else:
|
|
689
|
+
# This is a simple field, add column definition directly
|
|
690
|
+
column_defs.append(column_def)
|
|
691
|
+
|
|
692
|
+
return column_defs
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def make_table_entries(
|
|
696
|
+
model_instances, table_name=None, max_recursion_level=1, drop_id=True
|
|
697
|
+
):
|
|
698
|
+
"""
|
|
699
|
+
Create AG Grid table configuration for a list of model instances.
|
|
700
|
+
This is the main function to make tables that are visible in the ui.
|
|
701
|
+
|
|
702
|
+
To generate a table in the ui the ui_schema of the class should initialize
|
|
703
|
+
the grid_options for the table.
|
|
704
|
+
|
|
705
|
+
It is basically impossible to predict what a table should be for a class but
|
|
706
|
+
tables can be created only from lists. The make_table function of the class should
|
|
707
|
+
therefore call this function to generate the table for appropriate list fields and
|
|
708
|
+
merge these manually where it makes sense.
|
|
709
|
+
|
|
710
|
+
This function calls the helper functions make_table_entries_helper and make_column_defs_helper
|
|
711
|
+
which recursively process the model instances and their fields to generate the table data and column definitions.
|
|
712
|
+
|
|
713
|
+
for model fields these functions check if the nested model has functions make_columns or make_column_defs
|
|
714
|
+
which are called to generate the column data and definitions, respectively
|
|
715
|
+
|
|
716
|
+
These functions do not exist by default but can be used by models to modify the default definitions
|
|
717
|
+
where they dont work. This is typically the case if the models contain huge data sets that
|
|
718
|
+
we do not want to display in the table. An example of this is in the Molecule class.
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
Args:
|
|
722
|
+
model_instances: List of model instances to process
|
|
723
|
+
table_name: Name for the table (default: None, uses class name)
|
|
724
|
+
max_recursion_level: Maximum depth for processing nested models (default: 1)
|
|
725
|
+
drop_id: Whether to drop the ID field (default: True)
|
|
726
|
+
|
|
727
|
+
Returns:
|
|
728
|
+
dict: Dictionary with 'tableName', 'rowData' (for AG Grid), and 'columnDefs'
|
|
729
|
+
"""
|
|
730
|
+
if not model_instances:
|
|
731
|
+
return {
|
|
732
|
+
"tableName": table_name or "EmptyTable",
|
|
733
|
+
"rowData": [],
|
|
734
|
+
"columnDefs": [],
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
# Get the first instance and its class
|
|
738
|
+
|
|
739
|
+
first_instance = model_instances[0]
|
|
740
|
+
model_class = type(first_instance)
|
|
741
|
+
|
|
742
|
+
# Use the new helper functions
|
|
743
|
+
# table_data_result = make_table_entries_helper(
|
|
744
|
+
# first_instance, table_name, max_recursion_level, drop_id
|
|
745
|
+
# )
|
|
746
|
+
|
|
747
|
+
# Get column definitions using the model class
|
|
748
|
+
column_defs = make_column_defs_helper(
|
|
749
|
+
model_class, table_name, max_recursion_level, drop_id
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
# Process all instances to get row data
|
|
753
|
+
row_data = []
|
|
754
|
+
for instance in model_instances:
|
|
755
|
+
instance_result = make_table_entries_helper(
|
|
756
|
+
instance, table_name, max_recursion_level, drop_id
|
|
757
|
+
)
|
|
758
|
+
row_data.append(instance_result)
|
|
759
|
+
|
|
760
|
+
return {"tableName": "Table", "rowData": row_data, "columnDefs": column_defs}
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
def create_column_def(field_name, field_type, field_value, field_path):
|
|
764
|
+
"""
|
|
765
|
+
Create a column definition for AG Grid based on field type and value.
|
|
766
|
+
|
|
767
|
+
Args:
|
|
768
|
+
field_name: Name of the field
|
|
769
|
+
field_type: Type of the field from model definition
|
|
770
|
+
field_value: Actual value of the field (can be None for class-based definitions)
|
|
771
|
+
field_path: Full path to the field (for nested structures)
|
|
772
|
+
|
|
773
|
+
Returns:
|
|
774
|
+
dict: Column definition for AG Grid
|
|
775
|
+
"""
|
|
776
|
+
column_def = {
|
|
777
|
+
"headerName": field_name.capitalize(),
|
|
778
|
+
"field": field_path,
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
# Determine the column type based on field_value if available or field_type
|
|
782
|
+
if field_value is not None:
|
|
783
|
+
if isinstance(field_value, bool):
|
|
784
|
+
column_def["type"] = "booleanColumn"
|
|
785
|
+
elif isinstance(field_value, (int, float)):
|
|
786
|
+
column_def["type"] = "numericColumn"
|
|
787
|
+
elif isinstance(field_value, datetime):
|
|
788
|
+
column_def["type"] = "dateColumn"
|
|
789
|
+
elif isinstance(field_value, dict):
|
|
790
|
+
column_def["type"] = "objectColumn"
|
|
791
|
+
elif isinstance(field_value, list):
|
|
792
|
+
column_def["type"] = "arrayColumn"
|
|
793
|
+
else:
|
|
794
|
+
column_def["type"] = "textColumn"
|
|
795
|
+
else:
|
|
796
|
+
# Determine type from field_type if field_value is None
|
|
797
|
+
if field_type is bool or (
|
|
798
|
+
isinstance(field_type, type) and issubclass(field_type, bool)
|
|
799
|
+
):
|
|
800
|
+
column_def["type"] = "booleanColumn"
|
|
801
|
+
elif field_type in (int, float) or (
|
|
802
|
+
isinstance(field_type, type) and issubclass(field_type, (int, float))
|
|
803
|
+
):
|
|
804
|
+
column_def["type"] = "numericColumn"
|
|
805
|
+
elif hasattr(field_type, "__name__") and field_type.__name__ == "datetime":
|
|
806
|
+
column_def["type"] = "dateColumn"
|
|
807
|
+
else:
|
|
808
|
+
column_def["type"] = "textColumn"
|
|
809
|
+
|
|
810
|
+
return column_def
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
def ui_add_table(ui_options: Dict[str, Any]) -> Dict[str, Any]:
|
|
814
|
+
"""
|
|
815
|
+
Add table options to the UI schema for AG Grid.
|
|
816
|
+
|
|
817
|
+
the class must then implement o make_table function calling make_table
|
|
818
|
+
:param ui_options:
|
|
819
|
+
:return: modified ui_options
|
|
820
|
+
"""
|
|
821
|
+
ui_options["ui:table"] = { # this goes into gridOptions
|
|
822
|
+
"pagination": True,
|
|
823
|
+
"paginationPageSize": 20,
|
|
824
|
+
"domLayout": "autoHeight",
|
|
825
|
+
"defaultColDef": {"flex": 1, "minWidth": 80, "resizable": True},
|
|
826
|
+
}
|
|
827
|
+
return ui_options # for chaining
|