multimodalsim-viewer 0.0.1__py3-none-any.whl → 0.0.2__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 (32) hide show
  1. multimodalsim_viewer/common/__init__.py +0 -0
  2. multimodalsim_viewer/common/environments/.env +4 -0
  3. multimodalsim_viewer/common/utils.py +223 -0
  4. multimodalsim_viewer/server/http_routes.py +135 -125
  5. multimodalsim_viewer/server/log_manager.py +10 -15
  6. multimodalsim_viewer/server/scripts.py +106 -32
  7. multimodalsim_viewer/server/server.py +196 -210
  8. multimodalsim_viewer/server/simulation.py +167 -154
  9. multimodalsim_viewer/server/simulation_manager.py +570 -607
  10. multimodalsim_viewer/server/simulation_visualization_data_collector.py +729 -756
  11. multimodalsim_viewer/server/simulation_visualization_data_model.py +1552 -1693
  12. multimodalsim_viewer/ui/angular_app.py +40 -0
  13. multimodalsim_viewer/ui/static/chunk-6VAXIXEZ.js +7 -0
  14. multimodalsim_viewer/ui/static/{chunk-MTC2LSCT.js → chunk-IGIP6IOU.js} +1 -1
  15. multimodalsim_viewer/ui/static/environment.json +7 -0
  16. multimodalsim_viewer/ui/static/index.html +16 -15
  17. multimodalsim_viewer/ui/static/{main-X7OVCS3N.js → main-FGMGJ32M.js} +3648 -3648
  18. multimodalsim_viewer/ui/static/polyfills-FFHMD2TL.js +2 -2
  19. multimodalsim_viewer/ui/static/scripts/load-environment.script.js +20 -0
  20. multimodalsim_viewer/ui/static/styles-KU7LTPET.css +1 -1
  21. multimodalsim_viewer-0.0.2.dist-info/METADATA +70 -0
  22. multimodalsim_viewer-0.0.2.dist-info/RECORD +41 -0
  23. {multimodalsim_viewer-0.0.1.dist-info → multimodalsim_viewer-0.0.2.dist-info}/WHEEL +1 -1
  24. multimodalsim_viewer-0.0.2.dist-info/entry_points.txt +2 -0
  25. multimodalsim_viewer/server/server_utils.py +0 -129
  26. multimodalsim_viewer/ui/cli.py +0 -45
  27. multimodalsim_viewer/ui/server.py +0 -44
  28. multimodalsim_viewer/ui/static/chunk-U5CGW4P4.js +0 -7
  29. multimodalsim_viewer-0.0.1.dist-info/METADATA +0 -21
  30. multimodalsim_viewer-0.0.1.dist-info/RECORD +0 -38
  31. multimodalsim_viewer-0.0.1.dist-info/entry_points.txt +0 -8
  32. {multimodalsim_viewer-0.0.1.dist-info → multimodalsim_viewer-0.0.2.dist-info}/top_level.txt +0 -0
