simian-examples 3.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.
@@ -0,0 +1,211 @@
1
+ {
2
+ "components": [
3
+ {
4
+ "label": "Load GAP Minder data",
5
+ "showValidations": false,
6
+ "disableOnInvalid": true,
7
+ "tableView": false,
8
+ "key": "loadData",
9
+ "type": "button",
10
+ "action": "event",
11
+ "input": true,
12
+ "event": "LoadData"
13
+ },
14
+ {
15
+ "label": "No data",
16
+ "content": "<b>No data loaded.</b>",
17
+ "refreshOnChange": false,
18
+ "key": "noData",
19
+ "customConditional": "show = !('availableYears' in data) || data.availableYears.length == 0;",
20
+ "type": "htmlelement",
21
+ "input": false,
22
+ "tableView": false
23
+ },
24
+ {
25
+ "label": "Available continents",
26
+ "key": "availableContinents",
27
+ "type": "hidden",
28
+ "input": true,
29
+ "tableView": false
30
+ },
31
+ {
32
+ "label": "Select continent(s)",
33
+ "labelPosition": "left-left",
34
+ "widget": "choicesjs",
35
+ "tableView": true,
36
+ "multiple": true,
37
+ "dataSrc": "custom",
38
+ "data": {
39
+ "custom": "values=data.availableContinents"
40
+ },
41
+ "key": "selectContinents",
42
+ "customConditional": "show = 'availableContinents' in data && data.availableContinents.length != 0",
43
+ "type": "select",
44
+ "input": true
45
+ },
46
+ {
47
+ "label": "Tabs",
48
+ "components": [
49
+ {
50
+ "label": "Data",
51
+ "key": "dataTab",
52
+ "components": [
53
+ {
54
+ "label": "Data Columns",
55
+ "columns": [
56
+ {
57
+ "components": [
58
+ {
59
+ "label": "Available years",
60
+ "key": "availableYears",
61
+ "type": "hidden",
62
+ "input": true,
63
+ "tableView": false
64
+ },
65
+ {
66
+ "label": "Select year(s)",
67
+ "labelPosition": "left-left",
68
+ "widget": "choicesjs",
69
+ "tableView": true,
70
+ "multiple": true,
71
+ "dataSrc": "custom",
72
+ "data": {
73
+ "custom": "values=data.availableYears;"
74
+ },
75
+ "key": "selectYears",
76
+ "customConditional": "show = 'availableYears' in data && data.availableYears.length != 0;",
77
+ "type": "select",
78
+ "input": true
79
+ },
80
+ {
81
+ "label": "GAP table",
82
+ "customClass": "table-sm table-striped",
83
+ "hideLabel": true,
84
+ "tableView": false,
85
+ "key": "gapTable",
86
+ "type": "customdatatables",
87
+ "input": true
88
+ }
89
+ ],
90
+ "width": 6,
91
+ "offset": 0,
92
+ "push": 0,
93
+ "pull": 0,
94
+ "size": "md",
95
+ "currentWidth": 6
96
+ },
97
+ {
98
+ "components": [
99
+ {
100
+ "label": "Number of selected years",
101
+ "labelPosition": "left-left",
102
+ "mask": false,
103
+ "disabled": true,
104
+ "tableView": false,
105
+ "delimiter": false,
106
+ "requireDecimal": false,
107
+ "inputFormat": "plain",
108
+ "truncateMultipleSpaces": false,
109
+ "clearOnHide": false,
110
+ "calculateValue": "value = data.selectYears.length;",
111
+ "key": "numberSelectedYears",
112
+ "type": "number",
113
+ "input": true
114
+ },
115
+ {
116
+ "label": "Plot",
117
+ "aspectRatio": 1.3333333333333333,
118
+ "hideLabel": true,
119
+ "tableView": false,
120
+ "key": "plot",
121
+ "type": "customplotly",
122
+ "input": true
123
+ }
124
+ ],
125
+ "width": 6,
126
+ "offset": 0,
127
+ "push": 0,
128
+ "pull": 0,
129
+ "size": "md",
130
+ "currentWidth": 6
131
+ }
132
+ ],
133
+ "key": "dataColumns",
134
+ "type": "columns",
135
+ "input": false,
136
+ "tableView": false
137
+ }
138
+ ]
139
+ },
140
+ {
141
+ "label": "Scenarios",
142
+ "key": "scenariosTab",
143
+ "components": [
144
+ {
145
+ "label": "'Calculate' Scenarios",
146
+ "showValidations": false,
147
+ "tableView": false,
148
+ "key": "updateScenarios",
149
+ "type": "button",
150
+ "action": "event",
151
+ "input": true,
152
+ "event": "RunScenarios"
153
+ },
154
+ {
155
+ "label": "Scenarios",
156
+ "tableView": false,
157
+ "rowDrafts": false,
158
+ "key": "scenarios",
159
+ "type": "editgrid",
160
+ "displayAsTable": false,
161
+ "input": true,
162
+ "components": [
163
+ {
164
+ "label": "Selected year",
165
+ "labelPosition": "left-left",
166
+ "placeholder": "yyyy",
167
+ "applyMaskOn": "change",
168
+ "mask": false,
169
+ "tableView": true,
170
+ "delimiter": false,
171
+ "requireDecimal": false,
172
+ "inputFormat": "plain",
173
+ "truncateMultipleSpaces": false,
174
+ "validate": {
175
+ "custom": "valid = data.availableYears.includes(input);"
176
+ },
177
+ "errors": {
178
+ "custom": "Year must be one of the years in the dataset."
179
+ },
180
+ "key": "inputYear",
181
+ "type": "number",
182
+ "input": true
183
+ },
184
+ {
185
+ "label": "Total population",
186
+ "labelPosition": "left-left",
187
+ "applyMaskOn": "change",
188
+ "mask": false,
189
+ "disabled": true,
190
+ "tableView": true,
191
+ "delimiter": false,
192
+ "requireDecimal": false,
193
+ "inputFormat": "plain",
194
+ "truncateMultipleSpaces": false,
195
+ "key": "totalPop",
196
+ "type": "number",
197
+ "input": true
198
+ }
199
+ ]
200
+ }
201
+ ]
202
+ }
203
+ ],
204
+ "hideLabel": true,
205
+ "key": "tabs",
206
+ "type": "tabs",
207
+ "input": false,
208
+ "tableView": false
209
+ }
210
+ ]
211
+ }
@@ -0,0 +1,219 @@
1
+ """Workshop Example application.
2
+
3
+ Built for simian.gui 3.0.0.
4
+ """
5
+
6
+ import logging
7
+ import numbers
8
+
9
+ import plotly.express
10
+ from pandas import DataFrame as pandas_DataFrame
11
+ from simian.gui import Form, component, utils
12
+
13
+ # Import the GAP Minder data from the Plotly Express module.
14
+ # The resulting GAP_DATA is a pandas.DataFrame. The columns that will be used are:
15
+ # "year" and "continents"
16
+ GAP_DATA = plotly.express.data.gapminder()
17
+ GAP_DATA.insert(0, "id", GAP_DATA.index) # 'id' is needed in DataTables, so we are adding it here.
18
+
19
+
20
+ if __name__ == "__main__":
21
+ # Run as a script
22
+ from simian.local import run
23
+
24
+ run(
25
+ "simian.examples.workshop_example",
26
+ window_title="Workshop application",
27
+ debug=True,
28
+ show_refresh=True,
29
+ )
30
+
31
+
32
+ def gui_init(_meta_data: dict) -> dict:
33
+ """Initializes the form.
34
+
35
+ Initialization of the form and the components therein.
36
+
37
+ Returns:
38
+ payload: Form definition.
39
+ """
40
+ # Setup component initializers and create the Form from the definition in the .json file.
41
+ Form.componentInitializer(
42
+ gapTable=init_gap_table,
43
+ selectContinents=make_trigger_happy,
44
+ selectYears=make_trigger_happy,
45
+ )
46
+ form = Form(from_file=__file__)
47
+
48
+ # Put the form in the outputs.
49
+ payload = {
50
+ "form": form,
51
+ "navbar": {
52
+ "title": "Workshop Application",
53
+ "subtitle": "<small>Simian Demo</small>",
54
+ },
55
+ "showChanged": True, # Flag changes to the settings in the UI.
56
+ }
57
+
58
+ return payload
59
+
60
+
61
+ def init_gap_table(gap_table: component.DataTables):
62
+ """Initializer function for the gapTable."""
63
+ table_column_names = list(GAP_DATA.columns)
64
+
65
+ gap_table.setColumns(
66
+ titles=[i_name.capitalize() for i_name in table_column_names],
67
+ keys=table_column_names,
68
+ className=["noneditable"] * len(table_column_names),
69
+ visible=[False] + [True] * (len(table_column_names) - 1),
70
+ )
71
+
72
+
73
+ def make_trigger_happy(comp):
74
+ """Initializer to make input components triggerHappy."""
75
+ comp.properties["triggerHappy"] = "FilterChange"
76
+
77
+
78
+ def gui_event(meta_data: dict, payload: dict) -> dict:
79
+ """Event handling of the application.
80
+
81
+ Args:
82
+ meta_data: Form meta data.
83
+ payload: Current status of the Form's contents.
84
+
85
+ Returns:
86
+ payload: Updated Form contents.
87
+ """
88
+ # Register the events with their functions.
89
+ Form.eventHandler(
90
+ LoadData=load_data,
91
+ RunScenarios=update_scenarios,
92
+ FilterChange=filter_changed,
93
+ )
94
+ try:
95
+ caller = utils.getEventFunction(meta_data, payload)
96
+ payload = caller(meta_data, payload)
97
+
98
+ except ImportError as exc:
99
+ # No method with matching name found in the current namespace. May be used to have
100
+ # custom event routing. Here we only log the occurrence.
101
+ logging.exception(exc)
102
+
103
+ # Lower the "Form has changes" flag.
104
+ payload["pristine"] = True
105
+
106
+ return payload
107
+
108
+
109
+ def filter_changed(meta_data: dict, payload: dict) -> dict:
110
+ """Continent or Year selection change callback."""
111
+ # Continent or year selection changed. Update the table and plot.
112
+ update_table_and_plot(payload)
113
+
114
+ # Trigger a recalculate of the scenarios.
115
+ update_scenarios(meta_data, payload)
116
+
117
+ return payload
118
+
119
+
120
+ def filter_data(selected_continents: list, selected_years: list) -> pandas_DataFrame:
121
+ """Filter GAP data based on selected continents and years."""
122
+ if isinstance(selected_continents, str):
123
+ # Selected continent is a string. Put in a list for use in the filtering.
124
+ selected_continents = [selected_continents]
125
+
126
+ if isinstance(selected_years, numbers.Number):
127
+ # Selected year is a number. Put in a list for use in the filtering.
128
+ selected_years = [selected_years]
129
+
130
+ # Filter the GAP data.
131
+ new_gap = GAP_DATA[
132
+ GAP_DATA["continent"].isin(selected_continents) & GAP_DATA["year"].isin(selected_years)
133
+ ]
134
+
135
+ return new_gap
136
+
137
+
138
+ def load_data(_meta_data: dict, payload: dict) -> dict:
139
+ """Put the Gap Minder data in the table."""
140
+ utils.setSubmissionData(payload, "gapTable", GAP_DATA)
141
+
142
+ # Put the available continents in the Select component.
143
+ gap_continents = sorted(set(GAP_DATA["continent"]))
144
+ utils.setSubmissionData(payload, "selectContinents", gap_continents)
145
+ utils.setSubmissionData(payload, "availableContinents", gap_continents)
146
+
147
+ # Put the available years in the Select component and select the last year.
148
+ gap_years = sorted(set(GAP_DATA["year"]))
149
+ utils.setSubmissionData(payload, "selectYears", [gap_years[-1]])
150
+ utils.setSubmissionData(payload, "availableYears", gap_years)
151
+
152
+ # Plot the loaded data.
153
+ update_table_and_plot(payload)
154
+
155
+ return payload
156
+
157
+
158
+ def update_scenarios(_meta_data: dict, payload: dict) -> dict:
159
+ """Update the scenario results in the scenario list."""
160
+ scenario_list, _ = utils.getSubmissionData(payload, key="scenarios")
161
+
162
+ for scenario_row in scenario_list:
163
+ # Loop over the scenario rows.
164
+ selected_year = scenario_row["inputYear"]
165
+
166
+ # Get the selected continents and filter the GAP data for the scenario year.
167
+ selected_continents, _ = utils.getSubmissionData(payload, "selectContinents")
168
+ filtered_gap_data = filter_data(selected_continents, selected_year)
169
+
170
+ # Calculate the Total population of all nations in the selection.
171
+ scenario_row["totalPop"] = float(filtered_gap_data["pop"].sum())
172
+
173
+ utils.setSubmissionData(payload, "scenarios", scenario_list)
174
+
175
+ return payload
176
+
177
+
178
+ def update_table_and_plot(payload: dict) -> None:
179
+ """Put the GAP data in the table and plot."""
180
+ selected_year, _ = utils.getSubmissionData(payload, "selectYears")
181
+
182
+ # Get the selected continents and filter the GAP data for the selected years.
183
+ selected_continents, _ = utils.getSubmissionData(payload, "selectContinents")
184
+ filtered_gap_data = filter_data(selected_continents, selected_year)
185
+
186
+ # Fill table with GAP data.
187
+ utils.setSubmissionData(payload, "gapTable", filtered_gap_data)
188
+
189
+ update_plot(payload, filtered_gap_data=filtered_gap_data)
190
+
191
+
192
+ def update_plot(payload: dict, filtered_gap_data: pandas_DataFrame = GAP_DATA) -> None:
193
+ """Update the plot with a 'filtered' set of data."""
194
+ # Recreate the Plotly object.
195
+ plot_obj = utils.getSubmissionData(payload, key="plot")[0]
196
+
197
+ # Plot the new newly calculated x and y values, append the legend and put the plotly
198
+ # object in the submission data.
199
+ if len(filtered_gap_data) == 0:
200
+ # No data. Empty the plot.
201
+ plot_obj.figure = None
202
+
203
+ else:
204
+ # Fill the plot with data.
205
+ # https://plotly.com/python/line-and-scatter/
206
+ plot_obj.figure = plotly.express.scatter(
207
+ filtered_gap_data,
208
+ x="gdpPercap",
209
+ log_x=True,
210
+ y="lifeExp",
211
+ symbol="year",
212
+ size="pop",
213
+ size_max=60,
214
+ color="continent",
215
+ hover_name="country",
216
+ )
217
+
218
+ # Put the new plot contents in the payload.
219
+ utils.setSubmissionData(payload, "plot", plot_obj)
@@ -0,0 +1,233 @@
1
+ """Simian Wrapped Ball Thrower app.
2
+
3
+ Built for simian.gui 3.1.0.
4
+ """
5
+
6
+ import math
7
+
8
+ import numpy as np
9
+ import simian.examples.ballthrower as throw_gui
10
+ import simian.gui
11
+ from simian.examples.ballthrower import (
12
+ COLLAPSE_PANEL,
13
+ LABEL_LEFT,
14
+ causeError,
15
+ clearHistory,
16
+ gui_event,
17
+ )
18
+ from simian.gui import component, utils
19
+
20
+ try:
21
+ from monkeyproof.modelwrapper.mwapp.application import Application
22
+ except ImportError as exc:
23
+ raise RuntimeError("Simian wrapper could not be found. Is it installed?") from exc
24
+
25
+
26
+ if __name__ == "__main__":
27
+ # Run as a script
28
+ from simian.entrypoint import assert_simian_gui_imports
29
+
30
+ assert_simian_gui_imports()
31
+ from simian.local import run
32
+
33
+ run("simian.examples.wrapped_thrower", window_title="Wrapped Ball Thrower", debug=False)
34
+
35
+
36
+ def gui_init(meta_data) -> dict:
37
+ """Initializes the form.
38
+
39
+ Initialization of the form and the components therein.
40
+
41
+ Returns:
42
+ payload: Form definition.
43
+ """
44
+ app = Application("examples.thrower.ballthrower_model.ThrowerModel")
45
+ app.populate(
46
+ userData={
47
+ "Resources_XLS": {
48
+ "SelectedBall": {"value": "None"},
49
+ "SpeedHorizontal": {"value": 10 / math.sqrt(2)},
50
+ "SpeedVertical": {"value": 10 / math.sqrt(2)},
51
+ }
52
+ }
53
+ )
54
+ utils.setCache(meta_data, "app", app)
55
+
56
+ # Get the list of balls from the wrapped model.
57
+ balls_list = app.model.dataStore.resources.getValue("DefinedBalls")
58
+
59
+ # Create a form and add a label.
60
+ form_obj = simian.gui.Form("form")
61
+
62
+ # Create tabs in the form.
63
+ tabs = component.Tabs(key="tabs", parent=form_obj)
64
+ tab_inputs, tab_settings = tabs.setContent(labels=["Inputs", "Used settings"])
65
+
66
+ # Create a Columns object and fill it with two empty columns.
67
+ col = component.Columns(key="inputColumns", parent=tab_inputs)
68
+ left_column, right_column = col.setContent(components=[], widths=[6, 6])
69
+
70
+ # Add Throw speed and angle controls.
71
+ created_components = throw_gui.add_throw_speed_controls(left_column)
72
+
73
+ # Put a Ball selector component in the throw options panel
74
+ ball_selector = component.Select("ballSelector", created_components["throwPanel"])
75
+ ball_selector.label = "Selected Ball"
76
+ ball_selector.tooltip = "Ball to throw."
77
+ ball_selector.setRequired()
78
+ ball_selector.labelPosition = "left-left"
79
+
80
+ # Fill the Balls selector with the list of balls from the wrapped model.
81
+ ball_selector.setValues(balls_list["Name"], balls_list.to_dict("records"))
82
+
83
+ # Add the options panels and controls.
84
+ left_dis_opts = {"disabled": True, **LABEL_LEFT}
85
+
86
+ def ball_prop(name: str) -> dict:
87
+ return {
88
+ "calculateValue": f"""
89
+ if (data.ballSelector) {{
90
+ value=data.ballSelector['{name}']
91
+ }} else {{
92
+ value = 0
93
+ }}""",
94
+ **left_dis_opts,
95
+ }
96
+
97
+ ball_mass_opts = ball_prop("Weight [kg]")
98
+ ball_radius_opts = ball_prop("Diameter [m]")
99
+ ball_drag_opts = ball_prop("Drag Coefficient [-]")
100
+
101
+ utils.addComponentsFromTable(
102
+ parent=left_column,
103
+ build_table=[
104
+ # key class level options default label tooltip
105
+ ["ballPanel", "Panel", 1, COLLAPSE_PANEL, None, "Ball settings", None],
106
+ ["ballMass", "Number", 2, ball_mass_opts, 0, "Mass [kg]:", "Mass of the ball."],
107
+ ["ballRadius", "Number", 2, ball_radius_opts, 0, "Radius [m]:", "Radius of the ball."],
108
+ ["dragCoefficient", "Number", 2, ball_drag_opts, 0, "Drag coefficient [-]:", "Drag coefficient of the ball."],
109
+ ["constantsPanel", "Panel", 1, COLLAPSE_PANEL, None, "Constants", None],
110
+ ["gravity", "Number", 2, left_dis_opts, 9.81, "Gravity [m/s<sup>2</sup>]:", "Gravitational accelleration acting on the ball."],
111
+ ["airDensity", "Number", 2, left_dis_opts, 1.29, "Air density [kg/m<sup>3</sup>]:", "Density of the air through which the ball moves."] # fmt: skip
112
+ ],
113
+ )
114
+
115
+ # Show Scenario information.
116
+ scenario_grid = component.HtmlTable(
117
+ "scenario_info", left_column, column_names=["Scenario Name", "Wind speed"]
118
+ )
119
+ scenario_grid.setDefaultValue(
120
+ [[i_scen.name, i_scen.getValue("Wind")[0][0]] for i_scen in app.model.dataStore.scenarios]
121
+ )
122
+
123
+ # Fill the right column.
124
+ throw_gui.fill_right_column(right_column)
125
+
126
+ # Fill the Settings tab.
127
+ _fill_settings_tab(tab_settings)
128
+
129
+ # Put the form in the outputs.
130
+ payload = {
131
+ "form": form_obj,
132
+ "navbar": {
133
+ "title": "Wrapped Ball Thrower",
134
+ "subtitle": "<small>Simian demo</small>",
135
+ },
136
+ "showChanged": True, # Flag changes to the settings in the UI.
137
+ }
138
+
139
+ return payload
140
+
141
+
142
+ def throwing(meta_data: dict, payload: dict) -> dict:
143
+ """Simulate throwing a ball.
144
+
145
+ Get the settings from the payload.
146
+ """
147
+ throw_speed, _ = utils.getSubmissionData(payload, key="throwSpeed")
148
+ throw_angle, _ = utils.getSubmissionData(payload, key="throwAngle")
149
+ selected_ball, _ = utils.getSubmissionData(payload, key="ballSelector")
150
+
151
+ # Convert the speed and angle to the speed components.
152
+ ver_speed = throw_speed * math.sin(throw_angle / 180 * math.pi)
153
+ hor_speed = throw_speed * math.cos(throw_angle / 180 * math.pi)
154
+
155
+ app, _ = utils.getCache(meta_data, "app")
156
+
157
+ resources = app.model.dataStore.resources
158
+ resources.setValue("SelectedBall", selected_ball["Name"], overload=True)
159
+ resources.setValue("SpeedHorizontal", hor_speed, overload=True)
160
+ resources.setValue("SpeedVertical", ver_speed, overload=True)
161
+ app.model.run()
162
+
163
+ trajectories = app.model.dataStore.custom["trajectories"]
164
+
165
+ # Create a Plotly object from the information in the payload.
166
+ plot_obj, _ = utils.getSubmissionData(payload, key="plot")
167
+
168
+ # Plot the new newly calculated x and y values, append the legend and put the plotly
169
+ # object in the submission data.
170
+ nr = int(len(plot_obj.figure.data) / 4 + 1)
171
+
172
+ for i_scen, i_res in trajectories.items():
173
+ plot_obj.figure.add_scatter(
174
+ x=i_res["x"], y=i_res["y"], name=f"Attempt {nr} - {i_scen}", mode="lines"
175
+ )
176
+ utils.setSubmissionData(payload, key="plot", data=plot_obj)
177
+
178
+ # Get the maximum thrown distances.
179
+ distances = [d[-1] for d in app.model.dataStore.results.getValue("Distance")[1:]]
180
+
181
+ # Update the table with the settings used for the throws.
182
+ new_row = [
183
+ nr,
184
+ nr,
185
+ round(hor_speed, 3),
186
+ round(ver_speed, 3),
187
+ selected_ball["Weight [kg]"],
188
+ selected_ball["Diameter [m]"] / 2,
189
+ selected_ball["Drag Coefficient [-]"],
190
+ round(min(distances), 3),
191
+ round(max(distances), 3),
192
+ round(np.mean(distances), 3),
193
+ round(max(distances) - min(distances), 3),
194
+ ]
195
+
196
+ if nr == 1:
197
+ # First throw. Only put the new row in the table.
198
+ new_table_values = [new_row]
199
+ else:
200
+ # Extra row. Append the row to the DataFrame from the submission data.
201
+ new_table_values, _ = utils.getSubmissionData(payload, key="summary")
202
+ new_table_values.append(dict(zip(new_table_values[0].keys(), new_row)))
203
+
204
+ utils.setSubmissionData(payload, key="summary", data=new_table_values)
205
+
206
+ return payload
207
+
208
+
209
+ def _fill_settings_tab(tab_settings):
210
+ """Fill Settings tab."""
211
+ # Create a table to store the throw settings in.
212
+ table_out = component.DataTables(key="summary", parent=tab_settings)
213
+ table_out.setColumns(
214
+ *[
215
+ list(x)
216
+ for x in zip(
217
+ ["Id", "id"],
218
+ ["Attempt", "attempt"],
219
+ ["Horizontal speed", "hor_speed"],
220
+ ["Vertical speed", "ver_speed"],
221
+ ["Ball mass", "ball_mass"],
222
+ ["Radius", "radius"],
223
+ ["Drag Coeff.", "drag"],
224
+ ["Min. distance", "min_dist"],
225
+ ["Max. distance", "max_dist"],
226
+ ["Mean distance", "mean_dist"],
227
+ ["Spread distance", "spread_dist"],
228
+ )
229
+ ],
230
+ visible=[False] + [True] * 10,
231
+ )
232
+ table_out.label = "Used settings per throw"
233
+ table_out.setFeatures(searching=False, ordering=False, paging=False)
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.4
2
+ Name: simian-examples
3
+ Version: 3.3.0
4
+ Summary: Simian Examples for Python
5
+ Author-email: "MonkeyProof Solutions B.V." <info@simiansuite.com>
6
+ License-Expression: MIT
7
+ Project-URL: homepage, https://simiansuite.com
8
+ Project-URL: documentation, https://doc.simiansuite.com/simian-gui/index.html
9
+ Project-URL: releasenotes, https://doc.simiansuite.com/simian-gui/release_notes.html
10
+ Project-URL: issues, https://github.com/Simian-Web-Apps/Issue-Tracker
11
+ Project-URL: github, https://github.com/Simian-Web-Apps
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: Implementation :: CPython
16
+ Classifier: Operating System :: Microsoft :: Windows
17
+ Classifier: Operating System :: Unix
18
+ Requires-Python: >=3.10
19
+ Requires-Dist: simian-gui==3.3.0
20
+ Requires-Dist: scipy
21
+ Provides-Extra: local
22
+ Requires-Dist: simian-local==3.3.0; extra == "local"
23
+ Dynamic: provides-extra
24
+ Dynamic: requires-dist