algomancy-gui 0.3.16__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 (46) hide show
  1. algomancy_gui/__init__.py +0 -0
  2. algomancy_gui/admin_page/__init__.py +1 -0
  3. algomancy_gui/admin_page/admin.py +362 -0
  4. algomancy_gui/admin_page/sessions.py +57 -0
  5. algomancy_gui/appconfiguration.py +291 -0
  6. algomancy_gui/compare_page/__init__.py +1 -0
  7. algomancy_gui/compare_page/compare.py +360 -0
  8. algomancy_gui/compare_page/kpicard.py +236 -0
  9. algomancy_gui/compare_page/scenarioselector.py +99 -0
  10. algomancy_gui/componentids.py +177 -0
  11. algomancy_gui/contentregistry.py +167 -0
  12. algomancy_gui/cqmloader.py +58 -0
  13. algomancy_gui/data_page/__init__.py +1 -0
  14. algomancy_gui/data_page/data.py +77 -0
  15. algomancy_gui/data_page/datamanagementdeletemodal.py +260 -0
  16. algomancy_gui/data_page/datamanagementderivemodal.py +201 -0
  17. algomancy_gui/data_page/datamanagementdownloadmodal.py +193 -0
  18. algomancy_gui/data_page/datamanagementimportmodal.py +438 -0
  19. algomancy_gui/data_page/datamanagementsavemodal.py +191 -0
  20. algomancy_gui/data_page/datamanagementtopbar.py +123 -0
  21. algomancy_gui/data_page/datamanagementuploadmodal.py +366 -0
  22. algomancy_gui/data_page/dialogcallbacks.py +51 -0
  23. algomancy_gui/data_page/filenamematcher.py +109 -0
  24. algomancy_gui/defaultloader.py +36 -0
  25. algomancy_gui/gui_launcher.py +183 -0
  26. algomancy_gui/home_page/__init__.py +1 -0
  27. algomancy_gui/home_page/home.py +16 -0
  28. algomancy_gui/layout.py +199 -0
  29. algomancy_gui/layouthelpers.py +30 -0
  30. algomancy_gui/managergetters.py +28 -0
  31. algomancy_gui/overview_page/__init__.py +1 -0
  32. algomancy_gui/overview_page/overview.py +20 -0
  33. algomancy_gui/py.typed +0 -0
  34. algomancy_gui/scenario_page/__init__.py +0 -0
  35. algomancy_gui/scenario_page/delete_confirmation.py +29 -0
  36. algomancy_gui/scenario_page/new_scenario_creator.py +104 -0
  37. algomancy_gui/scenario_page/new_scenario_parameters_window.py +154 -0
  38. algomancy_gui/scenario_page/scenario_badge.py +36 -0
  39. algomancy_gui/scenario_page/scenario_cards.py +119 -0
  40. algomancy_gui/scenario_page/scenarios.py +596 -0
  41. algomancy_gui/sessionmanager.py +168 -0
  42. algomancy_gui/settingsmanager.py +43 -0
  43. algomancy_gui/stylingconfigurator.py +740 -0
  44. algomancy_gui-0.3.16.dist-info/METADATA +71 -0
  45. algomancy_gui-0.3.16.dist-info/RECORD +46 -0
  46. algomancy_gui-0.3.16.dist-info/WHEEL +4 -0
