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,98 @@
1
+ _SAMPLE_SIMSTACK_TOML_DICT = {
2
+ "parameters": {
3
+ "common": {
4
+ "resources": [
5
+ "local",
6
+ "int-nano",
7
+ "horeka",
8
+ "justus",
9
+ "self",
10
+ "exchange",
11
+ "uploads",
12
+ ],
13
+ "database": "wolfgang_data",
14
+ "test_database": "wolfgang_test",
15
+ "connection_string": "mongodb://localhost:27017/",
16
+ },
17
+ "local": {
18
+ "ssh-key": "C:\\Users\\bj7610\\Documents\\etc\\.ssh\\surface11_openssh",
19
+ "resource": "local",
20
+ "workdir": "C:\\Users\\bj7610\\simstack",
21
+ "python_path": [
22
+ "C:\\Users\\bj7610\\PyCharmProjects\\simstack-model",
23
+ "C:\\Users\\bj7610\\PyCharmProjects\\simstack-model\\src",
24
+ ],
25
+ },
26
+ "self": {
27
+ "ssh-key": "C:\\Users\\bj7610\\Documents\\etc\\.ssh\\surface11_openssh",
28
+ "resource": "local",
29
+ "workdir": "C:\\Users\\bj7610\\simstack",
30
+ "python_path": [
31
+ "C:\\Users\\bj7610\\PyCharmProjects\\simstack-model",
32
+ "C:\\Users\\bj7610\\PyCharmProjects\\simstack-model\\src",
33
+ ],
34
+ },
35
+ "uploads": {
36
+ "ssh-key": "C:\\Users\\bj7610\\Documents\\etc\\.ssh\\surface11_openssh",
37
+ "resource": "self",
38
+ "workdir": "C:\\Users\\bj7610\\simstack",
39
+ "python_path": [
40
+ "C:\\Users\\bj7610\\PyCharmProjects\\simstack-model",
41
+ "C:\\Users\\bj7610\\PyCharmProjects\\simstack-model\\src",
42
+ ],
43
+ },
44
+ "int-nano": {
45
+ "ssh-key": "/home/ws/bj7610/.ssh/idrsa",
46
+ "workdir": "/home/ws/bj7610/simstack",
47
+ "python_path": [
48
+ "/home/ws/bj7610/projects/simstack-model",
49
+ "/home/ws/bj7610/projects/simstack-model/src",
50
+ ],
51
+ "environment_start": "conda activate simstack-env",
52
+ },
53
+ },
54
+ "server": {
55
+ "port": 8000,
56
+ "secret_key": "secret_key",
57
+ "upload_dir": "C:\\Users\\bj7610\\simstack\\uploads",
58
+ },
59
+ "hosts": {
60
+ "local": "localhost",
61
+ "int-nano": "int-nano.int.kit.edu",
62
+ "justus": "justus.int.kit.edu",
63
+ "horeka": "horeka.int.kit.edu",
64
+ },
65
+ "routes": [
66
+ {"source": "local", "target": "int-nano", "host": "local"},
67
+ {"source": "int-nano", "target": "local", "host": "local"},
68
+ {"source": "horeka", "target": "local", "host": "horeka"},
69
+ {"source": "local", "target": "horeka", "host": "horeka"},
70
+ {"source": "justus", "target": "local", "host": "justus"},
71
+ {"source": "local", "target": "justus", "host": "justus"},
72
+ ],
73
+ "paths": {
74
+ "models": {"path": "src/simstack/models", "drops": "src"},
75
+ "methods": {"path": "src/simstack/methods", "drops": "src"},
76
+ "tests": {"path": "tests", "drops": "", "use_pickle": True},
77
+ },
78
+ }
79
+
80
+
81
+ def sample_config_writer_entrypoint() -> None:
82
+ """
83
+ Entry point to write a sample simstack.toml configuration file.
84
+ """
85
+ import os
86
+ import toml
87
+
88
+ config_path = os.path.join(os.getcwd(), "simstack.toml")
89
+ if os.path.exists(config_path):
90
+ print(
91
+ f"simstack.toml already exists at {config_path}. Aborting to avoid overwrite."
92
+ )
93
+ return
94
+
95
+ with open(config_path, "w") as config_file:
96
+ toml.dump(_SAMPLE_SIMSTACK_TOML_DICT, config_file)
97
+
98
+ print(f"Sample simstack.toml written to {config_path}")
@@ -0,0 +1,48 @@
1
+ from typing import Dict, Any
2
+
3
+
4
+ def filter_file_content(data: Dict[str, Any]) -> Dict[str, Any]:
5
+ """
6
+ Recursively searches through nested data structures to filter out sensitive content.
7
+ Specifically looks for 'content' fields in dictionaries, especially within 'files' structures.
8
+
9
+ Args:
10
+ data: Any Python data structure (dict, list, etc.)
11
+
12
+ Returns:
13
+ The filtered data structure with sensitive content removed
14
+ """
15
+ if isinstance(data, dict):
16
+ # Create a new dict to avoid modifying the original during iteration
17
+ result = {}
18
+
19
+ # Check if this is a FileStack-like dict with content
20
+ if "content" in data:
21
+ # Make a copy of the dict without the content field
22
+ result = {
23
+ k: filter_file_content(v) for k, v in data.items() if k != "content"
24
+ }
25
+ else:
26
+ # Process all items recursively
27
+ for key, value in data.items():
28
+ result[key] = filter_file_content(value)
29
+
30
+ return result
31
+
32
+ elif isinstance(data, list):
33
+ # Process each item in the list recursively
34
+ return [filter_file_content(item) for item in data]
35
+
36
+ elif hasattr(data, "__dict__"):
37
+ # Handle object instances by converting to dict and processing
38
+ obj_dict = data.__dict__.copy()
39
+ filtered_dict = filter_file_content(obj_dict)
40
+
41
+ # This is tricky - we'd ideally create a new instance with the filtered data
42
+ # but that's complex without knowing the class structure
43
+ # So we'll return the filtered dict instead
44
+ return filtered_dict
45
+
46
+ else:
47
+ # For primitive types, return as is
48
+ return data
@@ -0,0 +1,49 @@
1
+ import logging
2
+
3
+ from simstack.util.db_logger import DBLogHandler
4
+
5
+
6
+ def setup_logging(
7
+ connection_string: str,
8
+ db_name: str,
9
+ log_level=logging.INFO,
10
+ console=True,
11
+ log_format: str = "%(asctime)s - %(name)-15s - %(levelname)-10s - %(filename)-20s:%(lineno)4d - %(message)s",
12
+ ):
13
+ """
14
+ Configures the root logger with specified logging level, handlers for both database
15
+ and optionally the console.
16
+
17
+ :param log_format:
18
+ :param connection_string: A string representing the database connection.
19
+ :param db_name: A name of the database to log into.
20
+ :param log_level: Logging verbosity level, defaulting to logging.INFO.
21
+ :param console: A boolean indicating whether to log messages to the console.
22
+ :return: Configured root logger instance.
23
+ """
24
+ # Configure the root logger
25
+ root_logger = logging.getLogger()
26
+ root_logger.setLevel(log_level)
27
+
28
+ # Remove any existing handlers to avoid duplicates during reloads
29
+ for handler in root_logger.handlers[:]:
30
+ root_logger.removeHandler(handler)
31
+
32
+ # Create formatter
33
+ formatter = logging.Formatter(log_format, datefmt="%Y-%m-%d %H:%M:%S")
34
+
35
+ db_handler = DBLogHandler(connection_string, db_name, "logs")
36
+
37
+ # Add database handler
38
+ db_handler.setLevel(log_level)
39
+ db_handler.setFormatter(formatter)
40
+ root_logger.addHandler(db_handler)
41
+
42
+ # Optionally add console handler
43
+ if console:
44
+ console_handler = logging.StreamHandler()
45
+ console_handler.setFormatter(formatter)
46
+ console_handler.setLevel(log_level)
47
+ root_logger.addHandler(console_handler)
48
+
49
+ return root_logger
@@ -0,0 +1,106 @@
1
+ # python
2
+ import os
3
+ import time
4
+ from pathlib import Path
5
+ from uuid import uuid4
6
+
7
+ from pydantic import BaseModel
8
+
9
+ POLL_INTERVAL = 1.0
10
+ TIMEOUT_SEC = 60 * 60 * 72 # adjust
11
+
12
+
13
+ class WatchdogResult(BaseModel):
14
+ job_id: str
15
+ status: str
16
+ returncode: int | None
17
+ stdout: str
18
+ stderr: str
19
+ paths: dict[str, str]
20
+
21
+
22
+ def submit_to_watchdog(
23
+ command: str,
24
+ job_id: str,
25
+ queue_dir: Path = Path.cwd() / "queue",
26
+ env: dict | None = None,
27
+ cwd: str | None = None,
28
+ ) -> WatchdogResult:
29
+ """
30
+ Submit a command to be executed by the file-queue watcher.
31
+ Returns a WatchdogResult with status, exit_code, stdout, stderr, and paths.
32
+ """
33
+ base = queue_dir / job_id
34
+ cmd_file_tmp = base.with_suffix(".cmd.tmp")
35
+ cmd_file = base.with_suffix(".cmd")
36
+ ready_file = base.with_suffix(".ready")
37
+ done_file = base.with_suffix(".done")
38
+ fail_file = base.with_suffix(".fail")
39
+ exit_file = base.with_suffix(".exit")
40
+ out_file = base.with_suffix(".stdout")
41
+ err_file = base.with_suffix(".stderr")
42
+
43
+ spec_lines = []
44
+ if cwd:
45
+ spec_lines.append(f"CWD={cwd}")
46
+ if env:
47
+ for k, v in env.items():
48
+ spec_lines.append(f"ENV[{k}]={v}")
49
+ spec_lines.append("CMD=" + command)
50
+ data = "\n".join(spec_lines) + "\n"
51
+
52
+ queue_dir.mkdir(parents=True, exist_ok=True)
53
+ print(f"[watcher] Submitting {data} to {cmd_file}")
54
+ with open(cmd_file_tmp, "w", encoding="utf-8") as f:
55
+ f.write(data)
56
+ f.flush()
57
+ os.fsync(f.fileno())
58
+
59
+ os.replace(cmd_file_tmp, cmd_file) # atomic
60
+ ready_file.touch() # signal
61
+
62
+ deadline = time.time() + TIMEOUT_SEC
63
+ while time.time() < deadline:
64
+ if done_file.exists() or fail_file.exists():
65
+ break
66
+ time.sleep(POLL_INTERVAL)
67
+
68
+ result = WatchdogResult(
69
+ job_id=job_id,
70
+ status="timeout",
71
+ returncode=None,
72
+ stdout="",
73
+ stderr="",
74
+ paths={
75
+ "cmd": str(cmd_file),
76
+ "ready": str(ready_file),
77
+ "done": str(done_file),
78
+ "fail": str(fail_file),
79
+ "exit": str(exit_file),
80
+ "stdout": str(out_file),
81
+ "stderr": str(err_file),
82
+ },
83
+ )
84
+
85
+ if done_file.exists() or fail_file.exists():
86
+ exit_code: int | None = None
87
+ if exit_file.exists():
88
+ try:
89
+ exit_code = int(exit_file.read_text().strip())
90
+ except Exception:
91
+ exit_code = None
92
+ out = out_file.read_text(errors="replace") if out_file.exists() else ""
93
+ err = err_file.read_text(errors="replace") if err_file.exists() else ""
94
+ ok = done_file.exists() and (exit_code == 0)
95
+
96
+ result.status = "ok" if ok else "failed"
97
+ result.returncode = exit_code
98
+ result.stdout = out
99
+ result.stderr = err
100
+
101
+ return result
102
+
103
+
104
+ if __name__ == "__main__":
105
+ res = submit_to_watchdog("dir", uuid4().hex)
106
+ print(res)
@@ -0,0 +1,213 @@
1
+ from typing import Dict, Any, List
2
+
3
+
4
+ def ui_hide_fields(ui_schema: Dict[str, Any], fields: str | List[str]) -> dict:
5
+ """
6
+ Hides specified fields in the JSON schema.
7
+
8
+ Args:
9
+ ui_schema: The original JSON schema
10
+ fields: List of field names to hide
11
+ """
12
+
13
+ local_fields = [fields] if isinstance(fields, str) else fields
14
+ # Iterate through the fields to hide
15
+ for field in local_fields:
16
+ ui_schema[field] = {"ui:widget": "hidden"}
17
+ return ui_schema
18
+
19
+
20
+ def ui_make_properties_optional(
21
+ json_schema: Dict[str, Any], properties: List[str], option_field: str = "Show More"
22
+ ) -> dict:
23
+ """
24
+ Modifies a JSON schema to make specified properties visible only when an option is enabled.
25
+
26
+ Args:
27
+ cls: The class to which the JSON schema belongs
28
+ json_schema: The original JSON schema
29
+ properties: List of property names to make optional
30
+ option_field: Name of the boolean field that controls visibility
31
+
32
+ Returns:
33
+ Modified JSON schema with dependencies
34
+ """
35
+ # Add the option field
36
+
37
+ if option_field in json_schema["properties"]:
38
+ assert json_schema["properties"][option_field]["type"] == "boolean"
39
+ else:
40
+ json_schema["properties"][option_field] = {
41
+ "type": "boolean",
42
+ "title": option_field,
43
+ "default": False,
44
+ }
45
+
46
+ # Create dependencies object if it doesn't exist
47
+ if "dependencies" not in json_schema:
48
+ json_schema["dependencies"] = {}
49
+
50
+ new_props = {}
51
+ for prop in properties:
52
+ if prop not in json_schema["properties"]:
53
+ raise ValueError(f"Property {prop} is not in json_schema")
54
+ new_props[prop] = json_schema["properties"][prop]
55
+ del json_schema["properties"][prop]
56
+
57
+ # Add dependency rules for each property
58
+ sub_schema = {"properties": {option_field: {"enum": [True]}, **new_props}}
59
+ json_schema["dependencies"][option_field] = {
60
+ "oneOf": [
61
+ {
62
+ "properties": {
63
+ option_field: {"enum": [False]},
64
+ **{prop: {"type": "null"} for prop in properties},
65
+ }
66
+ },
67
+ sub_schema,
68
+ ]
69
+ }
70
+
71
+ return sub_schema
72
+
73
+
74
+ #
75
+ # def ui_make_dependencies_optional(cls, json_schema: Dict[str, Any], option_field_path: List[str],
76
+ # properties: List[str], new_option_field: str = "Show More Details") -> Dict[str, Any]:
77
+ # """
78
+ # Makes properties optional within an already optional section at any nesting level.
79
+ #
80
+ # Args:
81
+ # cls: The class to which the JSON schema belongs
82
+ # json_schema: The original JSON schema
83
+ # option_field_path: List of option field names representing the path to the target subschema
84
+ # properties: List of property names to make optional within the subschema
85
+ # new_option_field: Name of the new boolean field that controls visibility
86
+ #
87
+ # Returns:
88
+ # Modified JSON schema with nested optional properties
89
+ # """
90
+ #
91
+ # # Find the target subschema by traversing the path
92
+ # def find_subschema(schema, path, current_depth=0):
93
+ # if current_depth >= len(path):
94
+ # return schema
95
+ #
96
+ # current_field = path[current_depth]
97
+ #
98
+ # if "dependencies" not in schema or current_field not in schema["dependencies"]:
99
+ # raise ValueError(f"Option field '{current_field}' not found in dependencies at depth {current_depth}")
100
+ #
101
+ # dependency = schema["dependencies"][current_field]
102
+ # if "oneOf" not in dependency or len(dependency["oneOf"]) < 2:
103
+ # raise ValueError(f"Invalid dependency structure for '{current_field}' at depth {current_depth}")
104
+ #
105
+ # # The subschema is the second element in the oneOf array (index 1)
106
+ # subschema = dependency["oneOf"][1]
107
+ #
108
+ # # Continue traversing
109
+ # return find_subschema(subschema, path, current_depth + 1)
110
+ #
111
+ # # Find the target subschema
112
+ # target_schema = find_subschema(json_schema, option_field_path)
113
+ #
114
+ # # Add the new option field to the target schema
115
+ # target_schema["properties"][new_option_field] = {
116
+ # "type": "boolean",
117
+ # "title": new_option_field,
118
+ # "default": False
119
+ # }
120
+ #
121
+ # # Move specified properties to a new dependency within the target schema
122
+ # new_props = {}
123
+ # for prop in properties:
124
+ # if prop not in target_schema["properties"]:
125
+ # raise ValueError(f"Property '{prop}' is not in the target schema")
126
+ # new_props[prop] = target_schema["properties"][prop]
127
+ # del target_schema["properties"][prop]
128
+ #
129
+ # # Create dependencies in the target schema if it doesn't exist
130
+ # if "dependencies" not in target_schema:
131
+ # target_schema["dependencies"] = {}
132
+ #
133
+ # # Add nested dependency
134
+ # nested_sub_schema = {
135
+ # "properties": {
136
+ # new_option_field: {"enum": [True]},
137
+ # **new_props
138
+ # }
139
+ # }
140
+ #
141
+ # target_schema["dependencies"][new_option_field] = {
142
+ # "oneOf": [
143
+ # {
144
+ # "properties": {
145
+ # new_option_field: {"enum": [False]},
146
+ # **{prop: {"type": "null"} for prop in properties}
147
+ # }
148
+ # },
149
+ # nested_sub_schema
150
+ # ]
151
+ # }
152
+ #
153
+ # # The changes are made directly to the target schema which is a reference
154
+ # # to part of json_schema, so we don't need to explicitly update json_schema
155
+ # return json_schema
156
+ #
157
+
158
+
159
+ def ui_make_title(cls, ui_schema: Dict[str, Any], field: str, title: str) -> dict:
160
+ """
161
+ Adds a title to the JSON schema.
162
+
163
+ Args:
164
+ cls: The class to which the JSON schema belongs
165
+ ui_schema: The original ui_schema schema
166
+ title: Title to be added
167
+
168
+ Returns:
169
+ Modified JSON schema with title
170
+ """
171
+ ui_schema[field] = {"ui:title": title}
172
+ return ui_schema
173
+
174
+
175
+ def ui_make_foldable(ui_schema: Dict[str, Any], field: str) -> dict:
176
+ """
177
+ Adds a foldable section to the JSON schema.
178
+
179
+ Args:
180
+ field: The field to be made foldable
181
+ ui_schema: The original ui_schema schema
182
+ Returns:
183
+ Modified ui_schema with foldable section
184
+
185
+ """
186
+ # TODO add checks that the fields actually exist
187
+ if field not in ui_schema:
188
+ ui_schema[field] = {}
189
+ if "ui:options" not in ui_schema:
190
+ ui_schema[field]["ui:options"] = {}
191
+ ui_schema[field]["ui:options"] = {
192
+ "ui:foldable": "true",
193
+ }
194
+ return ui_schema
195
+
196
+
197
+ def ui_line_vector(ui_schema: Dict[str, Any], field: str) -> Dict[str, Any]:
198
+ """
199
+ Modifies the JSON schema to represent a line vector.
200
+
201
+ Args:
202
+ ui_schema: The original JSON schema
203
+
204
+ Returns:
205
+ Modified JSON schema with line vector representation
206
+ """
207
+ # TODO add checks that the fields actually exist and is a list
208
+ if field not in ui_schema:
209
+ ui_schema[field] = {}
210
+ if "ui:options" not in ui_schema:
211
+ ui_schema[field]["ui:options"] = {}
212
+ ui_schema[field]["ui:options"]["ui:field"] = "LineVector"
213
+ return ui_schema