floodmodeller-api 0.4.2__py3-none-any.whl → 0.4.3__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 (178) hide show
  1. floodmodeller_api/__init__.py +8 -9
  2. floodmodeller_api/_base.py +184 -176
  3. floodmodeller_api/backup.py +273 -273
  4. floodmodeller_api/dat.py +909 -838
  5. floodmodeller_api/diff.py +136 -119
  6. floodmodeller_api/ied.py +307 -311
  7. floodmodeller_api/ief.py +647 -646
  8. floodmodeller_api/ief_flags.py +253 -253
  9. floodmodeller_api/inp.py +266 -268
  10. floodmodeller_api/libs/libifcoremd.dll +0 -0
  11. floodmodeller_api/libs/libifcoremt.so.5 +0 -0
  12. floodmodeller_api/libs/libifport.so.5 +0 -0
  13. floodmodeller_api/{libmmd.dll → libs/libimf.so} +0 -0
  14. floodmodeller_api/libs/libintlc.so.5 +0 -0
  15. floodmodeller_api/libs/libmmd.dll +0 -0
  16. floodmodeller_api/libs/libsvml.so +0 -0
  17. floodmodeller_api/libs/libzzn_read.so +0 -0
  18. floodmodeller_api/libs/zzn_read.dll +0 -0
  19. floodmodeller_api/logs/__init__.py +2 -2
  20. floodmodeller_api/logs/lf.py +320 -314
  21. floodmodeller_api/logs/lf_helpers.py +354 -346
  22. floodmodeller_api/logs/lf_params.py +643 -529
  23. floodmodeller_api/mapping.py +84 -0
  24. floodmodeller_api/test/__init__.py +4 -4
  25. floodmodeller_api/test/conftest.py +9 -8
  26. floodmodeller_api/test/test_backup.py +117 -117
  27. floodmodeller_api/test/test_dat.py +221 -92
  28. floodmodeller_api/test/test_data/All Units 4_6.DAT +1081 -1081
  29. floodmodeller_api/test/test_data/All Units 4_6.feb +1081 -1081
  30. floodmodeller_api/test/test_data/BRIDGE.DAT +926 -926
  31. floodmodeller_api/test/test_data/Culvert_Inlet_Outlet.dat +36 -36
  32. floodmodeller_api/test/test_data/Culvert_Inlet_Outlet.feb +36 -36
  33. floodmodeller_api/test/test_data/DamBreakADI.xml +52 -52
  34. floodmodeller_api/test/test_data/DamBreakFAST.xml +58 -58
  35. floodmodeller_api/test/test_data/DamBreakFAST_dy.xml +53 -53
  36. floodmodeller_api/test/test_data/DamBreakTVD.xml +55 -55
  37. floodmodeller_api/test/test_data/DefenceBreach.xml +53 -53
  38. floodmodeller_api/test/test_data/DefenceBreachFAST.xml +60 -60
  39. floodmodeller_api/test/test_data/DefenceBreachFAST_dy.xml +55 -55
  40. floodmodeller_api/test/test_data/Domain1+2_QH.xml +76 -76
  41. floodmodeller_api/test/test_data/Domain1_H.xml +41 -41
  42. floodmodeller_api/test/test_data/Domain1_Q.xml +41 -41
  43. floodmodeller_api/test/test_data/Domain1_Q_FAST.xml +48 -48
  44. floodmodeller_api/test/test_data/Domain1_Q_FAST_dy.xml +48 -48
  45. floodmodeller_api/test/test_data/Domain1_Q_xml_expected.json +263 -0
  46. floodmodeller_api/test/test_data/Domain1_W.xml +41 -41
  47. floodmodeller_api/test/test_data/EX1.DAT +321 -321
  48. floodmodeller_api/test/test_data/EX1.ext +107 -107
  49. floodmodeller_api/test/test_data/EX1.feb +320 -320
  50. floodmodeller_api/test/test_data/EX1.gxy +107 -107
  51. floodmodeller_api/test/test_data/EX17.DAT +421 -422
  52. floodmodeller_api/test/test_data/EX17.ext +213 -213
  53. floodmodeller_api/test/test_data/EX17.feb +422 -422
  54. floodmodeller_api/test/test_data/EX18.DAT +375 -375
  55. floodmodeller_api/test/test_data/EX18_DAT_expected.json +3876 -0
  56. floodmodeller_api/test/test_data/EX2.DAT +302 -302
  57. floodmodeller_api/test/test_data/EX3.DAT +926 -926
  58. floodmodeller_api/test/test_data/EX3_DAT_expected.json +16235 -0
  59. floodmodeller_api/test/test_data/EX3_IEF_expected.json +61 -0
  60. floodmodeller_api/test/test_data/EX6.DAT +2084 -2084
  61. floodmodeller_api/test/test_data/EX6.ext +532 -532
  62. floodmodeller_api/test/test_data/EX6.feb +2084 -2084
  63. floodmodeller_api/test/test_data/EX6_DAT_expected.json +31647 -0
  64. floodmodeller_api/test/test_data/Event Data Example.DAT +336 -336
  65. floodmodeller_api/test/test_data/Event Data Example.ext +107 -107
  66. floodmodeller_api/test/test_data/Event Data Example.feb +336 -336
  67. floodmodeller_api/test/test_data/Linked1D2D.xml +52 -52
  68. floodmodeller_api/test/test_data/Linked1D2DFAST.xml +53 -53
  69. floodmodeller_api/test/test_data/Linked1D2DFAST_dy.xml +48 -48
  70. floodmodeller_api/test/test_data/Linked1D2D_xml_expected.json +313 -0
  71. floodmodeller_api/test/test_data/blockage.dat +50 -50
  72. floodmodeller_api/test/test_data/blockage.ext +45 -45
  73. floodmodeller_api/test/test_data/blockage.feb +9 -9
  74. floodmodeller_api/test/test_data/blockage.gxy +71 -71
  75. floodmodeller_api/test/test_data/defaultUnits.dat +127 -127
  76. floodmodeller_api/test/test_data/defaultUnits.ext +45 -45
  77. floodmodeller_api/test/test_data/defaultUnits.feb +9 -9
  78. floodmodeller_api/test/test_data/defaultUnits.fmpx +58 -58
  79. floodmodeller_api/test/test_data/defaultUnits.gxy +85 -85
  80. floodmodeller_api/test/test_data/ex3.ief +20 -20
  81. floodmodeller_api/test/test_data/ex3.lf1 +2800 -2800
  82. floodmodeller_api/test/test_data/ex4.DAT +1374 -1374
  83. floodmodeller_api/test/test_data/ex4_changed.DAT +1374 -1374
  84. floodmodeller_api/test/test_data/example1.inp +329 -329
  85. floodmodeller_api/test/test_data/example2.inp +158 -158
  86. floodmodeller_api/test/test_data/example3.inp +297 -297
  87. floodmodeller_api/test/test_data/example4.inp +388 -388
  88. floodmodeller_api/test/test_data/example5.inp +147 -147
  89. floodmodeller_api/test/test_data/example6.inp +154 -154
  90. floodmodeller_api/test/test_data/jump.dat +176 -176
  91. floodmodeller_api/test/test_data/network.dat +1374 -1374
  92. floodmodeller_api/test/test_data/network.ext +45 -45
  93. floodmodeller_api/test/test_data/network.exy +1 -1
  94. floodmodeller_api/test/test_data/network.feb +45 -45
  95. floodmodeller_api/test/test_data/network.ied +45 -45
  96. floodmodeller_api/test/test_data/network.ief +20 -20
  97. floodmodeller_api/test/test_data/network.inp +147 -147
  98. floodmodeller_api/test/test_data/network.pxy +57 -57
  99. floodmodeller_api/test/test_data/network.zzd +122 -122
  100. floodmodeller_api/test/test_data/network_dat_expected.json +21837 -0
  101. floodmodeller_api/test/test_data/network_from_tabularCSV.csv +87 -87
  102. floodmodeller_api/test/test_data/network_ied_expected.json +287 -0
  103. floodmodeller_api/test/test_data/rnweir.dat +9 -9
  104. floodmodeller_api/test/test_data/rnweir.ext +45 -45
  105. floodmodeller_api/test/test_data/rnweir.feb +9 -9
  106. floodmodeller_api/test/test_data/rnweir.gxy +45 -45
  107. floodmodeller_api/test/test_data/rnweir_default.dat +74 -74
  108. floodmodeller_api/test/test_data/rnweir_default.ext +45 -45
  109. floodmodeller_api/test/test_data/rnweir_default.feb +9 -9
  110. floodmodeller_api/test/test_data/rnweir_default.fmpx +58 -58
  111. floodmodeller_api/test/test_data/rnweir_default.gxy +53 -53
  112. floodmodeller_api/test/test_data/unit checks.dat +16 -16
  113. floodmodeller_api/test/test_ied.py +29 -29
  114. floodmodeller_api/test/test_ief.py +125 -24
  115. floodmodeller_api/test/test_inp.py +47 -48
  116. floodmodeller_api/test/test_json.py +114 -0
  117. floodmodeller_api/test/test_logs_lf.py +48 -51
  118. floodmodeller_api/test/test_tool.py +165 -154
  119. floodmodeller_api/test/test_toolbox_structure_log.py +234 -239
  120. floodmodeller_api/test/test_xml2d.py +151 -156
  121. floodmodeller_api/test/test_zzn.py +36 -34
  122. floodmodeller_api/to_from_json.py +218 -0
  123. floodmodeller_api/tool.py +332 -330
  124. floodmodeller_api/toolbox/__init__.py +5 -5
  125. floodmodeller_api/toolbox/example_tool.py +45 -45
  126. floodmodeller_api/toolbox/model_build/__init__.py +2 -2
  127. floodmodeller_api/toolbox/model_build/add_siltation_definition.py +100 -94
  128. floodmodeller_api/toolbox/model_build/structure_log/__init__.py +1 -1
  129. floodmodeller_api/toolbox/model_build/structure_log/structure_log.py +287 -289
  130. floodmodeller_api/toolbox/model_build/structure_log_definition.py +76 -72
  131. floodmodeller_api/units/__init__.py +10 -10
  132. floodmodeller_api/units/_base.py +214 -209
  133. floodmodeller_api/units/boundaries.py +467 -469
  134. floodmodeller_api/units/comment.py +52 -55
  135. floodmodeller_api/units/conduits.py +382 -403
  136. floodmodeller_api/units/helpers.py +123 -132
  137. floodmodeller_api/units/iic.py +107 -101
  138. floodmodeller_api/units/losses.py +305 -308
  139. floodmodeller_api/units/sections.py +444 -445
  140. floodmodeller_api/units/structures.py +1690 -1684
  141. floodmodeller_api/units/units.py +93 -102
  142. floodmodeller_api/units/unsupported.py +44 -44
  143. floodmodeller_api/units/variables.py +87 -89
  144. floodmodeller_api/urban1d/__init__.py +11 -11
  145. floodmodeller_api/urban1d/_base.py +188 -177
  146. floodmodeller_api/urban1d/conduits.py +93 -85
  147. floodmodeller_api/urban1d/general_parameters.py +58 -58
  148. floodmodeller_api/urban1d/junctions.py +81 -79
  149. floodmodeller_api/urban1d/losses.py +81 -74
  150. floodmodeller_api/urban1d/outfalls.py +114 -107
  151. floodmodeller_api/urban1d/raingauges.py +111 -108
  152. floodmodeller_api/urban1d/subsections.py +92 -93
  153. floodmodeller_api/urban1d/xsections.py +147 -141
  154. floodmodeller_api/util.py +77 -21
  155. floodmodeller_api/validation/parameters.py +660 -660
  156. floodmodeller_api/validation/urban_parameters.py +388 -404
  157. floodmodeller_api/validation/validation.py +110 -112
  158. floodmodeller_api/version.py +1 -1
  159. floodmodeller_api/xml2d.py +688 -684
  160. floodmodeller_api/xml2d_template.py +37 -37
  161. floodmodeller_api/zzn.py +387 -365
  162. {floodmodeller_api-0.4.2.dist-info → floodmodeller_api-0.4.3.dist-info}/LICENSE.txt +13 -13
  163. {floodmodeller_api-0.4.2.dist-info → floodmodeller_api-0.4.3.dist-info}/METADATA +82 -82
  164. floodmodeller_api-0.4.3.dist-info/RECORD +179 -0
  165. {floodmodeller_api-0.4.2.dist-info → floodmodeller_api-0.4.3.dist-info}/WHEEL +1 -1
  166. floodmodeller_api-0.4.3.dist-info/entry_points.txt +3 -0
  167. floodmodeller_api/libifcoremd.dll +0 -0
  168. floodmodeller_api/test/test_data/EX3.bmp +0 -0
  169. floodmodeller_api/test/test_data/test_output.csv +0 -87
  170. floodmodeller_api/zzn_read.dll +0 -0
  171. floodmodeller_api-0.4.2.data/scripts/fmapi-add_siltation.bat +0 -2
  172. floodmodeller_api-0.4.2.data/scripts/fmapi-add_siltation.py +0 -3
  173. floodmodeller_api-0.4.2.data/scripts/fmapi-structure_log.bat +0 -2
  174. floodmodeller_api-0.4.2.data/scripts/fmapi-structure_log.py +0 -3
  175. floodmodeller_api-0.4.2.data/scripts/fmapi-toolbox.bat +0 -2
  176. floodmodeller_api-0.4.2.data/scripts/fmapi-toolbox.py +0 -41
  177. floodmodeller_api-0.4.2.dist-info/RECORD +0 -169
  178. {floodmodeller_api-0.4.2.dist-info → floodmodeller_api-0.4.3.dist-info}/top_level.txt +0 -0
