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