ras-commander 0.33.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.
@@ -7,6 +7,18 @@ import shutil
7
7
  from typing import Union, List
8
8
  import csv
9
9
  from datetime import datetime
10
+ import logging
11
+ import re
12
+ from tqdm import tqdm
13
+
14
+ # Configure logging
15
+ logging.basicConfig(
16
+ level=logging.INFO, # Set the logging level to INFO
17
+ format='%(asctime)s - %(levelname)s - %(message)s', # Log message format
18
+ handlers=[
19
+ logging.StreamHandler() # Log to stderr
20
+ ]
21
+ )
10
22
 
11
23
  class RasExamples:
12
24
  """
@@ -14,48 +26,15 @@ class RasExamples:
14
26
 
15
27
  This class provides functionality to download, extract, and manage HEC-RAS example projects.
16
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.
17
31
 
18
- Expected folder structure: Notes:
19
- ras-commander/
20
- ├── examples/ # This is examples_dir
21
- │ ├── example_projects/ # This is projects_dir
22
- │ │ ├── Balde Eagle Creek/ # Individual Projects from Zip file
23
- │ │ ├── Muncie/
24
- │ │ └── ...
25
- │ ├── Example_Projects_6_5.zip # HEC-RAS Example Projects zip file will be downloaded here
26
- │ ├── example_projects.csv # CSV file containing cached project metadata
27
- │ └── 01_project_initialization.py # ras-commander library examples are also at this level
28
- │ └── ...
29
- └── ras_commander/ # Code for the ras-commander library
30
-
31
- Attributes:
32
- base_url (str): Base URL for downloading HEC-RAS example projects.
33
- valid_versions (list): List of valid HEC-RAS versions for example projects.
34
- base_dir (Path): Base directory for storing example projects.
35
- examples_dir (Path): Directory for example projects and related files. (assumed to be parent )
36
- projects_dir (Path): Directory where example projects are extracted.
37
- zip_file_path (Path): Path to the downloaded zip file.
38
- folder_df (pd.DataFrame): DataFrame containing folder structure information.
39
- csv_file_path (Path): Path to the CSV file for caching project metadata.
40
-
41
-
42
- Future Improvements:
43
- - Implement the ability for user-provided example projects (provided as a zip file) for their own repeatable examples.
44
- - 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.
45
- - 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.
46
-
32
+ [Documentation as previously provided]
47
33
  """
48
34
 
49
35
  def __init__(self):
50
36
  """
51
37
  Initialize the RasExamples class.
52
-
53
- This constructor sets up the necessary attributes and paths for managing HEC-RAS example projects.
54
- It initializes the base URL for downloads, valid versions, directory paths, and other essential
55
- attributes. It also creates the projects directory if it doesn't exist and loads the project data.
56
-
57
- The method also prints the location of the example projects folder and calls _load_project_data()
58
- to initialize the project data.
59
38
  """
60
39
  self.base_url = 'https://github.com/HydrologicEngineeringCenter/hec-downloads/releases/download/'
