multimodalsim-viewer 0.0.1__tar.gz → 0.0.2__tar.gz

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 (58) hide show
  1. multimodalsim_viewer-0.0.2/MANIFEST.in +7 -0
  2. multimodalsim_viewer-0.0.2/PKG-INFO +70 -0
  3. multimodalsim_viewer-0.0.2/README.md +37 -0
  4. multimodalsim_viewer-0.0.2/multimodalsim_viewer/common/environments/.env +4 -0
  5. multimodalsim_viewer-0.0.2/multimodalsim_viewer/common/utils.py +223 -0
  6. {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/server/http_routes.py +135 -125
  7. multimodalsim_viewer-0.0.2/multimodalsim_viewer/server/log_manager.py +10 -0
  8. multimodalsim_viewer-0.0.2/multimodalsim_viewer/server/scripts.py +146 -0
  9. {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/server/server.py +196 -210
  10. {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/server/simulation.py +167 -154
  11. {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/server/simulation_manager.py +570 -607
  12. {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/server/simulation_visualization_data_collector.py +729 -756
  13. {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/server/simulation_visualization_data_model.py +1552 -1693
  14. multimodalsim_viewer-0.0.2/multimodalsim_viewer/ui/__init__.py +0 -0
  15. multimodalsim_viewer-0.0.2/multimodalsim_viewer/ui/angular_app.py +40 -0
  16. multimodalsim_viewer-0.0.2/multimodalsim_viewer/ui/static/chunk-6VAXIXEZ.js +7 -0
  17. multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/chunk-MTC2LSCT.js → multimodalsim_viewer-0.0.2/multimodalsim_viewer/ui/static/chunk-IGIP6IOU.js +1 -1
  18. multimodalsim_viewer-0.0.2/multimodalsim_viewer/ui/static/environment.json +7 -0
  19. {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/ui/static/index.html +16 -15
  20. multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/main-X7OVCS3N.js → multimodalsim_viewer-0.0.2/multimodalsim_viewer/ui/static/main-FGMGJ32M.js +3648 -3648
  21. {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/ui/static/polyfills-FFHMD2TL.js +2 -2
  22. multimodalsim_viewer-0.0.2/multimodalsim_viewer/ui/static/scripts/load-environment.script.js +20 -0
  23. {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/ui/static/styles-KU7LTPET.css +1 -1
  24. multimodalsim_viewer-0.0.2/multimodalsim_viewer.egg-info/PKG-INFO +70 -0
  25. {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer.egg-info/SOURCES.txt +13 -7
  26. multimodalsim_viewer-0.0.2/multimodalsim_viewer.egg-info/entry_points.txt +2 -0
  27. {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer.egg-info/requires.txt +11 -0
  28. multimodalsim_viewer-0.0.2/pyproject.toml +5 -0
  29. {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/setup.cfg +4 -4
  30. multimodalsim_viewer-0.0.2/setup.py +57 -0
  31. multimodalsim_viewer-0.0.1/PKG-INFO +0 -21
  32. multimodalsim_viewer-0.0.1/multimodalsim_viewer/server/log_manager.py +0 -15
  33. multimodalsim_viewer-0.0.1/multimodalsim_viewer/server/scripts.py +0 -72
  34. multimodalsim_viewer-0.0.1/multimodalsim_viewer/server/server_utils.py +0 -129
  35. multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/cli.py +0 -45
  36. multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/server.py +0 -44
  37. multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/chunk-U5CGW4P4.js +0 -7
  38. multimodalsim_viewer-0.0.1/multimodalsim_viewer.egg-info/PKG-INFO +0 -21
  39. multimodalsim_viewer-0.0.1/multimodalsim_viewer.egg-info/entry_points.txt +0 -8
  40. multimodalsim_viewer-0.0.1/setup.py +0 -44
  41. {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/__init__.py +0 -0
  42. {multimodalsim_viewer-0.0.1/multimodalsim_viewer/server → multimodalsim_viewer-0.0.2/multimodalsim_viewer/common}/__init__.py +0 -0
  43. {multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui → multimodalsim_viewer-0.0.2/multimodalsim_viewer/server}/__init__.py +0 -0
  44. {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/ui/static/bitmap-fonts/custom-sans-serif.png +0 -0
  45. {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/ui/static/bitmap-fonts/custom-sans-serif.xml +0 -0
  46. {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/ui/static/favicon.ico +0 -0
  47. {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/ui/static/images/control-bar.png +0 -0
  48. {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/ui/static/images/sample-bus.png +0 -0
  49. {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/ui/static/images/sample-stop.png +0 -0
  50. {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/ui/static/images/sample-wait.png +0 -0
  51. {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/ui/static/images/simulation-control-bar.png +0 -0
  52. {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/ui/static/images/zoom-out-passenger.png +0 -0
  53. {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/ui/static/images/zoom-out-vehicle.png +0 -0
  54. {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/ui/static/media/layers-2x-TBM42ERR.png +0 -0
  55. {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/ui/static/media/layers-55W3Q4RM.png +0 -0
  56. {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer/ui/static/media/marker-icon-2V3QKKVC.png +0 -0
  57. {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer.egg-info/dependency_links.txt +0 -0
  58. {multimodalsim_viewer-0.0.1 → multimodalsim_viewer-0.0.2}/multimodalsim_viewer.egg-info/top_level.txt +0 -0
@@ -0,0 +1,7 @@
1
+ # Include built web application files
2
+ include multimodalsim_viewer/ui/static/*
3
+ include multimodalsim_viewer/ui/static/**/*
4
+
5
+ # Include environment files
6
+ include multimodalsim_viewer/common/environments/*
7
+ include multimodalsim_viewer/common/environments/**/*
@@ -0,0 +1,70 @@
1
+ Metadata-Version: 2.4
2
+ Name: multimodalsim_viewer
3
+ Version: 0.0.2
4
+ Summary: Multimodal simulation viewer
5
+ Keywords: flask angular ui multimodal server
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: flask==3.1.1
9
+ Requires-Dist: flask-socketio==5.5.1
10
+ Requires-Dist: eventlet==0.40.0
11
+ Requires-Dist: websocket-client==1.8.0
12
+ Requires-Dist: filelock==3.18.0
13
+ Requires-Dist: flask_cors==6.0.0
14
+ Requires-Dist: questionary==2.1.0
15
+ Requires-Dist: python-dotenv==1.1.0
16
+ Requires-Dist: multimodalsim==0.0.1
17
+ Requires-Dist: get_latest_version==1.0.3
18
+ Requires-Dist: setuptools==80.9.0
19
+ Provides-Extra: dev
20
+ Requires-Dist: black==25.1.0; extra == "dev"
21
+ Requires-Dist: pylint==3.3.7; extra == "dev"
22
+ Requires-Dist: isort==6.0.1; extra == "dev"
23
+ Provides-Extra: build
24
+ Requires-Dist: build; extra == "build"
25
+ Requires-Dist: twine; extra == "build"
26
+ Dynamic: description
27
+ Dynamic: description-content-type
28
+ Dynamic: keywords
29
+ Dynamic: provides-extra
30
+ Dynamic: requires-dist
31
+ Dynamic: requires-python
32
+ Dynamic: summary
33
+
34
+ # multimodalsim-viewer
35
+
36
+ This package provides an interface to the [multimodalsim simulation project](https://pypi.org/project/multimodalsim/), allowing you to run and visualize simulations easily through a web interface.
37
+
38
+ ## Usage
39
+
40
+ You have access to several commands that will allow you to run the project easily.
41
+
42
+ ```bash
43
+ viewer start
44
+ viewer start --ui # only start the UI side
45
+ viewer start --server # only start the server
46
+
47
+ viewer stop
48
+ viewer stop --ui # only stop the UI side
49
+ viewer stop --server # only stop the server
50
+ ```
51
+
52
+ You can also run a simulation from the command line. This is useful for debugging, when you want to run a simulation without the web interface, and also for running simulations that uses a different version of the multimodalsim package.
53
+
54
+ Several arguments are available to customize the simulation and can be found with the --help option, but the required arguments will be asked interactively if not provided. The command to run a simulation is:
55
+
56
+ ```bash
57
+ viewer simulate
58
+ ```
59
+
60
+ ## `DataCollector`
61
+
62
+ The `SimulationVisualizationDataCollector` class is used to collect data from the simulation and visualize it. You can pass an instance of this class to the simulation to collect data during the simulation. This might be useful if you work on the multimodalsim package and want to visualize the simulation data in real-time.
63
+
64
+ ## Input data
65
+
66
+ To run a simulation, you need to provide input data. You can upload input data folders through the web interface. Some basic input data folders are available [here](https://github.com/lab-core/multimodal-data). You can also clone the repository and use the data from there :
67
+
68
+ ```bash
69
+ git clone https://github.com/lab-core/multimodal-data.git
70
+ ```
@@ -0,0 +1,37 @@
1
+ # multimodalsim-viewer
2
+
3
+ This package provides an interface to the [multimodalsim simulation project](https://pypi.org/project/multimodalsim/), allowing you to run and visualize simulations easily through a web interface.
4
+
5
+ ## Usage
6
+
7
+ You have access to several commands that will allow you to run the project easily.
8
+
9
+ ```bash
10
+ viewer start
11
+ viewer start --ui # only start the UI side
12
+ viewer start --server # only start the server
13
+
14
+ viewer stop
15
+ viewer stop --ui # only stop the UI side
16
+ viewer stop --server # only stop the server
17
+ ```
18
+
19
+ You can also run a simulation from the command line. This is useful for debugging, when you want to run a simulation without the web interface, and also for running simulations that uses a different version of the multimodalsim package.
20
+
21
+ Several arguments are available to customize the simulation and can be found with the --help option, but the required arguments will be asked interactively if not provided. The command to run a simulation is:
22
+
23
+ ```bash
24
+ viewer simulate
25
+ ```
26
+
27
+ ## `DataCollector`
28
+
29
+ The `SimulationVisualizationDataCollector` class is used to collect data from the simulation and visualize it. You can pass an instance of this class to the simulation to collect data during the simulation. This might be useful if you work on the multimodalsim package and want to visualize the simulation data in real-time.
30
+
31
+ ## Input data
32
+
33
+ To run a simulation, you need to provide input data. You can upload input data folders through the web interface. Some basic input data folders are available [here](https://github.com/lab-core/multimodal-data). You can also clone the repository and use the data from there :
34
+
35
+ ```bash
36
+ git clone https://github.com/lab-core/multimodal-data.git
37
+ ```
@@ -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"})
@@ -0,0 +1,10 @@
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")