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/__init__.py ADDED
File without changes
simstack/_version.py ADDED
@@ -0,0 +1,34 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = [
5
+ "__version__",
6
+ "__version_tuple__",
7
+ "version",
8
+ "version_tuple",
9
+ "__commit_id__",
10
+ "commit_id",
11
+ ]
12
+
13
+ TYPE_CHECKING = False
14
+ if TYPE_CHECKING:
15
+ from typing import Tuple
16
+ from typing import Union
17
+
18
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
19
+ COMMIT_ID = Union[str, None]
20
+ else:
21
+ VERSION_TUPLE = object
22
+ COMMIT_ID = object
23
+
24
+ version: str
25
+ __version__: str
26
+ __version_tuple__: VERSION_TUPLE
27
+ version_tuple: VERSION_TUPLE
28
+ commit_id: COMMIT_ID
29
+ __commit_id__: COMMIT_ID
30
+
31
+ __version__ = version = '0.2.0'
32
+ __version_tuple__ = version_tuple = (0, 2, 0)
33
+
34
+ __commit_id__ = commit_id = None
File without changes
@@ -0,0 +1,447 @@
1
+ import importlib
2
+ import inspect
3
+ import logging
4
+ import re
5
+ from typing import Optional, List
6
+
7
+ from odmantic import ObjectId
8
+
9
+ from simstack.core.context import context
10
+ from simstack.core.engine import current_engine_context
11
+ from simstack.models.artifact_models import ArtifactMapping, ArtifactModel
12
+ from simstack.models.charts_artifact import ChartArtifactModel
13
+ from simstack.models.node_registry import find_child_nodes, NodeRegistry
14
+ from simstack.models.table_artifact import TableArtifactModel
15
+ from simstack.util.importer import function_from_model
16
+ from simstack.util.module_path_checker import is_module_subpath_of_path
17
+
18
+ logger = logging.getLogger("artifacts")
19
+
20
+
21
+ class ArtifactArguments:
22
+ def __init__(self, result, task_id: ObjectId = None):
23
+ self.task_id = task_id
24
+ self.result = result
25
+ self.child_artifacts = []
26
+
27
+ def add_attributes(self, func, *args, **kwargs):
28
+ if not callable(func):
29
+ raise ValueError("func must be callable")
30
+ # Get function parameter names
31
+ sig = inspect.signature(func)
32
+ parameters = list(sig.parameters.keys())
33
+ # Map positional args to parameter names
34
+ for param_name, arg_value in zip(parameters, args):
35
+ setattr(self, param_name, arg_value)
36
+ # Map keyword args directly
37
+ for param_name, arg_value in kwargs.items():
38
+ setattr(self, param_name, arg_value)
39
+
40
+
41
+ async def find_artifact_mappings(
42
+ node_registry_path: str, task_id: Optional[str] = None
43
+ ) -> List[ArtifactMapping]:
44
+ logger.debug(
45
+ f"task_id: {task_id} Loading artifacts with regex pattern: {node_registry_path} "
46
+ )
47
+ engine = current_engine_context.get()
48
+ all_mappings = await engine.find(ArtifactMapping)
49
+
50
+ # Filter them manually to find those whose patterns match your path
51
+ matching_mappings = [
52
+ mapping
53
+ for mapping in all_mappings
54
+ if re.match(mapping.regex_pattern, node_registry_path)
55
+ ]
56
+
57
+ for mapping in matching_mappings:
58
+ logger.debug(
59
+ f"task_id: {task_id} Artifact found: {mapping.name} with path: {mapping.regex_pattern}"
60
+ )
61
+ return matching_mappings
62
+
63
+
64
+ async def register_artifact_mapping(artifact_mapping: ArtifactMapping):
65
+ # Check if an artifact with the same name already exists
66
+ existing = await context.db.engine.find_one(
67
+ ArtifactMapping, ArtifactMapping.name == artifact_mapping.name
68
+ )
69
+ if existing:
70
+ logger.warning(
71
+ f"Replacing existing artifact mapping '{artifact_mapping.name}' already exists with path: {existing.regex_pattern}"
72
+ )
73
+ existing.set_values(artifact_mapping)
74
+ artifact_mapping = await context.db.save(existing)
75
+
76
+ # Check if the function_mapping is specified and not CODE
77
+ if (
78
+ artifact_mapping.function_mapping
79
+ and artifact_mapping.function_mapping != "CODE"
80
+ ):
81
+ try:
82
+ # Import the function to verify it exists
83
+ module_path, function_name = artifact_mapping.function_mapping.rsplit(
84
+ ".", 1
85
+ )
86
+ module = importlib.import_module(module_path)
87
+ if module is None:
88
+ logger.error(f"Module {module_path} could not be imported.")
89
+ raise ImportError(f"Module {module_path} could not be imported.")
90
+ # Get the function from the module
91
+ func = getattr(module, function_name)
92
+
93
+ if func:
94
+ # Function import succeeded, now check if it's in a path with use_pickle=True
95
+ # Check if the module path is in a non-excluded subdirectory of any path in the path registry
96
+ for path_name, path_info in context.path_manager.paths.items():
97
+ # Check if the path has use_pickle=True
98
+ current_module_is_child = is_module_subpath_of_path(
99
+ module_path, path_info["path"]
100
+ )
101
+ if path_info.get("use_pickle", True) and current_module_is_child:
102
+ # create a FunctionPickle for the function
103
+ try:
104
+ from simstack.models.pickle_models import FunctionPickle
105
+
106
+ # Create a FunctionPickle for the function
107
+ function_pickle = FunctionPickle(
108
+ name=func.__name__, module_path=func.__module__
109
+ )
110
+ function_pickle.store_function(func)
111
+
112
+ # Save the FunctionPickle to the database
113
+ await context.db.save(function_pickle)
114
+
115
+ # Associate the FunctionPickle with the ArtifactMapping
116
+ artifact_mapping.pickle_function = function_pickle
117
+
118
+ logger.info(
119
+ f"Created FunctionPickle for {artifact_mapping.function_mapping}"
120
+ )
121
+ break # We only need to create one FunctionPickle
122
+ except Exception as e:
123
+ logger.error(
124
+ f"Error creating FunctionPickle for {artifact_mapping.function_mapping}: {e}"
125
+ )
126
+ else:
127
+ logger.warning(
128
+ f"Could not import function {artifact_mapping.function_mapping}"
129
+ )
130
+ except Exception as e:
131
+ logger.error(
132
+ f"Error processing function {artifact_mapping.function_mapping}: {e}"
133
+ )
134
+ return await context.db.save(artifact_mapping)
135
+
136
+
137
+ async def find_all_artifacts(node_registry: NodeRegistry) -> List[ArtifactModel]:
138
+ # if not engine:
139
+ # engine = context.db.engine
140
+ engine = current_engine_context.get()
141
+ return [
142
+ await engine.find_one(ArtifactModel, ArtifactModel.id == artifact_id)
143
+ for artifact_id in node_registry.artifact_ids
144
+ ]
145
+
146
+
147
+ async def create_artifacts(
148
+ artifact_arguments: ArtifactArguments, node_registry: NodeRegistry
149
+ ) -> List[ArtifactModel]:
150
+ try:
151
+ call_path = node_registry.call_path
152
+ task_id = node_registry.id
153
+ log_string = f"create artifacts for task_id: {task_id} for {call_path}"
154
+
155
+ artifact_mappings_list = await find_artifact_mappings(
156
+ call_path, task_id=task_id
157
+ )
158
+
159
+ child_nodes = await find_child_nodes(task_id)
160
+ logger.info(
161
+ f"{log_string} Found {len(artifact_mappings_list)} artifact mappings for path: {call_path} and {len(child_nodes)} child nodes."
162
+ )
163
+ # Concatenate artifacts from all child nodes
164
+ child_artifacts = []
165
+ for child_node in child_nodes:
166
+ loaded_artifacts = await find_all_artifacts(child_node)
167
+ if len(loaded_artifacts) == 1:
168
+ child_artifacts.extend(loaded_artifacts)
169
+ elif len(loaded_artifacts) > 1:
170
+ # If multiple artifacts, consolidate them into a list
171
+ consolidated_artifact = ArtifactModel(
172
+ name=child_node.name,
173
+ path=call_path,
174
+ data={
175
+ node_registry.name: loaded_artifacts
176
+ }, # Store all artifacts in a single field
177
+ )
178
+ await context.db.save(consolidated_artifact)
179
+ child_artifacts.append(consolidated_artifact)
180
+
181
+ # child_artifacts = consolidate_artifacts(child_artifacts, call_path, task_id)
182
+ artifact_arguments.child_artifacts = child_artifacts
183
+ artifact_arguments.call_path = call_path
184
+
185
+ artifact_list = []
186
+ engine = current_engine_context.get()
187
+
188
+ if len(artifact_mappings_list) > 0:
189
+ for artifact_mapping in artifact_mappings_list:
190
+ # Count existing artifacts with the same mapping
191
+ existing_count = len(
192
+ [a for a in artifact_list if a.path.endswith(artifact_mapping.name)]
193
+ )
194
+ log_string_mapping = (
195
+ f"{log_string} Artifact: {artifact_mapping.name} #:{existing_count}"
196
+ )
197
+ logging.info(log_string_mapping)
198
+ if artifact_mapping.function_mapping != "CODE":
199
+ func = await function_from_model(
200
+ artifact_mapping, artifact_arguments.task_id
201
+ )
202
+ if not func:
203
+ logger.error(
204
+ f"{log_string_mapping} Function {artifact_mapping.function_mapping} not found for artifact mapping."
205
+ )
206
+ continue
207
+ logger.info(
208
+ f"{log_string_mapping} Executing function {artifact_mapping.function_mapping} for artifact mapping."
209
+ )
210
+ artifact_result = func(artifact_arguments)
211
+ else:
212
+ from simstack.util.safe_code_executor import safe_code_executor
213
+
214
+ result = safe_code_executor(
215
+ artifact_mapping.function_code, artifact_arguments
216
+ )
217
+ logger.info(
218
+ f"{log_string_mapping} Executing code for artifact mapping."
219
+ )
220
+ if result["success"]:
221
+ artifact_result = result["result"]
222
+ else:
223
+ logger.error(
224
+ f"{log_string_mapping} Code execution failed with error: {result['error']}"
225
+ )
226
+ continue
227
+
228
+ # An artifact result can be either an Artifact or a list of elements which can be Artifacts or ArtifactModels (from children)
229
+ if not isinstance(artifact_result, List):
230
+ artifact_result = [artifact_result]
231
+
232
+ for artifact in artifact_result:
233
+ if artifact is None:
234
+ logger.warning(
235
+ f"{log_string_mapping} Artifact is None, skipping."
236
+ )
237
+ continue
238
+ if isinstance(artifact, TableArtifactModel):
239
+ artifact.parent_id = node_registry.id
240
+ saved_artifact = await engine.save(artifact)
241
+ logger.debug(
242
+ f"{log_string_mapping} new table: {saved_artifact}"
243
+ )
244
+ elif isinstance(artifact, ChartArtifactModel):
245
+ artifact.parent_id = node_registry.id
246
+ saved_artifact = await engine.save(artifact)
247
+ logger.debug(
248
+ f"{log_string_mapping} new table: {saved_artifact}"
249
+ )
250
+ elif isinstance(artifact, ArtifactModel):
251
+ artifact.path = call_path
252
+ saved_artifact = await engine.save(artifact)
253
+ logger.debug(f"{log_string_mapping} new: {saved_artifact}")
254
+ artifact_list.append(saved_artifact)
255
+ else:
256
+ raise ValueError(
257
+ f"{log_string_mapping} not an ArtifactModel object. Got {artifact} instead."
258
+ )
259
+ else:
260
+ artifact_list = child_artifacts
261
+ logger.debug(f"{log_string} passing child artifacts")
262
+ return [artifact.id for artifact in artifact_list]
263
+ except Exception as e:
264
+ logger.exception(f"Error creating artifacts for node {node_registry.name}: {e}")
265
+ return []
266
+
267
+
268
+ async def save_artifact_model(artifact: ArtifactModel) -> ArtifactModel:
269
+ """
270
+ Custom save function for polymorphic models
271
+
272
+ If the item is a List type, it will also save any unsaved nested items.
273
+ """
274
+ # Special handling for List type
275
+ if artifact.type == "list":
276
+ # Save all nested items first
277
+ saved_items = []
278
+ for nested_item in artifact.items:
279
+ # Save the nested item
280
+ saved_item = await save_artifact_model(nested_item)
281
+ saved_items.append(saved_item)
282
+
283
+ # Update the items list with all saved items
284
+ artifact.items = saved_items
285
+
286
+ # Get the model's collection
287
+ collection = context.db.engine.get_collection(ArtifactModel)
288
+
289
+ # Convert model to dict
290
+ item_dict = artifact.model_dump(by_alias=True)
291
+
292
+ # For List types, we need to convert ArtifactModel objects to their IDs for storage
293
+ if artifact.type == "list":
294
+ # Replace ArtifactModel objects with their IDs in the dict for storage
295
+ item_dict["items"] = [nested_item.id for nested_item in artifact.items]
296
+
297
+ # Check if this artifact already exists in the database
298
+ if artifact.id is not None:
299
+ existing = await collection.find_one({"_id": artifact.id})
300
+ if existing:
301
+ # ArtifactModel exists, update it
302
+ await collection.replace_one({"_id": artifact.id}, item_dict)
303
+ return artifact
304
+
305
+ # ArtifactModel doesn't exist or has no ID, insert it
306
+ # Remove id field if it exists to let MongoDB generate it
307
+ if "_id" in item_dict:
308
+ del item_dict["_id"]
309
+
310
+ result = await collection.insert_one(item_dict)
311
+ artifact.id = result.inserted_id
312
+
313
+ return artifact
314
+
315
+
316
+ async def find_artifacts(
317
+ model_class: type[ArtifactModel], **kwargs
318
+ ) -> List[ArtifactModel]:
319
+ """
320
+ Custom find function for polymorphic models
321
+ """
322
+ # Get the collection
323
+ collection = context.db.engine.get_collection(ArtifactModel)
324
+
325
+ # Add item_type filter for subclasses
326
+ if model_class != ArtifactModel:
327
+ discriminator_value = model_class.model_fields["artifact_type"].default
328
+ kwargs["artifact_type"] = discriminator_value
329
+
330
+ # Execute find
331
+ cursor = collection.find(kwargs)
332
+ results = await cursor.to_list(length=None)
333
+
334
+ # Convert results to model instances
335
+ items = []
336
+ for doc in results:
337
+ artifact_type = doc.get("artifact_type")
338
+
339
+ # For List types, we need special handling
340
+ if artifact_type == "list":
341
+ # Store item_ids temporarily
342
+ item_ids = doc.get("items", [])
343
+ # Remove items field for validation
344
+ doc_copy = doc.copy()
345
+ doc_copy["items"] = []
346
+
347
+ # Create the List instance without items first
348
+ item = model_class.model_validate(doc_copy)
349
+
350
+ # Load all referenced items
351
+ loaded_items = []
352
+ for item_id in item_ids:
353
+ nested_item = await context.db.find_one(
354
+ ArtifactModel, ArtifactModel.id == item_id
355
+ )
356
+ if nested_item:
357
+ loaded_items.append(nested_item)
358
+
359
+ # Now assign the loaded items
360
+ item.items = loaded_items
361
+ else:
362
+ # For non-List types, normal validation is fine
363
+ item = model_class.model_validate(doc)
364
+
365
+ items.append(item)
366
+
367
+ return items
368
+
369
+
370
+ # async def find_one_artifact(artifact_id: ObjectId) -> Optional[ArtifactModel]:
371
+ # """
372
+ # Find an artifact by ID and return it as the appropriate subclass instance
373
+ # based on its artifact_type field.
374
+ #
375
+ # Args:
376
+ # artifact_id: The ObjectId of the item to find
377
+ #
378
+ # Returns:
379
+ # An instance of the appropriate ArtifactModel subclass, or None if not found
380
+ # """
381
+ # # Get the collection
382
+ # collection = context.db.engine.get_collection(ArtifactModel)
383
+ #
384
+ # # Find the document by ID
385
+ # doc = await collection.find_one({"_id": artifact_id})
386
+ #
387
+ # if doc is None:
388
+ # return None
389
+ #
390
+ # # Determine the appropriate class based on artifact_type
391
+ # artifact_type = doc.get("artifact_type")
392
+ #
393
+ # # Map artifact_type values to their respective classes
394
+ # type_to_class = {
395
+ # "int": IntArtifactModel,
396
+ # "float": FloatArtifactModel,
397
+ # "str": StringArtifactModel,
398
+ # "list": List,
399
+ # # Add more mappings as you add more subclasses
400
+ # }
401
+ #
402
+ # # Get the appropriate class, defaulting to base ArtifactModel if the type is unknown
403
+ # model_class = type_to_class.get(artifact_type, ArtifactModel)
404
+ #
405
+ # # For List type, we need special handling to load items
406
+ # if artifact_type == "list":
407
+ # # Store item_ids temporarily
408
+ # item_ids = doc.get("items", [])
409
+ # # Remove items field for now to allow validation
410
+ # doc["items"] = []
411
+ #
412
+ # # Create the List instance without items first
413
+ # item = model_class.model_validate(doc)
414
+ #
415
+ # # load all referenced items
416
+ # loaded_items = []
417
+ # for artifact_id in item_ids:
418
+ # nested_item = await context.db.find_one(ArtifactModel, ArtifactModel.id == artifact_id)
419
+ # if nested_item:
420
+ # loaded_items.append(nested_item)
421
+ #
422
+ # # Now assign the loaded items
423
+ # item.items = loaded_items
424
+ # else:
425
+ # # For non-List types, normal validation is fine
426
+ # item = model_class.model_validate(doc)
427
+ #
428
+ #
429
+ # return item
430
+
431
+ #
432
+ # async def load_list_items(list_item: List) -> List[ArtifactModel]:
433
+ # """
434
+ # Load all items in a List type and return them as a Python list.
435
+ # This is now a simple accessor since items are already ArtifactModel objects.
436
+ #
437
+ # Args:
438
+ # list_item: A List instance
439
+ #
440
+ # Returns:
441
+ # A list of ArtifactModel instances
442
+ # """
443
+ # if not isinstance(list_item, List):
444
+ # raise TypeError("Expected a List instance")
445
+ #
446
+ # return list_item.items
447
+ #
@@ -0,0 +1,14 @@
1
+ import asyncio
2
+ import functools
3
+
4
+ import nest_asyncio
5
+
6
+ nest_asyncio.apply()
7
+
8
+
9
+ def async_helper(func):
10
+ @functools.wraps(func)
11
+ def wrapper(*args, **kwargs):
12
+ return asyncio.run(func(*args, **kwargs))
13
+
14
+ return wrapper
@@ -0,0 +1,60 @@
1
+ from pathlib import Path
2
+ from typing import Optional
3
+
4
+
5
+ class ConfigFile:
6
+ """
7
+ Singleton class that manages the path to a configuration file.
8
+
9
+ This class ensures only one instance exists throughout the application
10
+ and provides a centralized way to access the configuration file path.
11
+ """
12
+
13
+ _instance: Optional["ConfigFile"] = None
14
+ _config_file_path: Optional[str] = "simstack.toml" # Default config file name
15
+
16
+ def __new__(cls) -> "ConfigFile":
17
+ """Create or return the singleton instance."""
18
+ if cls._instance is None:
19
+ cls._instance = super(ConfigFile, cls).__new__(cls)
20
+ return cls._instance
21
+
22
+ def __init__(self):
23
+ """Initialize the ConfigManager (only runs once due to singleton pattern)."""
24
+ # Only initialize once
25
+ if not hasattr(self, "_initialized"):
26
+ self._initialized = True
27
+
28
+ @property
29
+ def config_file_path(self) -> Optional[str]:
30
+ """Get the current configuration file path."""
31
+ return self._config_file_path
32
+
33
+ @config_file_path.setter
34
+ def config_file_path(self, path: str) -> None:
35
+ """
36
+ Set the configuration file path.
37
+
38
+ Args:
39
+ path: Path to the configuration file
40
+
41
+ Raises:
42
+ ValueError: If the path is empty or None
43
+ FileNotFoundError: If the file doesn't exist (optional validation)
44
+ """
45
+ if not path:
46
+ raise ValueError("Configuration file path cannot be empty or None")
47
+
48
+ # Convert to absolute path for consistency
49
+ self._config_file_path = str(Path(path).resolve())
50
+
51
+
52
+ # Convenience function to get the singleton instance
53
+ def get_config_file() -> ConfigFile:
54
+ """
55
+ Get the singleton ConfigManager instance.
56
+
57
+ Returns:
58
+ The ConfigManager singleton instance
59
+ """
60
+ return ConfigFile()