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.
Files changed (91) hide show
  1. simstack/__init__.py +0 -0
  2. simstack/_version.py +34 -0
  3. simstack/core/__init__.py +0 -0
  4. simstack/core/artifacts.py +447 -0
  5. simstack/core/asnyc_helper.py +14 -0
  6. simstack/core/config_file.py +60 -0
  7. simstack/core/context.py +167 -0
  8. simstack/core/data.py +139 -0
  9. simstack/core/definitions.py +21 -0
  10. simstack/core/engine.py +116 -0
  11. simstack/core/find_simstack_modules.py +43 -0
  12. simstack/core/hash.py +177 -0
  13. simstack/core/model_table.py +199 -0
  14. simstack/core/node.py +911 -0
  15. simstack/core/node_runner.py +254 -0
  16. simstack/core/node_table.py +380 -0
  17. simstack/core/recompute_artifacts.py +120 -0
  18. simstack/core/reset_db.py +12 -0
  19. simstack/core/resources.py +101 -0
  20. simstack/core/run_node.py +69 -0
  21. simstack/core/runner.py +435 -0
  22. simstack/core/simstack_result.py +41 -0
  23. simstack/core/submit_node.py +143 -0
  24. simstack/core/task_id.py +22 -0
  25. simstack/data/__init__.py +0 -0
  26. simstack/methods/__init__.py +0 -0
  27. simstack/methods/get_file.py +10 -0
  28. simstack/methods/multiple_executor.py +307 -0
  29. simstack/models/__init__.py +37 -0
  30. simstack/models/array_list.py +48 -0
  31. simstack/models/array_storage.py +54 -0
  32. simstack/models/artifact_models.py +127 -0
  33. simstack/models/base_types.py +208 -0
  34. simstack/models/charts_artifact.py +411 -0
  35. simstack/models/dataset.py +432 -0
  36. simstack/models/dataset_metadata.py +303 -0
  37. simstack/models/file_instance.py +77 -0
  38. simstack/models/file_list.py +321 -0
  39. simstack/models/files.py +229 -0
  40. simstack/models/log_entry_model.py +37 -0
  41. simstack/models/models.py +40 -0
  42. simstack/models/node_registry.py +105 -0
  43. simstack/models/pandas_model.py +210 -0
  44. simstack/models/parameters.py +388 -0
  45. simstack/models/pickle_models.py +311 -0
  46. simstack/models/runner_model.py +39 -0
  47. simstack/models/simple_table.py +32 -0
  48. simstack/models/simstack_model.py +97 -0
  49. simstack/models/slurm_info.py +17 -0
  50. simstack/models/table_artifact.py +87 -0
  51. simstack/util/__init__.py +0 -0
  52. simstack/util/async_zip_utils.py +18 -0
  53. simstack/util/cleaned_json_schema.py +20 -0
  54. simstack/util/config_reader.py +273 -0
  55. simstack/util/custom_model_dump.py +75 -0
  56. simstack/util/db.py +457 -0
  57. simstack/util/db_logger.py +117 -0
  58. simstack/util/default_from_dict.py +57 -0
  59. simstack/util/directory_iterator.py +216 -0
  60. simstack/util/file_hashing.py +68 -0
  61. simstack/util/files.py +396 -0
  62. simstack/util/find_python_files.py +33 -0
  63. simstack/util/generate_ui_schema.py +80 -0
  64. simstack/util/geometry_utils.py +118 -0
  65. simstack/util/get_module_path.py +26 -0
  66. simstack/util/git_pull.py +65 -0
  67. simstack/util/git_repository_status.py +93 -0
  68. simstack/util/import_module.py +65 -0
  69. simstack/util/importer.py +163 -0
  70. simstack/util/make_graph.py +107 -0
  71. simstack/util/make_table.py +827 -0
  72. simstack/util/minimal_route_finder.py +72 -0
  73. simstack/util/mock_db.py +144 -0
  74. simstack/util/module_path_checker.py +38 -0
  75. simstack/util/path_manager.py +234 -0
  76. simstack/util/project_root_finder.py +36 -0
  77. simstack/util/queue_watcher.py +262 -0
  78. simstack/util/route_finder.py +74 -0
  79. simstack/util/runner_util.py +162 -0
  80. simstack/util/safe_code_executor.py +194 -0
  81. simstack/util/sample_config.py +98 -0
  82. simstack/util/sensitive_content_filter.py +48 -0
  83. simstack/util/setup_logging.py +49 -0
  84. simstack/util/submit_to_watchdog.py +106 -0
  85. simstack/util/ui_tools.py +213 -0
  86. simstack-0.2.0.dist-info/METADATA +361 -0
  87. simstack-0.2.0.dist-info/RECORD +91 -0
  88. simstack-0.2.0.dist-info/WHEEL +5 -0
  89. simstack-0.2.0.dist-info/entry_points.txt +10 -0
  90. simstack-0.2.0.dist-info/licenses/LICENSE +21 -0
  91. 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