simstack 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- simstack/__init__.py +0 -0
- simstack/_version.py +34 -0
- simstack/core/__init__.py +0 -0
- simstack/core/artifacts.py +447 -0
- simstack/core/asnyc_helper.py +14 -0
- simstack/core/config_file.py +60 -0
- simstack/core/context.py +167 -0
- simstack/core/data.py +139 -0
- simstack/core/definitions.py +21 -0
- simstack/core/engine.py +116 -0
- simstack/core/find_simstack_modules.py +43 -0
- simstack/core/hash.py +177 -0
- simstack/core/model_table.py +199 -0
- simstack/core/node.py +911 -0
- simstack/core/node_runner.py +254 -0
- simstack/core/node_table.py +380 -0
- simstack/core/recompute_artifacts.py +120 -0
- simstack/core/reset_db.py +12 -0
- simstack/core/resources.py +101 -0
- simstack/core/run_node.py +69 -0
- simstack/core/runner.py +435 -0
- simstack/core/simstack_result.py +41 -0
- simstack/core/submit_node.py +143 -0
- simstack/core/task_id.py +22 -0
- simstack/data/__init__.py +0 -0
- simstack/methods/__init__.py +0 -0
- simstack/methods/get_file.py +10 -0
- simstack/methods/multiple_executor.py +307 -0
- simstack/models/__init__.py +37 -0
- simstack/models/array_list.py +48 -0
- simstack/models/array_storage.py +54 -0
- simstack/models/artifact_models.py +127 -0
- simstack/models/base_types.py +208 -0
- simstack/models/charts_artifact.py +411 -0
- simstack/models/dataset.py +432 -0
- simstack/models/dataset_metadata.py +303 -0
- simstack/models/file_instance.py +77 -0
- simstack/models/file_list.py +321 -0
- simstack/models/files.py +229 -0
- simstack/models/log_entry_model.py +37 -0
- simstack/models/models.py +40 -0
- simstack/models/node_registry.py +105 -0
- simstack/models/pandas_model.py +210 -0
- simstack/models/parameters.py +388 -0
- simstack/models/pickle_models.py +311 -0
- simstack/models/runner_model.py +39 -0
- simstack/models/simple_table.py +32 -0
- simstack/models/simstack_model.py +97 -0
- simstack/models/slurm_info.py +17 -0
- simstack/models/table_artifact.py +87 -0
- simstack/util/__init__.py +0 -0
- simstack/util/async_zip_utils.py +18 -0
- simstack/util/cleaned_json_schema.py +20 -0
- simstack/util/config_reader.py +273 -0
- simstack/util/custom_model_dump.py +75 -0
- simstack/util/db.py +457 -0
- simstack/util/db_logger.py +117 -0
- simstack/util/default_from_dict.py +57 -0
- simstack/util/directory_iterator.py +216 -0
- simstack/util/file_hashing.py +68 -0
- simstack/util/files.py +396 -0
- simstack/util/find_python_files.py +33 -0
- simstack/util/generate_ui_schema.py +80 -0
- simstack/util/geometry_utils.py +118 -0
- simstack/util/get_module_path.py +26 -0
- simstack/util/git_pull.py +65 -0
- simstack/util/git_repository_status.py +93 -0
- simstack/util/import_module.py +65 -0
- simstack/util/importer.py +163 -0
- simstack/util/make_graph.py +107 -0
- simstack/util/make_table.py +827 -0
- simstack/util/minimal_route_finder.py +72 -0
- simstack/util/mock_db.py +144 -0
- simstack/util/module_path_checker.py +38 -0
- simstack/util/path_manager.py +234 -0
- simstack/util/project_root_finder.py +36 -0
- simstack/util/queue_watcher.py +262 -0
- simstack/util/route_finder.py +74 -0
- simstack/util/runner_util.py +162 -0
- simstack/util/safe_code_executor.py +194 -0
- simstack/util/sample_config.py +98 -0
- simstack/util/sensitive_content_filter.py +48 -0
- simstack/util/setup_logging.py +49 -0
- simstack/util/submit_to_watchdog.py +106 -0
- simstack/util/ui_tools.py +213 -0
- simstack-0.2.0.dist-info/METADATA +361 -0
- simstack-0.2.0.dist-info/RECORD +91 -0
- simstack-0.2.0.dist-info/WHEEL +5 -0
- simstack-0.2.0.dist-info/entry_points.txt +10 -0
- simstack-0.2.0.dist-info/licenses/LICENSE +21 -0
- simstack-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from simstack.core.artifacts import ArtifactArguments, create_artifacts
|
|
4
|
+
from simstack.core.definitions import TaskStatus
|
|
5
|
+
from simstack.core.engine import current_engine_context
|
|
6
|
+
from simstack.core.node import node_from_database
|
|
7
|
+
from simstack.models import NodeRegistry, ArtifactModel
|
|
8
|
+
from simstack.models.charts_artifact import ChartArtifactModel
|
|
9
|
+
from simstack.models.table_artifact import TableArtifactModel
|
|
10
|
+
from simstack.util.importer import import_class
|
|
11
|
+
from simstack.util.importer import import_function
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("recompute_artifacts")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def recompute_artifacts(node_registry: NodeRegistry):
|
|
17
|
+
"""
|
|
18
|
+
Recomputes artifacts for a node and all its children recursively.
|
|
19
|
+
|
|
20
|
+
First processes all children nodes recursively, then removes and recomputes
|
|
21
|
+
the current node's artifacts.
|
|
22
|
+
|
|
23
|
+
:param node_registry: The registry entry for the node to process
|
|
24
|
+
:type node_registry: NodeRegistry
|
|
25
|
+
"""
|
|
26
|
+
# Create Node from the registry
|
|
27
|
+
|
|
28
|
+
engine = current_engine_context.get()
|
|
29
|
+
node = await node_from_database(node_registry)
|
|
30
|
+
if node is None:
|
|
31
|
+
logger.error(f"Failed to create node from registry task_id: {node_registry.id}")
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
if node.status != TaskStatus.COMPLETED:
|
|
35
|
+
logger.error(f"Cannot recompute artifacts for task_id: {node_registry.id} name: {node.name} status: {node.status}")
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
# Find all children of this node
|
|
39
|
+
children = await engine.find(NodeRegistry, NodeRegistry.parent_ids.in_([node_registry.id]))
|
|
40
|
+
|
|
41
|
+
# Recursively recompute artifacts for all children first
|
|
42
|
+
for child_registry in children:
|
|
43
|
+
await recompute_artifacts(child_registry)
|
|
44
|
+
|
|
45
|
+
# Remove current node's artifacts
|
|
46
|
+
if node_registry.artifact_ids:
|
|
47
|
+
logger.info(f"Removing {len(node_registry.artifact_ids)} artifacts for node {node_registry.id}")
|
|
48
|
+
|
|
49
|
+
table_artifacts = await engine.find(
|
|
50
|
+
TableArtifactModel, TableArtifactModel.parent_id == node_registry.id
|
|
51
|
+
)
|
|
52
|
+
for table_artifact in table_artifacts:
|
|
53
|
+
await engine.delete(table_artifact)
|
|
54
|
+
|
|
55
|
+
chart_artifacts = await engine.find(
|
|
56
|
+
ChartArtifactModel, ChartArtifactModel.parent_id == node_registry.id
|
|
57
|
+
)
|
|
58
|
+
for chart_artifact in chart_artifacts:
|
|
59
|
+
await engine.delete(chart_artifact)
|
|
60
|
+
# Delete artifacts from the database
|
|
61
|
+
for artifact_id in node_registry.artifact_ids:
|
|
62
|
+
instance = await engine.find_one(
|
|
63
|
+
ArtifactModel, ArtifactModel.id == artifact_id
|
|
64
|
+
)
|
|
65
|
+
if instance:
|
|
66
|
+
await engine.delete(instance)
|
|
67
|
+
else:
|
|
68
|
+
logger.warning(
|
|
69
|
+
f"task_id: {node_registry.id} Failed to delete artifact {artifact_id} from database"
|
|
70
|
+
)
|
|
71
|
+
node_registry.artifact_ids = []
|
|
72
|
+
|
|
73
|
+
# Recompute artifacts for this node
|
|
74
|
+
if node_registry.status == TaskStatus.COMPLETED:
|
|
75
|
+
logger.info(f"Recomputing artifacts for node {node_registry.id}")
|
|
76
|
+
|
|
77
|
+
# Load the result to create new artifacts
|
|
78
|
+
result = await node.load_results()
|
|
79
|
+
if result is not None:
|
|
80
|
+
artifact_arguments = ArtifactArguments(result, node_registry.id)
|
|
81
|
+
# Reconstruct the function arguments for artifact creation
|
|
82
|
+
args = []
|
|
83
|
+
for table, table_id in zip(
|
|
84
|
+
node_registry.input_tables, node_registry.input_ids
|
|
85
|
+
):
|
|
86
|
+
model = await import_class(table)
|
|
87
|
+
arg = await engine.find_one(model, model.id == table_id)
|
|
88
|
+
if arg:
|
|
89
|
+
args.append(arg)
|
|
90
|
+
|
|
91
|
+
# Get the function for artifact creation
|
|
92
|
+
wrapped_func = await import_function(node_registry.func_mapping)
|
|
93
|
+
func = (
|
|
94
|
+
wrapped_func
|
|
95
|
+
if not hasattr(wrapped_func, "_inner")
|
|
96
|
+
else wrapped_func._inner
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Create node kwargs similar to run_local
|
|
100
|
+
node_kwargs = {
|
|
101
|
+
"parent_id": node_registry.id,
|
|
102
|
+
"task_id": node_registry.id,
|
|
103
|
+
"call_path": node_registry.call_path,
|
|
104
|
+
"parent_parameters": node_registry.parameters,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
artifact_arguments.add_attributes(func, *args, **node_kwargs)
|
|
108
|
+
node_registry.artifact_ids = await create_artifacts(
|
|
109
|
+
artifact_arguments, node_registry
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Save the updated registry
|
|
113
|
+
await engine.save(node_registry)
|
|
114
|
+
logger.info(
|
|
115
|
+
f"Recomputed {len(node_registry.artifact_ids)} artifacts for node {node_registry.id}"
|
|
116
|
+
)
|
|
117
|
+
else:
|
|
118
|
+
logger.warning(
|
|
119
|
+
f"Node {node_registry.id} is not completed, cannot recompute artifacts"
|
|
120
|
+
)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import tomllib
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
from simstack.core.config_file import get_config_file
|
|
8
|
+
from simstack.util.project_root_finder import find_project_root
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("resources")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AllowedResources:
|
|
14
|
+
"""
|
|
15
|
+
Singleton class that holds a list of allowed resource strings.
|
|
16
|
+
# This class is independent of context because its initialized to an empty list.
|
|
17
|
+
Creating any resource will fail if the config has not been read
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
_instance = None
|
|
21
|
+
_resources: List[str] = []
|
|
22
|
+
|
|
23
|
+
def __new__(cls):
|
|
24
|
+
if cls._instance is None:
|
|
25
|
+
cls._instance = super(AllowedResources, cls).__new__(cls)
|
|
26
|
+
return cls._instance
|
|
27
|
+
|
|
28
|
+
def __init__(self):
|
|
29
|
+
# Only initialize once
|
|
30
|
+
if not hasattr(self, "_initialized"):
|
|
31
|
+
config_file = get_config_file().config_file_path
|
|
32
|
+
config_path = find_project_root()
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
toml_file = os.path.join(config_path, config_file)
|
|
36
|
+
if os.path.exists(toml_file):
|
|
37
|
+
with open(toml_file, "rb") as f:
|
|
38
|
+
self.config = tomllib.load(f)
|
|
39
|
+
else:
|
|
40
|
+
print("simstack.toml file not found in the project root.")
|
|
41
|
+
sys.exit(-1)
|
|
42
|
+
except tomllib.TOMLDecodeError:
|
|
43
|
+
print("There was an error decoding the TOML file.")
|
|
44
|
+
sys.exit(-1)
|
|
45
|
+
|
|
46
|
+
self._resources = (
|
|
47
|
+
self.config.get("parameters", {}).get("common", {}).get("resources", [])
|
|
48
|
+
)
|
|
49
|
+
logger.info(f"Initialized resources to: {self._resources}")
|
|
50
|
+
self._initialized = True
|
|
51
|
+
|
|
52
|
+
def set_resources(self, resources: List[str]) -> None:
|
|
53
|
+
"""Set the list of allowed resources."""
|
|
54
|
+
self._resources = resources.copy() if resources else []
|
|
55
|
+
|
|
56
|
+
def get_resources(self) -> List[str]:
|
|
57
|
+
"""Get the list of allowed resources."""
|
|
58
|
+
return self._resources.copy()
|
|
59
|
+
|
|
60
|
+
def add_resource(self, resource: str) -> None:
|
|
61
|
+
"""Add a single resource to the list."""
|
|
62
|
+
if resource not in self._resources:
|
|
63
|
+
self._resources.append(resource)
|
|
64
|
+
|
|
65
|
+
def remove_resource(self, resource: str) -> None:
|
|
66
|
+
if resource == "self":
|
|
67
|
+
raise ValueError("Cannot remove 'self' resource")
|
|
68
|
+
"""Remove a resource from the list."""
|
|
69
|
+
if resource in self._resources:
|
|
70
|
+
self._resources.remove(resource)
|
|
71
|
+
|
|
72
|
+
def clear_resources(self) -> None:
|
|
73
|
+
"""Clear all resources from the list."""
|
|
74
|
+
self._resources.clear()
|
|
75
|
+
|
|
76
|
+
def has_resource(self, resource: str) -> bool:
|
|
77
|
+
"""Check if a resource exists in the list."""
|
|
78
|
+
return resource in self._resources
|
|
79
|
+
|
|
80
|
+
def __len__(self) -> int:
|
|
81
|
+
"""Return the number of resources."""
|
|
82
|
+
return len(self._resources)
|
|
83
|
+
|
|
84
|
+
def __iter__(self):
|
|
85
|
+
"""Make the class iterable."""
|
|
86
|
+
return iter(self._resources)
|
|
87
|
+
|
|
88
|
+
def __contains__(self, resource: str) -> bool:
|
|
89
|
+
"""Support 'in' operator."""
|
|
90
|
+
return resource in self._resources
|
|
91
|
+
|
|
92
|
+
def __str__(self) -> str:
|
|
93
|
+
"""String representation."""
|
|
94
|
+
return f"AllowedResources({self._resources})"
|
|
95
|
+
|
|
96
|
+
def __repr__(self) -> str:
|
|
97
|
+
"""String representation."""
|
|
98
|
+
return f"AllowedResources({self._resources!r})"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
allowed_resources = AllowedResources()
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import asyncio
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from simstack.core.context import context
|
|
6
|
+
from simstack.core.definitions import TaskStatus
|
|
7
|
+
from simstack.core.node import node_from_database
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger("SlurmRunner")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def run_node(node_id: str, **kwargs):
|
|
13
|
+
"""Run a single node by its ID from the database"""
|
|
14
|
+
registry_entry = None
|
|
15
|
+
try:
|
|
16
|
+
registry_entry = await context.db.load_task_by_id(node_id)
|
|
17
|
+
|
|
18
|
+
if not registry_entry:
|
|
19
|
+
logger.error(f"Node with ID {node_id} not found in the database")
|
|
20
|
+
return False
|
|
21
|
+
# Create node from the registry entry
|
|
22
|
+
node = await node_from_database(registry_entry)
|
|
23
|
+
if not node:
|
|
24
|
+
logger.error(
|
|
25
|
+
f"Failed to create node from registry entry task_id: {registry_entry.id}"
|
|
26
|
+
)
|
|
27
|
+
registry_entry.status = TaskStatus.FAILED
|
|
28
|
+
await context.db.save(registry_entry)
|
|
29
|
+
return False
|
|
30
|
+
registry_entry = node.registry_entry # it may have changed
|
|
31
|
+
# if the node was recovered we do not have to run it again
|
|
32
|
+
if node.status == TaskStatus.SUBMITTED or node.status == TaskStatus.SLURM_QUEUED or node.status == TaskStatus.SLURM_QUEUED:
|
|
33
|
+
await node.execute_node_locally()
|
|
34
|
+
else:
|
|
35
|
+
logger.info(f"task_id: {registry_entry.id} skipping task: {registry_entry.name} with status {registry_entry.status}")
|
|
36
|
+
return node.status == TaskStatus.COMPLETED
|
|
37
|
+
except Exception as e:
|
|
38
|
+
logger.exception(f"Error running node task_id: {node_id}: {str(e)}")
|
|
39
|
+
if registry_entry:
|
|
40
|
+
registry_entry.status = TaskStatus.FAILED
|
|
41
|
+
await context.db.save(registry_entry)
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
def run_node_main():
|
|
45
|
+
parser = argparse.ArgumentParser(description="Run nodes for a specific resource")
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
"--node-id",
|
|
48
|
+
type=str,
|
|
49
|
+
help="Specific node ID to run (overrides resource-based polling)",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
parser.add_argument(
|
|
53
|
+
"--resource",
|
|
54
|
+
default="local",
|
|
55
|
+
nargs="?",
|
|
56
|
+
type=str,
|
|
57
|
+
help="resource to load",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
args = parser.parse_args()
|
|
61
|
+
context.initialize(resource=args.resource)
|
|
62
|
+
|
|
63
|
+
if args.node_id:
|
|
64
|
+
# Run a specific node once
|
|
65
|
+
asyncio.run(run_node(args.node_id))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
run_node_main()
|