multimodalsim-viewer 0.1.2.0__py3-none-any.whl → 0.1.3.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.
@@ -9,7 +9,6 @@ from json import dumps
9
9
  from dotenv import dotenv_values
10
10
  from filelock import FileLock
11
11
  from flask import request
12
- from flask_socketio import emit
13
12
 
14
13
  environment = {}
15
14
 
@@ -147,19 +146,11 @@ def build_simulation_id(name: str) -> tuple[str, str]:
147
146
  return simulation_id, start_time
148
147
 
149
148
 
150
- def log(message: str, auth_type: str, level=logging.INFO, should_emit=True) -> None:
149
+ def log(message: str, auth_type: str, level=logging.INFO) -> None:
151
150
  if auth_type == "server":
152
151
  logging.log(level, "[%s] %s", auth_type, message)
153
- if should_emit:
154
- emit("log", f"{level} [{auth_type}] {message}", to=CLIENT_ROOM)
155
152
  else:
156
153
  logging.log(level, "[%s] %s %s", auth_type, get_session_id(), message)
157
- if should_emit:
158
- emit(
159
- "log",
160
- f"{level} [{auth_type}] {get_session_id()} {message}",
161
- to=CLIENT_ROOM,
162
- )
163
154
 
164
155
 
165
156
  def verify_simulation_name(name: str | None) -> str | None:
@@ -499,34 +499,6 @@ class SimulationVisualizationDataManager: # pylint: disable=too-many-public-met
499
499
 
500
500
  return polylines, version
501
501
 
502
- @staticmethod
503
- def get_all_simulation_states(simulation_id: str) -> tuple[list[str], dict[list[str]]]:
504
- states = []
505
- updates = {}
506
-
507
- sorted_states = SimulationVisualizationDataManager.get_sorted_states(simulation_id)
508
-
509
- for update_index, timestamp in sorted_states:
510
- file_path = SimulationVisualizationDataManager.get_saved_simulation_state_file_path(
511
- simulation_id, update_index, timestamp
512
- )
513
-
514
- lock = FileLock(f"{file_path}.lock")
515
-
516
- with lock:
517
- with open(file_path, "r", encoding="utf-8") as file:
518
- state_data = file.readline()
519
- states.append(state_data)
520
-
521
- updates_data = file.readlines()
522
- current_state_updates = []
523
- for update_data in updates_data:
524
- current_state_updates.append(update_data)
525
-
526
- updates[update_index] = current_state_updates
527
-
528
- return states, updates
529
-
530
502
  # MARK: +- Simulation Data
531
503
  @staticmethod
532
504
  def get_output_data_directory_path() -> str:
@@ -3,130 +3,154 @@ import os
3
3
  import shutil
4
4
  import tempfile
5
5
  import zipfile
6
+ from typing import Callable
6
7
 
7
8
  from flask import Blueprint, jsonify, request, send_file
8
9
 
9
10
  from multimodalsim_viewer.server.data_manager import SimulationVisualizationDataManager
11
+ from multimodalsim_viewer.server.simulation_manager import SimulationManager
10
12
 
11
- http_routes = Blueprint("http_routes", __name__)
12
13
 
13
- # MARK: Zip Management
14
+ class InvalidFilesRequestError(Exception):
15
+ def __init__(self, error_message: str) -> None:
16
+ self.error_message = error_message
17
+
18
+
19
+ def http_routes(simulation_manager: SimulationManager): # pylint: disable=too-many-statements
20
+ blueprint = Blueprint("http_routes", __name__)
14
21
 
22
+ # MARK: Helpers
23
+ def get_unique_folder_name(base_path, folder_name):
24
+ counter = 1
25
+ original_name = folder_name
26
+ while os.path.exists(os.path.join(base_path, folder_name)):
27
+ folder_name = f"{original_name}_({counter})"
28
+ counter += 1
29
+ return folder_name
30
+
31
+ def zip_folder(folder_path, zip_name):
32
+ if not os.path.isdir(folder_path):
33
+ return None
34
+
35
+ zip_path = os.path.join(tempfile.gettempdir(), f"{zip_name}.zip")
36
+ with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zip_file:
37
+ for root, _, files in os.walk(folder_path):
38
+ for file in files:
39
+ file_path = os.path.join(root, file)
40
+ zip_file.write(file_path, os.path.relpath(file_path, folder_path))
15
41
 
