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
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,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()
|