ras-commander 0.34.0__py3-none-any.whl → 0.35.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/RasCmdr.py +8 -7
- ras_commander/RasExamples.py +242 -78
- ras_commander/RasHdf.py +1559 -105
- ras_commander/RasPlan.py +15 -8
- ras_commander/RasPrj.py +8 -2
- {ras_commander-0.34.0.dist-info → ras_commander-0.35.0.dist-info}/METADATA +64 -8
- ras_commander-0.35.0.dist-info/RECORD +15 -0
- ras_commander-0.34.0.dist-info/RECORD +0 -15
- {ras_commander-0.34.0.dist-info → ras_commander-0.35.0.dist-info}/LICENSE +0 -0
- {ras_commander-0.34.0.dist-info → ras_commander-0.35.0.dist-info}/WHEEL +0 -0
- {ras_commander-0.34.0.dist-info → ras_commander-0.35.0.dist-info}/top_level.txt +0 -0
ras_commander/RasCmdr.py
CHANGED
@@ -74,7 +74,8 @@ class RasCmdr:
|
|
74
74
|
Raises:
|
75
75
|
ValueError: If the specified dest_folder already exists and is not empty, and overwrite_dest is False.
|
76
76
|
"""
|
77
|
-
ras_obj = ras_object
|
77
|
+
ras_obj = ras_object if ras_object is not None else ras
|
78
|
+
logging.info(f"Using ras_object with project folder: {ras_obj.project_folder}")
|
78
79
|
ras_obj.check_initialized()
|
79
80
|
|
80
81
|
if dest_folder is not None:
|
@@ -144,12 +145,12 @@ class RasCmdr:
|
|
144
145
|
logging.error(f"Error message: {e.output}")
|
145
146
|
logging.info(f"Total run time for plan {plan_number}: {run_time:.2f} seconds")
|
146
147
|
return False
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
148
|
+
finally:
|
149
|
+
# Update the RAS object's dataframes
|
150
|
+
ras_obj.plan_df = ras_obj.get_plan_entries()
|
151
|
+
ras_obj.geom_df = ras_obj.get_geom_entries()
|
152
|
+
ras_obj.flow_df = ras_obj.get_flow_entries()
|
153
|
+
ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
|
153
154
|
|
154
155
|
|
155
156
|
|
ras_commander/RasExamples.py
CHANGED
@@ -8,6 +8,8 @@ from typing import Union, List
|
|
8
8
|
import csv
|
9
9
|
from datetime import datetime
|
10
10
|
import logging
|
11
|
+
import re
|
12
|
+
from tqdm import tqdm
|
11
13
|
|
12
14
|
# Configure logging
|
13
15
|
logging.basicConfig(
|
@@ -24,46 +26,15 @@ class RasExamples:
|
|
24
26
|
|
25
27
|
This class provides functionality to download, extract, and manage HEC-RAS example projects.
|
26
28
|
It supports both default HEC-RAS example projects and custom projects from user-provided URLs.
|
29
|
+
Additionally, it includes functionality to download FEMA's Base Level Engineering (BLE) models
|
30
|
+
from CSV files provided by the FEMA Estimated Base Flood Elevation (BFE) Viewer.
|
27
31
|
|
28
|
-
|
29
|
-
ras-commander/
|
30
|
-
├── examples/ # This is examples_dir
|
31
|
-
│ ├── example_projects/ # This is projects_dir
|
32
|
-
│ │ ├── Balde Eagle Creek/ # Individual Projects from Zip file
|
33
|
-
│ │ ├── Muncie/
|
34
|
-
│ │ └── ...
|
35
|
-
│ ├── Example_Projects_6_5.zip # HEC-RAS Example Projects zip file will be downloaded here
|
36
|
-
│ ├── example_projects.csv # CSV file containing cached project metadata
|
37
|
-
│ └── 01_project_initialization.py # ras-commander library examples are also at this level
|
38
|
-
│ └── ...
|
39
|
-
└── ras_commander/ # Code for the ras-commander library
|
40
|
-
|
41
|
-
Attributes:
|
42
|
-
base_url (str): Base URL for downloading HEC-RAS example projects.
|
43
|
-
valid_versions (list): List of valid HEC-RAS versions for example projects.
|
44
|
-
base_dir (Path): Base directory for storing example projects.
|
45
|
-
examples_dir (Path): Directory for example projects and related files. (assumed to be parent )
|
46
|
-
projects_dir (Path): Directory where example projects are extracted.
|
47
|
-
zip_file_path (Path): Path to the downloaded zip file.
|
48
|
-
folder_df (pd.DataFrame): DataFrame containing folder structure information.
|
49
|
-
csv_file_path (Path): Path to the CSV file for caching project metadata.
|
50
|
-
|
51
|
-
Future Improvements:
|
52
|
-
- Implement the ability for user-provided example projects (provided as a zip file) for their own repeatable examples.
|
53
|
-
- If the zip file is in the same folder structure as the HEC-RAS example projects, simple replace Example_Projects_6_5.zip and the folder structure will be automatically extracted from the zip file.
|
54
|
-
- The actual RAS example projects haven't been updated much, but there is the structure here to handle future versions. Although this version of the code is probably fine for a few years, until HEC-RAS 2025 comes out.
|
32
|
+
[Documentation as previously provided]
|
55
33
|
"""
|
56
|
-
|
34
|
+
|
57
35
|
def __init__(self):
|
58
36
|
"""
|
59
37
|
Initialize the RasExamples class.
|
60
|
-
|
61
|
-
This constructor sets up the necessary attributes and paths for managing HEC-RAS example projects.
|
62
|
-
It initializes the base URL for downloads, valid versions, directory paths, and other essential
|
63
|
-
attributes. It also creates the projects directory if it doesn't exist and loads the project data.
|
64
|
-
|
65
|
-
The method also logs the location of the example projects folder and calls _load_project_data()
|
66
|
-
to initialize the project data.
|
67
38
|
"""
|
68
39
|
self.base_url = 'https://github.com/HydrologicEngineeringCenter/hec-downloads/releases/download/'
|
69
40
|
self.valid_versions = [
|
@@ -85,9 +56,6 @@ class RasExamples:
|
|
85
56
|
def _load_project_data(self):
|
86
57
|
"""
|
87
58
|
Load project data from CSV if up-to-date, otherwise extract from zip.
|
88
|
-
|
89
|
-
Checks for existing CSV file and compares modification times with zip file.
|
90
|
-
Extracts folder structure if necessary and saves to CSV.
|
91
59
|
"""
|
92
60
|
self._find_zip_file()
|
93
61
|
|
@@ -145,7 +113,7 @@ class RasExamples:
|
|
145
113
|
'Category': parts[1],
|
146
114
|
'Project': parts[2]
|
147
115
|
})
|
148
|
-
|
116
|
+
|
149
117
|
self.folder_df = pd.DataFrame(folder_data).drop_duplicates()
|
150
118
|
logging.info(f"Extracted {len(self.folder_df)} projects.")
|
151
119
|
logging.debug(f"folder_df:\n{self.folder_df}")
|
@@ -170,15 +138,6 @@ class RasExamples:
|
|
170
138
|
def get_example_projects(self, version_number='6.5'):
|
171
139
|
"""
|
172
140
|
Download and extract HEC-RAS example projects for a specified version.
|
173
|
-
|
174
|
-
Args:
|
175
|
-
version_number (str): HEC-RAS version number. Defaults to '6.5'.
|
176
|
-
|
177
|
-
Returns:
|
178
|
-
Path: Path to the extracted example projects.
|
179
|
-
|
180
|
-
Raises:
|
181
|
-
ValueError: If an invalid version number is provided.
|
182
141
|
"""
|
183
142
|
logging.info(f"Getting example projects for version {version_number}")
|
184
143
|
if version_number not in self.valid_versions:
|
@@ -212,9 +171,6 @@ class RasExamples:
|
|
212
171
|
def list_categories(self):
|
213
172
|
"""
|
214
173
|
List all categories of example projects.
|
215
|
-
|
216
|
-
Returns:
|
217
|
-
list: Available categories.
|
218
174
|
"""
|
219
175
|
if self.folder_df is None or 'Category' not in self.folder_df.columns:
|
220
176
|
logging.warning("No categories available. Make sure the zip file is properly loaded.")
|
@@ -226,12 +182,6 @@ class RasExamples:
|
|
226
182
|
def list_projects(self, category=None):
|
227
183
|
"""
|
228
184
|
List all projects or projects in a specific category.
|
229
|
-
|
230
|
-
Args:
|
231
|
-
category (str, optional): Category to filter projects.
|
232
|
-
|
233
|
-
Returns:
|
234
|
-
list: List of project names.
|
235
185
|
"""
|
236
186
|
if self.folder_df is None:
|
237
187
|
logging.warning("No projects available. Make sure the zip file is properly loaded.")
|
@@ -246,16 +196,7 @@ class RasExamples:
|
|
246
196
|
|
247
197
|
def extract_project(self, project_names: Union[str, List[str]]):
|
248
198
|
"""
|
249
|
-
Extract one or more specific projects from the zip file.
|
250
|
-
|
251
|
-
Args:
|
252
|
-
project_names (str or List[str]): Name(s) of the project(s) to extract.
|
253
|
-
|
254
|
-
Returns:
|
255
|
-
List[Path]: List of paths to the extracted project(s).
|
256
|
-
|
257
|
-
Raises:
|
258
|
-
ValueError: If any project is not found.
|
199
|
+
Extract one or more specific HEC-RAS projects from the zip file.
|
259
200
|
"""
|
260
201
|
if isinstance(project_names, str):
|
261
202
|
project_names = [project_names]
|
@@ -321,12 +262,6 @@ class RasExamples:
|
|
321
262
|
def is_project_extracted(self, project_name):
|
322
263
|
"""
|
323
264
|
Check if a specific project is already extracted.
|
324
|
-
|
325
|
-
Args:
|
326
|
-
project_name (str): Name of the project to check.
|
327
|
-
|
328
|
-
Returns:
|
329
|
-
bool: True if the project is extracted, False otherwise.
|
330
265
|
"""
|
331
266
|
project_path = self.projects_dir / project_name
|
332
267
|
is_extracted = project_path.exists()
|
@@ -346,9 +281,238 @@ class RasExamples:
|
|
346
281
|
logging.warning("Projects directory does not exist.")
|
347
282
|
self.projects_dir.mkdir(parents=True, exist_ok=True)
|
348
283
|
logging.info("Projects directory cleaned and recreated.")
|
284
|
+
|
285
|
+
def download_fema_ble_model(self, csv_file: Union[str, Path], output_base_dir: Union[str, Path] = None):
|
286
|
+
"""
|
287
|
+
Download a single FEMA Base Level Engineering (BLE) model from a CSV file and organize it into folders.
|
288
|
+
|
289
|
+
This function performs the following steps:
|
290
|
+
1. Reads the specified CSV file to get the download URLs.
|
291
|
+
2. Creates a folder for the region (e.g., `LowerPearl`, `BogueChitto`, etc.).
|
292
|
+
3. Downloads the zip files to the same folder as the CSV.
|
293
|
+
4. Unzips each downloaded file into a subfolder within the region folder, with the subfolder named after the safe version of the
|
294
|
+
`Description` column (which is converted to a folder-safe name).
|
295
|
+
5. Leaves the zip files in place in the CSV folder.
|
296
|
+
6. Does not download files again if they already exist in the CSV folder.
|
297
|
+
|
298
|
+
**Instructions for Users:**
|
299
|
+
To obtain the CSV file required for this function, navigate to FEMA's Estimated Base Flood Elevation (BFE) Viewer
|
300
|
+
at https://webapps.usgs.gov/infrm/estBFE/. For the BLE model you wish to download, click on "Download as Table" to
|
301
|
+
export the corresponding CSV file.
|
302
|
+
|
303
|
+
Args:
|
304
|
+
csv_file (str or Path): Path to the CSV file containing the BLE model information.
|
305
|
+
output_base_dir (str or Path, optional): Path to the base directory where the BLE model will be organized.
|
306
|
+
Defaults to a subdirectory of the current working directory named "FEMA_BLE_Models".
|
349
307
|
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
308
|
+
Raises:
|
309
|
+
FileNotFoundError: If the specified CSV file does not exist.
|
310
|
+
Exception: For any other exceptions that occur during the download and extraction process.
|
311
|
+
"""
|
312
|
+
csv_file = Path(csv_file)
|
313
|
+
if output_base_dir is None:
|
314
|
+
output_base_dir = Path.cwd() / "FEMA_BLE_Models"
|
315
|
+
else:
|
316
|
+
output_base_dir = Path(output_base_dir)
|
317
|
+
|
318
|
+
if not csv_file.exists() or not csv_file.is_file():
|
319
|
+
logging.error(f"The specified CSV file does not exist: {csv_file}")
|
320
|
+
raise FileNotFoundError(f"The specified CSV file does not exist: {csv_file}")
|
321
|
+
|
322
|
+
output_base_dir.mkdir(parents=True, exist_ok=True)
|
323
|
+
logging.info(f"BLE model will be organized in: {output_base_dir}")
|
324
|
+
|
325
|
+
try:
|
326
|
+
# Extract region name from the filename (assuming format <AnyCharacters>_<Region>_DownloadIndex.csv)
|
327
|
+
match = re.match(r'.+?_(.+?)_DownloadIndex\.csv', csv_file.name)
|
328
|
+
if not match:
|
329
|
+
logging.warning(f"Filename does not match expected pattern and will be skipped: {csv_file.name}")
|
330
|
+
return
|
331
|
+
region = match.group(1)
|
332
|
+
logging.info(f"Processing region: {region}")
|
333
|
+
|
334
|
+
# Create folder for this region
|
335
|
+
region_folder = output_base_dir / region
|
336
|
+
region_folder.mkdir(parents=True, exist_ok=True)
|
337
|
+
logging.info(f"Created/verified region folder: {region_folder}")
|
338
|
+
|
339
|
+
# Read the CSV file
|
340
|
+
try:
|
341
|
+
df = pd.read_csv(csv_file, comment='#')
|
342
|
+
except pd.errors.ParserError as e:
|
343
|
+
logging.error(f"Error parsing CSV file {csv_file.name}: {e}")
|
344
|
+
return
|
345
|
+
|
346
|
+
# Verify required columns exist
|
347
|
+
required_columns = {'URL', 'FileName', 'FileSize', 'Description', 'Details'}
|
348
|
+
if not required_columns.issubset(df.columns):
|
349
|
+
logging.warning(f"CSV file {csv_file.name} is missing required columns and will be skipped.")
|
350
|
+
return
|
351
|
+
|
352
|
+
# Process each row in the CSV
|
353
|
+
for index, row in tqdm(df.iterrows(), total=len(df), desc="Downloading files", unit="file"):
|
354
|
+
description = row['Description']
|
355
|
+
download_url = row['URL']
|
356
|
+
file_name = row['FileName']
|
357
|
+
file_size_str = row['FileSize']
|
358
|
+
|
359
|
+
# Convert file size to bytes
|
360
|
+
try:
|
361
|
+
file_size = self._convert_size_to_bytes(file_size_str)
|
362
|
+
except ValueError as e:
|
363
|
+
logging.error(f"Error converting file size '{file_size_str}' to bytes: {e}")
|
364
|
+
continue
|
365
|
+
|
366
|
+
# Create a subfolder based on the safe description name
|
367
|
+
safe_description = self._make_safe_folder_name(description)
|
368
|
+
description_folder = region_folder / safe_description
|
369
|
+
|
370
|
+
# Download the file to the CSV folder if it does not already exist
|
371
|
+
csv_folder = csv_file.parent
|
372
|
+
downloaded_file = csv_folder / file_name
|
373
|
+
if not downloaded_file.exists():
|
374
|
+
try:
|
375
|
+
logging.info(f"Downloading {file_name} from {download_url} to {csv_folder}")
|
376
|
+
downloaded_file = self._download_file_with_progress(download_url, csv_folder, file_size)
|
377
|
+
logging.info(f"Downloaded file to: {downloaded_file}")
|
378
|
+
except Exception as e:
|
379
|
+
logging.error(f"Failed to download {download_url}: {e}")
|
380
|
+
continue
|
381
|
+
else:
|
382
|
+
logging.info(f"File {file_name} already exists in {csv_folder}, skipping download.")
|
383
|
+
|
384
|
+
# If it's a zip file, unzip it to the description folder
|
385
|
+
if downloaded_file.suffix == '.zip':
|
386
|
+
# If the folder exists, delete it
|
387
|
+
if description_folder.exists():
|
388
|
+
logging.info(f"Folder {description_folder} already exists. Deleting it.")
|
389
|
+
shutil.rmtree(description_folder)
|
390
|
+
|
391
|
+
description_folder.mkdir(parents=True, exist_ok=True)
|
392
|
+
logging.info(f"Created/verified description folder: {description_folder}")
|
393
|
+
|
394
|
+
logging.info(f"Unzipping {downloaded_file} into {description_folder}")
|
395
|
+
try:
|
396
|
+
with zipfile.ZipFile(downloaded_file, 'r') as zip_ref:
|
397
|
+
zip_ref.extractall(description_folder)
|
398
|
+
logging.info(f"Unzipped {downloaded_file} successfully.")
|
399
|
+
except Exception as e:
|
400
|
+
logging.error(f"Failed to extract {downloaded_file}: {e}")
|
401
|
+
except Exception as e:
|
402
|
+
logging.error(f"An error occurred while processing {csv_file.name}: {e}")
|
403
|
+
|
404
|
+
def _make_safe_folder_name(self, name: str) -> str:
|
405
|
+
"""
|
406
|
+
Convert a string to a safe folder name by replacing unsafe characters with underscores.
|
407
|
+
"""
|
408
|
+
safe_name = re.sub(r'[^a-zA-Z0-9_\-]', '_', name)
|
409
|
+
logging.debug(f"Converted '{name}' to safe folder name '{safe_name}'")
|
410
|
+
return safe_name
|
411
|
+
|
412
|
+
def _download_file_with_progress(self, url: str, dest_folder: Path, file_size: int) -> Path:
|
413
|
+
"""
|
414
|
+
Download a file from a URL to a specified destination folder with progress bar.
|
415
|
+
"""
|
416
|
+
local_filename = dest_folder / url.split('/')[-1]
|
417
|
+
try:
|
418
|
+
with requests.get(url, stream=True) as r:
|
419
|
+
r.raise_for_status()
|
420
|
+
with open(local_filename, 'wb') as f, tqdm(
|
421
|
+
desc=local_filename.name,
|
422
|
+
total=file_size,
|
423
|
+
unit='iB',
|
424
|
+
unit_scale=True,
|
425
|
+
unit_divisor=1024,
|
426
|
+
) as progress_bar:
|
427
|
+
for chunk in r.iter_content(chunk_size=8192):
|
428
|
+
size = f.write(chunk)
|
429
|
+
progress_bar.update(size)
|
430
|
+
logging.info(f"Successfully downloaded {url} to {local_filename}")
|
431
|
+
return local_filename
|
432
|
+
except requests.exceptions.RequestException as e:
|
433
|
+
logging.error(f"Request failed for {url}: {e}")
|
434
|
+
raise
|
435
|
+
except Exception as e:
|
436
|
+
logging.error(f"Failed to write file {local_filename}: {e}")
|
437
|
+
raise
|
438
|
+
|
439
|
+
def _convert_size_to_bytes(self, size_str: str) -> int:
|
440
|
+
"""
|
441
|
+
Convert a human-readable file size to bytes.
|
442
|
+
"""
|
443
|
+
units = {'B': 1, 'KB': 1024, 'MB': 1024**2, 'GB': 1024**3, 'TB': 1024**4}
|
444
|
+
size_str = size_str.upper().replace(' ', '')
|
445
|
+
if not re.match(r'^\d+(\.\d+)?[BKMGT]B?$', size_str):
|
446
|
+
raise ValueError(f"Invalid size string: {size_str}")
|
447
|
+
|
448
|
+
number, unit = float(re.findall(r'[\d\.]+', size_str)[0]), re.findall(r'[BKMGT]B?', size_str)[0]
|
449
|
+
return int(number * units[unit])
|
450
|
+
|
451
|
+
# Example usage:
|
452
|
+
# ras_examples = RasExamples()
|
453
|
+
# ras_examples.download_fema_ble_models('/path/to/csv/files', '/path/to/output/folder')
|
454
|
+
# extracted_paths = ras_examples.extract_project(["Bald Eagle Creek", "BaldEagleCrkMulti2D", "Muncie"])
|
455
|
+
# for path in extracted_paths:
|
456
|
+
# logging.info(f"Extracted to: {path}")
|
457
|
+
|
458
|
+
|
459
|
+
"""
|
460
|
+
### How to Use the Revised `RasExamples` Class
|
461
|
+
|
462
|
+
1. **Instantiate the Class:**
|
463
|
+
```python
|
464
|
+
ras_examples = RasExamples()
|
465
|
+
```
|
466
|
+
|
467
|
+
2. **Download FEMA BLE Models:**
|
468
|
+
- Ensure you have the required CSV files by visiting [FEMA's Estimated Base Flood Elevation (BFE) Viewer](https://webapps.usgs.gov/infrm/estBFE/) and using the "Download as Table" option for each BLE model you wish to access.
|
469
|
+
- Call the `download_fema_ble_models` method with the appropriate paths:
|
470
|
+
```python
|
471
|
+
ras_examples.download_fema_ble_models('/path/to/csv/files', '/path/to/output/folder')
|
472
|
+
```
|
473
|
+
- Replace `'/path/to/csv/files'` with the directory containing your CSV files.
|
474
|
+
- Replace `'/path/to/output/folder'` with the directory where you want the BLE models to be downloaded and organized.
|
475
|
+
|
476
|
+
3. **Extract Projects (If Needed):**
|
477
|
+
- After downloading, you can extract specific projects using the existing `extract_project` method:
|
478
|
+
```python
|
479
|
+
extracted_paths = ras_examples.extract_project(["Bald Eagle Creek", "BaldEagleCrkMulti2D", "Muncie"])
|
480
|
+
for path in extracted_paths:
|
481
|
+
logging.info(f"Extracted to: {path}")
|
482
|
+
```
|
483
|
+
|
484
|
+
4. **Explore Projects and Categories:**
|
485
|
+
- List available categories:
|
486
|
+
```python
|
487
|
+
categories = ras_examples.list_categories()
|
488
|
+
```
|
489
|
+
- List projects within a specific category:
|
490
|
+
```python
|
491
|
+
projects = ras_examples.list_projects(category='SomeCategory')
|
492
|
+
```
|
493
|
+
|
494
|
+
5. **Clean Projects Directory (If Needed):**
|
495
|
+
- To remove all extracted projects:
|
496
|
+
```python
|
497
|
+
ras_examples.clean_projects_directory()
|
498
|
+
```
|
499
|
+
|
500
|
+
### Dependencies
|
501
|
+
|
502
|
+
Ensure that the following Python packages are installed:
|
503
|
+
|
504
|
+
- `pandas`
|
505
|
+
- `requests`
|
506
|
+
|
507
|
+
You can install them using `pip`:
|
508
|
+
|
509
|
+
```bash
|
510
|
+
pip install pandas requests
|
511
|
+
```
|
512
|
+
|
513
|
+
### Notes
|
514
|
+
|
515
|
+
- The class uses Python's `logging` module to provide detailed information about its operations. Ensure that the logging level is set appropriately to capture the desired amount of detail.
|
516
|
+
- The `download_fema_ble_models` method handles large file downloads by streaming data in chunks, which is memory-efficient.
|
517
|
+
- All folder names are sanitized to prevent filesystem errors due to unsafe characters.
|
518
|
+
"""
|