16
- def get_unique_folder_name(base_path, folder_name):
17
- counter = 1
18
- original_name = folder_name
19
- while os.path.exists(os.path.join(base_path, folder_name)):
20
- folder_name = f"{original_name}_({counter})"
21
- counter += 1
22
- return folder_name
42
+ return zip_path
23
43
 
44
+ def save_and_extract_zip(path: str, files):
45
+ try:
46
+ if "file" not in files:
47
+ raise InvalidFilesRequestError("No file part")
24
48
 
25
- def zip_folder(folder_path, zip_name):
26
- if not os.path.isdir(folder_path):
27
- return None
49
+ file = files["file"]
50
+ if file.filename == "":
51
+ raise InvalidFilesRequestError("No selected file")
28
52
 
29
- zip_path = os.path.join(tempfile.gettempdir(), f"{zip_name}.zip")
30
- with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zip_file:
31
- for root, _, files in os.walk(folder_path):
32
- for file in files:
33
- file_path = os.path.join(root, file)
34
- zip_file.write(file_path, os.path.relpath(file_path, folder_path))
53
+ # Create temporary zip file
54
+ zip_path = os.path.join(tempfile.gettempdir(), file.filename)
55
+ file.save(zip_path)
35
56
 
36
- return zip_path
57
+ # Extract files
58
+ with zipfile.ZipFile(zip_path, "r") as zip_ref:
59
+ zip_ref.extractall(path)
60
+ logging.info("Extracted files: %s", zip_ref.namelist())
37
61
 
62
+ # Let the exception propagate
63
+ finally:
64
+ # Remove temporary zip file
65
+ if os.path.exists(zip_path):
66
+ os.remove(zip_path)
38
67
 
39
- def handle_zip_upload(folder_path):
40
- parent_dir = os.path.dirname(folder_path)
41
- base_folder_name = os.path.basename(folder_path)
68
+ def handle_zip_upload(folder_path, on_success: Callable | None = None):
69
+ parent_dir = os.path.dirname(folder_path)
70
+ base_folder_name = os.path.basename(folder_path)
42
71
 
43
- unique_folder_name = get_unique_folder_name(parent_dir, base_folder_name)
44
- actual_folder_path = os.path.join(parent_dir, unique_folder_name)
72
+ unique_folder_name = get_unique_folder_name(parent_dir, base_folder_name)
73
+ actual_folder_path = os.path.join(parent_dir, unique_folder_name)
45
74
 
46
- os.makedirs(actual_folder_path, exist_ok=True)
75
+ os.makedirs(actual_folder_path, exist_ok=True)
47
76
 
48
- if "file" not in request.files:
49
- return jsonify({"error": "No file part"}), 400
77
+ try:
78
+ save_and_extract_zip(actual_folder_path, request.files)
79
+ except InvalidFilesRequestError as error:
80
+ return jsonify({"error": error.error_message}), 400
81
+ except zipfile.BadZipFile:
82
+ return jsonify({"error": "Invalid ZIP file"}), 400
50
83
 
51
- file = request.files["file"]
52
- if file.filename == "":
53
- return jsonify({"error": "No selected file"}), 400
84
+ response_message = f"Folder '{unique_folder_name}' uploaded successfully"
85
+ if unique_folder_name != base_folder_name:
86
+ response_message += f" (renamed from '{base_folder_name}')"
54
87
 
55
- zip_path = os.path.join(tempfile.gettempdir(), file.filename)
56
- file.save(zip_path)
88
+ if on_success:
89
+ on_success()
57
90
 
58
- try:
59
- with zipfile.ZipFile(zip_path, "r") as zip_ref:
60
- zip_ref.extractall(actual_folder_path)
61
- logging.info("Extracted files: %s", zip_ref.namelist())
91
+ return (
92
+ jsonify({"message": response_message, "actual_folder_name": unique_folder_name}),
93
+ 201,
94
+ )
62
95
 
63
- os.remove(zip_path)
64
- except zipfile.BadZipFile:
65
- return jsonify({"error": "Invalid ZIP file"}), 400
96
+ # MARK: Instances
97
+ @blueprint.route("/api/input_data/<folder_name>", methods=["GET"])
98
+ def export_input_data(folder_name):
99
+ folder_path = SimulationVisualizationDataManager.get_input_data_directory_path(folder_name)
100
+ logging.info("Requested folder: %s", folder_path)
66
101
 