61
40
  self.valid_versions = [
@@ -71,34 +50,39 @@ class RasExamples:
71
50
  self.csv_file_path = self.examples_dir / 'example_projects.csv'
72
51
 
73
52
  self.projects_dir.mkdir(parents=True, exist_ok=True)
74
- print(f"Example projects folder: {self.projects_dir}")
53
+ logging.info(f"Example projects folder: {self.projects_dir}")
75
54
  self._load_project_data()
76
55
 
77
56
  def _load_project_data(self):
78
57
  """
79
58
  Load project data from CSV if up-to-date, otherwise extract from zip.
80
-
81
- Checks for existing CSV file and compares modification times with zip file.
82
- Extracts folder structure if necessary and saves to CSV.
83
59
  """
84
60
  self._find_zip_file()
85
61
 
86
62
  if not self.zip_file_path:
87
- print("No example projects zip file found. Downloading...")
63
+ logging.info("No example projects zip file found. Downloading...")
88
64
  self.get_example_projects()
89
65
 
90
- zip_modified_time = os.path.getmtime(self.zip_file_path)
66
+ try:
67
+ zip_modified_time = os.path.getmtime(self.zip_file_path)
68
+ except FileNotFoundError:
69
+ logging.error(f"Zip file not found at {self.zip_file_path}.")
70
+ return
91
71
 
92
72
  if self.csv_file_path.exists():
93
73
  csv_modified_time = os.path.getmtime(self.csv_file_path)
94
74
 
95
75
  if csv_modified_time >= zip_modified_time:
96
- print("Loading project data from CSV...")
97
- self.folder_df = pd.read_csv(self.csv_file_path)
98
- print(f"Loaded {len(self.folder_df)} projects from CSV, use list_categories() and list_projects() to explore them")
76
+ logging.info("Loading project data from CSV...")
77
+ try:
78
+ self.folder_df = pd.read_csv(self.csv_file_path)
79
+ logging.info(f"Loaded {len(self.folder_df)} projects from CSV. Use list_categories() and list_projects() to explore them.")
80
+ except Exception as e:
81
+ logging.error(f"Failed to read CSV file: {e}")
82
+ self.folder_df = None
99
83
  return
100
84
 
101
- print("Extracting folder structure from zip file...")
85
+ logging.info("Extracting folder structure from zip file...")
102
86
  self._extract_folder_structure()
103
87
  self._save_to_csv()
104
88
 
@@ -108,8 +92,10 @@ class RasExamples:
108
92
  potential_zip = self.examples_dir / f"Example_Projects_{version.replace('.', '_')}.zip"
109
93
  if potential_zip.exists():
110
94
  self.zip_file_path = potential_zip
111
- print(f"Found zip file: {self.zip_file_path}")
95
+ logging.info(f"Found zip file: {self.zip_file_path}")
112
96
  break
97
+ else:
98
+ logging.warning("No existing example projects zip file found.")
113
99
 
114
100
  def _extract_folder_structure(self):
115
101
  """
@@ -127,37 +113,37 @@ class RasExamples:
127
113
  'Category': parts[1],
128
114
  'Project': parts[2]
129
115
  })
130
-
116
+
131
117
  self.folder_df = pd.DataFrame(folder_data).drop_duplicates()
132
- print(f"Extracted {len(self.folder_df)} projects")
133
- print("folder_df:")
134
- display(self.folder_df)
118
+ logging.info(f"Extracted {len(self.folder_df)} projects.")
119
+ logging.debug(f"folder_df:\n{self.folder_df}")
120
+ except zipfile.BadZipFile:
121
+ logging.error(f"The file {self.zip_file_path} is not a valid zip file.")
122
+ self.folder_df = pd.DataFrame(columns=['Category', 'Project'])
135
123
  except Exception as e:
136
- print(f"An error occurred while extracting the folder structure: {str(e)}")
124
+ logging.error(f"An error occurred while extracting the folder structure: {str(e)}")
137
125
  self.folder_df = pd.DataFrame(columns=['Category', 'Project'])
138
126
 
139
127
  def _save_to_csv(self):
140
128
  """Save the extracted folder structure to CSV file."""
141
129
  if self.folder_df is not None and not self.folder_df.empty:
142
- self.folder_df.to_csv(self.csv_file_path, index=False)
143
- print(f"Saved project data to {self.csv_file_path}")
130
+ try:
131
+ self.folder_df.to_csv(self.csv_file_path, index=False)
132
+ logging.info(f"Saved project data to {self.csv_file_path}")
133
+ except Exception as e:
134
+ logging.error(f"Failed to save project data to CSV: {e}")
135
+ else:
136
+ logging.warning("No folder data to save to CSV.")
144
137
 
145
138
  def get_example_projects(self, version_number='6.5'):
146
139
  """
147
140
  Download and extract HEC-RAS example projects for a specified version.
148
-
149
- Args:
150
- version_number (str): HEC-RAS version number. Defaults to '6.5'.
151
-
152
- Returns:
153
- Path: Path to the extracted example projects.
154
-
155
- Raises:
156
- ValueError: If an invalid version number is provided.
157
141
  """
158
- print(f"Getting example projects for version {version_number}")
142
+ logging.info(f"Getting example projects for version {version_number}")
159
143
  if version_number not in self.valid_versions:
160
- raise ValueError(f"Invalid version number. Valid versions are: {', '.join(self.valid_versions)}")
144
+ error_msg = f"Invalid version number. Valid versions are: {', '.join(self.valid_versions)}"
145
+ logging.error(error_msg)
146
+ raise ValueError(error_msg)
161
147
 
162
148
  zip_url = f"{self.base_url}1.0.31/Example_Projects_{version_number.replace('.', '_')}.zip"
163
149
 
@@ -166,13 +152,18 @@ class RasExamples:
166
152
  self.zip_file_path = self.examples_dir / f"Example_Projects_{version_number.replace('.', '_')}.zip"
167
153
 
168
154
  if not self.zip_file_path.exists():
169
- print(f"Downloading HEC-RAS Example Projects from {zip_url}. \n The file is over 400 MB, so it may take a few minutes to download....")
170
- response = requests.get(zip_url)
171
- with open(self.zip_file_path, 'wb') as file:
172
- file.write(response.content)
173
- print(f"Downloaded to {self.zip_file_path}")
155
+ logging.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....")
156
+ try:
157
+ response = requests.get(zip_url, stream=True)
158
+ response.raise_for_status()
159
+ with open(self.zip_file_path, 'wb') as file:
160
+ shutil.copyfileobj(response.raw, file)
161
+ logging.info(f"Downloaded to {self.zip_file_path}")
162
+ except requests.exceptions.RequestException as e:
163
+ logging.error(f"Failed to download the zip file: {e}")
164
+ raise
174
165
  else:
175
- print("HEC-RAS Example Projects zip file already exists. Skipping download.")
166
+ logging.info("HEC-RAS Example Projects zip file already exists. Skipping download.")
176
167
 
177
168
  self._load_project_data()
178
169
  return self.projects_dir
@@ -180,48 +171,32 @@ class RasExamples:
180
171
  def list_categories(self):
181
172
  """
182
173
  List all categories of example projects.
183
-
184
- Returns:
185
- list: Available categories.
186
174
  """
187
175
  if self.folder_df is None or 'Category' not in self.folder_df.columns:
188
- print("No categories available. Make sure the zip file is properly loaded.")
176
+ logging.warning("No categories available. Make sure the zip file is properly loaded.")
189
177
  return []
190
178
  categories = self.folder_df['Category'].unique()
191
- print(f"Available categories: {', '.join(categories)}")
179
+ logging.info(f"Available categories: {', '.join(categories)}")
192
180
  return categories.tolist()
193
181
 
194
182
  def list_projects(self, category=None):
195
183
  """
196
184
  List all projects or projects in a specific category.
197
-
198
- Args:
199
- category (str, optional): Category to filter projects.
200
-
201
- Returns:
202
- list: List of project names.
203
185
  """
204
186
  if self.folder_df is None:
205
- print("No projects available. Make sure the zip file is properly loaded.")
187
+ logging.warning("No projects available. Make sure the zip file is properly loaded.")
206
188
  return []
207
189
  if category:
208
190
  projects = self.folder_df[self.folder_df['Category'] == category]['Project'].unique()
191
+ logging.info(f"Projects in category '{category}': {', '.join(projects)}")
209
192
  else:
210
193
  projects = self.folder_df['Project'].unique()
194
+ logging.info(f"All available projects: {', '.join(projects)}")
211
195
  return projects.tolist()
212
196
 
213
197
  def extract_project(self, project_names: Union[str, List[str]]):
214
198
  """
215
- Extract one or more specific projects from the zip file.
216
-
217
- Args:
218
- project_names (str or List[str]): Name(s) of the project(s) to extract.
219
-
220
- Returns:
221
- List[Path]: List of paths to the extracted project(s).
222
-
223
- Raises:
224
- ValueError: If any project is not found.
199
+ Extract one or more specific HEC-RAS projects from the zip file.
225
200
  """
226
201
  if isinstance(project_names, str):
227
202
  project_names = [project_names]
@@ -229,21 +204,29 @@ class RasExamples:
229
204
  extracted_paths = []
230
205
 
231
206
  for project_name in project_names:
232
- print("----- RasExamples Extracting Project -----")
233
- print(f"Extracting project '{project_name}'")
207
+ logging.info("----- RasExamples Extracting Project -----")
208
+ logging.info(f"Extracting project '{project_name}'")
234
209
  project_path = self.projects_dir / project_name
235
210
 
236
211
  if project_path.exists():
237
- print(f"Project '{project_name}' already exists. Deleting existing folder...")
238
- shutil.rmtree(project_path)
239
- print(f"Existing folder for project '{project_name}' has been deleted.")
212
+ logging.info(f"Project '{project_name}' already exists. Deleting existing folder...")
213
+ try:
214
+ shutil.rmtree(project_path)
215
+ logging.info(f"Existing folder for project '{project_name}' has been deleted.")
216
+ except Exception as e:
217
+ logging.error(f"Failed to delete existing project folder '{project_name}': {e}")
218
+ continue
240
219
 
241
220
  if self.folder_df is None or self.folder_df.empty:
242
- raise ValueError("No project information available. Make sure the zip file is properly loaded.")
221
+ error_msg = "No project information available. Make sure the zip file is properly loaded."
222
+ logging.error(error_msg)
223
+ raise ValueError(error_msg)
243
224
 
244
225
  project_info = self.folder_df[self.folder_df['Project'] == project_name]
245
226
  if project_info.empty:
246
- raise ValueError(f"Project '{project_name}' not found in the zip file.")
227
+ error_msg = f"Project '{project_name}' not found in the zip file."
228
+ logging.error(error_msg)
229
+ raise ValueError(error_msg)
247
230
 
248
231
  category = project_info['Category'].iloc[0]
249
232
 
@@ -265,40 +248,271 @@ class RasExamples:
265
248
  with zip_ref.open(file) as source, open(extract_path, "wb") as target:
266
249
  shutil.copyfileobj(source, target)
267
250
 
268
- print(f"Successfully extracted project '{project_name}' to {project_path}")
251
+ logging.info(f"Successfully extracted project '{project_name}' to {project_path}")
269
252
  extracted_paths.append(project_path)
270
253
  except zipfile.BadZipFile:
271
- print(f"Error: The file {self.zip_file_path} is not a valid zip file.")
254
+ logging.error(f"Error: The file {self.zip_file_path} is not a valid zip file.")
272
255
  except FileNotFoundError:
273
- print(f"Error: The file {self.zip_file_path} was not found.")
256
+ logging.error(f"Error: The file {self.zip_file_path} was not found.")
274
257
  except Exception as e:
275
- print(f"An unexpected error occurred while extracting the project: {str(e)}")
276
- #print("----- RasExamples Extraction Complete -----")
258
+ logging.error(f"An unexpected error occurred while extracting the project: {str(e)}")
259
+ logging.info("----- RasExamples Extraction Complete -----")
277
260
  return extracted_paths
278
261
 
279
262
  def is_project_extracted(self, project_name):
280
263
  """
281
264
  Check if a specific project is already extracted.
282
-
283
- Args:
284
- project_name (str): Name of the project to check.
285
-
286
- Returns:
287
- bool: True if the project is extracted, False otherwise.
288
265
  """
289
266
  project_path = self.projects_dir / project_name
290
- return project_path.exists()
267
+ is_extracted = project_path.exists()
268
+ logging.info(f"Project '{project_name}' extracted: {is_extracted}")
269
+ return is_extracted
291
270
 
292
271
  def clean_projects_directory(self):
293
272
  """Remove all extracted projects from the example_projects directory."""
294
- print(f"Cleaning projects directory: {self.projects_dir}")
273
+ logging.info(f"Cleaning projects directory: {self.projects_dir}")
295
274
  if self.projects_dir.exists():
296
- shutil.rmtree(self.projects_dir)
275
+ try:
276
+ shutil.rmtree(self.projects_dir)
277
+ logging.info("All projects have been removed.")
278
+ except Exception as e:
279
+ logging.error(f"Failed to remove projects directory: {e}")
280
+ else:
281
+ logging.warning("Projects directory does not exist.")
297
282
  self.projects_dir.mkdir(parents=True, exist_ok=True)
298
- print("Projects directory cleaned.")
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".
307
+
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
299
411
 
300
- # Example usage:
301
- # ras_examples = RasExamples()
302
- # extracted_paths = ras_examples.extract_project(["Bald Eagle Creek", "BaldEagleCrkMulti2D", "Muncie"])
303
- # for path in extracted_paths:
304
- # print(f"Extracted to: {path}")
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
+ """
ras_commander/RasGeo.py CHANGED
@@ -5,8 +5,18 @@ from pathlib import Path
5
5
  from typing import List, Union
6
6
  from .RasPlan import RasPlan
7
7
  from .RasPrj import ras
8
+ import logging
8
9
  import re
9
10
 
11
+ # Configure logging at the module level
12
+ logging.basicConfig(
13
+ level=logging.INFO,
14
+ format='%(asctime)s - %(levelname)s - %(message)s',
15
+ # You can add a filename parameter here to log to a file
16
+ # filename='rasgeo.log',
17
+ # Uncomment the above line to enable file logging
18
+ )
19
+
10
20
  class RasGeo:
11
21
  """
12
22
  A class for operations on HEC-RAS geometry files.
@@ -62,27 +72,38 @@ class RasGeo:
62
72
  geom_preprocessor_file = plan_path.with_suffix(geom_preprocessor_suffix)
63
73
  if geom_preprocessor_file.exists():
64
74
  try:
65
- print(f"Deleting geometry preprocessor file: {geom_preprocessor_file}")
75
+ logging.info(f"Deleting geometry preprocessor file: {geom_preprocessor_file}")
66
76
  geom_preprocessor_file.unlink()
67
- print("File deletion completed successfully.")
77
+ logging.info("File deletion completed successfully.")
68
78
  except PermissionError:
79
+ logging.error(f"Permission denied: Unable to delete geometry preprocessor file: {geom_preprocessor_file}.")
69
80
  raise PermissionError(f"Unable to delete geometry preprocessor file: {geom_preprocessor_file}. Permission denied.")
70
81
  except OSError as e:
82
+ logging.error(f"Error deleting geometry preprocessor file: {geom_preprocessor_file}. {str(e)}")
71
83
  raise OSError(f"Error deleting geometry preprocessor file: {geom_preprocessor_file}. {str(e)}")
72
84
  else:
73
- print(f"No geometry preprocessor file found for: {plan_file}")
85
+ logging.warning(f"No geometry preprocessor file found for: {plan_file}")
74
86
 
75
87
  if plan_files is None:
76
- print("Clearing all geometry preprocessor files in the project directory.")
88
+ logging.info("Clearing all geometry preprocessor files in the project directory.")
77
89
  plan_files_to_clear = list(ras_obj.project_folder.glob(r'*.p*'))
78
90
  elif isinstance(plan_files, (str, Path)):
79
91
  plan_files_to_clear = [plan_files]
92
+ logging.info(f"Clearing geometry preprocessor file for single plan: {plan_files}")
80
93
  elif isinstance(plan_files, list):
81
94
  plan_files_to_clear = plan_files
95
+ logging.info(f"Clearing geometry preprocessor files for multiple plans: {plan_files}")
82
96
  else:
97
+ logging.error("Invalid input type for plan_files.")
83
98
  raise ValueError("Invalid input. Please provide a string, Path, list of paths, or None.")
84
99
 
85
100
  for plan_file in plan_files_to_clear:
86
101
  clear_single_file(plan_file, ras_obj)
87
- ras_obj.geom_df = ras_obj.get_geom_entries()
88
-
102
+
103
+ # Update the geometry dataframe
104
+ try:
105
+ ras_obj.geom_df = ras_obj.get_geom_entries()
106
+ logging.info("Geometry dataframe updated successfully.")
107
+ except Exception as e:
108
+ logging.error(f"Failed to update geometry dataframe: {str(e)}")
109
+ raise