@@ -0,0 +1,193 @@
1
+ from dash.dcc import send_file
2
+ from dash import html, dcc, callback, Output, Input, State, no_update, get_app
3
+ from dash.exceptions import PreventUpdate
4
+
5
+ from ..componentids import (
6
+ DM_DOWNLOAD_MODAL,
7
+ DM_DOWNLOAD_CHECKLIST,
8
+ DM_DOWNLOAD_SUBMIT_BUTTON,
9
+ DM_DOWNLOAD_MODAL_CLOSE_BTN,
10
+ DM_DOWNLOAD_OPEN_BUTTON,
11
+ ACTIVE_SESSION,
12
+ )
13
+
14
+ import dash_bootstrap_components as dbc
15
+ import io
16
+ import zipfile
17
+ import datetime
18
+ import os
19
+ import re
20
+ import uuid
21
+ import tempfile
22
+ import threading
23
+
24
+ from algomancy_scenario import ScenarioManager
25
+ from algomancy_gui.managergetters import get_scenario_manager
26
+
27
+ """
28
+ Modal component for downloading data files into the application.
29
+
30
+ This module provides a modal dialog that allows users to select datasources to download,
31
+ and download the selected data as a zip archive. The downloaded archive contains
32
+ one file for each selected datasource, with the file name based on the datasource name..
33
+ """
34
+
35
+
36
+ def data_management_download_modal(sm: ScenarioManager, themed_styling):
37
+ """
38
+ Creates a modal dialog component for downloading data files.
39
+
40
+ The modal contains a file upload area, a collapsible section for displaying
41
+ file mapping information, an input field for naming the new dataset, and
42
+ an alert area for displaying messages.
43
+
44
+ Args:
45
+ sm: ScenarioManager instance used for data loading operations
46
+
47
+ Returns:
48
+ dbc.Modal: A Dash Bootstrap Components modal dialog
49
+ """
50
+ return dbc.Modal(
51
+ [
52
+ dbc.ModalHeader(dbc.ModalTitle("Download Data"), close_button=False),
53
+ dbc.ModalBody(
54
+ [
55
+ html.Div(
56
+ [
57
+ dbc.Label("Select datasets to download"),
58
+ dbc.Checklist(
59
+ options=[
60
+ {"label": ds, "value": ds}
61
+ for ds in sm.get_data_keys()
62
+ ],
63
+ value=[],
64
+ id=DM_DOWNLOAD_CHECKLIST,
65
+ ),
66
+ ]
67
+ )
68
+ ]
69
+ ),
70
+ dbc.ModalFooter(
71
+ [
72
+ dbc.Button(
73
+ "Download",
74
+ id=DM_DOWNLOAD_SUBMIT_BUTTON,
75
+ class_name="dm-download-modal-confirm-btn",
76
+ ),
77
+ dbc.Button(
78
+ "Close",
79
+ id=DM_DOWNLOAD_MODAL_CLOSE_BTN,
80
+ class_name="dm-download-modal-cancel-btn ms-auto",
81
+ ),
82
+ ]
83
+ ),
84
+ dcc.Download(id="dm-download"), # persistent Download component
85
+ ],
86
+ id=DM_DOWNLOAD_MODAL,
87
+ is_open=False,
88
+ centered=True,
89
+ class_name="themed-modal",
90
+ style=themed_styling,
91
+ keyboard=False,
92
+ backdrop="static",
93
+ )
94
+
95
+
96
+ @callback(
97
+ Output(DM_DOWNLOAD_MODAL, "is_open", allow_duplicate=True),
98
+ Input(DM_DOWNLOAD_OPEN_BUTTON, "n_clicks"),
99
+ State(DM_DOWNLOAD_MODAL, "is_open"),
100
+ prevent_initial_call=True,
101
+ )
102
+ def open_download_modal(n, is_open):
103
+ if n:
104
+ return not is_open
105
+ return is_open
106
+
107
+
108
+ @callback(
109
+ Output(DM_DOWNLOAD_MODAL, "is_open", allow_duplicate=True),
110
+ Input(DM_DOWNLOAD_MODAL_CLOSE_BTN, "n_clicks"),
111
+ State(DM_DOWNLOAD_MODAL, "is_open"),
112
+ prevent_initial_call=True,
113
+ )
114
+ def close_download_modal(n, is_open):
115
+ if n:
116
+ return not is_open
117
+ return is_open
118
+
119
+
120
+ @callback(Output(DM_DOWNLOAD_CHECKLIST, "value"), Input(DM_DOWNLOAD_MODAL, "is_open"))
121
+ def reset_on_close(is_open):
122
+ return [] if not is_open else no_update
123
+
124
+
125
+ def _sanitize_filename(name: str) -> str:
126
+ # keep ascii-safe filename characters only
127
+ return re.sub(r"[^A-Za-z0-9_.-]", "_", name)
128
+
129
+
130
+ @callback(
131
+ Output(DM_DOWNLOAD_MODAL, "is_open"), # will close the modal
132
+ Output("dm-download", "data"), # send file to the persistent Download component
133
+ Input(DM_DOWNLOAD_SUBMIT_BUTTON, "n_clicks"),
134
+ State(DM_DOWNLOAD_CHECKLIST, "value"),
135
+ State(ACTIVE_SESSION, "data"),
136
+ prevent_initial_call=True,
137
+ )
138
+ def download_modal_children(n, selected_keys, session_id: str):
139
+ if not selected_keys:
140
+ # nothing selected -> ignore
141
+ raise PreventUpdate
142
+
143
+ sm: ScenarioManager = get_scenario_manager(get_app().server, session_id)
144
+
145
+ # Build file contents mapping
146
+ files = {}
147
+ if sm.save_type == "json":
148
+ for key in selected_keys:
149
+ name = _sanitize_filename(key) + ".json"
150
+ files[name] = sm.get_data_as_json(key)
151
+ else:
152
+ # unknown save type
153
+ raise PreventUpdate
154
+
155
+ # Create zip in memory
156
+ buffer = io.BytesIO()
157
+ with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as zf:
158
+ for filename, content in files.items():
159
+ if isinstance(content, (bytes, bytearray)):
160
+ zf.writestr(filename, content)
161
+ else:
162
+ # assume str -> encode as utf-8
163
+ zf.writestr(filename, content.encode("utf-8"))
164
+ buffer.seek(0)
165
+
166
+ # create timestamped filename
167
+ ts = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
168
+ zip_filename = f"downloaded-files-{ts}.zip"
169
+
170
+ # Write zip to a temp file (unique)
171
+ uid = uuid.uuid4().hex
172
+ tmp_dir = tempfile.gettempdir()
173
+ tmp_name = f"{uid}-{zip_filename}"
174
+ tmp_path = os.path.join(tmp_dir, tmp_name)
175
+ with open(tmp_path, "wb") as f:
176
+ f.write(buffer.getvalue())
177
+
178
+ # Schedule cleanup after configured delay
179
+ def _cleanup(path):
180
+ try:
181
+ os.remove(path)
182
+ except Exception as e:
183
+ sm.logger.error("Failed to remove temp file")
184
+ sm.logger.log_traceback(e)
185
+ pass
186
+
187
+ cleanup_delay_seconds = 30 # file is stored on server for 30 seconds
188
+ t = threading.Timer(cleanup_delay_seconds, _cleanup, args=(tmp_path,))
189
+ t.daemon = True
190
+ t.start()
191
+
192
+ # Return: close modal, and send the temp file to the user
193
+ return False, send_file(tmp_path, filename=zip_filename)
@@ -0,0 +1,438 @@
1
+ from datetime import datetime
2
+
3
+ import dash
4
+ import dash_bootstrap_components as dbc
5
+ from dash import html, dcc, get_app, callback, Output, Input, no_update, State
6
+
7
+ from algomancy_data import ValidationError, DataManager
8
+ from algomancy_scenario import ScenarioManager
9
+ from .filenamematcher import match_file_names
10
+
11
+ from ..cqmloader import cqm_loader
12
+ from ..defaultloader import default_loader
13
+ from algomancy_gui.managergetters import get_scenario_manager
14
+ from ..settingsmanager import SettingsManager
15
+ from ..componentids import (
16
+ DM_IMPORT_MODAL_CLOSE_BTN,
17
+ DM_IMPORT_MODAL,
18
+ DM_IMPORT_SUBMIT_BUTTON,
19
+ DM_IMPORT_UPLOADER,
20
+ DM_IMPORT_MODAL_FILEVIEWER_COLLAPSE,
21
+ DM_IMPORT_MODAL_FILEVIEWER_CARD,
22
+ DM_IMPORT_MODAL_NAME_INPUT,
23
+ DM_IMPORT_MODAL_FILEVIEWER_ALERT,
24
+ DM_IMPORT_OPEN_BUTTON,
25
+ DM_LIST_UPDATER_STORE,
26
+ DATA_MAN_SUCCESS_ALERT,
27
+ DATA_MAN_ERROR_ALERT,
28
+ ACTIVE_SESSION,
29
+ )
30
+
31
+ """
32
+ Modal component for loading data files into the application.
33
+
34
+ This module provides a modal dialog that allows users to upload CSV files,
35
+ view file mapping information, and create new datasets from the uploaded files.
36
+ """
37
+
38
+
39
+ def data_management_import_modal(sm: ScenarioManager, themed_styling):
40
+ """
41
+ Creates a modal dialog component for loading data files.
42
+
43
+ The modal contains a file upload area, a collapsible section for displaying
44
+ file mapping information, an input field for naming the new dataset, and
45
+ an alert area for displaying messages.
46
+
47
+ Returns:
48
+ dbc.Modal: A Dash Bootstrap Components modal dialog
49
+ """
50
+ settings: SettingsManager = get_app().server.settings
51
+
52
+ if settings.use_cqm_loader:
53
+ spinner = cqm_loader(
54
+ "Importing data..."
55
+ ) # requires letter-c.svg, letter-q.svg and letter-m.svg
56
+ else:
57
+ spinner = default_loader("Importing data...")
58
+
59
+ return dbc.Modal(
60
+ [
61
+ dbc.ModalHeader(dbc.ModalTitle("Import Data"), close_button=False),
62
+ dbc.ModalBody(
63
+ dcc.Loading(
64
+ [
65
+ dcc.Upload(
66
+ id=DM_IMPORT_UPLOADER,
67
+ children=html.Div(
68
+ ["Drag and Drop or ", html.A("Select Files")]
69
+ ),
70
+ style={
71
+ "width": "100%",
72
+ "height": "60px",
73
+ "lineHeight": "60px",
74
+ "borderWidth": "1px",
75
+ "borderStyle": "dashed",
76
+ "borderRadius": "4px",
77
+ "textAlign": "center",
78
+ },
79
+ multiple=True, # Allow only single file upload
80
+ ),
81
+ dbc.Collapse(
82
+ children=[
83
+ dbc.Card(
84
+ dbc.CardBody(id=DM_IMPORT_MODAL_FILEVIEWER_CARD),
85
+ className="uploaded-files-card",
86
+ ),
87
+ dbc.Input(
88
+ id=DM_IMPORT_MODAL_NAME_INPUT,
89
+ placeholder="Name of new dataset",
90
+ class_name="mt-2",
91
+ ),
92
+ ],
93
+ id=DM_IMPORT_MODAL_FILEVIEWER_COLLAPSE,
94
+ is_open=False,
95
+ class_name="mt-2",
96
+ ),
97
+ dbc.Alert(
98
+ id=DM_IMPORT_MODAL_FILEVIEWER_ALERT,
99
+ color="danger",
100
+ is_open=False,
101
+ dismissable=True,
102
+ duration=4000,
103
+ class_name="mt-2",
104
+ ),
105
+ dcc.Store(id="dm-import-modal-dummy-store", data=""),
106
+ ],
107
+ overlay_style={
108
+ "visibility": "visible",
109
+ "opacity": 0.5,
110
+ "backgroundColor": "white",
111
+ },
112
+ custom_spinner=spinner,
113
+ delay_hide=50,
114
+ delay_show=50,
115
+ )
116
+ ),
117
+ dbc.ModalFooter(
118
+ [
119
+ dbc.Button(
120
+ "Import",
121
+ id=DM_IMPORT_SUBMIT_BUTTON,
122
+ class_name="dm-import-modal-confirm-btn",
123
+ ),
124
+ dbc.Button(
125
+ "Close",
126
+ id=DM_IMPORT_MODAL_CLOSE_BTN,
127
+ class_name="dm-import-modal-cancel-btn ms-auto",
128
+ ),
129
+ ]
130
+ ),
131
+ ],
132
+ id=DM_IMPORT_MODAL,
133
+ is_open=False,
134
+ centered=True,
135
+ class_name="themed-modal",
136
+ style=themed_styling,
137
+ keyboard=False,
138
+ backdrop="static",
139
+ )
140
+
141
+
142
+ @callback(
143
+ Output(DM_IMPORT_MODAL, "is_open"),
144
+ [
145
+ Input(DM_IMPORT_OPEN_BUTTON, "n_clicks"),
146
+ Input(DM_IMPORT_MODAL_CLOSE_BTN, "n_clicks"),
147
+ ],
148
+ [dash.dependencies.State(DM_IMPORT_MODAL, "is_open")],
149
+ )
150
+ def toggle_modal_load(open_clicks, close_clicks, is_open):
151
+ """
152
+ Toggles the visibility of the load modal dialog.
153
+
154
+ Opens the modal when the open button is clicked and closes it when
155
+ the close button is clicked.
156
+
157
+ Args:
158
+ open_clicks: Number of times the open button has been clicked
159
+ close_clicks: Number of times the close button has been clicked
160
+ is_open: Current state of the modal (open or closed)
161
+
162
+ Returns:
163
+ bool: New state for the modal
164
+ """
165
+ if open_clicks or close_clicks:
166
+ return not is_open
167
+ return is_open
168
+
169
+
170
+ def render_file_mapping_table(mapping):
171
+ """
172
+ Creates a Dash html.Div containing a table visualizing the mapping between
173
+ InputFileConfiguration file names and selected real file names.
174
+
175
+ Parameters:
176
+ mapping (dict, optional): Optionally allow passing in a mapping if already known.
177
+
178
+ Returns:
179
+ html.Div: a Div containing the table
180
+ """
181
+
182
+ # Compose table header
183
+ header = [html.Tr([html.Th("Expected"), html.Th("Found")])]
184
+
185
+ # Compose table rows
186
+ rows = []
187
+ for expected, found in mapping.items():
188
+ rows.append(html.Tr([html.Td(expected), html.Td(found)]))
189
+
190
+ table = html.Table(
191
+ header + rows,
192
+ style={
193
+ "width": "100%",
194
+ "borderCollapse": "separate", # More space than "collapse"
195
+ "border": "none", # No border on the table
196
+ "borderSpacing": "10px 6px", # Horizontal and vertical spacing between cells
197
+ "margin": "8px 0", # Additional space around the table
198
+ },
199
+ )
200
+ return html.Div([html.Strong("File Mapping:"), table])
201
+
202
+
203
+ @callback(
204
+ [
205
+ Output(DM_IMPORT_MODAL_FILEVIEWER_CARD, "children"),
206
+ Output(DM_IMPORT_MODAL_FILEVIEWER_COLLAPSE, "is_open"),
207
+ Output(DM_IMPORT_MODAL_FILEVIEWER_ALERT, "is_open"),
208
+ Output(DM_IMPORT_MODAL_FILEVIEWER_ALERT, "children"),
209
+ ],
210
+ Input(DM_IMPORT_UPLOADER, "filename"),
211
+ State(ACTIVE_SESSION, "data"),
212
+ prevent_initial_call=True,
213
+ )
214
+ def show_uploaded_filename(filename, session_id: str):
215
+ """
216
+ Displays information about uploaded files in the load modal.
217
+
218
+ Attempts to match uploaded filenames with expected file configurations and
219
+ displays a mapping table or an error message if matching fails.
220
+
221
+ Args:
222
+ filename: String or list of strings containing uploaded filenames
223
+ session_id: ID of the active session
224
+
225
+ Returns:
226
+ tuple: (card_children, collapse_is_open, alert_is_open, alert_message) where:
227
+ - card_children: HTML content showing file mapping
228
+ - collapse_is_open: Boolean indicating if the file viewer should be visible
229
+ - alert_is_open: Boolean indicating if an alert should be shown
230
+ - alert_message: Text message for the alert
231
+ """
232
+ if not filename:
233
+ return no_update, False, False, ""
234
+
235
+ sm: ScenarioManager = get_scenario_manager(get_app().server, session_id)
236
+
237
+ # Allow for possible list/file array
238
+ if isinstance(filename, list):
239
+ filenames = filename
240
+ else:
241
+ filenames = [filename]
242
+
243
+ from .filenamematcher import match_file_names
244
+
245
+ try:
246
+ mapping = match_file_names(sm.input_configurations, filenames)
247
+ except Exception as e:
248
+ sm.logger.error(f"Problem with loading: {str(e)}")
249
+ sm.logger.log_traceback(e)
250
+ return (
251
+ no_update,
252
+ False,
253
+ True,
254
+ "Could not match files uniquely. Close and try again",
255
+ )
256
+
257
+ return html.Div([render_file_mapping_table(mapping)]), True, False, ""
258
+
259
+
260
+ @callback(
261
+ [
262
+ Output(DM_LIST_UPDATER_STORE, "data", allow_duplicate=True),
263
+ Output(DM_IMPORT_MODAL, "is_open", allow_duplicate=True),
264
+ Output(DATA_MAN_SUCCESS_ALERT, "children", allow_duplicate=True),
265
+ Output(DATA_MAN_SUCCESS_ALERT, "is_open", allow_duplicate=True),
266
+ Output(DATA_MAN_ERROR_ALERT, "children", allow_duplicate=True),
267
+ Output(DATA_MAN_ERROR_ALERT, "is_open", allow_duplicate=True),
268
+ Output("dm-import-modal-dummy-store", "data", allow_duplicate=True),
269
+ ],
270
+ [
271
+ Input(DM_IMPORT_SUBMIT_BUTTON, "n_clicks"),
272
+ ],
273
+ [
274
+ State(DM_IMPORT_UPLOADER, "contents"),
275
+ State(DM_IMPORT_UPLOADER, "filename"),
276
+ State(DM_IMPORT_MODAL_NAME_INPUT, "value"),
277
+ State(ACTIVE_SESSION, "data"),
278
+ ],
279
+ prevent_initial_call=True,
280
+ )
281
+ def process_imports(n_clicks, contents, filenames, dataset_name, session_id: str):
282
+ """
283
+ Processes uploaded files when the import submit button is clicked.
284
+
285
+ Args:
286
+ n_clicks: Number of times the submit button has been clicked
287
+ contents: Base64-encoded contents of the uploaded files
288
+ filenames: Names of the uploaded files
289
+ dataset_name: Name for the new dataset
290
+ session_id: ID of the active session
291
+
292
+ Returns:
293
+ Tuple containing updated dropdown options, modal state, and alert messages
294
+ """
295
+ # Guard clause for empty inputs
296
+ if not n_clicks or not contents or not filenames or not dataset_name:
297
+ return no_update, no_update, "", False, "", False, ""
298
+
299
+ # Get scenario manager from app context
300
+ sm: ScenarioManager = get_scenario_manager(get_app().server, session_id)
301
+
302
+ try:
303
+ sm.log(f"Loading {filenames} into {dataset_name}")
304
+
305
+ # Process the files
306
+ files = prepare_files_from_upload(sm, filenames, contents)
307
+
308
+ # Load the data
309
+ sm.etl_data(files, dataset_name)
310
+
311
+ # Return successful response
312
+ return datetime.now(), False, "Data loaded successfully!", True, "", False, ""
313
+
314
+ except ValidationError as e:
315
+ sm.logger.error(f"Validation error: {str(e)}")
316
+ return no_update, False, "", False, f"Validation error: {str(e)}", True, ""
317
+
318
+ except Exception as e:
319
+ sm.logger.error(f"Problem with loading: {str(e)}")
320
+ sm.logger.log_traceback(e)
321
+ return no_update, False, "", False, f"Problem with loading: {str(e)}", True, ""
322
+
323
+
324
+ def prepare_files_from_upload(sm, filenames, contents):
325
+ """
326
+ Prepares file objects from uploaded content.
327
+
328
+ Args:
329
+ sm: Scenario manager instance
330
+ filenames: Names of the uploaded files
331
+ contents: Base64-encoded contents of the uploaded files
332
+
333
+ Returns:
334
+ Dictionary of file objects ready for processing
335
+ """
336
+ # Match uploaded filenames to expected file configurations
337
+ mapping = match_file_names(sm.input_configurations, filenames)
338
+ reverse_mapping = {value: key for key, value in mapping.items()}
339
+
340
+ # Extract file extensions and create content dictionary
341
+ extensions = {
342
+ file_name: file_name.split(".")[-1].lower() for file_name in filenames
343
+ }
344
+ content_dict = dict(zip(filenames, contents))
345
+
346
+ # Prepare file items with content
347
+ file_items = [
348
+ (reverse_mapping[file_name], extensions[file_name], content_dict[file_name])
349
+ for file_name in filenames
350
+ ]
351
+
352
+ # Return prepared files
353
+ return DataManager.prepare_files(file_items_with_content=file_items)
354
+
355
+
356
+ @callback(
357
+ Output(DM_IMPORT_UPLOADER, "content"),
358
+ Output(DM_IMPORT_UPLOADER, "filename"),
359
+ Input(DM_IMPORT_MODAL, "is_open"),
360
+ prevent_initial_call=True,
361
+ )
362
+ def clean_contents_on_close(modal_is_open: bool):
363
+ """
364
+ Clears the uploader contents when the load modal is closed.
365
+
366
+ Args:
367
+ modal_is_open: Boolean indicating if the modal is open
368
+
369
+ Returns:
370
+ tuple: (content, filename) where both are None if the modal is closed,
371
+ or no_update if the modal is open
372
+ """
373
+ if not modal_is_open:
374
+ return None, None
375
+ return no_update, no_update
376
+
377
+
378
+ def create_dropdown_options(sm):
379
+ """
380
+ Creates dropdown options from available data keys.
381
+
382
+ Args:
383
+ sm: Scenario manager instance
384
+
385
+ Returns:
386
+ List of option dictionaries for dropdowns
387
+ """
388
+ return [{"label": ds, "value": ds} for ds in sm.get_data_keys()]
389
+
390
+
391
+ def create_derived_dropdown_options(sm):
392
+ """
393
+ Creates dropdown options for derived datasets only.
394
+
395
+ Args:
396
+ sm: Scenario manager instance
397
+
398
+ Returns:
399
+ List of option dictionaries for derived data dropdowns
400
+ """
401
+ return [
402
+ {"label": ds, "value": ds}
403
+ for ds in sm.get_data_keys()
404
+ if not sm.get_data(ds).is_master_data()
405
+ ]
406
+
407
+
408
+ #
409
+ # def decode_contents(contents):
410
+ # """
411
+ # Decodes the uploaded contents string from dcc.Upload.
412
+ #
413
+ # Parameters:
414
+ # contents (str): The contents string (data URI) from the uploader
415
+ #
416
+ # Returns:
417
+ # tuple: (mime_type, decoded_bytes)
418
+ # """
419
+ # if not contents:
420
+ # return None, None
421
+ #
422
+ # content_type, content_string = contents.split(",", 1)
423
+ # mime_type = content_type.split(";")[0][5:]
424
+ # decoded = base64.b64decode(content_string)
425
+ # return mime_type, decoded
426
+ #
427
+ #
428
+ # def handle_csv_upload(contents):
429
+ # mime_type, decoded = decode_contents(contents)
430
+ # if mime_type == "text/csv":
431
+ # from io import StringIO
432
+ #
433
+ # data_str = decoded.decode("utf-8")
434
+ # df = pd.read_csv(StringIO(data_str))
435
+ # return df
436
+ # else:
437
+ # raise ValueError("Unsupported file type")
438
+ #