67
- response_message = f"Folder '{unique_folder_name}' uploaded successfully"
68
- if unique_folder_name != base_folder_name:
69
- response_message += f" (renamed from '{base_folder_name}')"
102
+ zip_path = zip_folder(folder_path, folder_name)
103
+ if not zip_path:
104
+ return jsonify({"error": "Folder not found"}), 404
70
105
 
71
- return (
72
- jsonify({"message": response_message, "actual_folder_name": unique_folder_name}),
73
- 201,
74
- )
106
+ return send_file(zip_path, as_attachment=True)
75
107
 
108
+ @blueprint.route("/api/input_data/<folder_name>", methods=["POST"])
109
+ def import_input_data(folder_name):
110
+ folder_path = SimulationVisualizationDataManager.get_input_data_directory_path(folder_name)
111
+ return handle_zip_upload(folder_path)
76
112
 
77
- # MARK: Input Data Routes
78
- @http_routes.route("/api/input_data/<folder_name>", methods=["GET"])
79
- def export_input_data(folder_name):
80
- folder_path = SimulationVisualizationDataManager.get_input_data_directory_path(folder_name)
81
- logging.info("Requested folder: %s", folder_path)
113
+ @blueprint.route("/api/input_data/<folder_name>", methods=["DELETE"])
114
+ def delete_input_data(folder_name):
115
+ folder_path = SimulationVisualizationDataManager.get_input_data_directory_path(folder_name)
116
+ if not os.path.isdir(folder_path):
117
+ return jsonify({"error": "Folder not found"}), 404
82
118
 
83
- zip_path = zip_folder(folder_path, folder_name)
84
- if not zip_path:
85
- return jsonify({"error": "Folder not found"}), 404
119
+ shutil.rmtree(folder_path)
120
+ return jsonify({"message": f"Folder '{folder_name}' deleted successfully"})
86
121
 
87
- return send_file(zip_path, as_attachment=True)
122
+ # MARK: Visualizations
123
+ @blueprint.route("/api/simulation/<folder_name>", methods=["GET"])
124
+ def export_saved_simulation(folder_name):
125
+ folder_path = SimulationVisualizationDataManager.get_saved_simulation_directory_path(folder_name)
126
+ logging.info("Requested folder: %s", folder_path)
88
127
 
128
+ zip_path = zip_folder(folder_path, folder_name)
129
+ if not zip_path:
130
+ return jsonify({"error": "Folder not found"}), 404
89
131
 
90
- @http_routes.route("/api/input_data/<folder_name>", methods=["POST"])
91
- def import_input_data(folder_name):
92
- folder_path = SimulationVisualizationDataManager.get_input_data_directory_path(folder_name)
93
- return handle_zip_upload(folder_path)
132
+ return send_file(zip_path, as_attachment=True)
94
133
 
134
+ @blueprint.route("/api/simulation/<folder_name>", methods=["POST"])
135
+ def import_saved_simulation(folder_name):
136
+ folder_path = SimulationVisualizationDataManager.get_saved_simulation_directory_path(folder_name)
95
137
 
96
- @http_routes.route("/api/input_data/<folder_name>", methods=["DELETE"])
97
- def delete_input_data(folder_name):
98
- folder_path = SimulationVisualizationDataManager.get_input_data_directory_path(folder_name)
99
- if not os.path.isdir(folder_path):
100
- return jsonify({"error": "Folder not found"}), 404
138
+ def on_success():
139
+ simulation_manager.emit_simulation(folder_name)
101
140
 
102
- shutil.rmtree(folder_path)
103
- return jsonify({"message": f"Folder '{folder_name}' deleted successfully"})
141
+ return handle_zip_upload(folder_path, on_success)
104
142
 
143
+ @blueprint.route("/api/simulation/<folder_name>", methods=["DELETE"])
144
+ def delete_saved_simulation(folder_name):
145
+ folder_path = SimulationVisualizationDataManager.get_saved_simulation_directory_path(folder_name)
105
146
 
106
- # MARK: Saved Simulations Routes
107
- @http_routes.route("/api/simulation/<folder_name>", methods=["GET"])
108
- def export_saved_simulation(folder_name):
109
- folder_path = SimulationVisualizationDataManager.get_saved_simulation_directory_path(folder_name)
110
- logging.info("Requested folder: %s", folder_path)
147
+ if not os.path.isdir(folder_path):
148
+ return jsonify({"error": "Folder not found"}), 404
111
149
 
