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,58 @@
1
+ from dash import html
2
+
3
+
4
+ # todo rename to cqm_loading_animation
5
+ def cqm_loader(text="Loading...", scale: float | None = None):
6
+ """
7
+ Creates a custom CQM animated loader with three letters (C, Q, M) that fade in sequence.
8
+
9
+ Args:
10
+ scale (float): float to control the size of the spinner. min: 0.5, max: 5
11
+ text (str): The text to display below the animation
12
+
13
+ Returns:
14
+ html.Div: A div containing the animated loader
15
+ """
16
+ assert (
17
+ not scale or 0.5 <= scale <= 5
18
+ ), f"Invalid scale for loader: {scale}. (min: 0.5, max: 5)"
19
+
20
+ default_height = 100
21
+ default_width = 100
22
+ default_font_size = 1.5
23
+
24
+ letter_style = {}
25
+ if scale is not None:
26
+ letter_style["height"] = f"{scale * default_height}px"
27
+ letter_style["width"] = f"{scale * default_width}px"
28
+
29
+ text_style = {}
30
+ if scale is not None:
31
+ text_style["font-size"] = f"{scale * default_font_size}rem"
32
+
33
+ return html.Div(
34
+ [
35
+ html.Div(
36
+ [
37
+ html.Img(
38
+ src="/assets/letter-c.svg",
39
+ className="cqm-letter c",
40
+ style=letter_style,
41
+ ),
42
+ html.Img(
43
+ src="/assets/letter-q.svg",
44
+ className="cqm-letter q",
45
+ style=letter_style,
46
+ ),
47
+ html.Img(
48
+ src="/assets/letter-m.svg",
49
+ className="cqm-letter m",
50
+ style=letter_style,
51
+ ),
52
+ ],
53
+ className="cqm-loader",
54
+ ),
55
+ html.Div(text, className="cqm-loader-text", style=text_style),
56
+ ],
57
+ style={"textAlign": "center", "padding": "20px"},
58
+ )
@@ -0,0 +1 @@
1
+ # This file is intentionally left empty to make the directory a Python package.
@@ -0,0 +1,77 @@
1
+ from dash import html, get_app, callback, Output, Input, State
2
+
3
+ from algomancy_scenario import ScenarioManager
4
+
5
+ from ..componentids import (
6
+ DATA_PAGE_CONTENT,
7
+ DATA_SELECTOR_DROPDOWN,
8
+ ACTIVE_SESSION,
9
+ DATA_PAGE,
10
+ )
11
+ from ..data_page.datamanagementtopbar import top_bar
12
+ from algomancy_gui.managergetters import get_scenario_manager
13
+ from ..layouthelpers import create_wrapped_content_div
14
+ from ..contentregistry import ContentRegistry
15
+
16
+ from ..data_page import dialogcallbacks # noqa
17
+
18
+
19
+ def data_page() -> html.Div:
20
+ """
21
+ Creates the data page layout with raw data view and warehouse layout visualization.
22
+
23
+ Returns:
24
+ html.Div: A Dash HTML component representing the data page
25
+ """
26
+ # Placeholder will be filled by callback
27
+ return html.Div(
28
+ id=DATA_PAGE,
29
+ )
30
+
31
+
32
+ @callback(
33
+ Output(DATA_PAGE, "children"),
34
+ Input(ACTIVE_SESSION, "data"),
35
+ )
36
+ def render_data_page(active_session_name):
37
+ """Creates the data page layout with raw data view and warehouse layout visualization."""
38
+ if not active_session_name:
39
+ return html.Div("No active session selected")
40
+
41
+ sm: ScenarioManager = get_scenario_manager(get_app().server, active_session_name)
42
+
43
+ settings = get_app().server.settings
44
+
45
+ main_div = create_wrapped_content_div(
46
+ content_div(),
47
+ settings.show_loading_on_datapage,
48
+ settings.use_cqm_loader,
49
+ )
50
+
51
+ return [html.H1("Data"), top_bar(sm), main_div]
52
+
53
+
54
+ def content_div() -> html.Div:
55
+ return html.Div(
56
+ html.Div(className="data-page-content"), # placeholder
57
+ id=DATA_PAGE_CONTENT,
58
+ )
59
+
60
+
61
+ @callback(
62
+ Output(DATA_PAGE_CONTENT, "children"),
63
+ Input(DATA_SELECTOR_DROPDOWN, "value"),
64
+ State(ACTIVE_SESSION, "data"),
65
+ prevent_initial_call=True,
66
+ )
67
+ def fill_data_page_content(data_key: str, session_id: str):
68
+ sm: ScenarioManager = get_scenario_manager(get_app().server, session_id)
69
+ cr: ContentRegistry = get_app().server.content_registry
70
+
71
+ if data_key not in sm.get_data_keys():
72
+ return [html.P("Select a dataset.")]
73
+
74
+ data = sm.get_data(data_key)
75
+ page = cr.data_content(data)
76
+
77
+ return page
@@ -0,0 +1,260 @@
1
+ from datetime import datetime
2
+
3
+ import dash
4
+ import dash_bootstrap_components as dbc
5
+ from dash import html, dcc, callback, Output, Input, no_update, get_app, State
6
+
7
+ from algomancy_scenario import ScenarioManager
8
+ from ..componentids import (
9
+ DM_DELETE_SET_SELECTOR,
10
+ DM_DELETE_COLLAPSE,
11
+ DM_DELETE_CONFIRM_INPUT,
12
+ DM_DELETE_SUBMIT_BUTTON,
13
+ DM_DELETE_CLOSE_BUTTON,
14
+ DM_DELETE_MODAL,
15
+ DM_LIST_UPDATER_STORE,
16
+ DATA_MAN_SUCCESS_ALERT,
17
+ DATA_MAN_ERROR_ALERT,
18
+ DM_DELETE_OPEN_BUTTON,
19
+ ACTIVE_SESSION,
20
+ )
21
+ from algomancy_gui.managergetters import get_scenario_manager
22
+
23
+ """
24
+ Modal component for deleting datasets from the application.
25
+
26
+ This module provides a modal dialog that allows users to select and delete
27
+ datasets, with additional confirmation required for master data deletion.
28
+ """
29
+
30
+
31
+ def data_management_delete_modal(sm: ScenarioManager, themed_styling):
32
+ """
33
+ Creates a modal dialog component for deleting datasets.
34
+
35
+ The modal contains a dropdown to select the dataset to delete and a confirmation
36
+ input field that appears when master data is selected. The confirmation field
37
+ requires the user to type "DELETE" to proceed with deletion of master data.
38
+
39
+ Args:
40
+ sm: ScenarioManager instance used to populate the dataset dropdown
41
+ themed_styling: Dictionary of CSS styling properties
42
+
43
+ Returns:
44
+ dbc.Modal: A Dash Bootstrap Components modal dialog
45
+ """
46
+ return dbc.Modal(
47
+ [
48
+ dbc.ModalHeader(dbc.ModalTitle("Delete"), close_button=False),
49
+ dbc.ModalBody(
50
+ dbc.Row(
51
+ [
52
+ dbc.Col(
53
+ html.Div(
54
+ html.P("Set to delete: "),
55
+ ),
56
+ width=3,
57
+ className="justify-content-right",
58
+ ),
59
+ dbc.Col(
60
+ [
61
+ dcc.Dropdown(
62
+ id=DM_DELETE_SET_SELECTOR,
63
+ options=[
64
+ {"label": ds, "value": ds}
65
+ for ds in sm.get_data_keys()
66
+ ],
67
+ value="",
68
+ placeholder="Select dataset",
69
+ ),
70
+ ],
71
+ width=9,
72
+ ),
73
+ dbc.Collapse(
74
+ children=[
75
+ html.P(
76
+ "WARNING: you are about to delete master data. "
77
+ "Associated files will be permanently removed.",
78
+ className="mt-2",
79
+ ),
80
+ html.P(
81
+ "Enter DELETE to confirm deletion:",
82
+ ),
83
+ dcc.Input(
84
+ id=DM_DELETE_CONFIRM_INPUT,
85
+ placeholder="DELETE",
86
+ className="mt-2",
87
+ ),
88
+ ],
89
+ id=DM_DELETE_COLLAPSE,
90
+ is_open=False,
91
+ class_name="mt-2",
92
+ ),
93
+ ],
94
+ className="mb-4",
95
+ ),
96
+ ),
97
+ dbc.ModalFooter(
98
+ [
99
+ dbc.Button(
100
+ "Delete",
101
+ id=DM_DELETE_SUBMIT_BUTTON,
102
+ class_name="dm-delete-modal-confirm-btn",
103
+ ),
104
+ dbc.Button(
105
+ "Close",
106
+ id=DM_DELETE_CLOSE_BUTTON,
107
+ class_name="dm-delete-modal-cancel-btn ms-auto",
108
+ n_clicks=0,
109
+ ),
110
+ ]
111
+ ),
112
+ ],
113
+ id=DM_DELETE_MODAL,
114
+ is_open=False,
115
+ centered=True,
116
+ class_name="themed-modal",
117
+ style=themed_styling,
118
+ keyboard=False,
119
+ backdrop="static",
120
+ )
121
+
122
+
123
+ @callback(
124
+ Output(DM_DELETE_SET_SELECTOR, "value"),
125
+ Input(DM_DELETE_MODAL, "is_open"),
126
+ prevent_initial_call=True,
127
+ )
128
+ def reset_on_close(modal_is_open: bool):
129
+ """
130
+ Resets the delete dataset selector when the delete modal is closed.
131
+
132
+ Args:
133
+ modal_is_open: Boolean indicating if the modal is open
134
+
135
+ Returns:
136
+ None if the modal is closed, no_update otherwise
137
+ """
138
+ if not modal_is_open:
139
+ return None
140
+ return no_update
141
+
142
+
143
+ @callback(
144
+ [
145
+ Output(DM_DELETE_COLLAPSE, "is_open"),
146
+ Output(DM_DELETE_CONFIRM_INPUT, "value"),
147
+ ],
148
+ Input(DM_DELETE_SET_SELECTOR, "value"),
149
+ State(ACTIVE_SESSION, "data"),
150
+ prevent_initial_call=True,
151
+ )
152
+ def open_confirm_section(selected_data_key, session_id: str):
153
+ """
154
+ Controls the visibility of the confirmation section in the delete modal.
155
+
156
+ Shows the confirmation section if a dataset is selected and sets the confirmation
157
+ input value based on whether the selected dataset is master data.
158
+
159
+ Args:
160
+ selected_data_key: Key of the selected dataset
161
+ session_id: ID of the active session
162
+
163
+ Returns:
164
+ tuple: (is_open, confirm_input_value) where:
165
+ - is_open: Boolean indicating if the confirmation section should be visible
166
+ - confirm_input_value: Initial value for the confirmation input field
167
+ """
168
+ if not selected_data_key:
169
+ return False, ""
170
+ sm: ScenarioManager = get_scenario_manager(get_app().server, session_id)
171
+
172
+ is_master_data = sm.get_data(selected_data_key).is_master_data()
173
+ if is_master_data:
174
+ return True, ""
175
+ else:
176
+ return False, "DELETE"
177
+
178
+
179
+ @callback(
180
+ [
181
+ Output(DM_LIST_UPDATER_STORE, "data", allow_duplicate=True),
182
+ Output(DATA_MAN_SUCCESS_ALERT, "children", allow_duplicate=True),
183
+ Output(DATA_MAN_SUCCESS_ALERT, "is_open", allow_duplicate=True),
184
+ Output(DATA_MAN_ERROR_ALERT, "children", allow_duplicate=True),
185
+ Output(DATA_MAN_ERROR_ALERT, "is_open", allow_duplicate=True),
186
+ Output(DM_DELETE_MODAL, "is_open", allow_duplicate=True),
187
+ ],
188
+ [Input(DM_DELETE_SUBMIT_BUTTON, "n_clicks")],
189
+ [
190
+ State(DM_DELETE_SET_SELECTOR, "value"),
191
+ State(DM_DELETE_CONFIRM_INPUT, "value"),
192
+ State(ACTIVE_SESSION, "data"),
193
+ ],
194
+ prevent_initial_call=True,
195
+ )
196
+ def delete_data_callback(n_clicks, selected_data_key, confirm_str, session_id: str):
197
+ """
198
+ Deletes the selected dataset when the delete button is clicked.
199
+
200
+ Requires confirmation by typing "DELETE" in the confirmation input field.
201
+ Updates dropdown options across the application with the new dataset list,
202
+ displays success or error messages, and closes the modal upon completion.
203
+
204
+ Args:
205
+ n_clicks: Number of times the submit button has been clicked
206
+ selected_data_key: Key of the dataset to delete
207
+ confirm_str: Confirmation string that must equal "DELETE" to proceed
208
+ session_id: ID of the active session
209
+
210
+ Returns:
211
+ Tuple containing updated dropdown options, alert messages, and modal state
212
+ """
213
+ if not selected_data_key:
214
+ return (
215
+ no_update,
216
+ no_update,
217
+ no_update,
218
+ no_update,
219
+ "",
220
+ False,
221
+ "Select a dataset to delete!",
222
+ True,
223
+ False,
224
+ )
225
+ if confirm_str != "DELETE":
226
+ return no_update, no_update, no_update, no_update, no_update, no_update
227
+ sm: ScenarioManager = get_scenario_manager(get_app().server, session_id)
228
+ try:
229
+ sm.delete_data(selected_data_key)
230
+ return datetime.now(), "Dataset deleted successfully!", True, "", False, False
231
+ except AssertionError as e:
232
+ return no_update, "", False, f"Problem with deletion: {str(e)}", True, False
233
+
234
+
235
+ @callback(
236
+ Output(DM_DELETE_MODAL, "is_open"),
237
+ [
238
+ Input(DM_DELETE_OPEN_BUTTON, "n_clicks"),
239
+ Input(DM_DELETE_CLOSE_BUTTON, "n_clicks"),
240
+ ],
241
+ [dash.dependencies.State(DM_DELETE_MODAL, "is_open")],
242
+ )
243
+ def toggle_modal_delete(open_clicks, close_clicks, is_open):
244
+ """
245
+ Toggles the visibility of the delete modal dialog.
246
+
247
+ Opens the modal when the open button is clicked and closes it when
248
+ the close button is clicked.
249
+
250
+ Args:
251
+ open_clicks: Number of times the open button has been clicked
252
+ close_clicks: Number of times the close button has been clicked
253
+ is_open: Current state of the modal (open or closed)
254
+
255
+ Returns:
256
+ bool: New state for the modal
257
+ """
258
+ if open_clicks or close_clicks:
259
+ return not is_open
260
+ return is_open
@@ -0,0 +1,201 @@
1
+ import re
2
+ from datetime import datetime
3
+
4
+ import dash
5
+ import dash_bootstrap_components as dbc
6
+ from dash import html, dcc, callback, Output, Input, State, no_update, get_app
7
+
8
+ from algomancy_scenario import ScenarioManager
9
+ from ..componentids import (
10
+ DM_DERIVE_SET_SELECTOR,
11
+ DM_DERIVE_MODAL_SUBMIT_BTN,
12
+ DM_DERIVE_SET_NAME_INPUT,
13
+ DM_DERIVE_MODAL_CLOSE_BTN,
14
+ DM_DERIVE_MODAL,
15
+ DM_LIST_UPDATER_STORE,
16
+ DATA_MAN_SUCCESS_ALERT,
17
+ DATA_MAN_ERROR_ALERT,
18
+ DM_DERIVE_OPEN_BTN,
19
+ ACTIVE_SESSION,
20
+ )
21
+ from algomancy_gui.managergetters import get_scenario_manager
22
+
23
+ """
24
+ Modal component for deriving new datasets from existing ones.
25
+
26
+ This module provides a modal dialog that allows users to create derived datasets
27
+ by selecting an existing dataset and providing a name for the new derived dataset.
28
+ """
29
+
30
+
31
+ def data_management_derive_modal(sm: ScenarioManager, themed_styling):
32
+ """
33
+ Creates a modal dialog component for deriving new datasets.
34
+
35
+ The modal contains a dropdown to select the source dataset and an input field
36
+ for naming the new derived dataset, along with submit and close buttons.
37
+
38
+ Args:
39
+ sm: ScenarioManager instance used to populate the dataset dropdown
40
+
41
+ Returns:
42
+ dbc.Modal: A Dash Bootstrap Components modal dialog
43
+ """
44
+
45
+ return dbc.Modal(
46
+ [
47
+ dbc.ModalHeader(dbc.ModalTitle("Derive"), close_button=False),
48
+ dbc.ModalBody(
49
+ html.Div(
50
+ [
51
+ dbc.Row(
52
+ [
53
+ dbc.Col(
54
+ html.Div(
55
+ html.P("Set to derive: "),
56
+ ),
57
+ width=3,
58
+ className="justify-content-right",
59
+ ),
60
+ dbc.Col(
61
+ [
62
+ dcc.Dropdown(
63
+ id=DM_DERIVE_SET_SELECTOR,
64
+ options=[
65
+ {"label": ds, "value": ds}
66
+ for ds in sm.get_data_keys()
67
+ ],
68
+ value=sm.get_data_keys()[0]
69
+ if sm.get_data_keys()
70
+ else "",
71
+ placeholder="Select dataset",
72
+ ),
73
+ ],
74
+ width=9,
75
+ ),
76
+ ],
77
+ className="mb-4",
78
+ ),
79
+ dbc.Row(
80
+ [
81
+ dbc.Col(html.Div(html.P("Name: ")), width=3),
82
+ dbc.Col(
83
+ dbc.Input(
84
+ id=DM_DERIVE_SET_NAME_INPUT,
85
+ placeholder="Name of new dataset",
86
+ ),
87
+ width=9,
88
+ ),
89
+ ]
90
+ ),
91
+ ]
92
+ )
93
+ ),
94
+ dbc.ModalFooter(
95
+ [
96
+ dbc.Button(
97
+ "Derive",
98
+ id=DM_DERIVE_MODAL_SUBMIT_BTN,
99
+ class_name="dm-derive-modal-confirm-btn",
100
+ ),
101
+ dbc.Button(
102
+ "Close",
103
+ id=DM_DERIVE_MODAL_CLOSE_BTN,
104
+ class_name="dm-derive-modal-cancel-btn ms-auto",
105
+ n_clicks=0,
106
+ ),
107
+ ]
108
+ ),
109
+ ],
110
+ id=DM_DERIVE_MODAL,
111
+ is_open=False,
112
+ centered=True,
113
+ class_name="themed-modal",
114
+ style=themed_styling,
115
+ keyboard=False,
116
+ backdrop="static",
117
+ )
118
+
119
+
120
+ def _sanitize(name: str) -> str:
121
+ # keep ascii-safe filename characters only
122
+ return re.sub(r"[^A-Za-z0-9_.-]", "_", name)
123
+
124
+
125
+ @callback(
126
+ [
127
+ Output(DM_LIST_UPDATER_STORE, "data", allow_duplicate=True),
128
+ Output(DATA_MAN_SUCCESS_ALERT, "children", allow_duplicate=True),
129
+ Output(DATA_MAN_SUCCESS_ALERT, "is_open", allow_duplicate=True),
130
+ Output(DATA_MAN_ERROR_ALERT, "children", allow_duplicate=True),
131
+ Output(DATA_MAN_ERROR_ALERT, "is_open", allow_duplicate=True),
132
+ Output(DM_DERIVE_MODAL, "is_open", allow_duplicate=True),
133
+ ],
134
+ [Input(DM_DERIVE_MODAL_SUBMIT_BTN, "n_clicks")],
135
+ [
136
+ State(DM_DERIVE_SET_SELECTOR, "value"),
137
+ State(DM_DERIVE_SET_NAME_INPUT, "value"),
138
+ State(ACTIVE_SESSION, "data"),
139
+ ],
140
+ prevent_initial_call=True,
141
+ )
142
+ def derive_data_callback(n_clicks, selected_data_key, derived_name, session_id: str):
143
+ """
144
+ Creates a derived dataset from an existing one when the derive button is clicked.
145
+
146
+ Updates dropdown options across the application with the new dataset list,
147
+ displays success or error messages, and closes the modal upon completion.
148
+
149
+ Args:
150
+ n_clicks: Number of times the submit button has been clicked
151
+ selected_data_key: Key of the dataset to derive from
152
+ derived_name: Name for the new derived dataset
153
+ session_id: ID of the active session
154
+
155
+ Returns:
156
+ Tuple containing updated dropdown options, alert messages, and modal state
157
+ """
158
+ if not selected_data_key or not derived_name:
159
+ return no_update, "", False, "Choose a dataset and enter a name!", True, False
160
+ sm: ScenarioManager = get_scenario_manager(get_app().server, session_id)
161
+ try:
162
+ sanitized_name = _sanitize(derived_name)
163
+ sm.derive_data(selected_data_key, sanitized_name)
164
+ return (
165
+ datetime.now(),
166
+ "Successfully created derived dataset!",
167
+ True,
168
+ "",
169
+ False,
170
+ False,
171
+ )
172
+ except Exception as e:
173
+ return no_update, "", False, f"Problem with deriving: {str(e)}", True, False
174
+
175
+
176
+ @callback(
177
+ Output(DM_DERIVE_MODAL, "is_open"),
178
+ [
179
+ Input(DM_DERIVE_OPEN_BTN, "n_clicks"),
180
+ Input(DM_DERIVE_MODAL_CLOSE_BTN, "n_clicks"),
181
+ ],
182
+ [dash.dependencies.State(DM_DERIVE_MODAL, "is_open")],
183
+ )
184
+ def toggle_modal_derive(open_clicks, close_clicks, is_open):
185
+ """
186
+ Toggles the visibility of the derive modal dialog.
187
+
188
+ Opens the modal when the open button is clicked and closes it when
189
+ the close button is clicked.
190
+
191
+ Args:
192
+ open_clicks: Number of times the open button has been clicked
193
+ close_clicks: Number of times the close button has been clicked
194
+ is_open: Current state of the modal (open or closed)
195
+
196
+ Returns:
197
+ bool: New state for the modal
198
+ """
199
+ if open_clicks or close_clicks:
200
+ return not is_open
201
+ return is_open