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
simstack/util/db.py ADDED
@@ -0,0 +1,457 @@
1
+ import logging
2
+ from typing import List, Type, TypeVar, Dict, Any, Union, Optional
3
+
4
+ from bson import ObjectId
5
+ from motor.motor_asyncio import AsyncIOMotorClient
6
+ from odmantic import Model
7
+
8
+ from simstack.core.definitions import DBType, TaskStatus
9
+ from simstack.core.engine import current_engine_context, AIOEngineProxy
10
+ from simstack.models import NodeModel
11
+ from simstack.models.node_registry import NodeRegistry
12
+ from simstack.util.importer import import_class
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ T = TypeVar("T", bound=Model)
17
+
18
+
19
+ class Database:
20
+ """
21
+ Asynchronous MongoDB database access class using ODMantic ORM.
22
+ Provides a cleaner interface for database operations.
23
+ """
24
+
25
+ def __init__(
26
+ self, db_type: DBType, db_name: str = "simstack", connection_string: str = ""
27
+ ):
28
+ """
29
+ Initialize the MongoDB connection
30
+
31
+ Args:
32
+ db_type: Type of database configuration
33
+ connection_string: MongoDB connection string (if not using default)
34
+ db_name: Name of the MongoDB database
35
+ """
36
+
37
+ if db_type == DBType.IN_MEMORY:
38
+ # For tests, use in-memory MongoDB (mongomock)
39
+ try:
40
+ # import mongomock
41
+ from mongomock_motor import AsyncMongoMockClient
42
+
43
+ self.client = AsyncMongoMockClient()
44
+ logger.info("Using in-memory MongoDB mock")
45
+ except ImportError:
46
+ logger.warning(
47
+ "mongomock not installed, falling back to localhost MongoDB"
48
+ )
49
+ self.client = AsyncIOMotorClient("mongodb://localhost:27017")
50
+
51
+ elif db_type == DBType.MONGODB:
52
+ if not connection_string:
53
+ connection_string = "mongodb://localhost:27017"
54
+ self.client = AsyncIOMotorClient(connection_string)
55
+ logger.info("Connected to MongoDB")
56
+
57
+ else:
58
+ raise ValueError(f"Unsupported database type for MongoDB: {db_type}")
59
+
60
+ # Create engine
61
+ self.engine = AIOEngineProxy(client=self.client, database=db_name)
62
+ # this will set the engine for all functions that are either called from the core package or the server
63
+ current_engine_context.set(self.engine)
64
+ self.db_name = db_name
65
+
66
+ async def list_collections(self):
67
+ """
68
+ List all collections in the database
69
+ """
70
+ db = self.client[self.db_name]
71
+ return await db.list_collection_names()
72
+
73
+ async def upsert(self, model: Model) -> Model:
74
+ """
75
+ Save or update a model instance including all references and list fields
76
+
77
+ Args:
78
+ model: The ODMantic model instance to save
79
+
80
+ Returns:
81
+ The saved model with updated fields
82
+ """
83
+ # First, handle all references to ensure they exist in the database
84
+ if isinstance(Model, NodeRegistry):
85
+ if not hasattr(model, "name"):
86
+ logger.exception(
87
+ f"Fatal Error A trying to save node_registry: {model.model_dump()} for task_id: {model.id}"
88
+ )
89
+ return model
90
+ if model.name is None:
91
+ logger.exception(
92
+ f"Fatal Error B trying to save node_registry: {model.model_dump()} for task_id: {model.id}"
93
+ )
94
+ return model
95
+
96
+ await self._save_references(model)
97
+
98
+ # Then save the model itself
99
+ return await self.engine.save(model)
100
+
101
+ async def _save_references(self, model: Model, visited=None):
102
+ """
103
+ Recursively save all references within a model
104
+
105
+ Args:
106
+ model: The model containing references to save
107
+ visited: Set of object IDs already processed to prevent infinite recursion
108
+ """
109
+ if visited is None:
110
+ visited = set()
111
+
112
+ # Skip if we've already processed this object (prevents circular references)
113
+ model_id = id(model)
114
+ if model_id in visited:
115
+ return
116
+ visited.add(model_id)
117
+
118
+ # Get all model fields, including those with default_factory
119
+ model_fields = getattr(model.__class__, "model_fields", {})
120
+
121
+ # Process each field in the model
122
+ for field_name, field_value in model.__dict__.items():
123
+ if field_value is None:
124
+ continue
125
+
126
+ # Get field info if available
127
+ field_info = model_fields.get(field_name)
128
+ if not field_info:
129
+ continue
130
+
131
+ # Handle different field types
132
+
133
+ # Case 1: Direct Reference fields
134
+ if hasattr(field_info, "annotation") and "Reference" in str(
135
+ field_info.annotation
136
+ ):
137
+ if field_value is not None:
138
+ logger.info(
139
+ f"Saving reference field {field_name} of type {type(field_value).__name__}"
140
+ )
141
+ await self._save_references(field_value, visited)
142
+ await self.engine.save(field_value)
143
+
144
+ # Case 2: List fields that might contain models
145
+ elif isinstance(field_value, list):
146
+ for item in field_value:
147
+ if isinstance(item, Model):
148
+ logger.info(
149
+ f"Saving list item of type {type(item).__name__} in field {field_name}"
150
+ )
151
+ await self._save_references(item, visited)
152
+ await self.engine.save(item)
153
+
154
+ # Case 3: Embedded models (like in FileInstance within FileStack)
155
+ elif isinstance(field_value, Model):
156
+ logger.info(
157
+ f"Saving embedded model of type {type(field_value).__name__} in field {field_name}"
158
+ )
159
+ await self._save_references(field_value, visited)
160
+
161
+ async def save(self, model: Model) -> Model:
162
+ return await self.upsert(model)
163
+
164
+ async def find_one(self, model_class: Type[T], query=None, **kwargs) -> Optional[T]:
165
+ """
166
+ Find a single document matching the query
167
+ :param model_class:
168
+ :param query:
169
+ Returns:
170
+ The found model instance or None
171
+
172
+ """
173
+ return await self.engine.find_one(model_class, query, **kwargs)
174
+
175
+ async def find_one_by_model_name(
176
+ self, model_mapping: str, item_id: str
177
+ ) -> Optional[Any]:
178
+ """
179
+ Find a single document matching the query by model name
180
+
181
+ Args:
182
+ model_name: The name of the ODMantic model class as a string
183
+
184
+ Returns:
185
+ The found model instance or None
186
+
187
+ Raises:
188
+ ValueError: If model name is not found in the global namespace
189
+ """
190
+ # Import common models that might be used
191
+
192
+ # Find the model class based on its name
193
+ # model_class = import_class(model_mapping)
194
+ # model_elements = model_mapping.split(".")
195
+ # if len(model_elements) > 1:
196
+ # model_name = model_mapping.split(".")[-1]
197
+ # else:
198
+ # model_name = model_mapping
199
+ # # Search through modules in current namespace
200
+ # for module_name, module in sys.modules.items():
201
+ # if hasattr(module, model_name):
202
+ # potential_class = getattr(module, model_name)
203
+ # # Check if it's likely a model class (has attributes like id, __collection__)
204
+ # if hasattr(potential_class, "id") and hasattr(
205
+ # potential_class, "__collection__"
206
+ # ):
207
+ # model_class = potential_class
208
+ # break
209
+ # if model_class is None:
210
+ # logger.info(f"Trying to import model: {model_mapping}")
211
+
212
+ model_class = await import_class(model_mapping)
213
+ if model_class is None:
214
+ raise ValueError(
215
+ f"DB: model class {model_mapping} not found in the available modules"
216
+ )
217
+
218
+ if isinstance(item_id, str):
219
+ item_id = ObjectId(item_id)
220
+
221
+ instance = await self.engine.find_one(model_class, model_class.id == item_id)
222
+ if not instance:
223
+ logger.error(
224
+ f"Instance of '{model_class.__name__}' with id '{item_id}' does not exist"
225
+ )
226
+ raise ValueError(
227
+ f"Instance of '{model_class.__name__}' with id '{item_id}' does not exist"
228
+ )
229
+ return instance
230
+
231
+ async def find_all(self, model_class: Type[T], **kwargs) -> List[T]:
232
+ """
233
+ Find all documents of a given model class
234
+
235
+ Args:
236
+ model_class: The ODMantic model class
237
+ **kwargs: Query filters
238
+ """
239
+ return await self.engine.find(model_class, **kwargs)
240
+
241
+ async def find_many(self, model_class: Type[T], query, **kwargs) -> List[T]:
242
+ """
243
+ Find multiple documents matching the query
244
+
245
+ Args:
246
+ model_class: The ODMantic model class
247
+ :param query:
248
+ **kwargs: Query filters
249
+
250
+ Returns:
251
+ List of matching model instances
252
+
253
+ """
254
+ return await self.engine.find(model_class, query, **kwargs)
255
+
256
+ async def delete(self, model: Model) -> None:
257
+ """
258
+ Delete a model instance
259
+
260
+ Args:
261
+ model: The model instance to delete
262
+ """
263
+ await self.engine.delete(model)
264
+
265
+ async def delete_by_id(
266
+ self, model_class: Type[T], id: Union[str, ObjectId]
267
+ ) -> None:
268
+ """
269
+ Delete a document by its ID
270
+
271
+ Args:
272
+ model_class: The ODMantic model class
273
+ id: The document ID (either string or ObjectId)
274
+ """
275
+ # Convert string ID to ObjectId if needed
276
+ if isinstance(id, str):
277
+ id = ObjectId(id)
278
+
279
+ instance = await self.engine.find_one(model_class, model_class.id == id)
280
+ if instance:
281
+ await self.engine.delete(instance)
282
+ else:
283
+ logger.error(f"No data found in '{model_class.__name__}' with id '{id}'")
284
+
285
+ async def drop_collection(self, model_class: Type[T]) -> None:
286
+ """
287
+ Drop the collection for the given model class
288
+
289
+ Args:
290
+ model_class: The ODMantic model class
291
+ """
292
+ collection = self.engine.get_collection(model_class)
293
+ if collection is None:
294
+ logger.error(f"Could not drop collection {model_class.__name__}")
295
+ await collection.drop()
296
+
297
+ # collection_name = model_class.__collection__
298
+ # db = self.client[self.db_name]
299
+ # await db[collection_name].drop()
300
+
301
+ async def load_from_collection(
302
+ self, model_class: Type[T], id: Union[str, ObjectId]
303
+ ) -> Optional[T]:
304
+ """
305
+ Load a document by its ID
306
+
307
+ Args:
308
+ model_class: The ODMantic model class
309
+ id: The document ID (either string or ObjectId)
310
+
311
+ Returns:
312
+ The found model instance or None
313
+
314
+ Raises:
315
+ ValueError: If document is not found
316
+ """
317
+ # Convert string ID to ObjectId if needed
318
+ if isinstance(id, str):
319
+ id = ObjectId(id)
320
+
321
+ instance = await self.engine.find_one(model_class, model_class.id == id)
322
+ if not instance:
323
+ logger.error(f"No data found in '{model_class.__name__}' with id '{id}'")
324
+ raise ValueError(
325
+ f"No data found in '{model_class.__name__}' with id '{id}'"
326
+ )
327
+ return instance
328
+
329
+ async def load_task(
330
+ self, name: str, arg_hash: str, function_hash: str
331
+ ) -> Optional[NodeRegistry]:
332
+ """
333
+ Load a task based on name, arg_hash and function_hash
334
+
335
+ Args:
336
+ name: Node name
337
+ arg_hash: Hash of the arguments
338
+ function_hash: Hash of the function
339
+
340
+ Returns:
341
+ The found NodeRegistry instance or None
342
+ """
343
+ result = await self.engine.find_one(
344
+ NodeRegistry,
345
+ (NodeRegistry.name == name)
346
+ & (NodeRegistry.arg_hash == arg_hash)
347
+ & (NodeRegistry.function_hash == function_hash),
348
+ )
349
+ return result
350
+
351
+ async def load_node_model_by_name(
352
+ self, node_model_name: str
353
+ ) -> Optional[NodeModel]:
354
+ """
355
+ Load a node based on its name
356
+
357
+ Args:
358
+ node_model_name: The node name
359
+
360
+ Returns:
361
+ The found NodeRegistry instance or None
362
+ """
363
+ return await self.engine.find_one(NodeModel, NodeModel.name == node_model_name)
364
+
365
+ async def load_task_by_id(
366
+ self, task_id: Union[str, ObjectId]
367
+ ) -> Optional[NodeRegistry]:
368
+ """
369
+ Load a task based on its ID
370
+
371
+ Args:
372
+ task_id: The task ID
373
+
374
+ Returns:
375
+ The found NodeRegistry instance or None
376
+ """
377
+ if isinstance(task_id, str):
378
+ task_id = ObjectId(task_id)
379
+
380
+ return await self.engine.find_one(NodeRegistry, NodeRegistry.id == task_id)
381
+
382
+ async def load_waiting_tasks_for_resource(
383
+ self, resource: str
384
+ ) -> List[NodeRegistry]:
385
+ """
386
+ Load all waiting tasks for a specific resource
387
+
388
+ Args:
389
+ resource: The resource name
390
+
391
+ Returns:
392
+ List of matching NodeRegistry instances
393
+ """
394
+ submitted_tasks = await self.engine.find(
395
+ NodeRegistry, NodeRegistry.status == TaskStatus.SUBMITTED
396
+ )
397
+ # Then filter them in Python by checking the resource field
398
+ matching_tasks = []
399
+ for task in submitted_tasks:
400
+ # Check if parameters has a resource attribute and if it matches our resource
401
+ # the local runner will also do the immidiate tasks
402
+ if hasattr(task.parameters, "resource") and (
403
+ task.parameters.resource == resource
404
+ or (resource == "local" and task.parameters.resource == "self")
405
+ ):
406
+ if resource == "local" and task.parameters.resource == "self":
407
+ logger.info(f"local runner taking job for 'self' with {task.id}")
408
+ matching_tasks.append(task)
409
+ return matching_tasks
410
+
411
+ async def reset_database(self) -> None:
412
+ """
413
+ Reset the database by dropping all collections and recreating them
414
+ """
415
+ db = self.client[self.db_name]
416
+ collections = await db.list_collection_names()
417
+
418
+ for collection in collections:
419
+ await db[collection].drop()
420
+
421
+ logger.info(f"Database {self.db_name} has been reset")
422
+
423
+ async def count(self, model_class: Type[T], **kwargs) -> int:
424
+ """
425
+ Count documents matching the query
426
+
427
+ Args:
428
+ model_class: The ODMantic model class
429
+ **kwargs: Query filters
430
+
431
+ Returns:
432
+ Number of matching documents
433
+ """
434
+ return await self.engine.count(model_class, **kwargs)
435
+
436
+ async def aggregate(
437
+ self, model_class: Type[Model], pipeline: List[Dict]
438
+ ) -> List[Dict]:
439
+ """
440
+ Perform an aggregation operation
441
+
442
+ Args:
443
+ model_class: The ODMantic model class
444
+ pipeline: MongoDB aggregation pipeline
445
+
446
+ Returns:
447
+ List of aggregation results
448
+ """
449
+ collection = self.engine.get_collection(model_class)
450
+ cursor = collection.aggregate(pipeline)
451
+ return await cursor.to_list(length=None)
452
+
453
+ async def close(self) -> None:
454
+ """
455
+ Close database connections
456
+ """
457
+ self.client.close()
@@ -0,0 +1,117 @@
1
+ import logging
2
+ import re
3
+ from datetime import datetime
4
+ from typing import Optional, Dict, Any
5
+
6
+ from pymongo import MongoClient
7
+
8
+ # Regular expression patterns
9
+ task_pattern = re.compile(r"task_id: ([\w\-\.]+)")
10
+ resource_pattern = re.compile(r"resource_id: ([\w\-\.]+)")
11
+
12
+
13
+ def extract_task_id(message: str) -> Optional[str]:
14
+ """Extract task ID from log message."""
15
+ match = task_pattern.search(message)
16
+ return match.group(1) if match else None
17
+
18
+
19
+ def extract_resource_id(message: str) -> Optional[str]:
20
+ """Extract resource ID from log message."""
21
+ match = resource_pattern.search(message)
22
+ return match.group(1) if match else None
23
+
24
+
25
+ class DBLogHandler(logging.Handler):
26
+ """Logging handler that stores logs in MongoDB using pymongo."""
27
+
28
+ def __init__(
29
+ self,
30
+ connection_string: str,
31
+ db_name: str,
32
+ collection_name: str = "logs",
33
+ ):
34
+ super().__init__()
35
+
36
+ # Validate configuration
37
+ if not connection_string or not isinstance(connection_string, str):
38
+ raise ValueError("Invalid connection string")
39
+ if not db_name or not isinstance(db_name, str):
40
+ raise ValueError("Invalid database name")
41
+ if not collection_name or not isinstance(collection_name, str):
42
+ raise ValueError("Invalid collection name")
43
+
44
+ # Store configuration
45
+ self.connection_string = connection_string
46
+ self.db_name = db_name
47
+ self.collection_name = collection_name
48
+
49
+ # Create MongoDB client
50
+ self.client = MongoClient(self.connection_string)
51
+ self.db = self.client[self.db_name]
52
+ self.collection = self.db[self.collection_name]
53
+
54
+ def emit(self, record: logging.LogRecord) -> None:
55
+ """Process a log record by converting it to a log entry and inserting it into MongoDB."""
56
+ try:
57
+ # Create log entry as a dictionary
58
+ log_entry = self.create_log_entry(record)
59
+
60
+ # Insert the log entry into the collection
61
+ self.collection.insert_one(log_entry)
62
+
63
+ except Exception as e:
64
+ # Use print instead of logging to avoid potential recursion
65
+ print(f"Error in emit: {e}")
66
+ self.handleError(record)
67
+
68
+ def create_log_entry(self, record: logging.LogRecord) -> Dict[str, Any]:
69
+ # Format message
70
+ message = self.format(record)
71
+
72
+ # Extract task and resource IDs
73
+ task_id = extract_task_id(message)
74
+ resource_id = extract_resource_id(message)
75
+
76
+ # Create log entry dictionary
77
+ log_entry_dict = {
78
+ "timestamp": datetime.fromtimestamp(record.created),
79
+ "level": record.levelname,
80
+ "logger_name": record.name,
81
+ "message": message,
82
+ "module": record.module,
83
+ "function": record.funcName,
84
+ "line": record.lineno,
85
+ "task_id": task_id,
86
+ "resource": resource_id,
87
+ "thread_name": record.threadName,
88
+ "process_name": record.processName,
89
+ }
90
+
91
+ # Add exception info if available
92
+ if record.exc_info:
93
+ log_entry_dict.update(
94
+ {
95
+ "exception_type": record.exc_info[0].__name__
96
+ if record.exc_info and record.exc_info[0]
97
+ else None,
98
+ "exception_message": str(record.exc_info[1])
99
+ if record.exc_info and record.exc_info[1]
100
+ else None,
101
+ "exception_traceback": record.exc_text,
102
+ }
103
+ )
104
+
105
+ return log_entry_dict
106
+
107
+ def close(self) -> None:
108
+ """Close the MongoDB client connection."""
109
+ try:
110
+ if hasattr(self, "client") and self.client:
111
+ self.client.close()
112
+ except Exception as e:
113
+ print(f"Error closing DB log handler client: {e}")
114
+
115
+ def __del__(self):
116
+ """Ensure connection is closed when handler is destroyed."""
117
+ self.close()
@@ -0,0 +1,57 @@
1
+ import logging
2
+ from typing import Any
3
+
4
+ from odmantic import Model
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ def default_from_model(cls_method, model: Model, **kwargs) -> Model:
10
+ model_dict = model.model_dump(exclude={"id"})
11
+ return cls_method.from_dict(model_dict, **kwargs)
12
+
13
+
14
+ def default_from_dict(cls_method, data: dict, **kwargs) -> Any:
15
+ """
16
+ Create an instance of the model from a dictionary.
17
+ Handles nested models and enum values.
18
+ """
19
+ if not isinstance(data, dict):
20
+ return data # Return as is if it's not a dictionary
21
+
22
+ processed_data = {}
23
+ for field_name, field in cls_method.model_fields.items():
24
+ if field_name not in data:
25
+ continue
26
+
27
+ field_type = field.annotation
28
+ field_value = data[field_name]
29
+
30
+ # Handle enum values
31
+ if hasattr(field_type, "__members__"):
32
+ logger.debug("Handling enum field: %s", field_name)
33
+ if isinstance(field_value, field_type):
34
+ # If it's already an enum instance, use it directly
35
+ logger.debug("Field value is enum instance: %s", field_value)
36
+ processed_data[field_name] = field_value
37
+ else:
38
+ # Try to create enum from the value
39
+ try:
40
+ processed_data[field_name] = field_type(field_value)
41
+ logger.debug(
42
+ "Converted field value to enum: %s", processed_data[field_name]
43
+ )
44
+ except ValueError:
45
+ logger.debug(
46
+ "Invalid enum value for field %s: %s", field_name, field_value
47
+ )
48
+ raise ValueError(
49
+ f"Invalid enum value '{field_value}' for field {field_name}"
50
+ )
51
+ # Handle nested models
52
+ elif hasattr(field_type, "from_dict") and isinstance(field_value, dict):
53
+ processed_data[field_name] = field_type.from_dict(field_value)
54
+ else:
55
+ processed_data[field_name] = field_value
56
+
57
+ return cls_method.model_validate(processed_data)