multimodalsim-viewer 0.1.1.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.
@@ -2,5 +2,6 @@ CLIENT_PORT=8085
2
2
  SERVER_PORT=8089
3
3
  SIMULATION_SAVE_FILE_SEPARATOR=---
4
4
  INPUT_DATA_DIRECTORY_PATH=data
5
+ OUTPUT_DATA_DIRECTORY_PATH=output
5
6
  NUMBER_OF_UPDATES_BETWEEN_STATES=1000
6
7
  NUMBER_OF_STATES_TO_SEND_AT_ONCE=1
@@ -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
 
@@ -79,6 +78,10 @@ class _Environment:
79
78
  def input_data_directory_path(self) -> str:
80
79
  return environment.get("INPUT_DATA_DIRECTORY_PATH")
81
80
 
81
+ @property
82
+ def output_data_directory_path(self) -> str:
83
+ return environment.get("OUTPUT_DATA_DIRECTORY_PATH")
84
+
82
85
  @property
83
86
  def number_of_updates_between_states(self) -> int:
84
87
  return int(environment.get("NUMBER_OF_UPDATES_BETWEEN_STATES"))
@@ -94,6 +97,7 @@ CLIENT_PORT = _environment.client_port
94
97
  HOST = _environment.host
95
98
  SIMULATION_SAVE_FILE_SEPARATOR = _environment.simulation_save_file_separator
96
99
  INPUT_DATA_DIRECTORY_PATH = _environment.input_data_directory_path
100
+ OUTPUT_DATA_DIRECTORY_PATH = _environment.output_data_directory_path
97
101
  NUMBER_OF_UPDATES_BETWEEN_STATES = _environment.number_of_updates_between_states
98
102
  NUMBER_OF_STATES_TO_SEND_AT_ONCE = _environment.number_of_states_to_send_at_once
99
103
 
@@ -142,19 +146,11 @@ def build_simulation_id(name: str) -> tuple[str, str]:
142
146
  return simulation_id, start_time
143
147
 
144
148
 
145
- 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:
146
150
  if auth_type == "server":
147
151
  logging.log(level, "[%s] %s", auth_type, message)
148
- if should_emit:
149
- emit("log", f"{level} [{auth_type}] {message}", to=CLIENT_ROOM)
150
152
  else:
151
153
  logging.log(level, "[%s] %s %s", auth_type, get_session_id(), message)
152
- if should_emit:
153
- emit(
154
- "log",
155
- f"{level} [{auth_type}] {get_session_id()} {message}",
156
- to=CLIENT_ROOM,
157
- )
158
154
 
159
155
 
160
156
  def verify_simulation_name(name: str | None) -> str | None:
@@ -7,6 +7,7 @@ from filelock import FileLock
7
7
  from multimodalsim_viewer.common.utils import (
8
8
  INPUT_DATA_DIRECTORY_PATH,
9
9
  NUMBER_OF_STATES_TO_SEND_AT_ONCE,
10
+ OUTPUT_DATA_DIRECTORY_PATH,
10
11
  SIMULATION_SAVE_FILE_SEPARATOR,
11
12
  )
12
13
  from multimodalsim_viewer.models.environment import VisualizedEnvironment
@@ -47,7 +48,7 @@ class SimulationVisualizationDataManager: # pylint: disable=too-many-public-met
47
48
  @staticmethod
48
49
  def get_saved_simulations_directory_path() -> str:
49
50
  directory_path = os.path.join(
50
- SimulationVisualizationDataManager.get_data_directory_path(),
51
+ SimulationVisualizationDataManager.get_output_data_directory_path(),
51
52
  SimulationVisualizationDataManager.__SAVED_SIMULATIONS_DIRECTORY_NAME,
52
53
  )
53
54
 
@@ -498,49 +499,19 @@ class SimulationVisualizationDataManager: # pylint: disable=too-many-public-met
498
499
 
499
500
  return polylines, version
500
501
 
501
- @staticmethod
502
- def get_all_simulation_states(simulation_id: str) -> tuple[list[str], dict[list[str]]]:
503
- states = []
504
- updates = {}
505
-
506
- sorted_states = SimulationVisualizationDataManager.get_sorted_states(simulation_id)
507
-
508
- for update_index, timestamp in sorted_states:
509
- file_path = SimulationVisualizationDataManager.get_saved_simulation_state_file_path(
510
- simulation_id, update_index, timestamp
511
- )
512
-
513
- lock = FileLock(f"{file_path}.lock")
514
-
515
- with lock:
516
- with open(file_path, "r", encoding="utf-8") as file:
517
- state_data = file.readline()
518
- states.append(state_data)
519
-
520
- updates_data = file.readlines()
521
- current_state_updates = []
522
- for update_data in updates_data:
523
- current_state_updates.append(update_data)
524
-
525
- updates[update_index] = current_state_updates
526
-
527
- return states, updates
528
-
529
502
  # MARK: +- Simulation Data
530
503
  @staticmethod
531
- def get_data_directory_path() -> str:
532
- current_file_path = os.path.abspath(__file__)
533
- current_file_dir = os.path.dirname(current_file_path)
534
- data_directory_path = os.path.join(current_file_dir, "..", "data")
504
+ def get_output_data_directory_path() -> str:
505
+ output_data_directory = OUTPUT_DATA_DIRECTORY_PATH
535
506
 
536
- if not os.path.exists(data_directory_path):
537
- os.makedirs(data_directory_path)
507
+ if not os.path.exists(output_data_directory):
508
+ os.makedirs(output_data_directory)
538
509
 
539
- return data_directory_path
510
+ return output_data_directory
540
511
 
541
512
  @staticmethod
542
513
  def get_saved_logs_directory_path() -> str:
543
- data_directory_path = SimulationVisualizationDataManager.get_data_directory_path()
514
+ data_directory_path = SimulationVisualizationDataManager.get_output_data_directory_path()
544
515
  saved_logs_directory_path = os.path.join(data_directory_path, "saved_logs")
545
516
 
546
517
  if not os.path.exists(saved_logs_directory_path):
@@ -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(