floodmodeller_api/tool.py CHANGED
@@ -1,330 +1,332 @@
1
- import argparse
2
- import sys
3
- import tkinter as tk
4
- from dataclasses import dataclass
5
-
6
-
7
- @dataclass()
8
- class Parameter:
9
- """Class to represent an FM Tool parameter.
10
- There should be one parameter for each argument of the tool_function function
11
-
12
- Args:
13
- name (str): The name of the parameter.
14
- dtype (type): The expected data type of the parameter.
15
- description (str): A description of the parameter.
16
- help_text (str): The help text to be displayed for the parameter.
17
- required (bool): A flag indicating whether the parameter is required or optional. Default is True.
18
-
19
- Methods:
20
- __eq__: Compare two parameters by their name attribute.
21
- __hash__: Return the hash value of the parameter name.
22
- __repr__: Return a string representation of the parameter.
23
-
24
- """
25
-
26
- name: str
27
- dtype: type
28
- description: str = None
29
- help_text: str = None
30
- required: bool = True
31
-
32
- def __eq__(self, other: object) -> bool:
33
- self.name == other.name
34
-
35
- def __hash__(self):
36
- return hash(self.name)
37
-
38
- def __repr__(self):
39
- return f"Parameter({self.name})"
40
-
41
-
42
- def validate_int(value):
43
- """Function to validate integer input.
44
-
45
- Args:
46
- value (str): The input value to be validated.
47
-
48
- Returns:
49
- bool: True if input value is a valid integer or an empty string, False otherwise.
50
- """
51
- if value.isdigit():
52
- return True
53
- elif value == "":
54
- return True
55
- else:
56
- return False
57
-
58
-
59
- def validate_float(value):
60
- """Function to validate float input.
61
-
62
- Args:
63
- value (str): The input value to be validated.
64
-
65
- Returns:
66
- bool: True if input value is a valid float or an empty string, False otherwise.
67
- """
68
- try:
69
- float(value)
70
- return True
71
- except ValueError:
72
- if value == "":
73
- return True
74
- else:
75
- return False
76
-
77
-
78
- class Gui:
79
- """
80
- Method to generate a Tkinter graphical user interface (GUI).
81
- This method generates a GUI based upon a function, its parameters, as well as descriptive properties allowing any tool to be
82
- run from a GUI rather than as python code.
83
-
84
- Args:
85
- master (tkinter.Tk): a tkinter root object. Contains app methods and attributes
86
- title (str): The Apps title
87
- description (str): A description of the application
88
- parameters (list[Parameter]): a list of parameters. This is used to generate the input boxes and pass kwargs to the run function
89
- run_function (function): a function that should be run by the app
90
-
91
- """
92
-
93
- def __init__(
94
- self,
95
- master: tk.Tk,
96
- title: str,
97
- description: str,
98
- parameters: list[Parameter],
99
- run_function,
100
- ):
101
- self.master = master
102
- self.master.title(title)
103
- # self.master.resizable(False, False)
104
- self.master.geometry("400x300")
105
- self.master.configure(bg="#f5f5f5")
106
- self.parameters = parameters
107
- self.run_function = run_function
108
- self.create_widgets(description)
109
-
110
- def create_widgets(self, description):
111
- # Create and place the description label
112
- desc_label = tk.Label(self.master, text=description, font=("Arial", 14), bg="#f5f5f5")
113
- desc_label.pack(pady=(20, 10))
114
- # Run the method to add inputs based upon parameters
115
- self.add_inputs()
116
- # Create and place the button
117
- self.button = tk.Button(
118
- self.master, text="Run", font=("Arial", 14), command=self.run_gui_callback
119
- )
120
- self.button.pack(pady=10)
121
- # Add other widgets to the app
122
- ###
123
-
124
- def add_inputs(self):
125
- """
126
- Method to add inputs widgets to the app based upon parameters.
127
-
128
- This method adds an input widget to the app for each parameter.
129
- """
130
- # Extract the parameters to a list to iterate through
131
- parameters = [(param.name, param.dtype) for param in self.parameters]
132
-
133
- # Create a label and entry box for each parameter
134
- # Adding the input boxes as a class attribute dictionary
135
- # this enables us to easily get the values of in each input box and pass them to
136
- # the run function. It also makes it easier to debug since you can create an instance, generate the GUI
137
- # and then inspect the attributes.
138
- self.root_entries = {}
139
- for name, data_type in parameters:
140
- label = tk.Label(self.master, text=name, anchor="w")
141
- label.pack()
142
- # Conditional stuff to add validation for different data types.
143
- # This ensures that you can't enter text if the input should be a number, etc.
144
- if data_type == str:
145
- entry = tk.Entry(self.master)
146
- elif data_type == int:
147
- entry = tk.Entry(self.master, validate="key")
148
- entry.config(validatecommand=(entry.register(validate_int), "%P"))
149
- elif data_type == float:
150
- entry = tk.Entry(self.master, validate="key")
151
- entry.config(validatecommand=(entry.register(validate_float), "%P"))
152
- else:
153
- raise ValueError("Invalid data type")
154
- entry.pack()
155
- self.root_entries[name] = entry
156
-
157
- # TODO: Add a progress bar if appropriate
158
- # TODO: Present some useful information: either tool outputs or logs
159
-
160
- def run_gui_callback(self):
161
- """
162
- Method to run the gui callback function.
163
-
164
- This extracts the parameter values from the GUI and passes them to the run function. It is triggered using
165
- the run button in the GUI.
166
- """
167
- input_kwargs = {}
168
- for input_param in self.parameters:
169
- # Get the parameter value but subsetting the dictionary of GUI entry points (text boxes)
170
- input_var = self.root_entries[input_param.name].get()
171
- # Assert that the value (which is initially a string) is the right type
172
- # insert the value to the input_kwargs dictionary to pass to the run function
173
- input_kwargs[input_param.name] = input_param.dtype(input_var)
174
- # Run the callback function
175
- return self.run_function(**input_kwargs)
176
-
177
-
178
- class FMTool:
179
- """
180
- Compare two parameters by their name attribute.
181
-
182
- Use the class by wrapping it in a child class which defines the parameters and function to call:
183
-
184
- This class provides a consistent method to structure flood modeller tools that
185
- use the API to automate processes, extract data and visualise results. The class also provides
186
- methods to extend any tool with a command line interface or tkinter GUI.
187
-
188
- We plan to add more extensions in the future.
189
-
190
- Args:
191
- name (str): The name of the tool to display in the GUI or cmd line
192
- description (str): A description of the tool and what it does.
193
- parameters (list[Parameter]): the Tool parameters, one per input function
194
- tool_function (function): The function to be called by the tool
195
-
196
- .. code:: python
197
-
198
- # concatenates strings
199
- def concat(str1, str2):
200
- return str1 + str2
201
- class MyTool(FMTool):
202
- name = "Name"
203
- description = "Tool description"
204
- parameters = [
205
- Parameter("str1", str),
206
- Parameter("str2", str)
207
- ]
208
- tool_function = concat
209
-
210
- """
211
-
212
- parameters = []
213
-
214
- @property
215
- def name(self):
216
- """
217
- A property method to ensure a tool name is provided in child class. Overwritten by child.
218
- """
219
- raise NotImplementedError("Tools need a name")
220
-
221
- @property
222
- def description(self):
223
- """
224
- A property method to ensure a tool description is provided in child class. Overwritten by child.
225
- """
226
- raise NotImplementedError("Tools need a description")
227
-
228
- @property
229
- def tool_function(self):
230
- """
231
- A property method to ensure an tool_function is provided in child class. Overwritten by child.
232
- """
233
- raise NotImplementedError("You must provide an entry point function")
234
-
235
- def __init__(self):
236
- self.check_parameters()
237
-
238
- def check_parameters(self):
239
- """
240
- A function to check that all parameter names are unique.
241
-
242
- Since parameter names correspond to function arguments, this function is required to check that all
243
- are unique.
244
-
245
- Raises:
246
- ValueError: if parameter names aren't unique
247
- """
248
- params = []
249
- for parameter in self.parameters:
250
- if parameter.name in params:
251
- raise ValueError("Parameter names must be unique")
252
- else:
253
- params.append(parameter.name)
254
-
255
- # TODO: Explain why using a class method
256
- # This is defined as a class method because of the use of **kwargs
257
- # When using this approach to pass around function arguments, the self object is appended to the **kwargs
258
- # passed into the function and this results in the wrong number of arguments being passed to the tool_function function
259
- @classmethod
260
- def run(cls, **kwargs):
261
- """
262
- Method to run the entry point function.
263
-
264
- This approach allows the function to be run either from the command line interface, the GUI or any other extensions
265
- that we add in the future.
266
- Args:
267
- **kwargs: keyword arguments for the tool_function function.
268
- """
269
- return cls.tool_function(**kwargs)
270
-
271
- def run_from_command_line(self):
272
- """
273
- Method to run the tool from the command line.
274
-
275
- This method parses command line arguments (as defined in self.parameters) and passes them to run to execute the tool
276
- """
277
- run_gui = False
278
- try:
279
- if sys.argv[1] == "gui":
280
- # gui flag added so running as gui
281
- run_gui = True
282
- except IndexError:
283
- pass
284
-
285
- if run_gui:
286
- self.run_gui()
287
- return
288
-
289
- # Create an argument parse and add each argument defined in the parameters
290
- parser = argparse.ArgumentParser(description=self.description)
291
-
292
- # Parse the aruments from the commandline
293
- for input_param in self.parameters:
294
- parser.add_argument(
295
- f"--{input_param.name}",
296
- required=input_param.required,
297
- help=input_param.help_text,
298
- )
299
-
300
- args = parser.parse_args()
301
- # And then construct a dictionary of them that can be passed to the run function as keyword arguments
302
- input_kwargs = {}
303
- for input_param in self.parameters:
304
- value = getattr(args, input_param.name)
305
- input_kwargs[input_param.name] = input_param.dtype(value)
306
-
307
- print(f"Running {self.name}")
308
- self.run(**input_kwargs)
309
- print("Completed")
310
- # Return nothing
311
-
312
- def generate_gui(self):
313
- """
314
- Method to build the GUI
315
- """
316
- root = tk.Tk()
317
- self.app = Gui(
318
- root,
319
- title=self.name,
320
- description=self.description,
321
- parameters=self.parameters,
322
- run_function=self.run,
323
- )
324
-
325
- def run_gui(self):
326
- """
327
- Method to run the GUI
328
- """
329
- self.generate_gui()
330
- self.app.master.mainloop()
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import sys
5
+ import tkinter as tk
6
+ from dataclasses import dataclass
7
+
8
+
9
+ @dataclass()
10
+ class Parameter:
11
+ """Class to represent an FM Tool parameter.
12
+ There should be one parameter for each argument of the tool_function function
13
+
14
+ Args:
15
+ name (str): The name of the parameter.
16
+ dtype (type): The expected data type of the parameter.
17
+ description (str): A description of the parameter.
18
+ help_text (str): The help text to be displayed for the parameter.
19
+ required (bool): A flag indicating whether the parameter is required or optional. Default is True.
20
+
21
+ Methods:
22
+ __eq__: Compare two parameters by their name attribute.
23
+ __hash__: Return the hash value of the parameter name.
24
+ __repr__: Return a string representation of the parameter.
25
+
26
+ """
27
+
28
+ name: str
29
+ dtype: type
30
+ description: str | None = None
31
+ help_text: str | None = None
32
+ required: bool = True
33
+
34
+ def __eq__(self, other: object) -> bool:
35
+ if not isinstance(other, Parameter):
36
+ return NotImplemented
37
+ return self.name == other.name
38
+
39
+ def __hash__(self):
40
+ return hash(self.name)
41
+
42
+ def __repr__(self):
43
+ return f"Parameter({self.name})"
44
+
45
+
46
+ def validate_int(value):
47
+ """Function to validate integer input.
48
+
49
+ Args:
50
+ value (str): The input value to be validated.
51
+
52
+ Returns:
53
+ bool: True if input value is a valid integer or an empty string, False otherwise.
54
+ """
55
+ if value.isdigit():
56
+ return True
57
+ if value == "":
58
+ return True
59
+ return False
60
+
61
+
62
+ def validate_float(value):
63
+ """Function to validate float input.
64
+
65
+ Args:
66
+ value (str): The input value to be validated.
67
+
68
+ Returns:
69
+ bool: True if input value is a valid float or an empty string, False otherwise.
70
+ """
71
+ try:
72
+ float(value)
73
+ return True
74
+ except ValueError:
75
+ if value == "":
76
+ return True
77
+ return False
78
+
79
+
80
+ class Gui:
81
+ """
82
+ Method to generate a Tkinter graphical user interface (GUI).
83
+ This method generates a GUI based upon a function, its parameters, as well as descriptive properties allowing any tool to be
84
+ run from a GUI rather than as python code.
85
+
86
+ Args:
87
+ master (tkinter.Tk): a tkinter root object. Contains app methods and attributes
88
+ title (str): The Apps title
89
+ description (str): A description of the application
90
+ parameters (list[Parameter]): a list of parameters. This is used to generate the input boxes and pass kwargs to the run function
91
+ run_function (function): a function that should be run by the app
92
+
93
+ """
94
+
95
+ def __init__( # noqa: PLR0913
96
+ self,
97
+ master: tk.Tk,
98
+ title: str,
99
+ description: str,
100
+ parameters: list[Parameter],
101
+ run_function,
102
+ ):
103
+ self.master = master
104
+ self.master.title(title)
105
+ self.master.geometry("400x300")
106
+ self.master.configure(bg="#f5f5f5")
107
+ self.parameters = parameters
108
+ self.run_function = run_function
109
+ self.create_widgets(description)
110
+
111
+ def create_widgets(self, description):
112
+ # Create and place the description label
113
+ desc_label = tk.Label(self.master, text=description, font=("Arial", 14), bg="#f5f5f5")
114
+ desc_label.pack(pady=(20, 10))
115
+ # Run the method to add inputs based upon parameters
116
+ self.add_inputs()
117
+ # Create and place the button
118
+ self.button = tk.Button(
119
+ self.master,
120
+ text="Run",
121
+ font=("Arial", 14),
122
+ command=self.run_gui_callback,
123
+ )
124
+ self.button.pack(pady=10)
125
+ # Add other widgets to the app
126
+ ###
127
+
128
+ def add_inputs(self):
129
+ """
130
+ Method to add inputs widgets to the app based upon parameters.
131
+
132
+ This method adds an input widget to the app for each parameter.
133
+ """
134
+ # Extract the parameters to a list to iterate through
135
+ parameters = [(param.name, param.dtype) for param in self.parameters]
136
+
137
+ # Create a label and entry box for each parameter
138
+ # Adding the input boxes as a class attribute dictionary
139
+ # this enables us to easily get the values of in each input box and pass them to
140
+ # the run function. It also makes it easier to debug since you can create an instance, generate the GUI
141
+ # and then inspect the attributes.
142
+ self.root_entries = {}
143
+ for name, data_type in parameters:
144
+ label = tk.Label(self.master, text=name, anchor="w")
145
+ label.pack()
146
+ # Conditional stuff to add validation for different data types.
147
+ # This ensures that you can't enter text if the input should be a number, etc.
148
+ if data_type == str:
149
+ entry = tk.Entry(self.master)
150
+ elif data_type == int:
151
+ entry = tk.Entry(self.master, validate="key")
152
+ entry.config(validatecommand=(entry.register(validate_int), "%P"))
153
+ elif data_type == float:
154
+ entry = tk.Entry(self.master, validate="key")
155
+ entry.config(validatecommand=(entry.register(validate_float), "%P"))
156
+ else:
157
+ raise ValueError("Invalid data type")
158
+ entry.pack()
159
+ self.root_entries[name] = entry
160
+
161
+ # TODO: Add a progress bar if appropriate
162
+ # TODO: Present some useful information: either tool outputs or logs
163
+
164
+ def run_gui_callback(self):
165
+ """
166
+ Method to run the gui callback function.
167
+
168
+ This extracts the parameter values from the GUI and passes them to the run function. It is triggered using
169
+ the run button in the GUI.
170
+ """
171
+ input_kwargs = {}
172
+ for input_param in self.parameters:
173
+ # Get the parameter value but subsetting the dictionary of GUI entry points (text boxes)
174
+ input_var = self.root_entries[input_param.name].get()
175
+ # Assert that the value (which is initially a string) is the right type
176
+ # insert the value to the input_kwargs dictionary to pass to the run function
177
+ input_kwargs[input_param.name] = input_param.dtype(input_var)
178
+ # Run the callback function
179
+ return self.run_function(**input_kwargs)
180
+
181
+
182
+ class FMTool:
183
+ """
184
+ Compare two parameters by their name attribute.
185
+
186
+ Use the class by wrapping it in a child class which defines the parameters and function to call:
187
+
188
+ This class provides a consistent method to structure flood modeller tools that
189
+ use the API to automate processes, extract data and visualise results. The class also provides
190
+ methods to extend any tool with a command line interface or tkinter GUI.
191
+
192
+ We plan to add more extensions in the future.
193
+
194
+ Args:
195
+ name (str): The name of the tool to display in the GUI or cmd line
196
+ description (str): A description of the tool and what it does.
197
+ parameters (list[Parameter]): the Tool parameters, one per input function
198
+ tool_function (function): The function to be called by the tool
199
+
200
+ .. code:: python
201
+
202
+ # concatenates strings
203
+ def concat(str1, str2):
204
+ return str1 + str2
205
+ class MyTool(FMTool):
206
+ name = "Name"
207
+ description = "Tool description"
208
+ parameters = [
209
+ Parameter("str1", str),
210
+ Parameter("str2", str)
211
+ ]
212
+ tool_function = concat
213
+
214
+ """
215
+
216
+ parameters: list[Parameter] = []
217
+
218
+ @property
219
+ def name(self):
220
+ """
221
+ A property method to ensure a tool name is provided in child class. Overwritten by child.
222
+ """
223
+ raise NotImplementedError("Tools need a name")
224
+
225
+ @property
226
+ def description(self):
227
+ """
228
+ A property method to ensure a tool description is provided in child class. Overwritten by child.
229
+ """
230
+ raise NotImplementedError("Tools need a description")
231
+
232
+ @property
233
+ def tool_function(self):
234
+ """
235
+ A property method to ensure an tool_function is provided in child class. Overwritten by child.
236
+ """
237
+ raise NotImplementedError("You must provide an entry point function")
238
+
239
+ def __init__(self):
240
+ self.check_parameters()
241
+
242
+ def check_parameters(self):
243
+ """
244
+ A function to check that all parameter names are unique.
245
+
246
+ Since parameter names correspond to function arguments, this function is required to check that all
247
+ are unique.
248
+
249
+ Raises:
250
+ ValueError: if parameter names aren't unique
251
+ """
252
+ params = []
253
+ for parameter in self.parameters:
254
+ if parameter.name in params:
255
+ raise ValueError("Parameter names must be unique")
256
+ params.append(parameter.name)
257
+
258
+ # This is defined as a class method because of the use of **kwargs
259
+ # When using this approach to pass around function arguments, the self object is appended to the **kwargs
260
+ # passed into the function and this results in the wrong number of arguments being passed to the tool_function function
261
+ @classmethod
262
+ def run(cls, **kwargs):
263
+ """
264
+ Method to run the entry point function.
265
+
266
+ This approach allows the function to be run either from the command line interface, the GUI or any other extensions
267
+ that we add in the future.
268
+ Args:
269
+ **kwargs: keyword arguments for the tool_function function.
270
+ """
271
+ return cls.tool_function(**kwargs)
272
+
273
+ def run_from_command_line(self):
274
+ """
275
+ Method to run the tool from the command line.
276
+
277
+ This method parses command line arguments (as defined in self.parameters) and passes them to run to execute the tool
278
+ """
279
+ run_gui = False
280
+ try:
281
+ if sys.argv[1] == "gui":
282
+ # gui flag added so running as gui
283
+ run_gui = True
284
+ except IndexError:
285
+ pass
286
+
287
+ if run_gui:
288
+ self.run_gui()
289
+ return
290
+
291
+ # Create an argument parse and add each argument defined in the parameters
292
+ parser = argparse.ArgumentParser(description=self.description)
293
+
294
+ # Parse the aruments from the commandline
295
+ for input_param in self.parameters:
296
+ parser.add_argument(
297
+ f"--{input_param.name}",
298
+ required=input_param.required,
299
+ help=input_param.help_text,
300
+ )
301
+
302
+ args = parser.parse_args()
303
+ # And then construct a dictionary of them that can be passed to the run function as keyword arguments
304
+ input_kwargs = {}
305
+ for input_param in self.parameters:
306
+ value = getattr(args, input_param.name)
307
+ input_kwargs[input_param.name] = input_param.dtype(value)
308
+
309
+ print(f"Running {self.name}")
310
+ self.run(**input_kwargs)
311
+ print("Completed")
312
+ # Return nothing
313
+
314
+ def generate_gui(self):
315
+ """
316
+ Method to build the GUI
317
+ """
318
+ root = tk.Tk()
319
+ self.app = Gui(
320
+ root,
321
+ title=self.name,
322
+ description=self.description,
323
+ parameters=self.parameters,
324
+ run_function=self.run,
325
+ )
326
+
327
+ def run_gui(self):
328
+ """
329
+ Method to run the GUI
330
+ """
331
+ self.generate_gui()
332
+ self.app.master.mainloop()