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