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 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 or ras
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
- ras_obj = ras_object or ras
149
- ras_obj.plan_df = ras_obj.get_plan_entries()
150
- ras_obj.geom_df = ras_obj.get_geom_entries()
151
- ras_obj.flow_df = ras_obj.get_flow_entries()
152
- ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
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
 
@@ -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
- Expected folder structure: Notes:
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
- # Example usage:
351
- # ras_examples = RasExamples()
352
- # extracted_paths = ras_examples.extract_project(["Bald Eagle Creek", "BaldEagleCrkMulti2D", "Muncie"])
353
- # for path in extracted_paths:
354
- # logging.info(f"Extracted to: {path}")
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
+ """