ras-commander 0.76.0__py3-none-any.whl → 0.78.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ras_commander/Decorators.py +256 -256
- ras_commander/HdfInfiltration.py +1529 -1529
- ras_commander/HdfResultsPlan.py +380 -380
- ras_commander/RasExamples.py +543 -424
- ras_commander/RasGeo.py +3 -3
- ras_commander/RasMap.py +467 -252
- ras_commander/RasPlan.py +1536 -1536
- ras_commander/RasPrj.py +1479 -1474
- ras_commander/__init__.py +1 -1
- {ras_commander-0.76.0.dist-info → ras_commander-0.78.0.dist-info}/METADATA +17 -12
- {ras_commander-0.76.0.dist-info → ras_commander-0.78.0.dist-info}/RECORD +14 -14
- {ras_commander-0.76.0.dist-info → ras_commander-0.78.0.dist-info}/WHEEL +1 -1
- {ras_commander-0.76.0.dist-info → ras_commander-0.78.0.dist-info}/licenses/LICENSE +0 -0
- {ras_commander-0.76.0.dist-info → ras_commander-0.78.0.dist-info}/top_level.txt +0 -0
ras_commander/RasExamples.py
CHANGED
@@ -1,424 +1,543 @@
|
|
1
|
-
"""
|
2
|
-
RasExamples - Manage and load HEC-RAS example projects for testing and development
|
3
|
-
|
4
|
-
This module is part of the ras-commander library and uses a centralized logging configuration.
|
5
|
-
|
6
|
-
Logging Configuration:
|
7
|
-
- The logging is set up in the logging_config.py file.
|
8
|
-
- A @log_call decorator is available to automatically log function calls.
|
9
|
-
- Log levels: DEBUG, INFO, WARNING, ERROR, CRITICAL
|
10
|
-
- Logs are written to both console and a rotating file handler.
|
11
|
-
- The default log file is 'ras_commander.log' in the 'logs' directory.
|
12
|
-
- The default log level is INFO.
|
13
|
-
|
14
|
-
To use logging in this module:
|
15
|
-
1. Use the @log_call decorator for automatic function call logging.
|
16
|
-
2. For additional logging, use logger.[level]() calls (e.g., logger.info(), logger.debug()).
|
17
|
-
3. Obtain the logger using: logger = logging.getLogger(__name__)
|
18
|
-
|
19
|
-
Example:
|
20
|
-
@log_call
|
21
|
-
def my_function():
|
22
|
-
logger = logging.getLogger(__name__)
|
23
|
-
logger.debug("Additional debug information")
|
24
|
-
# Function logic here
|
25
|
-
|
26
|
-
|
27
|
-
-----
|
28
|
-
|
29
|
-
All of the methods in this class are static and are designed to be used without instantiation.
|
30
|
-
|
31
|
-
List of Functions in RasExamples:
|
32
|
-
- get_example_projects()
|
33
|
-
- list_categories()
|
34
|
-
- list_projects()
|
35
|
-
- extract_project()
|
36
|
-
- is_project_extracted()
|
37
|
-
- clean_projects_directory()
|
38
|
-
|
39
|
-
"""
|
40
|
-
import os
|
41
|
-
import requests
|
42
|
-
import zipfile
|
43
|
-
import pandas as pd
|
44
|
-
from pathlib import Path
|
45
|
-
import shutil
|
46
|
-
from typing import Union, List
|
47
|
-
import csv
|
48
|
-
from datetime import datetime
|
49
|
-
import logging
|
50
|
-
import re
|
51
|
-
from tqdm import tqdm
|
52
|
-
from ras_commander import get_logger
|
53
|
-
from ras_commander.LoggingConfig import log_call
|
54
|
-
|
55
|
-
logger = get_logger(__name__)
|
56
|
-
|
57
|
-
class RasExamples:
|
58
|
-
"""
|
59
|
-
A class for quickly loading HEC-RAS example projects for testing and development of ras-commander.
|
60
|
-
All methods are class methods, so no initialization is required.
|
61
|
-
"""
|
62
|
-
base_url = 'https://github.com/HydrologicEngineeringCenter/hec-downloads/releases/download/'
|
63
|
-
valid_versions = [
|
64
|
-
"6.6", "6.5", "6.4.1", "6.3.1", "6.3", "6.2", "6.1", "6.0",
|
65
|
-
"5.0.7", "5.0.6", "5.0.5", "5.0.4", "5.0.3", "5.0.1", "5.0",
|
66
|
-
"4.1", "4.0", "3.1.3", "3.1.2", "3.1.1", "3.0", "2.2"
|
67
|
-
]
|
68
|
-
base_dir = Path.cwd()
|
69
|
-
examples_dir = base_dir
|
70
|
-
projects_dir = examples_dir / 'example_projects'
|
71
|
-
csv_file_path = examples_dir / 'example_projects.csv'
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
def
|
93
|
-
"""
|
94
|
-
|
95
|
-
self.
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
logger.
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
"
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
continue
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
logger.
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
"""
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
""
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
1
|
+
"""
|
2
|
+
RasExamples - Manage and load HEC-RAS example projects for testing and development
|
3
|
+
|
4
|
+
This module is part of the ras-commander library and uses a centralized logging configuration.
|
5
|
+
|
6
|
+
Logging Configuration:
|
7
|
+
- The logging is set up in the logging_config.py file.
|
8
|
+
- A @log_call decorator is available to automatically log function calls.
|
9
|
+
- Log levels: DEBUG, INFO, WARNING, ERROR, CRITICAL
|
10
|
+
- Logs are written to both console and a rotating file handler.
|
11
|
+
- The default log file is 'ras_commander.log' in the 'logs' directory.
|
12
|
+
- The default log level is INFO.
|
13
|
+
|
14
|
+
To use logging in this module:
|
15
|
+
1. Use the @log_call decorator for automatic function call logging.
|
16
|
+
2. For additional logging, use logger.[level]() calls (e.g., logger.info(), logger.debug()).
|
17
|
+
3. Obtain the logger using: logger = logging.getLogger(__name__)
|
18
|
+
|
19
|
+
Example:
|
20
|
+
@log_call
|
21
|
+
def my_function():
|
22
|
+
logger = logging.getLogger(__name__)
|
23
|
+
logger.debug("Additional debug information")
|
24
|
+
# Function logic here
|
25
|
+
|
26
|
+
|
27
|
+
-----
|
28
|
+
|
29
|
+
All of the methods in this class are static and are designed to be used without instantiation.
|
30
|
+
|
31
|
+
List of Functions in RasExamples:
|
32
|
+
- get_example_projects()
|
33
|
+
- list_categories()
|
34
|
+
- list_projects()
|
35
|
+
- extract_project()
|
36
|
+
- is_project_extracted()
|
37
|
+
- clean_projects_directory()
|
38
|
+
|
39
|
+
"""
|
40
|
+
import os
|
41
|
+
import requests
|
42
|
+
import zipfile
|
43
|
+
import pandas as pd
|
44
|
+
from pathlib import Path
|
45
|
+
import shutil
|
46
|
+
from typing import Union, List
|
47
|
+
import csv
|
48
|
+
from datetime import datetime
|
49
|
+
import logging
|
50
|
+
import re
|
51
|
+
from tqdm import tqdm
|
52
|
+
from ras_commander import get_logger
|
53
|
+
from ras_commander.LoggingConfig import log_call
|
54
|
+
|
55
|
+
logger = get_logger(__name__)
|
56
|
+
|
57
|
+
class RasExamples:
|
58
|
+
"""
|
59
|
+
A class for quickly loading HEC-RAS example projects for testing and development of ras-commander.
|
60
|
+
All methods are class methods, so no initialization is required.
|
61
|
+
"""
|
62
|
+
base_url = 'https://github.com/HydrologicEngineeringCenter/hec-downloads/releases/download/'
|
63
|
+
valid_versions = [
|
64
|
+
"6.6", "6.5", "6.4.1", "6.3.1", "6.3", "6.2", "6.1", "6.0",
|
65
|
+
"5.0.7", "5.0.6", "5.0.5", "5.0.4", "5.0.3", "5.0.1", "5.0",
|
66
|
+
"4.1", "4.0", "3.1.3", "3.1.2", "3.1.1", "3.0", "2.2"
|
67
|
+
]
|
68
|
+
base_dir = Path.cwd()
|
69
|
+
examples_dir = base_dir
|
70
|
+
projects_dir = examples_dir / 'example_projects'
|
71
|
+
csv_file_path = examples_dir / 'example_projects.csv'
|
72
|
+
|
73
|
+
# Special projects that are not in the main zip file
|
74
|
+
SPECIAL_PROJECTS = {
|
75
|
+
'NewOrleansMetro': 'https://www.hec.usace.army.mil/confluence/rasdocs/hgt/files/latest/299502039/299502111/1/1747692522764/NewOrleansMetroPipesExample.zip',
|
76
|
+
'BeaverLake': 'https://www.hec.usace.army.mil/confluence/rasdocs/hgt/files/latest/299501780/299502090/1/1747692179014/BeaverLake-SWMM-Import-Solution.zip'
|
77
|
+
}
|
78
|
+
|
79
|
+
_folder_df = None
|
80
|
+
_zip_file_path = None
|
81
|
+
|
82
|
+
def __init__(self):
|
83
|
+
"""Initialize RasExamples and ensure data is loaded"""
|
84
|
+
self._ensure_initialized()
|
85
|
+
|
86
|
+
@property
|
87
|
+
def folder_df(self):
|
88
|
+
"""Access the folder DataFrame"""
|
89
|
+
self._ensure_initialized()
|
90
|
+
return self._folder_df
|
91
|
+
|
92
|
+
def _ensure_initialized(self):
|
93
|
+
"""Ensure the class is initialized with required data"""
|
94
|
+
self.projects_dir.mkdir(parents=True, exist_ok=True)
|
95
|
+
if self._folder_df is None:
|
96
|
+
self._load_project_data()
|
97
|
+
|
98
|
+
def _load_project_data(self):
|
99
|
+
"""Load project data from CSV if up-to-date, otherwise extract from zip."""
|
100
|
+
logger.debug("Loading project data")
|
101
|
+
self._find_zip_file()
|
102
|
+
|
103
|
+
if not self._zip_file_path:
|
104
|
+
logger.info("No example projects zip file found. Downloading...")
|
105
|
+
self.get_example_projects()
|
106
|
+
|
107
|
+
try:
|
108
|
+
zip_modified_time = os.path.getmtime(self._zip_file_path)
|
109
|
+
except FileNotFoundError:
|
110
|
+
logger.error(f"Zip file not found at {self._zip_file_path}.")
|
111
|
+
return
|
112
|
+
|
113
|
+
if self.csv_file_path.exists():
|
114
|
+
csv_modified_time = os.path.getmtime(self.csv_file_path)
|
115
|
+
|
116
|
+
if csv_modified_time >= zip_modified_time:
|
117
|
+
logger.info("Loading project data from CSV...")
|
118
|
+
try:
|
119
|
+
self._folder_df = pd.read_csv(self.csv_file_path)
|
120
|
+
logger.info(f"Loaded {len(self._folder_df)} projects from CSV.")
|
121
|
+
return
|
122
|
+
except Exception as e:
|
123
|
+
logger.error(f"Failed to read CSV file: {e}")
|
124
|
+
self._folder_df = None
|
125
|
+
|
126
|
+
logger.info("Extracting folder structure from zip file...")
|
127
|
+
self._extract_folder_structure()
|
128
|
+
self._save_to_csv()
|
129
|
+
|
130
|
+
@classmethod
|
131
|
+
def extract_project(cls, project_names: Union[str, List[str]]) -> Union[Path, List[Path]]:
|
132
|
+
"""Extract one or more specific HEC-RAS projects from the zip file.
|
133
|
+
|
134
|
+
Args:
|
135
|
+
project_names: Single project name as string or list of project names
|
136
|
+
|
137
|
+
Returns:
|
138
|
+
Path: Single Path object if one project extracted
|
139
|
+
List[Path]: List of Path objects if multiple projects extracted
|
140
|
+
"""
|
141
|
+
logger.debug(f"Extracting projects: {project_names}")
|
142
|
+
|
143
|
+
# Initialize if needed
|
144
|
+
if cls._folder_df is None:
|
145
|
+
cls._find_zip_file()
|
146
|
+
if not cls._zip_file_path:
|
147
|
+
logger.info("No example projects zip file found. Downloading...")
|
148
|
+
cls.get_example_projects()
|
149
|
+
cls._load_project_data()
|
150
|
+
|
151
|
+
if isinstance(project_names, str):
|
152
|
+
project_names = [project_names]
|
153
|
+
|
154
|
+
extracted_paths = []
|
155
|
+
|
156
|
+
for project_name in project_names:
|
157
|
+
# Check if this is a special project
|
158
|
+
if project_name in cls.SPECIAL_PROJECTS:
|
159
|
+
try:
|
160
|
+
special_path = cls._extract_special_project(project_name)
|
161
|
+
extracted_paths.append(special_path)
|
162
|
+
continue
|
163
|
+
except Exception as e:
|
164
|
+
logger.error(f"Failed to extract special project '{project_name}': {e}")
|
165
|
+
continue
|
166
|
+
|
167
|
+
# Regular project extraction logic
|
168
|
+
logger.info("----- RasExamples Extracting Project -----")
|
169
|
+
logger.info(f"Extracting project '{project_name}'")
|
170
|
+
project_path = cls.projects_dir
|
171
|
+
|
172
|
+
if (project_path / project_name).exists():
|
173
|
+
logger.info(f"Project '{project_name}' already exists. Deleting existing folder...")
|
174
|
+
try:
|
175
|
+
shutil.rmtree(project_path / project_name)
|
176
|
+
logger.info(f"Existing folder for project '{project_name}' has been deleted.")
|
177
|
+
except Exception as e:
|
178
|
+
logger.error(f"Failed to delete existing project folder '{project_name}': {e}")
|
179
|
+
continue
|
180
|
+
|
181
|
+
project_info = cls._folder_df[cls._folder_df['Project'] == project_name]
|
182
|
+
if project_info.empty:
|
183
|
+
error_msg = f"Project '{project_name}' not found in the zip file."
|
184
|
+
logger.error(error_msg)
|
185
|
+
raise ValueError(error_msg)
|
186
|
+
|
187
|
+
try:
|
188
|
+
with zipfile.ZipFile(cls._zip_file_path, 'r') as zip_ref:
|
189
|
+
for file in zip_ref.namelist():
|
190
|
+
parts = Path(file).parts
|
191
|
+
if len(parts) > 1 and parts[1] == project_name:
|
192
|
+
relative_path = Path(*parts[1:])
|
193
|
+
extract_path = project_path / relative_path
|
194
|
+
if file.endswith('/'):
|
195
|
+
extract_path.mkdir(parents=True, exist_ok=True)
|
196
|
+
else:
|
197
|
+
extract_path.parent.mkdir(parents=True, exist_ok=True)
|
198
|
+
with zip_ref.open(file) as source, open(extract_path, "wb") as target:
|
199
|
+
shutil.copyfileobj(source, target)
|
200
|
+
|
201
|
+
logger.info(f"Successfully extracted project '{project_name}' to {project_path / project_name}")
|
202
|
+
extracted_paths.append(project_path / project_name)
|
203
|
+
except Exception as e:
|
204
|
+
logger.error(f"An error occurred while extracting project '{project_name}': {str(e)}")
|
205
|
+
|
206
|
+
# Return single path if only one project was extracted, otherwise return list
|
207
|
+
return extracted_paths[0] if len(project_names) == 1 else extracted_paths
|
208
|
+
|
209
|
+
@classmethod
|
210
|
+
def _find_zip_file(cls):
|
211
|
+
"""Locate the example projects zip file in the examples directory."""
|
212
|
+
for version in cls.valid_versions:
|
213
|
+
potential_zip = cls.examples_dir / f"Example_Projects_{version.replace('.', '_')}.zip"
|
214
|
+
if potential_zip.exists():
|
215
|
+
cls._zip_file_path = potential_zip
|
216
|
+
logger.info(f"Found zip file: {cls._zip_file_path}")
|
217
|
+
break
|
218
|
+
else:
|
219
|
+
logger.warning("No existing example projects zip file found.")
|
220
|
+
|
221
|
+
@classmethod
|
222
|
+
def get_example_projects(cls, version_number='6.6'):
|
223
|
+
"""
|
224
|
+
Download and extract HEC-RAS example projects for a specified version.
|
225
|
+
"""
|
226
|
+
logger.info(f"Getting example projects for version {version_number}")
|
227
|
+
if version_number not in cls.valid_versions:
|
228
|
+
error_msg = f"Invalid version number. Valid versions are: {', '.join(cls.valid_versions)}"
|
229
|
+
logger.error(error_msg)
|
230
|
+
raise ValueError(error_msg)
|
231
|
+
|
232
|
+
zip_url = f"{cls.base_url}1.0.33/Example_Projects_{version_number.replace('.', '_')}.zip"
|
233
|
+
|
234
|
+
cls.examples_dir.mkdir(parents=True, exist_ok=True)
|
235
|
+
|
236
|
+
cls._zip_file_path = cls.examples_dir / f"Example_Projects_{version_number.replace('.', '_')}.zip"
|
237
|
+
|
238
|
+
if not cls._zip_file_path.exists():
|
239
|
+
logger.info(f"Downloading HEC-RAS Example Projects from {zip_url}. \nThe file is over 400 MB, so it may take a few minutes to download....")
|
240
|
+
try:
|
241
|
+
response = requests.get(zip_url, stream=True)
|
242
|
+
response.raise_for_status()
|
243
|
+
with open(cls._zip_file_path, 'wb') as file:
|
244
|
+
shutil.copyfileobj(response.raw, file)
|
245
|
+
logger.info(f"Downloaded to {cls._zip_file_path}")
|
246
|
+
except requests.exceptions.RequestException as e:
|
247
|
+
logger.error(f"Failed to download the zip file: {e}")
|
248
|
+
raise
|
249
|
+
else:
|
250
|
+
logger.info("HEC-RAS Example Projects zip file already exists. Skipping download.")
|
251
|
+
|
252
|
+
cls._load_project_data()
|
253
|
+
return cls.projects_dir
|
254
|
+
|
255
|
+
@classmethod
|
256
|
+
def _load_project_data(cls):
|
257
|
+
"""Load project data from CSV if up-to-date, otherwise extract from zip."""
|
258
|
+
logger.debug("Loading project data")
|
259
|
+
|
260
|
+
try:
|
261
|
+
zip_modified_time = os.path.getmtime(cls._zip_file_path)
|
262
|
+
except FileNotFoundError:
|
263
|
+
logger.error(f"Zip file not found at {cls._zip_file_path}.")
|
264
|
+
return
|
265
|
+
|
266
|
+
if cls.csv_file_path.exists():
|
267
|
+
csv_modified_time = os.path.getmtime(cls.csv_file_path)
|
268
|
+
|
269
|
+
if csv_modified_time >= zip_modified_time:
|
270
|
+
logger.info("Loading project data from CSV...")
|
271
|
+
try:
|
272
|
+
cls._folder_df = pd.read_csv(cls.csv_file_path)
|
273
|
+
logger.info(f"Loaded {len(cls._folder_df)} projects from CSV.")
|
274
|
+
return
|
275
|
+
except Exception as e:
|
276
|
+
logger.error(f"Failed to read CSV file: {e}")
|
277
|
+
cls._folder_df = None
|
278
|
+
|
279
|
+
logger.info("Extracting folder structure from zip file...")
|
280
|
+
cls._extract_folder_structure()
|
281
|
+
cls._save_to_csv()
|
282
|
+
|
283
|
+
@classmethod
|
284
|
+
def _extract_folder_structure(cls):
|
285
|
+
"""
|
286
|
+
Extract folder structure from the zip file.
|
287
|
+
|
288
|
+
Populates folder_df with category and project information.
|
289
|
+
"""
|
290
|
+
folder_data = []
|
291
|
+
try:
|
292
|
+
with zipfile.ZipFile(cls._zip_file_path, 'r') as zip_ref:
|
293
|
+
for file in zip_ref.namelist():
|
294
|
+
parts = Path(file).parts
|
295
|
+
if len(parts) > 1:
|
296
|
+
folder_data.append({
|
297
|
+
'Category': parts[0],
|
298
|
+
'Project': parts[1]
|
299
|
+
})
|
300
|
+
|
301
|
+
cls._folder_df = pd.DataFrame(folder_data).drop_duplicates()
|
302
|
+
logger.info(f"Extracted {len(cls._folder_df)} projects.")
|
303
|
+
logger.debug(f"folder_df:\n{cls._folder_df}")
|
304
|
+
except zipfile.BadZipFile:
|
305
|
+
logger.error(f"The file {cls._zip_file_path} is not a valid zip file.")
|
306
|
+
cls._folder_df = pd.DataFrame(columns=['Category', 'Project'])
|
307
|
+
except Exception as e:
|
308
|
+
logger.error(f"An error occurred while extracting the folder structure: {str(e)}")
|
309
|
+
cls._folder_df = pd.DataFrame(columns=['Category', 'Project'])
|
310
|
+
|
311
|
+
@classmethod
|
312
|
+
def _save_to_csv(cls):
|
313
|
+
"""Save the extracted folder structure to CSV file."""
|
314
|
+
if cls._folder_df is not None and not cls._folder_df.empty:
|
315
|
+
try:
|
316
|
+
cls._folder_df.to_csv(cls.csv_file_path, index=False)
|
317
|
+
logger.info(f"Saved project data to {cls.csv_file_path}")
|
318
|
+
except Exception as e:
|
319
|
+
logger.error(f"Failed to save project data to CSV: {e}")
|
320
|
+
else:
|
321
|
+
logger.warning("No folder data to save to CSV.")
|
322
|
+
|
323
|
+
@classmethod
|
324
|
+
def list_categories(cls):
|
325
|
+
"""
|
326
|
+
List all categories of example projects.
|
327
|
+
"""
|
328
|
+
if cls._folder_df is None or 'Category' not in cls._folder_df.columns:
|
329
|
+
logger.warning("No categories available. Make sure the zip file is properly loaded.")
|
330
|
+
return []
|
331
|
+
categories = cls._folder_df['Category'].unique()
|
332
|
+
logger.info(f"Available categories: {', '.join(categories)}")
|
333
|
+
return categories.tolist()
|
334
|
+
|
335
|
+
@classmethod
|
336
|
+
def list_projects(cls, category=None):
|
337
|
+
"""
|
338
|
+
List all projects or projects in a specific category.
|
339
|
+
|
340
|
+
Note: Special projects (NewOrleansMetro, BeaverLake) are also available but not listed
|
341
|
+
in categories as they are downloaded separately.
|
342
|
+
"""
|
343
|
+
if cls._folder_df is None:
|
344
|
+
logger.warning("No projects available. Make sure the zip file is properly loaded.")
|
345
|
+
return []
|
346
|
+
if category:
|
347
|
+
projects = cls._folder_df[cls._folder_df['Category'] == category]['Project'].unique()
|
348
|
+
logger.info(f"Projects in category '{category}': {', '.join(projects)}")
|
349
|
+
else:
|
350
|
+
projects = cls._folder_df['Project'].unique()
|
351
|
+
# Add special projects to the list
|
352
|
+
all_projects = list(projects) + list(cls.SPECIAL_PROJECTS.keys())
|
353
|
+
logger.info(f"All available projects: {', '.join(all_projects)}")
|
354
|
+
return all_projects
|
355
|
+
return projects.tolist()
|
356
|
+
|
357
|
+
@classmethod
|
358
|
+
def is_project_extracted(cls, project_name):
|
359
|
+
"""
|
360
|
+
Check if a specific project is already extracted.
|
361
|
+
"""
|
362
|
+
project_path = cls.projects_dir / project_name
|
363
|
+
is_extracted = project_path.exists()
|
364
|
+
logger.info(f"Project '{project_name}' extracted: {is_extracted}")
|
365
|
+
return is_extracted
|
366
|
+
|
367
|
+
@classmethod
|
368
|
+
def clean_projects_directory(cls):
|
369
|
+
"""Remove all extracted projects from the example_projects directory."""
|
370
|
+
logger.info(f"Cleaning projects directory: {cls.projects_dir}")
|
371
|
+
if cls.projects_dir.exists():
|
372
|
+
try:
|
373
|
+
shutil.rmtree(cls.projects_dir)
|
374
|
+
logger.info("All projects have been removed.")
|
375
|
+
except Exception as e:
|
376
|
+
logger.error(f"Failed to remove projects directory: {e}")
|
377
|
+
else:
|
378
|
+
logger.warning("Projects directory does not exist.")
|
379
|
+
cls.projects_dir.mkdir(parents=True, exist_ok=True)
|
380
|
+
logger.info("Projects directory cleaned and recreated.")
|
381
|
+
|
382
|
+
@classmethod
|
383
|
+
def download_fema_ble_model(cls, huc8, output_dir=None):
|
384
|
+
"""
|
385
|
+
Download a FEMA Base Level Engineering (BLE) model for a given HUC8.
|
386
|
+
|
387
|
+
Args:
|
388
|
+
huc8 (str): The 8-digit Hydrologic Unit Code (HUC) for the desired watershed.
|
389
|
+
output_dir (str, optional): The directory to save the downloaded files. If None, uses the current working directory.
|
390
|
+
|
391
|
+
Returns:
|
392
|
+
str: The path to the downloaded and extracted model directory.
|
393
|
+
|
394
|
+
Note:
|
395
|
+
This method downloads the BLE model from the FEMA website and extracts it to the specified directory.
|
396
|
+
"""
|
397
|
+
# Method implementation...
|
398
|
+
|
399
|
+
@classmethod
|
400
|
+
def _make_safe_folder_name(cls, name: str) -> str:
|
401
|
+
"""
|
402
|
+
Convert a string to a safe folder name by replacing unsafe characters with underscores.
|
403
|
+
"""
|
404
|
+
safe_name = re.sub(r'[^a-zA-Z0-9_\-]', '_', name)
|
405
|
+
logger.debug(f"Converted '{name}' to safe folder name '{safe_name}'")
|
406
|
+
return safe_name
|
407
|
+
|
408
|
+
@classmethod
|
409
|
+
def _download_file_with_progress(cls, url: str, dest_folder: Path, file_size: int) -> Path:
|
410
|
+
"""
|
411
|
+
Download a file from a URL to a specified destination folder with progress bar.
|
412
|
+
"""
|
413
|
+
local_filename = dest_folder / url.split('/')[-1]
|
414
|
+
try:
|
415
|
+
with requests.get(url, stream=True) as r:
|
416
|
+
r.raise_for_status()
|
417
|
+
with open(local_filename, 'wb') as f, tqdm(
|
418
|
+
desc=local_filename.name,
|
419
|
+
total=file_size,
|
420
|
+
unit='iB',
|
421
|
+
unit_scale=True,
|
422
|
+
unit_divisor=1024,
|
423
|
+
) as progress_bar:
|
424
|
+
for chunk in r.iter_content(chunk_size=8192):
|
425
|
+
size = f.write(chunk)
|
426
|
+
progress_bar.update(size)
|
427
|
+
logger.info(f"Successfully downloaded {url} to {local_filename}")
|
428
|
+
return local_filename
|
429
|
+
except requests.exceptions.RequestException as e:
|
430
|
+
logger.error(f"Request failed for {url}: {e}")
|
431
|
+
raise
|
432
|
+
except Exception as e:
|
433
|
+
logger.error(f"Failed to write file {local_filename}: {e}")
|
434
|
+
raise
|
435
|
+
|
436
|
+
@classmethod
|
437
|
+
def _convert_size_to_bytes(cls, size_str: str) -> int:
|
438
|
+
"""
|
439
|
+
Convert a human-readable file size to bytes.
|
440
|
+
"""
|
441
|
+
units = {'B': 1, 'KB': 1024, 'MB': 1024**2, 'GB': 1024**3, 'TB': 1024**4}
|
442
|
+
size_str = size_str.upper().replace(' ', '')
|
443
|
+
if not re.match(r'^\d+(\.\d+)?[BKMGT]B?$', size_str):
|
444
|
+
raise ValueError(f"Invalid size string: {size_str}")
|
445
|
+
|
446
|
+
number, unit = float(re.findall(r'[\d\.]+', size_str)[0]), re.findall(r'[BKMGT]B?', size_str)[0]
|
447
|
+
return int(number * units[unit])
|
448
|
+
|
449
|
+
@classmethod
|
450
|
+
def _extract_special_project(cls, project_name: str) -> Path:
|
451
|
+
"""
|
452
|
+
Download and extract special projects that are not in the main zip file.
|
453
|
+
|
454
|
+
Args:
|
455
|
+
project_name: Name of the special project ('NewOrleansMetro' or 'BeaverLake')
|
456
|
+
|
457
|
+
Returns:
|
458
|
+
Path: Path to the extracted project directory
|
459
|
+
|
460
|
+
Raises:
|
461
|
+
ValueError: If the project is not a recognized special project
|
462
|
+
"""
|
463
|
+
if project_name not in cls.SPECIAL_PROJECTS:
|
464
|
+
raise ValueError(f"'{project_name}' is not a recognized special project")
|
465
|
+
|
466
|
+
logger.info(f"----- RasExamples Extracting Special Project -----")
|
467
|
+
logger.info(f"Extracting special project '{project_name}'")
|
468
|
+
|
469
|
+
# Create the project directory
|
470
|
+
project_path = cls.projects_dir / project_name
|
471
|
+
|
472
|
+
# Check if already exists
|
473
|
+
if project_path.exists():
|
474
|
+
logger.info(f"Special project '{project_name}' already exists. Deleting existing folder...")
|
475
|
+
try:
|
476
|
+
shutil.rmtree(project_path)
|
477
|
+
logger.info(f"Existing folder for project '{project_name}' has been deleted.")
|
478
|
+
except Exception as e:
|
479
|
+
logger.error(f"Failed to delete existing project folder '{project_name}': {e}")
|
480
|
+
raise
|
481
|
+
|
482
|
+
# Create the project directory
|
483
|
+
project_path.mkdir(parents=True, exist_ok=True)
|
484
|
+
|
485
|
+
# Download the zip file
|
486
|
+
url = cls.SPECIAL_PROJECTS[project_name]
|
487
|
+
zip_file_path = cls.projects_dir / f"{project_name}_temp.zip"
|
488
|
+
|
489
|
+
logger.info(f"Downloading special project from: {url}")
|
490
|
+
logger.info("This may take a few moments...")
|
491
|
+
|
492
|
+
try:
|
493
|
+
response = requests.get(url, stream=True, timeout=300)
|
494
|
+
response.raise_for_status()
|
495
|
+
|
496
|
+
# Get total file size if available
|
497
|
+
total_size = int(response.headers.get('content-length', 0))
|
498
|
+
|
499
|
+
# Download with progress bar
|
500
|
+
with open(zip_file_path, 'wb') as file:
|
501
|
+
if total_size > 0:
|
502
|
+
with tqdm(
|
503
|
+
desc=f"Downloading {project_name}",
|
504
|
+
total=total_size,
|
505
|
+
unit='iB',
|
506
|
+
unit_scale=True,
|
507
|
+
unit_divisor=1024,
|
508
|
+
) as progress_bar:
|
509
|
+
for chunk in response.iter_content(chunk_size=8192):
|
510
|
+
size = file.write(chunk)
|
511
|
+
progress_bar.update(size)
|
512
|
+
else:
|
513
|
+
# No content length, download without progress bar
|
514
|
+
for chunk in response.iter_content(chunk_size=8192):
|
515
|
+
file.write(chunk)
|
516
|
+
|
517
|
+
logger.info(f"Downloaded special project zip file to {zip_file_path}")
|
518
|
+
|
519
|
+
except requests.exceptions.RequestException as e:
|
520
|
+
logger.error(f"Failed to download special project '{project_name}': {e}")
|
521
|
+
if zip_file_path.exists():
|
522
|
+
zip_file_path.unlink()
|
523
|
+
raise
|
524
|
+
|
525
|
+
# Extract the zip file directly to the project directory
|
526
|
+
try:
|
527
|
+
with zipfile.ZipFile(zip_file_path, 'r') as zip_ref:
|
528
|
+
# Extract directly to the project directory (no internal folder structure)
|
529
|
+
zip_ref.extractall(project_path)
|
530
|
+
logger.info(f"Successfully extracted special project '{project_name}' to {project_path}")
|
531
|
+
|
532
|
+
except Exception as e:
|
533
|
+
logger.error(f"Failed to extract special project '{project_name}': {e}")
|
534
|
+
if project_path.exists():
|
535
|
+
shutil.rmtree(project_path)
|
536
|
+
raise
|
537
|
+
finally:
|
538
|
+
# Clean up the temporary zip file
|
539
|
+
if zip_file_path.exists():
|
540
|
+
zip_file_path.unlink()
|
541
|
+
logger.debug(f"Removed temporary zip file: {zip_file_path}")
|
542
|
+
|
543
|
+
return project_path
|