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
@@ -1,273 +1,273 @@
1
- """
2
- Flood Modeller Python API
3
- Copyright (C) 2023 Jacobs U.K. Limited
4
-
5
- This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License
6
- as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
7
-
8
- This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
9
- of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
10
-
11
- You should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses/.
12
-
13
- If you have any query about this program or this License, please contact us at support@floodmodeller.com or write to the following
14
- address: Jacobs UK Limited, Flood Modeller, Cottons Centre, Cottons Lane, London, SE1 2QG, United Kingdom.
15
- """
16
-
17
- import filecmp
18
- import re
19
- import tempfile
20
- from datetime import datetime
21
- from hashlib import sha1
22
- from pathlib import Path
23
- from shutil import copy
24
-
25
- import pandas as pd
26
-
27
-
28
- class BackupControl:
29
- """
30
- The BackupControl class provides functionality for creating and managing file backups.
31
-
32
-
33
- Attributes:
34
- temp_dir (str): The temporary directory used for backups.
35
- backup_dirname (str): The name of the backup directory.
36
- backup_dir (str): The full path to the backup directory.
37
- backup_csv_path (str): The full path to the backup CSV file.
38
-
39
- Methods:
40
- _init_backup(): Initialises the backup directory and creates a CSV file for logging backup information.
41
- clear_backup(): Removes all backup files in the backup directory.
42
-
43
- Usage:
44
- The BackUp class can be used to create backups of files and directories. The backups are stored in a temporary
45
- directory and are logged in a CSV file. The backups can be cleared using the clear_backup method.
46
-
47
- Example:
48
- # Create a new BackUp object
49
- backup = BackUp(backup_directory_name='my_backups')
50
-
51
- # Create a backup of a file
52
- backup_file_path = '/path/to/my/file.txt'
53
- backup.backup_file(backup_file_path)
54
-
55
- # Clear all backups
56
- backup.clear_backup()
57
- """
58
-
59
- def __init__(self):
60
- """
61
- Initialises a new BackUp object.
62
-
63
- Args:
64
- backup_directory_name (str): The name of the backup directory. Defaults to "floodmodeller_api_backup".
65
- """
66
- self.temp_dir = tempfile.gettempdir()
67
- self.backup_dirname = "floodmodeller_api_backup"
68
- self.backup_dir = Path(self.temp_dir, self.backup_dirname)
69
- self.backup_csv_path = Path(self.backup_dir, "file-backups.csv")
70
- self._init_backup()
71
-
72
- def _init_backup(self):
73
- """
74
- Initialises the backup directory and creates a CSV file for logging backup information.
75
- """
76
- # Create the backup directory if it doesn't exist
77
- if not self.backup_dir.exists():
78
- self.backup_dir.mkdir()
79
- print(f"{self.__class__.__name__}: Initialised backup directory at {self.backup_dir}")
80
-
81
- # Create the backup CSV file if it doesn't exist
82
- if not self.backup_csv_path.exists():
83
- with open(self.backup_csv_path, "w") as f:
84
- f.write("path,file_id,dttm\n")
85
-
86
- def clear_backup(self, file_id="*"):
87
- """
88
- Removes all backup files in the backup directory.
89
- Args:
90
- file_id (str): The ID of the file to clear, default value is "*" to clear all files
91
- If this is called from the file class then the file Id of that file will be used
92
- """
93
- # If the user wants to clear a specific file, then suffix * to match it
94
- if file_id != "*":
95
- file_id = f"{file_id}*"
96
-
97
- files = self.backup_dir.glob(file_id)
98
- for f in files:
99
- Path(f).unlink()
100
-
101
-
102
- def parse_backup_dttm(path):
103
- # Extract datetime from string
104
- datetime_str = re.search(r"\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}", path).group(0)
105
- # Convert datetime string to datetime object
106
- datetime_obj = datetime.strptime(datetime_str, "%Y-%m-%d-%H-%M-%S")
107
- return datetime_obj
108
-
109
-
110
- class BackupFile:
111
- """
112
- Defines a backed up file and functionality to restore it
113
-
114
- Args:
115
- path (str): The path to the file that you want to back up or retrieve.
116
- file_id (str): A unique identifier for the file generated by hashing its absolute path.
117
-
118
- Attributes:
119
- path (Path): The absolute path to the original file.
120
- dttm (str): The datetime that the original file was loaded and backed up in the format '%Y-%m-%d-%H-%M-%S'.
121
- Identifies a unique backup.
122
- file_id (str): A unique identifier for the file generated by hashing its absolute path.
123
-
124
- """
125
-
126
- def __init__(self, file_id: str, path: str):
127
- self.file_id = file_id
128
- self.path = path
129
- self.dttm = parse_backup_dttm(str(path))
130
-
131
- def restore(self, to):
132
- """
133
- Restore the file from the last backup if one exists.
134
-
135
- Args:
136
- to (str): The path to where you want to restore the file.
137
- """
138
- copy(self.path, to)
139
-
140
- def __repr__(self):
141
- return f"BackupFile(file_id={self.file_id}, path = {self.path})"
142
-
143
-
144
- class File(BackupControl):
145
- """
146
- Provides functionality to backup files and retrieve them.
147
-
148
- Args:
149
- path (str): The path to the file that you want to back up or retrieve.
150
-
151
- Attributes:
152
- path (Path): The absolute path to the original file.
153
- ext (str): The file extension.
154
- dttm_str (str): The current date and time as a string in the format '%Y-%m-%d-%H-%M-%S'.
155
- file_id (str): A unique identifier for the file generated by hashing its absolute path.
156
- backup_dir (str): The path to the directory where backup files will be saved.
157
- backup_filename (str): The name of the backup file, constructed from the unique file id, the datetime it was loaded and the extension.
158
- backup_csv_path (str): The path to the backup CSV file where information about each backup is logged.
159
-
160
- Methods:
161
- backup(self) -> None:
162
- Makes a backup of the file. Backups are saved in the users Temporary Files (see `tempfile.gettempdir()` or `File.backup_dir`).
163
-
164
- restore(self, to:str) -> None:
165
- Restores the file from the last backup if one exists.
166
-
167
- _generate_file_id(self) -> None:
168
- Generates the file's unique identifier as using a hash of the absolute file path.
169
-
170
- _generate_file_name(self) -> None:
171
- Generates the name of the file, constructed from the unique file id, the datetime it was loaded and the extension.
172
- This allows the file to be backed up as a unique version of a particular file at a particular time.
173
-
174
- _make_backup(self) -> None:
175
- Makes a backup of the file. This function copies the file to the backup directory with a unique filename.
176
- It also logs a row in the backup csv file to help find a particular backup when inspecting the file system.
177
-
178
- list_backups(self) -> List[str]:
179
- Lists backed up versions of the File, ordered from newest to oldest.
180
-
181
- Note:
182
- Inherits from the BackUp class.
183
-
184
- Example:
185
- >>> file = File('path/to/my/file.txt')
186
- >>> file.backup()
187
- >>> file.restore('path/to/my/restored_file.txt')
188
- """
189
-
190
- def __init__(self, path: str, **args):
191
- # TODO: Make protected properties so they can't be manipulated
192
- self.path = Path(path)
193
- # Check if the file exists
194
- if not self.path.exists():
195
- raise OSError("File not found!")
196
- self.ext = self.path.suffix
197
- self.dttm_str = datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
198
- self._generate_file_id()
199
- self._generate_file_name()
200
- super().__init__(**args)
201
-
202
- def __repr__(self):
203
- """Print method"""
204
- return f"Flood Modeller {self.ext} File\nPath: {self.path.__str__()}\nID: {self.file_id}"
205
-
206
- def _generate_file_id(self):
207
- """
208
- Generate the file's unique identifier as using a hash of the absolute file path
209
- """
210
- # hash the absolute path becuase the same file name / directroy structure may be mirrored across projects
211
- # TODO: Use a function that produces a shorter has to make interpretation of the directory easier
212
- fp_bytes = self.path.absolute().__str__().encode()
213
- self.file_id = sha1(fp_bytes).hexdigest()
214
-
215
- def _generate_file_name(self) -> None:
216
- """
217
- Generate the name of the file, constructed from the unique file id, the datetime it was loaded and the extension.
218
- This allows the file to be backed up as a unique version of a particular file at a particular time.
219
- """
220
- self.backup_filename = self.file_id + "_" + self.dttm_str + self.ext
221
-
222
- def _make_backup(self) -> None:
223
- """
224
- Make a backup of the file. This function copies the file to the backup directory with a unique filename.
225
- It also logs a row in the backup csv file to help find a particular backup when inspecting the file system.
226
- """
227
- # Construct the path and copy the file
228
- backup_filepath = Path(self.backup_dir, self.backup_filename)
229
- copy(self.path, backup_filepath)
230
- # Log an entry to the csv to make it easy to find the file
231
- # TODO: Only log file_id and poath, don't log duplicate lines. Needs to be fast so it doesn't slow FMFile down
232
- log_str = f"{self.path.__str__()},{self.file_id},{self.dttm_str}\n"
233
- with open(self.backup_csv_path, "a") as f:
234
- f.write(log_str)
235
-
236
- def list_backups(self) -> list:
237
- """
238
- List backed up versions of the File, ordered from newest to oldest.
239
- """
240
- backup_files = list(self.backup_dir.glob(f"{self.file_id}*"))
241
- backup_files.sort(reverse=True)
242
- if len(backup_files) > 0:
243
- return [BackupFile(file_id=self.file_id, path=path) for path in backup_files]
244
- else:
245
- return []
246
-
247
- def backup(self) -> None:
248
- """
249
- High level method to make a backup of the file.
250
- This function will make a backup of a file, only if there isn't already an equivalent backup in the temporary folder.
251
- Backups are saved in the users Temporary Files (see `tempfile.gettempdir()` or `File.backup_dir`).
252
- """
253
- # get the backups of that file
254
- backups = self.list_backups()
255
- # If there aren't any backups then backup the file
256
- if len(backups) == 0:
257
- self._make_backup()
258
- # If the file doesn't match the last backup then do a back up
259
- # TODO: Use FloodModeller API implemented equivalence testing. This is implemented at a higher level than FMFile where this method is called.
260
- elif not (filecmp.cmp(self.path, backups[0].path)):
261
- self._make_backup()
262
- # TODO: Return the file path?
263
-
264
- def clear_backup(self):
265
- """
266
- Clears all backups for the file and removes entries from the logs
267
- """
268
- # Clear the backup
269
- super().clear_backup(file_id=self.file_id)
270
- # Drop the files entries from the log
271
- backup_logs = pd.read_csv(self.backup_csv_path)
272
- backup_logs = backup_logs[backup_logs.file_id != self.file_id]
273
- backup_logs.to_csv(self.backup_csv_path, index=False)
1
+ """
2
+ Flood Modeller Python API
3
+ Copyright (C) 2024 Jacobs U.K. Limited
4
+
5
+ This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License
6
+ as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
7
+
8
+ This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
9
+ of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
10
+
11
+ You should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses/.
12
+
13
+ If you have any query about this program or this License, please contact us at support@floodmodeller.com or write to the following
14
+ address: Jacobs UK Limited, Flood Modeller, Cottons Centre, Cottons Lane, London, SE1 2QG, United Kingdom.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import filecmp
20
+ import re
21
+ import tempfile
22
+ from datetime import datetime
23
+ from hashlib import sha1
24
+ from pathlib import Path
25
+ from shutil import copy
26
+
27
+ import pandas as pd
28
+
29
+ from .to_from_json import Jsonable
30
+
31
+
32
+ class BackupControl(Jsonable):
33
+ """
34
+ The BackupControl class provides functionality for creating and managing file backups.
35
+
36
+
37
+ Attributes:
38
+ temp_dir (str): The temporary directory used for backups.
39
+ backup_dirname (str): The name of the backup directory.
40
+ backup_dir (str): The full path to the backup directory.
41
+ backup_csv_path (str): The full path to the backup CSV file.
42
+
43
+ Methods:
44
+ _init_backup(): Initialises the backup directory and creates a CSV file for logging backup information.
45
+ clear_backup(): Removes all backup files in the backup directory.
46
+
47
+ Usage:
48
+ The BackUp class can be used to create backups of files and directories. The backups are stored in a temporary
49
+ directory and are logged in a CSV file. The backups can be cleared using the clear_backup method.
50
+
51
+ Example:
52
+ # Create a new BackUp object
53
+ backup = BackUp(backup_directory_name='my_backups')
54
+
55
+ # Create a backup of a file
56
+ backup_file_path = '/path/to/my/file.txt'
57
+ backup.backup_file(backup_file_path)
58
+
59
+ # Clear all backups
60
+ backup.clear_backup()
61
+ """
62
+
63
+ def __init__(self):
64
+ """
65
+ Initialises a new BackUp object.
66
+
67
+ Args:
68
+ backup_directory_name (str): The name of the backup directory. Defaults to "floodmodeller_api_backup".
69
+ """
70
+ self.temp_dir = tempfile.gettempdir()
71
+ self.backup_dirname = "floodmodeller_api_backup"
72
+ self.backup_dir = Path(self.temp_dir, self.backup_dirname)
73
+ self.backup_csv_path = Path(self.backup_dir, "file-backups.csv")
74
+ self._init_backup()
75
+
76
+ def _init_backup(self):
77
+ """
78
+ Initialises the backup directory and creates a CSV file for logging backup information.
79
+ """
80
+ # Create the backup directory if it doesn't exist
81
+ if not self.backup_dir.exists():
82
+ self.backup_dir.mkdir()
83
+ print(f"{self.__class__.__name__}: Initialised backup directory at {self.backup_dir}")
84
+
85
+ # Create the backup CSV file if it doesn't exist
86
+ if not self.backup_csv_path.exists():
87
+ with open(self.backup_csv_path, "w") as f:
88
+ f.write("path,file_id,dttm\n")
89
+
90
+ def clear_backup(self, file_id="*"):
91
+ """
92
+ Removes all backup files in the backup directory.
93
+ Args:
94
+ file_id (str): The ID of the file to clear, default value is "*" to clear all files
95
+ If this is called from the file class then the file Id of that file will be used
96
+ """
97
+ # If the user wants to clear a specific file, then suffix * to match it
98
+ if file_id != "*":
99
+ file_id = f"{file_id}*"
100
+
101
+ files = self.backup_dir.glob(file_id)
102
+ for f in files:
103
+ Path(f).unlink()
104
+
105
+
106
+ def parse_backup_dttm(path):
107
+ # Extract datetime from string
108
+ datetime_str = re.search(r"\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}", path).group(0)
109
+ # Convert datetime string to datetime object
110
+ return datetime.strptime(datetime_str, "%Y-%m-%d-%H-%M-%S")
111
+
112
+
113
+ class BackupFile:
114
+ """
115
+ Defines a backed up file and functionality to restore it
116
+
117
+ Args:
118
+ path (str): The path to the file that you want to back up or retrieve.
119
+ file_id (str): A unique identifier for the file generated by hashing its absolute path.
120
+
121
+ Attributes:
122
+ path (Path): The absolute path to the original file.
123
+ dttm (str): The datetime that the original file was loaded and backed up in the format '%Y-%m-%d-%H-%M-%S'.
124
+ Identifies a unique backup.
125
+ file_id (str): A unique identifier for the file generated by hashing its absolute path.
126
+
127
+ """
128
+
129
+ def __init__(self, file_id: str, path: str):
130
+ self.file_id = file_id
131
+ self.path = path
132
+ self.dttm = parse_backup_dttm(str(path))
133
+
134
+ def restore(self, to):
135
+ """
136
+ Restore the file from the last backup if one exists.
137
+
138
+ Args:
139
+ to (str): The path to where you want to restore the file.
140
+ """
141
+ copy(self.path, to)
142
+
143
+ def __repr__(self):
144
+ return f"BackupFile(file_id={self.file_id}, path = {self.path})"
145
+
146
+
147
+ class File(BackupControl):
148
+ """
149
+ Provides functionality to backup files and retrieve them.
150
+
151
+ Args:
152
+ path (str): The path to the file that you want to back up or retrieve.
153
+
154
+ Attributes:
155
+ path (Path): The absolute path to the original file.
156
+ ext (str): The file extension.
157
+ dttm_str (str): The current date and time as a string in the format '%Y-%m-%d-%H-%M-%S'.
158
+ file_id (str): A unique identifier for the file generated by hashing its absolute path.
159
+ backup_dir (str): The path to the directory where backup files will be saved.
160
+ backup_filename (str): The name of the backup file, constructed from the unique file id, the datetime it was loaded and the extension.
161
+ backup_csv_path (str): The path to the backup CSV file where information about each backup is logged.
162
+
163
+ Methods:
164
+ backup(self) -> None:
165
+ Makes a backup of the file. Backups are saved in the users Temporary Files (see `tempfile.gettempdir()` or `File.backup_dir`).
166
+
167
+ restore(self, to:str) -> None:
168
+ Restores the file from the last backup if one exists.
169
+
170
+ _generate_file_id(self) -> None:
171
+ Generates the file's unique identifier as using a hash of the absolute file path.
172
+
173
+ _generate_file_name(self) -> None:
174
+ Generates the name of the file, constructed from the unique file id, the datetime it was loaded and the extension.
175
+ This allows the file to be backed up as a unique version of a particular file at a particular time.
176
+
177
+ _make_backup(self) -> None:
178
+ Makes a backup of the file. This function copies the file to the backup directory with a unique filename.
179
+ It also logs a row in the backup csv file to help find a particular backup when inspecting the file system.
180
+
181
+ list_backups(self) -> List[str]:
182
+ Lists backed up versions of the File, ordered from newest to oldest.
183
+
184
+ Note:
185
+ Inherits from the BackUp class.
186
+
187
+ Example:
188
+ >>> file = File('path/to/my/file.txt')
189
+ >>> file.backup()
190
+ >>> file.restore('path/to/my/restored_file.txt')
191
+ """
192
+
193
+ def __init__(self, path: str | Path = "", from_json: bool = False, **args):
194
+ # TODO: Make protected properties so they can't be manipulated
195
+ self.path = Path(path)
196
+ # Check if the file exists
197
+ if not self.path.exists():
198
+ raise OSError("File not found!")
199
+ self.ext = self.path.suffix
200
+ self.dttm_str = datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
201
+ self._generate_file_id()
202
+ self._generate_file_name()
203
+ super().__init__(**args)
204
+
205
+ def __repr__(self):
206
+ """Print method"""
207
+ return f"Flood Modeller {self.ext} File\nPath: {self.path.__str__()}\nID: {self.file_id}"
208
+
209
+ def _generate_file_id(self):
210
+ """
211
+ Generate the file's unique identifier as using a hash of the absolute file path
212
+ """
213
+ # hash the absolute path becuase the same file name / directroy structure may be mirrored across projects
214
+ # TODO: Use a function that produces a shorter has to make interpretation of the directory easier
215
+ fp_bytes = str(self.path.absolute()).encode()
216
+ self.file_id = sha1(fp_bytes).hexdigest()
217
+
218
+ def _generate_file_name(self) -> None:
219
+ """
220
+ Generate the name of the file, constructed from the unique file id, the datetime it was loaded and the extension.
221
+ This allows the file to be backed up as a unique version of a particular file at a particular time.
222
+ """
223
+ self.backup_filename = self.file_id + "_" + self.dttm_str + self.ext
224
+
225
+ def _make_backup(self) -> None:
226
+ """
227
+ Make a backup of the file. This function copies the file to the backup directory with a unique filename.
228
+ It also logs a row in the backup csv file to help find a particular backup when inspecting the file system.
229
+ """
230
+ # Construct the path and copy the file
231
+ backup_filepath = Path(self.backup_dir, self.backup_filename)
232
+ copy(self.path, backup_filepath)
233
+ # Log an entry to the csv to make it easy to find the file
234
+ # TODO: Only log file_id and poath, don't log duplicate lines. Needs to be fast so it doesn't slow FMFile down
235
+ log_str = f"{str(self.path)},{self.file_id},{self.dttm_str}\n"
236
+ with open(self.backup_csv_path, "a") as f:
237
+ f.write(log_str)
238
+
239
+ def list_backups(self) -> list:
240
+ """
241
+ List backed up versions of the File, ordered from newest to oldest.
242
+ """
243
+ backup_files = list(self.backup_dir.glob(f"{self.file_id}*"))
244
+ backup_files.sort(reverse=True)
245
+ if len(backup_files) <= 0:
246
+ return []
247
+ return [BackupFile(file_id=self.file_id, path=path) for path in backup_files]
248
+
249
+ def backup(self) -> None:
250
+ """
251
+ High level method to make a backup of the file.
252
+ This function will make a backup of a file, only if there isn't already an equivalent backup in the temporary folder.
253
+ Backups are saved in the users Temporary Files (see `tempfile.gettempdir()` or `File.backup_dir`).
254
+ """
255
+ # get the backups of that file
256
+ backups = self.list_backups()
257
+ # If there aren't any backups then backup the file
258
+ if len(backups) == 0 or not filecmp.cmp(self.path, backups[0].path):
259
+ self._make_backup()
260
+ # If the file doesn't match the last backup then do a back up
261
+ # TODO: Use FloodModeller API implemented equivalence testing. This is implemented at a higher level than FMFile where this method is called.
262
+ # TODO: Return the file path?
263
+
264
+ def clear_backup(self):
265
+ """
266
+ Clears all backups for the file and removes entries from the logs
267
+ """
268
+ # Clear the backup
269
+ super().clear_backup(file_id=self.file_id)
270
+ # Drop the files entries from the log
271
+ backup_logs = pd.read_csv(self.backup_csv_path)
272
+ backup_logs = backup_logs[backup_logs.file_id != self.file_id]
273
+ backup_logs.to_csv(self.backup_csv_path, index=False)