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,72 @@
1
+ from collections import deque
2
+ from typing import List, Dict
3
+
4
+
5
+ def find_minimal_route(
6
+ routes: List[Dict[str, str]], source: str, target: str
7
+ ) -> List[Dict[str, str]]:
8
+ """
9
+ Find the minimal (shortest) route from source to target.
10
+
11
+ Args:
12
+ routes: List of route dictionaries with 'source', 'target', and 'host'
13
+ source: The starting node
14
+ target: The destination node
15
+
16
+ Returns:
17
+ A list of route dictionaries representing the shortest path from source to target,
18
+ or an empty list if no path exists
19
+ """
20
+ if not routes:
21
+ return []
22
+
23
+ # Build the graph and route map
24
+ adj: Dict[str, List[str]] = {}
25
+ route_map: Dict[tuple, Dict[str, str]] = {}
26
+ nodes = set()
27
+
28
+ for route in routes:
29
+ src = route["source"]
30
+ dst = route["target"]
31
+ nodes.add(src)
32
+ nodes.add(dst)
33
+
34
+ if src not in adj:
35
+ adj[src] = []
36
+
37
+ # Avoid adding duplicates to adjacency list, but allow route_map update (last one wins)
38
+ if dst not in adj[src]:
39
+ adj[src].append(dst)
40
+
41
+ route_map[(src, dst)] = route
42
+
43
+ if source not in nodes or target not in nodes:
44
+ return []
45
+
46
+ # BFS to find shortest path
47
+ queue = deque([(source, [source])])
48
+ visited = {source}
49
+
50
+ while queue:
51
+ current_node, path = queue.popleft()
52
+
53
+ if current_node == target:
54
+ # Convert the path of nodes to a list of route dictionaries
55
+ path_routes = []
56
+ for i in range(len(path) - 1):
57
+ u = path[i]
58
+ v = path[i + 1]
59
+ path_routes.append(route_map[(u, v)])
60
+ return path_routes
61
+
62
+ if current_node in adj:
63
+ for neighbor in adj[current_node]:
64
+ if neighbor not in visited:
65
+ visited.add(neighbor)
66
+ queue.append((neighbor, path + [neighbor]))
67
+
68
+ return []
69
+
70
+
71
+ # Add an alias for find_minimal_route as find_shortest_route for compatibility
72
+ find_shortest_route = find_minimal_route
@@ -0,0 +1,144 @@
1
+ import asyncio
2
+ from typing import Type, TypeVar, Dict, List, Optional
3
+
4
+ from odmantic import Model, ObjectId
5
+
6
+ T = TypeVar("T", bound=Model)
7
+
8
+
9
+ class InMemoryCollection:
10
+ def __init__(self, engine: "InMemoryAIOEngine"):
11
+ self._engine = engine
12
+ self._store: List[Dict] = []
13
+
14
+ def __getitem__(self, index: int) -> Dict:
15
+ return self._store[index]
16
+
17
+ def __setitem__(self, index: int, value: Dict):
18
+ self._store[index] = value
19
+
20
+ def __iter__(self):
21
+ return iter(self._store)
22
+
23
+
24
+ class InMemoryAIOEngine:
25
+ def __init__(self):
26
+ # Stores data as {collection_name: [documents]}
27
+ self._store: Dict[str, InMemoryCollection] = {}
28
+
29
+ async def save(self, instance: T) -> T:
30
+ collection_name = (
31
+ instance.__collection__
32
+ if hasattr(instance, "__collection__")
33
+ else instance.__class__.__name__.lower()
34
+ )
35
+
36
+ obj_dict = instance.model_dump()
37
+
38
+ # Ensure the instance has an ObjectId assigned
39
+ if not instance.id:
40
+ instance.id = ObjectId()
41
+ obj_dict["id"] = instance.id
42
+
43
+ # Initialize collection if it doesn't exist
44
+ if collection_name not in self._store:
45
+ self._store[collection_name] = InMemoryCollection(self)
46
+
47
+ # Update if already exists
48
+ existing_index = next(
49
+ (
50
+ i
51
+ for i, obj in enumerate(self._store[collection_name])
52
+ if obj["id"] == instance.id
53
+ ),
54
+ None,
55
+ )
56
+ if existing_index is not None:
57
+ self._store[collection_name][existing_index] = obj_dict
58
+ else:
59
+ self._store[collection_name].append(obj_dict)
60
+
61
+ return instance
62
+
63
+ async def find(self, model: Type[T], **query) -> List[T]:
64
+ collection_name = (
65
+ model.__collection__
66
+ if hasattr(model, "__collection__")
67
+ else model.__name__.lower()
68
+ )
69
+ all_objects = self._store.get(collection_name, [])
70
+ results = []
71
+
72
+ for obj_dict in all_objects:
73
+ if all(obj_dict.get(k) == v for k, v in query.items()):
74
+ results.append(model.model_validate(obj_dict))
75
+ return results
76
+
77
+
78
+ async def find_one(self, model: Type[T], condition=None, **query) -> Optional[T]:
79
+ # Handle both keyword arguments and condition objects
80
+ if condition is not None:
81
+ # For conditions like ModelMapping.name == class_name
82
+ # This is a more robust approach to extract field name and value
83
+ condition_str = str(condition)
84
+
85
+ # Look for the pattern: field_name == value or field_name: {$eq: value}
86
+ if "==" in condition_str:
87
+ # Handle direct comparison like ModelMapping.name == 'TestClass'
88
+ parts = condition_str.split("==")
89
+ if len(parts) == 2:
90
+ field_name = parts[0].strip().split(".")[-1]
91
+ value = parts[1].strip().strip("'\"")
92
+ query[field_name] = value
93
+ elif "$eq" in condition_str:
94
+ # Handle MongoDB-style query like {'mapping': {'$eq': 'tests.core.test_import_class.TestClass'}}
95
+ import re
96
+
97
+ # Extract field name and value using regex
98
+ field_match = re.search(
99
+ r"'(\w+)':\s*\{'?\$eq'?:\s*'([^']+)'", condition_str
100
+ )
101
+ if field_match:
102
+ field_name = field_match.group(1)
103
+ value = field_match.group(2)
104
+ query[field_name] = value
105
+ else:
106
+ # Fallback - try to extract from the string representation
107
+ # Look for pattern like "ModelMapping.field_name"
108
+ field_match = re.search(r"(\w+)\.(\w+)", condition_str)
109
+ if field_match:
110
+ field_name = field_match.group(2)
111
+ # Try to extract the value (this is still a fallback)
112
+ value_match = re.search(r"'([^']+)'(?:[^']*$)", condition_str)
113
+ if value_match:
114
+ value = value_match.group(1)
115
+ query[field_name] = value
116
+
117
+ found = await self.find(model, **query)
118
+ return found[0] if found else None
119
+
120
+
121
+ # Example Usage:
122
+ if __name__ == "__main__":
123
+ from odmantic import Model
124
+
125
+ class BinaryOperationInput(Model):
126
+ arg1: int
127
+ arg2: int
128
+
129
+ async def test_engine():
130
+ engine = InMemoryAIOEngine()
131
+ obj = BinaryOperationInput(arg1=1, arg2=2)
132
+
133
+ # Saving object
134
+ await engine.save(obj)
135
+
136
+ # Finding objects
137
+ found_objs = await engine.find(BinaryOperationInput, arg1=1)
138
+ print("Found objects:", found_objs)
139
+
140
+ # Find one object
141
+ found_one = await engine.find_one(BinaryOperationInput, arg2=2)
142
+ print("Found single object:", found_one)
143
+
144
+ asyncio.run(test_engine())
@@ -0,0 +1,38 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ from simstack.util.project_root_finder import find_project_root
5
+
6
+
7
+ def is_module_subpath_of_path(module_path: str, path_info_path: str) -> bool:
8
+ """
9
+ Check whether a dot-separated module path is a subpath of an absolute path.
10
+
11
+ Args:
12
+ module_path: A dot-separated path relative to the project root (e.g., "src.simstack.core.artifacts")
13
+ path_info_path: An absolute path below the project root (e.g., "/path/to/project/src/simstack/core")
14
+
15
+ Returns:
16
+ True if the module path corresponds to a location under the given absolute path, False otherwise
17
+ """
18
+
19
+ # Convert the module path to file system path
20
+ # Replace dots with path separators
21
+ module_file_path = module_path.replace(".", os.sep)
22
+
23
+ # Convert path_info_path to Path object for easier manipulation
24
+ path_info_path_obj = Path(path_info_path)
25
+
26
+ # Get the project root using the existing utility function
27
+ project_root = Path(find_project_root())
28
+
29
+ # Convert module path to absolute path relative to project root
30
+ module_absolute_path = project_root / module_file_path
31
+
32
+ try:
33
+ # Check if the module path is under the given path_info_path
34
+ module_absolute_path.relative_to(path_info_path_obj)
35
+ return True
36
+ except ValueError:
37
+ # If relative_to raises ValueError, the module path is not under path_info_path
38
+ return False
@@ -0,0 +1,234 @@
1
+ import os
2
+ from pathlib import Path
3
+ from typing import List, Dict, Optional, Iterator, Any
4
+
5
+ from simstack.util.directory_iterator import DirectoryPath
6
+ from simstack.util.project_root_finder import find_project_root
7
+
8
+
9
+ class PathManager:
10
+ """
11
+ Manages paths for the SimStack application, providing mechanisms to find Python files
12
+ and other resources. Uses DirectoryPath for efficient directory traversal.
13
+ """
14
+
15
+ def __init__(self, use_pickle: bool = False):
16
+ """
17
+ Initialize the PathManager.
18
+
19
+ Args:
20
+ use_pickle: Boolean flag indicating whether to use pickle for serialization
21
+ """
22
+ self.use_pickle = use_pickle
23
+ self.root_dir = find_project_root()
24
+ self.paths: Dict[str, Dict[str, str]] = {}
25
+ self.packages: Dict[str] = {}
26
+ self.precompiled: Dict[str] = {}
27
+ self._default_excluded_patterns = [
28
+ "__pycache__",
29
+ "*.pyc",
30
+ ".git",
31
+ ".venv",
32
+ "venv",
33
+ ]
34
+
35
+ def add_path(
36
+ self, name: str, path: str, drops: str = "", use_pickle: bool = False
37
+ ) -> None:
38
+ """
39
+ Add a path to the manager.
40
+
41
+ Args:
42
+ name: Name identifier for the path
43
+ path: The directory path
44
+ drops: Prefix to drop from module names (for import paths)
45
+ use_pickle: Whether to use pickle for this path
46
+ """
47
+ if not os.path.isdir(path):
48
+ raise ValueError(f"'{path}' is not a valid directory")
49
+
50
+ self.paths[name] = {"path": path, "drops": drops, "use_pickle": use_pickle}
51
+
52
+ def get_path(self, name: str) -> Dict[str, str]:
53
+ """
54
+ Get a path by name.
55
+
56
+ Args:
57
+ name: Name of the path to retrieve
58
+
59
+ Returns:
60
+ Dictionary containing path information
61
+ """
62
+ if name not in self.paths:
63
+ raise KeyError(f"Path '{name}' not found in PathManager")
64
+
65
+ return self.paths[name]
66
+
67
+ def find_python_files(
68
+ self, path_name: str, excluded_patterns: Optional[List[str]] = None
69
+ ) -> List[str]:
70
+ """
71
+ Find Python files in the specified path, excluding __init__.py files.
72
+
73
+ Args:
74
+ path_name: Name of the path to search in
75
+ excluded_patterns: Additional patterns to exclude
76
+
77
+ Returns:
78
+ List of absolute file paths to Python files
79
+ """
80
+ path_info = self.get_path(path_name)
81
+ path = path_info["path"]
82
+
83
+ # Combine default and additional exclusion patterns
84
+ all_excluded_patterns = self._default_excluded_patterns.copy()
85
+ if excluded_patterns:
86
+ all_excluded_patterns.extend(excluded_patterns)
87
+
88
+ # Add __init__.py to excluded patterns
89
+ all_excluded_patterns.append("__init__.py")
90
+
91
+ # Use DirectoryPath to find Python files
92
+ dir_path = DirectoryPath(path, all_excluded_patterns, [".py"])
93
+
94
+ # Convert Path objects to strings for compatibility with existing code
95
+ return [str(file_path) for file_path in dir_path.get_files_list()]
96
+
97
+ def iterate_python_files(
98
+ self, path_name: str, excluded_patterns: Optional[List[str]] = None
99
+ ) -> Iterator[Path]:
100
+ """
101
+ Iterate over Python files in the specified path, excluding __init__.py files.
102
+
103
+ Args:
104
+ path_name: Name of the path to search in
105
+ excluded_patterns: Additional patterns to exclude
106
+
107
+ Returns:
108
+ Iterator of Path objects for Python files
109
+ """
110
+ path_info = self.get_path(path_name)
111
+ path = path_info["path"]
112
+
113
+ # Combine default and additional exclusion patterns
114
+ all_excluded_patterns = self._default_excluded_patterns.copy()
115
+ if excluded_patterns:
116
+ all_excluded_patterns.extend(excluded_patterns)
117
+
118
+ # Add __init__.py to excluded patterns
119
+ all_excluded_patterns.append("__init__.py")
120
+
121
+ # Use DirectoryPath to iterate over Python files
122
+ dir_path = DirectoryPath(path, all_excluded_patterns, [".py"])
123
+ return dir_path.iterate_files()
124
+
125
+ def get_drops(self, path_name: str) -> str:
126
+ """
127
+ Get the drops value for a path.
128
+
129
+ Args:
130
+ path_name: Name of the path
131
+
132
+ Returns:
133
+ The drops value for the path
134
+ """
135
+ path_info = self.get_path(path_name)
136
+ return path_info["drops"]
137
+
138
+ @classmethod
139
+ def from_config(cls, config: Any) -> "PathManager":
140
+ """
141
+ Create a PathManager from configuration.
142
+
143
+ Args:
144
+ config: Configuration object containing path information
145
+
146
+ Returns:
147
+ Initialized PathManager instance
148
+ """
149
+ # Get use_pickle from config
150
+ use_pickle = False
151
+ if hasattr(config, "config"):
152
+ # If config is a ConfigReader instance
153
+ use_pickle = (
154
+ config.config.get("parameters", {})
155
+ .get("common", {})
156
+ .get("use_pickle", False)
157
+ )
158
+ else:
159
+ # If config is a dictionary
160
+ use_pickle = config.get("use_pickle", False)
161
+
162
+ path_manager = cls(use_pickle=use_pickle)
163
+
164
+ # Get paths from config
165
+ if hasattr(config, "paths"):
166
+ # If config is a ConfigReader instance
167
+ paths = config.paths
168
+ else:
169
+ # If config is a dictionary
170
+ paths = config.get("paths", {})
171
+
172
+ # Add paths from configuration if available
173
+ root_dir = find_project_root()
174
+ for name, path_info in paths.items():
175
+ if isinstance(path_info, dict) and "path" in path_info:
176
+ # Get path, drops, and use_pickle from path_info
177
+ path = path_info["path"]
178
+ # Convert Unix-style path to Windows path if on Windows
179
+ if os.name == "nt": # Windows OS
180
+ path = path.replace("/", "\\")
181
+
182
+ drops = path_info.get("drops", "")
183
+ path_use_pickle = path_info.get("use_pickle", False)
184
+
185
+ # Convert relative paths to absolute paths
186
+ if not os.path.isabs(path):
187
+ path = os.path.join(root_dir, path)
188
+
189
+ # Add path to PathManager
190
+ path_manager.add_path(name, path, drops, path_use_pickle)
191
+
192
+ return path_manager
193
+
194
+ def find_parent_path(self, path: str) -> Optional[str]:
195
+ """
196
+ Find the parent path from self.paths that contains the given path.
197
+
198
+ Args:
199
+ path: The path to find the parent of
200
+
201
+ Returns:
202
+ The name of the parent path from self.paths that contains the given path,
203
+ or None if no parent is found
204
+ """
205
+ path_obj = Path(path)
206
+
207
+ # Convert to the absolute path if needed
208
+ if not path_obj.is_absolute():
209
+ path_obj = self.root_dir / path_obj
210
+
211
+ # Search through all paths in self.paths to find the one that contains path_obj
212
+ best_match = None
213
+ best_match_depth = -1
214
+
215
+ for path_name, path_info in self.paths.items():
216
+ registered_path = Path(path_info["path"])
217
+
218
+ try:
219
+ # Check if path_obj is under this registered path
220
+ path_obj.relative_to(registered_path)
221
+
222
+ # Calculate depth (number of parent directories)
223
+ depth = len(registered_path.parts)
224
+
225
+ # Keep the deepest match (the most specific parent)
226
+ if depth > best_match_depth:
227
+ best_match = path_name
228
+ best_match_depth = depth
229
+
230
+ except ValueError:
231
+ # path_obj is not under this registered path, continue
232
+ continue
233
+
234
+ return best_match
@@ -0,0 +1,36 @@
1
+ import os
2
+
3
+
4
+ def find_project_root(
5
+ current_file=None, marker_files=(".git", "simstack.toml", "setup.py")
6
+ ):
7
+ """
8
+ Find the project root directory by searching for common marker files
9
+
10
+ Args:
11
+ current_file: Path to the current file, defaults to __file__ if None
12
+ marker_files: Tuple of files/directories that indicate the project root
13
+
14
+ Returns:
15
+ Absolute path to the project root directory
16
+ """
17
+ if current_file is None:
18
+ current_file = __file__
19
+
20
+ # Get the directory of the current file
21
+ current_dir = os.path.abspath(os.path.dirname(current_file))
22
+
23
+ # Walk up the directory tree until we find a marker file
24
+ prev_dir = None
25
+ while current_dir != prev_dir:
26
+ # Check if any marker files/directories exist in the current directory
27
+ for marker in marker_files:
28
+ if os.path.exists(os.path.join(current_dir, marker)):
29
+ return current_dir
30
+
31
+ # Move up one directory
32
+ prev_dir = current_dir
33
+ current_dir = os.path.abspath(os.path.join(current_dir, os.pardir))
34
+
35
+ # If we can't find any markers, return the directory of the current file
36
+ return os.path.abspath(os.path.dirname(current_file))