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.
- algomancy_gui/__init__.py +0 -0
- algomancy_gui/admin_page/__init__.py +1 -0
- algomancy_gui/admin_page/admin.py +362 -0
- algomancy_gui/admin_page/sessions.py +57 -0
- algomancy_gui/appconfiguration.py +291 -0
- algomancy_gui/compare_page/__init__.py +1 -0
- algomancy_gui/compare_page/compare.py +360 -0
- algomancy_gui/compare_page/kpicard.py +236 -0
- algomancy_gui/compare_page/scenarioselector.py +99 -0
- algomancy_gui/componentids.py +177 -0
- algomancy_gui/contentregistry.py +167 -0
- algomancy_gui/cqmloader.py +58 -0
- algomancy_gui/data_page/__init__.py +1 -0
- algomancy_gui/data_page/data.py +77 -0
- algomancy_gui/data_page/datamanagementdeletemodal.py +260 -0
- algomancy_gui/data_page/datamanagementderivemodal.py +201 -0
- algomancy_gui/data_page/datamanagementdownloadmodal.py +193 -0
- algomancy_gui/data_page/datamanagementimportmodal.py +438 -0
- algomancy_gui/data_page/datamanagementsavemodal.py +191 -0
- algomancy_gui/data_page/datamanagementtopbar.py +123 -0
- algomancy_gui/data_page/datamanagementuploadmodal.py +366 -0
- algomancy_gui/data_page/dialogcallbacks.py +51 -0
- algomancy_gui/data_page/filenamematcher.py +109 -0
- algomancy_gui/defaultloader.py +36 -0
- algomancy_gui/gui_launcher.py +183 -0
- algomancy_gui/home_page/__init__.py +1 -0
- algomancy_gui/home_page/home.py +16 -0
- algomancy_gui/layout.py +199 -0
- algomancy_gui/layouthelpers.py +30 -0
- algomancy_gui/managergetters.py +28 -0
- algomancy_gui/overview_page/__init__.py +1 -0
- algomancy_gui/overview_page/overview.py +20 -0
- algomancy_gui/py.typed +0 -0
- algomancy_gui/scenario_page/__init__.py +0 -0
- algomancy_gui/scenario_page/delete_confirmation.py +29 -0
- algomancy_gui/scenario_page/new_scenario_creator.py +104 -0
- algomancy_gui/scenario_page/new_scenario_parameters_window.py +154 -0
- algomancy_gui/scenario_page/scenario_badge.py +36 -0
- algomancy_gui/scenario_page/scenario_cards.py +119 -0
- algomancy_gui/scenario_page/scenarios.py +596 -0
- algomancy_gui/sessionmanager.py +168 -0
- algomancy_gui/settingsmanager.py +43 -0
- algomancy_gui/stylingconfigurator.py +740 -0
- algomancy_gui-0.3.16.dist-info/METADATA +71 -0
- algomancy_gui-0.3.16.dist-info/RECORD +46 -0
- 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
|
+
#
|