112
- zip_path = zip_folder(folder_path, folder_name)
113
- if not zip_path:
114
- return jsonify({"error": "Folder not found"}), 404
150
+ shutil.rmtree(folder_path)
115
151
 
116
- return send_file(zip_path, as_attachment=True)
152
+ simulation_manager.on_simulation_delete(folder_name)
117
153
 
154
+ return jsonify({"message": f"Folder '{folder_name}' deleted successfully"})
118
155
 
119
- @http_routes.route("/api/simulation/<folder_name>", methods=["POST"])
120
- def import_saved_simulation(folder_name):
121
- folder_path = SimulationVisualizationDataManager.get_saved_simulation_directory_path(folder_name)
122
- return handle_zip_upload(folder_path)
123
-
124
-
125
- @http_routes.route("/api/simulation/<folder_name>", methods=["DELETE"])
126
- def delete_saved_simulation(folder_name):
127
- folder_path = SimulationVisualizationDataManager.get_saved_simulation_directory_path(folder_name)
128
- if not os.path.isdir(folder_path):
129
- return jsonify({"error": "Folder not found"}), 404
130
-
131
- shutil.rmtree(folder_path)
132
- return jsonify({"message": f"Folder '{folder_name}' deleted successfully"})
156
+ return blueprint
@@ -7,11 +7,7 @@ from requests.exceptions import ConnectionError as RequestsConnectionError
7
7
  from socketio import Client
8
8
  from socketio.exceptions import ConnectionError as SocketIOConnectionError
9
9
 
10
- from multimodalsim_viewer.common.utils import (
11
- CLIENT_PORT,
12
- HOST,
13
- SERVER_PORT,
14
- )
10
+ from multimodalsim_viewer.common.utils import CLIENT_PORT, HOST, SERVER_PORT
15
11
  from multimodalsim_viewer.server.server import configure_server
16
12
  from multimodalsim_viewer.server.simulation import (
17
13
  configure_simulation_parser,
@@ -14,17 +14,17 @@ from multimodalsim_viewer.server.simulation_manager import SimulationManager
14
14
  def configure_server() -> tuple[Flask, SocketIO]: # pylint: disable=too-many-statements, too-many-locals
15
15
  app = Flask(__name__)
16
16
 
17
+ socketio = SocketIO(app, cors_allowed_origins="*")
18
+
19
+ simulation_manager = SimulationManager(socketio)
20
+
17
21
  # Register HTTP routes
18
22
  CORS(app)
19
- app.register_blueprint(http_routes)
20
-
21
- socketio = SocketIO(app, cors_allowed_origins="*")
23
+ app.register_blueprint(http_routes(simulation_manager))
22
24
 
23
25
  # key = session id, value = auth type
24
26
  sockets_types_by_session_id = {}
25
27
 
26
- simulation_manager = SimulationManager()
27
-
28
28
  # MARK: Main events
29
29
  @socketio.on("connect")
30
30
  def on_connect(auth):
@@ -69,9 +69,9 @@ def configure_server() -> tuple[Flask, SocketIO]: # pylint: disable=too-many-st
69
69
  simulation_manager.resume_simulation(simulation_id)
70
70
 
71
71
  @socketio.on("get-simulations")
72
- def on_client_get_simulations():
73
- log("getting simulations", "client")
74
- simulation_manager.emit_simulations()
72
+ def on_client_get_simulations(loaded_simulations_ids: list[str]):
73
+ log(f"getting simulations with already loaded ids {loaded_simulations_ids}", "client")
74
+ simulation_manager.emit_simulations(loaded_simulations_ids)
75
75
 
76
76
  @socketio.on("get-available-data")
77
77
  def on_client_get_data():
@@ -136,10 +136,6 @@ def configure_server() -> tuple[Flask, SocketIO]: # pylint: disable=too-many-st
136
136
  log(f"simulation {simulation_id} resumed", "simulation")
137
137
  simulation_manager.on_simulation_resume(simulation_id)
138
138
 
139
- @socketio.on("log")
140
- def on_simulation_log(simulation_id, message):
141
- log(f"simulation {simulation_id}: {message}", "simulation", logging.DEBUG)
142
-
143
139
  @socketio.on("simulation-update-time")
144
140
  def on_simulation_update_time(simulation_id, timestamp):
145
141
  log(