File without changes
@@ -0,0 +1,4 @@
1
+ CLIENT_PORT=8085
2
+ SERVER_PORT=8089
3
+ SIMULATION_SAVE_FILE_SEPARATOR=---
4
+ INPUT_DATA_DIRECTORY_PATH=data
@@ -0,0 +1,223 @@
1
+ import datetime
2
+ import logging
3
+ import os
4
+ import shutil
5
+ import threading
6
+ from enum import Enum
7
+ from json import dumps
8
+
9
+ from dotenv import dotenv_values
10
+ from filelock import FileLock
11
+ from flask import request
12
+ from flask_socketio import emit
13
+
14
+ environment = {}
15
+
16
+
17
+ def load_environment() -> None:
18
+ # Copy .env if it exists
19
+ current_directory = os.path.dirname(os.path.abspath(__file__))
20
+ default_environment_path = os.path.join(current_directory, "../../../.env")
21
+ environment_path = os.path.join(current_directory, "environments/.env")
22
+
23
+ if os.path.exists(default_environment_path):
24
+ shutil.copy(default_environment_path, environment_path)
25
+
26
+ # Load environment variables from .env
27
+ def load_environment_file(path: str, previous_environment: dict) -> None:
28
+ if not os.path.exists(path):
29
+ return
30
+
31
+ values = dotenv_values(path)
32
+ for key in values:
33
+ previous_environment[key] = values[key]
34
+
35
+ # Load default environment
36
+ load_environment_file(environment_path, environment)
37
+ # Load environment from the current working directory
38
+ load_environment_file(os.path.join(os.getcwd(), ".env"), environment)
39
+
40
+ # Get host from docker if available and set it in environment
41
+ environment["HOST"] = os.getenv("HOST", "127.0.0.1")
42
+
43
+ # Write environment into static folder
44
+ static_environment_path = os.path.join(current_directory, "../ui/static/environment.json")
45
+ lock = FileLock(f"{static_environment_path}.lock")
46
+ with lock:
47
+ with open(static_environment_path, "w", encoding="utf-8") as static_environment_file:
48
+ static_environment_file.write(dumps(environment, indent=2, separators=(",", ": "), sort_keys=True))
49
+
50
+
51
+ class _Environment:
52
+ is_environment_loaded = False
53
+
54
+ def __init__(self):
55
+ if _Environment.is_environment_loaded:
56
+ return
57
+
58
+ load_environment()
59
+ _Environment.is_environment_loaded = True
60
+ print(f"Environment loaded {environment}")
61
+
62
+ @property
63
+ def server_port(self) -> int:
64
+ return int(environment.get("SERVER_PORT"))
65
+
66
+ @property
67
+ def client_port(self) -> int:
68
+ return int(environment.get("CLIENT_PORT"))
69
+
70
+ @property
71
+ def host(self) -> str:
72
+ return environment.get("HOST")
73
+
74
+ @property
75
+ def simulation_save_file_separator(self) -> str:
76
+ return environment.get("SIMULATION_SAVE_FILE_SEPARATOR")
77
+
78
+ @property
79
+ def input_data_directory_path(self) -> str:
80
+ return environment.get("INPUT_DATA_DIRECTORY_PATH")
81
+
82
+
83
+ _environment = _Environment()
84
+ SERVER_PORT = _environment.server_port
85
+ CLIENT_PORT = _environment.client_port
86
+ HOST = _environment.host
87
+ SIMULATION_SAVE_FILE_SEPARATOR = _environment.simulation_save_file_separator
88
+ INPUT_DATA_DIRECTORY_PATH = _environment.input_data_directory_path
89
+
90
+
91
+ CLIENT_ROOM = "client"
92
+ SIMULATION_ROOM = "simulation"
93
+ SCRIPT_ROOM = "script"
94
+
95
+ # Save the state of the simulation every STATE_SAVE_STEP events
96
+ STATE_SAVE_STEP = 1000
97
+
98
+ # If the version is identical, the save file can be loaded
99
+ SAVE_VERSION = 9
100
+
101
+
102
+ class SimulationStatus(Enum):
103
+ STARTING = "starting"
104
+ PAUSED = "paused"
105
+ RUNNING = "running"
106
+ STOPPING = "stopping"
107
+ COMPLETED = "completed"
108
+ LOST = "lost"
109
+ CORRUPTED = "corrupted"
110
+ OUTDATED = "outdated"
111
+ FUTURE = "future"
112
+
113
+
114
+ RUNNING_SIMULATION_STATUSES = [
115
+ SimulationStatus.STARTING,
116
+ SimulationStatus.RUNNING,
117
+ SimulationStatus.PAUSED,
118
+ SimulationStatus.STOPPING,
119
+ SimulationStatus.LOST,
120
+ ]
121
+
122
+
123
+ def get_session_id():
124
+ return request.sid
125
+
126
+
127
+ def build_simulation_id(name: str) -> tuple[str, str]:
128
+ # Get the current time
129
+ start_time = datetime.datetime.now().strftime("%Y%m%d-%H%M%S%f")
130
+ # Remove microseconds
131
+ start_time = start_time[:-3]
132
+
133
+ # Start time first to sort easily
134
+ simulation_id = f"{start_time}{SIMULATION_SAVE_FILE_SEPARATOR}{name}"
135
+ return simulation_id, start_time
136
+
137
+
138
+ def get_data_directory_path() -> str:
139
+ current_file_path = os.path.abspath(__file__)
140
+ current_file_dir = os.path.dirname(current_file_path)
141
+ data_directory_path = os.path.join(current_file_dir, "..", "data")
142
+
143
+ if not os.path.exists(data_directory_path):
144
+ os.makedirs(data_directory_path)
145
+
146
+ return data_directory_path
147
+
148
+
149
+ def get_saved_logs_directory_path() -> str:
150
+ data_directory_path = get_data_directory_path()
151
+ saved_logs_directory_path = os.path.join(data_directory_path, "saved_logs")
152
+
153
+ if not os.path.exists(saved_logs_directory_path):
154
+ os.makedirs(saved_logs_directory_path)
155
+
156
+ return saved_logs_directory_path
157
+
158
+
159
+ def get_input_data_directory_path(data: str | None = None) -> str:
160
+ input_data_directory = INPUT_DATA_DIRECTORY_PATH
161
+
162
+ if data is not None:
163
+ input_data_directory = os.path.join(input_data_directory, data)
164
+
165
+ return input_data_directory
166
+
167
+
168
+ def get_available_data():
169
+ input_data_directory = get_input_data_directory_path()
170
+
171
+ if not os.path.exists(input_data_directory):
172
+ return []
173
+
174
+ # List all directories in the input data directory
175
+ return [
176
+ name
177
+ for name in os.listdir(input_data_directory)
178
+ if os.path.isdir(os.path.join(input_data_directory, name)) and not name.startswith(".")
179
+ ]
180
+
181
+
182
+ def log(message: str, auth_type: str, level=logging.INFO, should_emit=True) -> None:
183
+ if auth_type == "server":
184
+ logging.log(level, "[%s] %s", auth_type, message)
185
+ if should_emit:
186
+ emit("log", f"{level} [{auth_type}] {message}", to=CLIENT_ROOM)
187
+ else:
188
+ logging.log(level, "[%s] %s %s", auth_type, get_session_id(), message)
189
+ if should_emit:
190
+ emit(
191
+ "log",
192
+ f"{level} [{auth_type}] {get_session_id()} {message}",
193
+ to=CLIENT_ROOM,
194
+ )
195
+
196
+
197
+ def verify_simulation_name(name: str | None) -> str | None:
198
+ if name is None:
199
+ return "Name is required"
200
+ if len(name) < 3:
201
+ return "Name must be at least 3 characters"
202
+ if len(name) > 50:
203
+ return "Name must be at most 50 characters"
204
+ if name.count(SIMULATION_SAVE_FILE_SEPARATOR) > 0:
205
+ return "Name must not contain three consecutive dashes"
206
+ if any(char in name for char in ["/", "\\", ":", "*", "?", '"', "<", ">", "|"]):
207
+ return (
208
+ 'The name muse not contain characters that might affect the file system (e.g. /, \\, :, *, ?, ", <, >, |)'
209
+ )
210
+ return None
211
+
212
+
213
+ def set_event_on_input(action: str, key: str, event: threading.Event) -> None:
214
+ try:
215
+ user_input = ""
216
+ while user_input != key:
217
+ user_input = input(f"Press {key} to {action}: ")
218
+
219
+ except EOFError:
220
+ pass
221
+
222
+ print(f"Received {key}: {action}")
223
+ event.set()
@@ -1,125 +1,135 @@
1
- import os
2
- import shutil
3
- import zipfile
4
- from flask import Blueprint, request, jsonify, send_file
5
- import logging
6
- import tempfile
7
- from pathlib import Path
8
-
9
- BASE_DIR = Path(__file__).resolve().parent.parent
10
- data_dir = BASE_DIR / "data"
11
- saved_simulations_dir = Path(__file__).parent / "saved_simulations"
12
-
13
- http_routes = Blueprint("http_routes", __name__)
14
-
15
- # MARK: Zip Management
16
-
17
- def get_unique_folder_name(base_path, folder_name):
18
- counter = 1
19
- original_name = folder_name
20
- while os.path.exists(os.path.join(base_path, folder_name)):
21
- folder_name = f"{original_name}_({counter})"
22
- counter += 1
23
- return folder_name
24
-
25
- def zip_folder(folder_path, zip_name):
26
- if not os.path.isdir(folder_path):
27
- return None
28
-
29
- zip_path = os.path.join(tempfile.gettempdir(), f"{zip_name}.zip")
30
- with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
31
- for root, _, files in os.walk(folder_path):
32
- for file in files:
33
- file_path = os.path.join(root, file)
34
- zipf.write(file_path, os.path.relpath(file_path, folder_path))
35
-
36
- return zip_path
37
-
38
- def handle_zip_upload(folder_path):
39
- parent_dir = os.path.dirname(folder_path)
40
- base_folder_name = os.path.basename(folder_path)
41
-
42
- unique_folder_name = get_unique_folder_name(parent_dir, base_folder_name)
43
- actual_folder_path = os.path.join(parent_dir, unique_folder_name)
44
-
45
- os.makedirs(actual_folder_path, exist_ok=True)
46
-
47
- if 'file' not in request.files:
48
- return jsonify({"error": "No file part"}), 400
49
-
50
- file = request.files['file']
51
- if file.filename == '':
52
- return jsonify({"error": "No selected file"}), 400
53
-
54
- zip_path = os.path.join(tempfile.gettempdir(), file.filename)
55
- file.save(zip_path)
56
-
57
- try:
58
- with zipfile.ZipFile(zip_path, 'r') as zip_ref:
59
- zip_ref.extractall(actual_folder_path)
60
- logging.info(f"Extracted files: {zip_ref.namelist()}")
61
-
62
- os.remove(zip_path)
63
- except zipfile.BadZipFile:
64
- return jsonify({"error": "Invalid ZIP file"}), 400
65
-
66
- response_message = f"Folder '{unique_folder_name}' uploaded successfully"
67
- if unique_folder_name != base_folder_name:
68
- response_message += f" (renamed from '{base_folder_name}')"
69
-
70
- return jsonify({
71
- "message": response_message,
72
- "actual_folder_name": unique_folder_name
73
- }), 201
74
-
75
- # MARK: Input Data Routes
76
- @http_routes.route("/api/input_data/<folder_name>", methods=["GET"])
77
- def export_input_data(folder_name):
78
- folder_path = os.path.join(data_dir, folder_name)
79
- logging.info(f"Requested folder: {folder_path}")
80
-
81
- zip_path = zip_folder(folder_path, folder_name)
82
- if not zip_path:
83
- return jsonify({"error": "Folder not found"}), 404
84
-
85
- return send_file(zip_path, as_attachment=True)
86
-
87
- @http_routes.route("/api/input_data/<folder_name>", methods=["POST"])
88
- def import_input_data(folder_name):
89
- folder_path = os.path.join(data_dir, folder_name)
90
- return handle_zip_upload(folder_path)
91
-
92
- @http_routes.route("/api/input_data/<folder_name>", methods=["DELETE"])
93
- def delete_input_data(folder_name):
94
- folder_path = os.path.join(data_dir, folder_name)
95
- if not os.path.isdir(folder_path):
96
- return jsonify({"error": "Folder not found"}), 404
97
-
98
- shutil.rmtree(folder_path)
99
- return jsonify({"message": f"Folder '{folder_name}' deleted successfully"})
100
-
101
- # MARK: Saved Simulations Routes
102
- @http_routes.route("/api/simulation/<folder_name>", methods=["GET"])
103
- def export_saved_simulation(folder_name):
104
- folder_path = os.path.join(saved_simulations_dir, folder_name)
105
- logging.info(f"Requested folder: {folder_path}")
106
-
107
- zip_path = zip_folder(folder_path, folder_name)
108
- if not zip_path:
109
- return jsonify({"error": "Folder not found"}), 404
110
-
111
- return send_file(zip_path, as_attachment=True)
112
-
113
- @http_routes.route("/api/simulation/<folder_name>", methods=["POST"])
114
- def import_saved_simulation(folder_name):
115
- folder_path = os.path.join(saved_simulations_dir, folder_name)
116
- return handle_zip_upload(folder_path)
117
-
118
- @http_routes.route("/api/simulation/<folder_name>", methods=["DELETE"])
119
- def delete_saved_simulation(folder_name):
120
- folder_path = os.path.join(saved_simulations_dir, folder_name)
121
- if not os.path.isdir(folder_path):
122
- return jsonify({"error": "Folder not found"}), 404
123
-
124
- shutil.rmtree(folder_path)
125
- return jsonify({"message": f"Folder '{folder_name}' deleted successfully"})
1
+ import logging
2
+ import os
3
+ import shutil
4
+ import tempfile
5
+ import zipfile
6
+
7
+ from flask import Blueprint, jsonify, request, send_file
8
+
9
+ from multimodalsim_viewer.common.utils import get_input_data_directory_path
10
+ from multimodalsim_viewer.server.simulation_visualization_data_model import (
11
+ SimulationVisualizationDataManager,
12
+ )
13
+
14
+ http_routes = Blueprint("http_routes", __name__)
15
+
16
+ # MARK: Zip Management
17
+
18
+
19
+ def get_unique_folder_name(base_path, folder_name):
20
+ counter = 1
21
+ original_name = folder_name
22
+ while os.path.exists(os.path.join(base_path, folder_name)):
23
+ folder_name = f"{original_name}_({counter})"
24
+ counter += 1
25
+ return folder_name
26
+
27
+
28
+ def zip_folder(folder_path, zip_name):
29
+ if not os.path.isdir(folder_path):
30
+ return None
31
+
32
+ zip_path = os.path.join(tempfile.gettempdir(), f"{zip_name}.zip")
33
+ with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zip_file:
34
+ for root, _, files in os.walk(folder_path):
35
+ for file in files:
36
+ file_path = os.path.join(root, file)
37
+ zip_file.write(file_path, os.path.relpath(file_path, folder_path))
38
+
39
+ return zip_path
40
+
41
+
42
+ def handle_zip_upload(folder_path):
43
+ parent_dir = os.path.dirname(folder_path)
44
+ base_folder_name = os.path.basename(folder_path)
45
+
46
+ unique_folder_name = get_unique_folder_name(parent_dir, base_folder_name)
47
+ actual_folder_path = os.path.join(parent_dir, unique_folder_name)
48
+
49
+ os.makedirs(actual_folder_path, exist_ok=True)
50
+
51
+ if "file" not in request.files:
52
+ return jsonify({"error": "No file part"}), 400
53
+
54
+ file = request.files["file"]
55
+ if file.filename == "":
56
+ return jsonify({"error": "No selected file"}), 400
57
+
58
+ zip_path = os.path.join(tempfile.gettempdir(), file.filename)
59
+ file.save(zip_path)
60
+
61
+ try:
62
+ with zipfile.ZipFile(zip_path, "r") as zip_ref:
63
+ zip_ref.extractall(actual_folder_path)
64
+ logging.info("Extracted files: %s", zip_ref.namelist())
65
+
66
+ os.remove(zip_path)
67
+ except zipfile.BadZipFile:
68
+ return jsonify({"error": "Invalid ZIP file"}), 400
69
+
70
+ response_message = f"Folder '{unique_folder_name}' uploaded successfully"
71
+ if unique_folder_name != base_folder_name:
72
+ response_message += f" (renamed from '{base_folder_name}')"
73
+
74
+ return (
75
+ jsonify({"message": response_message, "actual_folder_name": unique_folder_name}),
76
+ 201,
77
+ )
78
+
79
+
80
+ # MARK: Input Data Routes
81
+ @http_routes.route("/api/input_data/<folder_name>", methods=["GET"])
82
+ def export_input_data(folder_name):
83
+ folder_path = get_input_data_directory_path(folder_name)
84
+ logging.info("Requested folder: %s", folder_path)
85
+
86
+ zip_path = zip_folder(folder_path, folder_name)
87
+ if not zip_path:
88
+ return jsonify({"error": "Folder not found"}), 404
89
+
90
+ return send_file(zip_path, as_attachment=True)
91
+
92
+
93
+ @http_routes.route("/api/input_data/<folder_name>", methods=["POST"])
94
+ def import_input_data(folder_name):
95
+ folder_path = get_input_data_directory_path(folder_name)
96
+ return handle_zip_upload(folder_path)
97
+
98
+
99
+ @http_routes.route("/api/input_data/<folder_name>", methods=["DELETE"])
100
+ def delete_input_data(folder_name):
101
+ folder_path = get_input_data_directory_path(folder_name)
102
+ if not os.path.isdir(folder_path):
103
+ return jsonify({"error": "Folder not found"}), 404
104
+
105
+ shutil.rmtree(folder_path)
106
+ return jsonify({"message": f"Folder '{folder_name}' deleted successfully"})
107
+
108
+
109
+ # MARK: Saved Simulations Routes
110
+ @http_routes.route("/api/simulation/<folder_name>", methods=["GET"])
111
+ def export_saved_simulation(folder_name):
112
+ folder_path = SimulationVisualizationDataManager.get_saved_simulation_directory_path(folder_name)
113
+ logging.info("Requested folder: %s", folder_path)
114
+
115
+ zip_path = zip_folder(folder_path, folder_name)
116
+ if not zip_path:
117
+ return jsonify({"error": "Folder not found"}), 404
118
+
119
+ return send_file(zip_path, as_attachment=True)
120
+
121
+
122
+ @http_routes.route("/api/simulation/<folder_name>", methods=["POST"])
123
+ def import_saved_simulation(folder_name):
124
+ folder_path = SimulationVisualizationDataManager.get_saved_simulation_directory_path(folder_name)
125
+ return handle_zip_upload(folder_path)
126
+
127
+
128
+ @http_routes.route("/api/simulation/<folder_name>", methods=["DELETE"])
129
+ def delete_saved_simulation(folder_name):
130
+ folder_path = SimulationVisualizationDataManager.get_saved_simulation_directory_path(folder_name)
131
+ if not os.path.isdir(folder_path):
132
+ return jsonify({"error": "Folder not found"}), 404
133
+
134
+ shutil.rmtree(folder_path)
135
+ return jsonify({"message": f"Folder '{folder_name}' deleted successfully"})
@@ -1,15 +1,10 @@
1
- import os
2
-
3
-
4
- def register_log(simulation_id, message):
5
- current_directory = os.path.dirname(os.path.abspath(__file__))
6
- log_directory_name = "saved_logs"
7
- log_directory_path = f"{current_directory}/{log_directory_name}"
8
- file_name = f"{simulation_id}.txt"
9
- file_path = f"{log_directory_path}/{file_name}"
10
-
11
- if not os.path.exists(log_directory_path):
12
- os.makedirs(log_directory_path)
13
-
14
- with open(file_path, "a") as file:
15
- file.write(message + "\n")
1
+ from multimodalsim_viewer.common.utils import get_saved_logs_directory_path
2
+
3
+
4
+ def register_log(simulation_id, message):
5
+ saved_logs_directory_path = get_saved_logs_directory_path()
6
+ file_name = f"{simulation_id}.txt"
7
+ file_path = f"{saved_logs_directory_path}/{file_name}"
8
+
9
+ with open(file_path, "a", encoding="utf-8") as file:
10
+ file.write(message + "\n")