ras-commander 0.56.0__py3-none-any.whl → 0.57.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.
@@ -1,395 +1,415 @@
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
-
61
- This class provides functionality to download, extract, and manage HEC-RAS example projects.
62
- It supports both default HEC-RAS example projects and custom projects from user-provided URLs.
63
- Additionally, it includes functionality to download FEMA's Base Level Engineering (BLE) models
64
- from CSV files provided by the FEMA Estimated Base Flood Elevation (BFE) Viewer.
65
- """
66
- @log_call
67
- def __init__(self):
68
- """
69
- Initialize the RasExamples class.
70
- """
71
- self.base_url = 'https://github.com/HydrologicEngineeringCenter/hec-downloads/releases/download/'
72
- self.valid_versions = [
73
- "6.6", "6.5", "6.4.1", "6.3.1", "6.3", "6.2", "6.1", "6.0",
74
- "5.0.7", "5.0.6", "5.0.5", "5.0.4", "5.0.3", "5.0.1", "5.0",
75
- "4.1", "4.0", "3.1.3", "3.1.2", "3.1.1", "3.0", "2.2"
76
- ]
77
- self.base_dir = Path.cwd()
78
- self.examples_dir = self.base_dir
79
- self.projects_dir = self.examples_dir / 'example_projects'
80
- self.zip_file_path = None
81
- self.folder_df = None
82
- self.csv_file_path = self.examples_dir / 'example_projects.csv'
83
-
84
- self.projects_dir.mkdir(parents=True, exist_ok=True)
85
- logger.info(f"Example projects folder: {self.projects_dir}")
86
- self._load_project_data()
87
-
88
- @log_call
89
- def get_example_projects(self, version_number='6.6'):
90
- """
91
- Download and extract HEC-RAS example projects for a specified version.
92
- """
93
- logger.info(f"Getting example projects for version {version_number}")
94
- if version_number not in self.valid_versions:
95
- error_msg = f"Invalid version number. Valid versions are: {', '.join(self.valid_versions)}"
96
- logger.error(error_msg)
97
- raise ValueError(error_msg)
98
-
99
- zip_url = f"{self.base_url}1.0.33/Example_Projects_{version_number.replace('.', '_')}.zip"
100
-
101
- self.examples_dir.mkdir(parents=True, exist_ok=True)
102
-
103
- self.zip_file_path = self.examples_dir / f"Example_Projects_{version_number.replace('.', '_')}.zip"
104
-
105
- if not self.zip_file_path.exists():
106
- 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....")
107
- try:
108
- response = requests.get(zip_url, stream=True)
109
- response.raise_for_status()
110
- with open(self.zip_file_path, 'wb') as file:
111
- shutil.copyfileobj(response.raw, file)
112
- logger.info(f"Downloaded to {self.zip_file_path}")
113
- except requests.exceptions.RequestException as e:
114
- logger.error(f"Failed to download the zip file: {e}")
115
- raise
116
- else:
117
- logger.info("HEC-RAS Example Projects zip file already exists. Skipping download.")
118
-
119
- self._load_project_data()
120
- return self.projects_dir
121
-
122
-
123
- @log_call
124
- def _load_project_data(self):
125
- """
126
- Load project data from CSV if up-to-date, otherwise extract from zip.
127
- """
128
- self._find_zip_file()
129
-
130
- if not self.zip_file_path:
131
- logger.info("No example projects zip file found. Downloading...")
132
- self.get_example_projects()
133
-
134
- try:
135
- zip_modified_time = os.path.getmtime(self.zip_file_path)
136
- except FileNotFoundError:
137
- logger.error(f"Zip file not found at {self.zip_file_path}.")
138
- return
139
-
140
- if self.csv_file_path.exists():
141
- csv_modified_time = os.path.getmtime(self.csv_file_path)
142
-
143
- if csv_modified_time >= zip_modified_time:
144
- logger.info("Loading project data from CSV...")
145
- try:
146
- self.folder_df = pd.read_csv(self.csv_file_path)
147
- logger.info(f"Loaded {len(self.folder_df)} projects from CSV. Use list_categories() and list_projects() to explore them.")
148
- except Exception as e:
149
- logger.error(f"Failed to read CSV file: {e}")
150
- self.folder_df = None
151
- return
152
-
153
- logger.info("Extracting folder structure from zip file...")
154
- self._extract_folder_structure()
155
- self._save_to_csv()
156
-
157
- @log_call
158
- def _find_zip_file(self):
159
- """Locate the example projects zip file in the examples directory."""
160
- for version in self.valid_versions:
161
- potential_zip = self.examples_dir / f"Example_Projects_{version.replace('.', '_')}.zip"
162
- if potential_zip.exists():
163
- self.zip_file_path = potential_zip
164
- logger.info(f"Found zip file: {self.zip_file_path}")
165
- break
166
- else:
167
- logger.warning("No existing example projects zip file found.")
168
-
169
- @log_call
170
- def _extract_folder_structure(self):
171
- """
172
- Extract folder structure from the zip file.
173
-
174
- Populates folder_df with category and project information.
175
- """
176
- folder_data = []
177
- try:
178
- with zipfile.ZipFile(self.zip_file_path, 'r') as zip_ref:
179
- for file in zip_ref.namelist():
180
- parts = Path(file).parts
181
- if len(parts) > 1:
182
- folder_data.append({
183
- 'Category': parts[0],
184
- 'Project': parts[1]
185
- })
186
-
187
- self.folder_df = pd.DataFrame(folder_data).drop_duplicates()
188
- logger.info(f"Extracted {len(self.folder_df)} projects.")
189
- logger.debug(f"folder_df:\n{self.folder_df}")
190
- except zipfile.BadZipFile:
191
- logger.error(f"The file {self.zip_file_path} is not a valid zip file.")
192
- self.folder_df = pd.DataFrame(columns=['Category', 'Project'])
193
- except Exception as e:
194
- logger.error(f"An error occurred while extracting the folder structure: {str(e)}")
195
- self.folder_df = pd.DataFrame(columns=['Category', 'Project'])
196
-
197
- @log_call
198
- def _save_to_csv(self):
199
- """Save the extracted folder structure to CSV file."""
200
- if self.folder_df is not None and not self.folder_df.empty:
201
- try:
202
- self.folder_df.to_csv(self.csv_file_path, index=False)
203
- logger.info(f"Saved project data to {self.csv_file_path}")
204
- except Exception as e:
205
- logger.error(f"Failed to save project data to CSV: {e}")
206
- else:
207
- logger.warning("No folder data to save to CSV.")
208
-
209
-
210
- @log_call
211
- def list_categories(self):
212
- """
213
- List all categories of example projects.
214
- """
215
- if self.folder_df is None or 'Category' not in self.folder_df.columns:
216
- logger.warning("No categories available. Make sure the zip file is properly loaded.")
217
- return []
218
- categories = self.folder_df['Category'].unique()
219
- logger.info(f"Available categories: {', '.join(categories)}")
220
- return categories.tolist()
221
-
222
- @log_call
223
- def list_projects(self, category=None):
224
- """
225
- List all projects or projects in a specific category.
226
- """
227
- if self.folder_df is None:
228
- logger.warning("No projects available. Make sure the zip file is properly loaded.")
229
- return []
230
- if category:
231
- projects = self.folder_df[self.folder_df['Category'] == category]['Project'].unique()
232
- logger.info(f"Projects in category '{category}': {', '.join(projects)}")
233
- else:
234
- projects = self.folder_df['Project'].unique()
235
- logger.info(f"All available projects: {', '.join(projects)}")
236
- return projects.tolist()
237
-
238
- @log_call
239
- def extract_project(self, project_names: Union[str, List[str]]):
240
- """
241
- Extract one or more specific HEC-RAS projects from the zip file.
242
- """
243
- if isinstance(project_names, str):
244
- project_names = [project_names]
245
-
246
- extracted_paths = []
247
-
248
- for project_name in project_names:
249
- logger.info("----- RasExamples Extracting Project -----")
250
- logger.info(f"Extracting project '{project_name}'")
251
- project_path = self.projects_dir
252
-
253
- if (project_path / project_name).exists():
254
- logger.info(f"Project '{project_name}' already exists. Deleting existing folder...")
255
- try:
256
- shutil.rmtree(project_path / project_name)
257
- logger.info(f"Existing folder for project '{project_name}' has been deleted.")
258
- except Exception as e:
259
- logger.error(f"Failed to delete existing project folder '{project_name}': {e}")
260
- continue
261
-
262
- if self.folder_df is None or self.folder_df.empty:
263
- error_msg = "No project information available. Make sure the zip file is properly loaded."
264
- logger.error(error_msg)
265
- raise ValueError(error_msg)
266
-
267
- project_info = self.folder_df[self.folder_df['Project'] == project_name]
268
- if project_info.empty:
269
- error_msg = f"Project '{project_name}' not found in the zip file."
270
- logger.error(error_msg)
271
- raise ValueError(error_msg)
272
-
273
- category = project_info['Category'].iloc[0]
274
-
275
- # Ensure the project directory exists
276
- project_path.mkdir(parents=True, exist_ok=True)
277
-
278
- try:
279
- with zipfile.ZipFile(self.zip_file_path, 'r') as zip_ref:
280
- for file in zip_ref.namelist():
281
- parts = Path(file).parts
282
- if len(parts) > 1 and parts[1] == project_name:
283
- # Remove the first level (category)
284
- relative_path = Path(*parts[1:])
285
- extract_path = project_path / relative_path
286
- if file.endswith('/'):
287
- extract_path.mkdir(parents=True, exist_ok=True)
288
- else:
289
- extract_path.parent.mkdir(parents=True, exist_ok=True)
290
- with zip_ref.open(file) as source, open(extract_path, "wb") as target:
291
- shutil.copyfileobj(source, target)
292
-
293
- logger.info(f"Successfully extracted project '{project_name}' to {project_path / project_name}")
294
- extracted_paths.append(project_path / project_name)
295
- except zipfile.BadZipFile:
296
- logger.error(f"Error: The file {self.zip_file_path} is not a valid zip file.")
297
- except FileNotFoundError:
298
- logger.error(f"Error: The file {self.zip_file_path} was not found.")
299
- except Exception as e:
300
- logger.error(f"An unexpected error occurred while extracting the project: {str(e)}")
301
- logger.info("----- RasExamples Extraction Complete -----")
302
- return extracted_paths
303
-
304
- @log_call
305
- def is_project_extracted(self, project_name):
306
- """
307
- Check if a specific project is already extracted.
308
- """
309
- project_path = self.projects_dir / project_name
310
- is_extracted = project_path.exists()
311
- logger.info(f"Project '{project_name}' extracted: {is_extracted}")
312
- return is_extracted
313
-
314
- @log_call
315
- def clean_projects_directory(self):
316
- """Remove all extracted projects from the example_projects directory."""
317
- logger.info(f"Cleaning projects directory: {self.projects_dir}")
318
- if self.projects_dir.exists():
319
- try:
320
- shutil.rmtree(self.projects_dir)
321
- logger.info("All projects have been removed.")
322
- except Exception as e:
323
- logger.error(f"Failed to remove projects directory: {e}")
324
- else:
325
- logger.warning("Projects directory does not exist.")
326
- self.projects_dir.mkdir(parents=True, exist_ok=True)
327
- logger.info("Projects directory cleaned and recreated.")
328
-
329
-
330
- @log_call
331
- def download_fema_ble_model(self, huc8, output_dir=None):
332
- """
333
- Download a FEMA Base Level Engineering (BLE) model for a given HUC8.
334
-
335
- Args:
336
- huc8 (str): The 8-digit Hydrologic Unit Code (HUC) for the desired watershed.
337
- output_dir (str, optional): The directory to save the downloaded files. If None, uses the current working directory.
338
-
339
- Returns:
340
- str: The path to the downloaded and extracted model directory.
341
-
342
- Note:
343
- This method downloads the BLE model from the FEMA website and extracts it to the specified directory.
344
- """
345
- # Method implementation...
346
-
347
- @log_call
348
- def _make_safe_folder_name(self, name: str) -> str:
349
- """
350
- Convert a string to a safe folder name by replacing unsafe characters with underscores.
351
- """
352
- safe_name = re.sub(r'[^a-zA-Z0-9_\-]', '_', name)
353
- logger.debug(f"Converted '{name}' to safe folder name '{safe_name}'")
354
- return safe_name
355
-
356
- @log_call
357
- def _download_file_with_progress(self, url: str, dest_folder: Path, file_size: int) -> Path:
358
- """
359
- Download a file from a URL to a specified destination folder with progress bar.
360
- """
361
- local_filename = dest_folder / url.split('/')[-1]
362
- try:
363
- with requests.get(url, stream=True) as r:
364
- r.raise_for_status()
365
- with open(local_filename, 'wb') as f, tqdm(
366
- desc=local_filename.name,
367
- total=file_size,
368
- unit='iB',
369
- unit_scale=True,
370
- unit_divisor=1024,
371
- ) as progress_bar:
372
- for chunk in r.iter_content(chunk_size=8192):
373
- size = f.write(chunk)
374
- progress_bar.update(size)
375
- logger.info(f"Successfully downloaded {url} to {local_filename}")
376
- return local_filename
377
- except requests.exceptions.RequestException as e:
378
- logger.error(f"Request failed for {url}: {e}")
379
- raise
380
- except Exception as e:
381
- logger.error(f"Failed to write file {local_filename}: {e}")
382
- raise
383
-
384
- @log_call
385
- def _convert_size_to_bytes(self, size_str: str) -> int:
386
- """
387
- Convert a human-readable file size to bytes.
388
- """
389
- units = {'B': 1, 'KB': 1024, 'MB': 1024**2, 'GB': 1024**3, 'TB': 1024**4}
390
- size_str = size_str.upper().replace(' ', '')
391
- if not re.match(r'^\d+(\.\d+)?[BKMGT]B?$', size_str):
392
- raise ValueError(f"Invalid size string: {size_str}")
393
-
394
- number, unit = float(re.findall(r'[\d\.]+', size_str)[0]), re.findall(r'[BKMGT]B?', size_str)[0]
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
+ _folder_df = None
74
+ _zip_file_path = None
75
+
76
+ def __init__(self):
77
+ """Initialize RasExamples and ensure data is loaded"""
78
+ self._ensure_initialized()
79
+
80
+ @property
81
+ def folder_df(self):
82
+ """Access the folder DataFrame"""
83
+ self._ensure_initialized()
84
+ return self._folder_df
85
+
86
+ def _ensure_initialized(self):
87
+ """Ensure the class is initialized with required data"""
88
+ self.projects_dir.mkdir(parents=True, exist_ok=True)
89
+ if self._folder_df is None:
90
+ self._load_project_data()
91
+
92
+ def _load_project_data(self):
93
+ """Load project data from CSV if up-to-date, otherwise extract from zip."""
94
+ logger.debug("Loading project data")
95
+ self._find_zip_file()
96
+
97
+ if not self._zip_file_path:
98
+ logger.info("No example projects zip file found. Downloading...")
99
+ self.get_example_projects()
100
+
101
+ try:
102
+ zip_modified_time = os.path.getmtime(self._zip_file_path)
103
+ except FileNotFoundError:
104
+ logger.error(f"Zip file not found at {self._zip_file_path}.")
105
+ return
106
+
107
+ if self.csv_file_path.exists():
108
+ csv_modified_time = os.path.getmtime(self.csv_file_path)
109
+
110
+ if csv_modified_time >= zip_modified_time:
111
+ logger.info("Loading project data from CSV...")
112
+ try:
113
+ self._folder_df = pd.read_csv(self.csv_file_path)
114
+ logger.info(f"Loaded {len(self._folder_df)} projects from CSV.")
115
+ return
116
+ except Exception as e:
117
+ logger.error(f"Failed to read CSV file: {e}")
118
+ self._folder_df = None
119
+
120
+ logger.info("Extracting folder structure from zip file...")
121
+ self._extract_folder_structure()
122
+ self._save_to_csv()
123
+
124
+ @classmethod
125
+ def extract_project(cls, project_names: Union[str, List[str]]):
126
+ """Extract one or more specific HEC-RAS projects from the zip file."""
127
+ logger.debug(f"Extracting projects: {project_names}")
128
+
129
+ # Initialize if needed
130
+ if cls._folder_df is None:
131
+ cls._find_zip_file()
132
+ if not cls._zip_file_path:
133
+ logger.info("No example projects zip file found. Downloading...")
134
+ cls.get_example_projects()
135
+ cls._load_project_data()
136
+
137
+ if isinstance(project_names, str):
138
+ project_names = [project_names]
139
+
140
+ extracted_paths = []
141
+
142
+ for project_name in project_names:
143
+ logger.info("----- RasExamples Extracting Project -----")
144
+ logger.info(f"Extracting project '{project_name}'")
145
+ project_path = cls.projects_dir
146
+
147
+ if (project_path / project_name).exists():
148
+ logger.info(f"Project '{project_name}' already exists. Deleting existing folder...")
149
+ try:
150
+ shutil.rmtree(project_path / project_name)
151
+ logger.info(f"Existing folder for project '{project_name}' has been deleted.")
152
+ except Exception as e:
153
+ logger.error(f"Failed to delete existing project folder '{project_name}': {e}")
154
+ continue
155
+
156
+ project_info = cls._folder_df[cls._folder_df['Project'] == project_name]
157
+ if project_info.empty:
158
+ error_msg = f"Project '{project_name}' not found in the zip file."
159
+ logger.error(error_msg)
160
+ raise ValueError(error_msg)
161
+
162
+ try:
163
+ with zipfile.ZipFile(cls._zip_file_path, 'r') as zip_ref:
164
+ for file in zip_ref.namelist():
165
+ parts = Path(file).parts
166
+ if len(parts) > 1 and parts[1] == project_name:
167
+ relative_path = Path(*parts[1:])
168
+ extract_path = project_path / relative_path
169
+ if file.endswith('/'):
170
+ extract_path.mkdir(parents=True, exist_ok=True)
171
+ else:
172
+ extract_path.parent.mkdir(parents=True, exist_ok=True)
173
+ with zip_ref.open(file) as source, open(extract_path, "wb") as target:
174
+ shutil.copyfileobj(source, target)
175
+
176
+ logger.info(f"Successfully extracted project '{project_name}' to {project_path / project_name}")
177
+ extracted_paths.append(project_path / project_name)
178
+ except Exception as e:
179
+ logger.error(f"An error occurred while extracting project '{project_name}': {str(e)}")
180
+
181
+ return extracted_paths
182
+
183
+ @classmethod
184
+ def _find_zip_file(cls):
185
+ """Locate the example projects zip file in the examples directory."""
186
+ for version in cls.valid_versions:
187
+ potential_zip = cls.examples_dir / f"Example_Projects_{version.replace('.', '_')}.zip"
188
+ if potential_zip.exists():
189
+ cls._zip_file_path = potential_zip
190
+ logger.info(f"Found zip file: {cls._zip_file_path}")
191
+ break
192
+ else:
193
+ logger.warning("No existing example projects zip file found.")
194
+
195
+ @classmethod
196
+ def get_example_projects(cls, version_number='6.6'):
197
+ """
198
+ Download and extract HEC-RAS example projects for a specified version.
199
+ """
200
+ logger.info(f"Getting example projects for version {version_number}")
201
+ if version_number not in cls.valid_versions:
202
+ error_msg = f"Invalid version number. Valid versions are: {', '.join(cls.valid_versions)}"
203
+ logger.error(error_msg)
204
+ raise ValueError(error_msg)
205
+
206
+ zip_url = f"{cls.base_url}1.0.33/Example_Projects_{version_number.replace('.', '_')}.zip"
207
+
208
+ cls.examples_dir.mkdir(parents=True, exist_ok=True)
209
+
210
+ cls._zip_file_path = cls.examples_dir / f"Example_Projects_{version_number.replace('.', '_')}.zip"
211
+
212
+ if not cls._zip_file_path.exists():
213
+ 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....")
214
+ try:
215
+ response = requests.get(zip_url, stream=True)
216
+ response.raise_for_status()
217
+ with open(cls._zip_file_path, 'wb') as file:
218
+ shutil.copyfileobj(response.raw, file)
219
+ logger.info(f"Downloaded to {cls._zip_file_path}")
220
+ except requests.exceptions.RequestException as e:
221
+ logger.error(f"Failed to download the zip file: {e}")
222
+ raise
223
+ else:
224
+ logger.info("HEC-RAS Example Projects zip file already exists. Skipping download.")
225
+
226
+ cls._load_project_data()
227
+ return cls.projects_dir
228
+
229
+ @classmethod
230
+ def _load_project_data(cls):
231
+ """Load project data from CSV if up-to-date, otherwise extract from zip."""
232
+ logger.debug("Loading project data")
233
+
234
+ try:
235
+ zip_modified_time = os.path.getmtime(cls._zip_file_path)
236
+ except FileNotFoundError:
237
+ logger.error(f"Zip file not found at {cls._zip_file_path}.")
238
+ return
239
+
240
+ if cls.csv_file_path.exists():
241
+ csv_modified_time = os.path.getmtime(cls.csv_file_path)
242
+
243
+ if csv_modified_time >= zip_modified_time:
244
+ logger.info("Loading project data from CSV...")
245
+ try:
246
+ cls._folder_df = pd.read_csv(cls.csv_file_path)
247
+ logger.info(f"Loaded {len(cls._folder_df)} projects from CSV.")
248
+ return
249
+ except Exception as e:
250
+ logger.error(f"Failed to read CSV file: {e}")
251
+ cls._folder_df = None
252
+
253
+ logger.info("Extracting folder structure from zip file...")
254
+ cls._extract_folder_structure()
255
+ cls._save_to_csv()
256
+
257
+ @classmethod
258
+ def _extract_folder_structure(cls):
259
+ """
260
+ Extract folder structure from the zip file.
261
+
262
+ Populates folder_df with category and project information.
263
+ """
264
+ folder_data = []
265
+ try:
266
+ with zipfile.ZipFile(cls._zip_file_path, 'r') as zip_ref:
267
+ for file in zip_ref.namelist():
268
+ parts = Path(file).parts
269
+ if len(parts) > 1:
270
+ folder_data.append({
271
+ 'Category': parts[0],
272
+ 'Project': parts[1]
273
+ })
274
+
275
+ cls._folder_df = pd.DataFrame(folder_data).drop_duplicates()
276
+ logger.info(f"Extracted {len(cls._folder_df)} projects.")
277
+ logger.debug(f"folder_df:\n{cls._folder_df}")
278
+ except zipfile.BadZipFile:
279
+ logger.error(f"The file {cls._zip_file_path} is not a valid zip file.")
280
+ cls._folder_df = pd.DataFrame(columns=['Category', 'Project'])
281
+ except Exception as e:
282
+ logger.error(f"An error occurred while extracting the folder structure: {str(e)}")
283
+ cls._folder_df = pd.DataFrame(columns=['Category', 'Project'])
284
+
285
+ @classmethod
286
+ def _save_to_csv(cls):
287
+ """Save the extracted folder structure to CSV file."""
288
+ if cls._folder_df is not None and not cls._folder_df.empty:
289
+ try:
290
+ cls._folder_df.to_csv(cls.csv_file_path, index=False)
291
+ logger.info(f"Saved project data to {cls.csv_file_path}")
292
+ except Exception as e:
293
+ logger.error(f"Failed to save project data to CSV: {e}")
294
+ else:
295
+ logger.warning("No folder data to save to CSV.")
296
+
297
+ @classmethod
298
+ def list_categories(cls):
299
+ """
300
+ List all categories of example projects.
301
+ """
302
+ if cls._folder_df is None or 'Category' not in cls._folder_df.columns:
303
+ logger.warning("No categories available. Make sure the zip file is properly loaded.")
304
+ return []
305
+ categories = cls._folder_df['Category'].unique()
306
+ logger.info(f"Available categories: {', '.join(categories)}")
307
+ return categories.tolist()
308
+
309
+ @classmethod
310
+ def list_projects(cls, category=None):
311
+ """
312
+ List all projects or projects in a specific category.
313
+ """
314
+ if cls._folder_df is None:
315
+ logger.warning("No projects available. Make sure the zip file is properly loaded.")
316
+ return []
317
+ if category:
318
+ projects = cls._folder_df[cls._folder_df['Category'] == category]['Project'].unique()
319
+ logger.info(f"Projects in category '{category}': {', '.join(projects)}")
320
+ else:
321
+ projects = cls._folder_df['Project'].unique()
322
+ logger.info(f"All available projects: {', '.join(projects)}")
323
+ return projects.tolist()
324
+
325
+ @classmethod
326
+ def is_project_extracted(cls, project_name):
327
+ """
328
+ Check if a specific project is already extracted.
329
+ """
330
+ project_path = cls.projects_dir / project_name
331
+ is_extracted = project_path.exists()
332
+ logger.info(f"Project '{project_name}' extracted: {is_extracted}")
333
+ return is_extracted
334
+
335
+ @classmethod
336
+ def clean_projects_directory(cls):
337
+ """Remove all extracted projects from the example_projects directory."""
338
+ logger.info(f"Cleaning projects directory: {cls.projects_dir}")
339
+ if cls.projects_dir.exists():
340
+ try:
341
+ shutil.rmtree(cls.projects_dir)
342
+ logger.info("All projects have been removed.")
343
+ except Exception as e:
344
+ logger.error(f"Failed to remove projects directory: {e}")
345
+ else:
346
+ logger.warning("Projects directory does not exist.")
347
+ cls.projects_dir.mkdir(parents=True, exist_ok=True)
348
+ logger.info("Projects directory cleaned and recreated.")
349
+
350
+ @classmethod
351
+ def download_fema_ble_model(cls, huc8, output_dir=None):
352
+ """
353
+ Download a FEMA Base Level Engineering (BLE) model for a given HUC8.
354
+
355
+ Args:
356
+ huc8 (str): The 8-digit Hydrologic Unit Code (HUC) for the desired watershed.
357
+ output_dir (str, optional): The directory to save the downloaded files. If None, uses the current working directory.
358
+
359
+ Returns:
360
+ str: The path to the downloaded and extracted model directory.
361
+
362
+ Note:
363
+ This method downloads the BLE model from the FEMA website and extracts it to the specified directory.
364
+ """
365
+ # Method implementation...
366
+
367
+ @classmethod
368
+ def _make_safe_folder_name(cls, name: str) -> str:
369
+ """
370
+ Convert a string to a safe folder name by replacing unsafe characters with underscores.
371
+ """
372
+ safe_name = re.sub(r'[^a-zA-Z0-9_\-]', '_', name)
373
+ logger.debug(f"Converted '{name}' to safe folder name '{safe_name}'")
374
+ return safe_name
375
+
376
+ @classmethod
377
+ def _download_file_with_progress(cls, url: str, dest_folder: Path, file_size: int) -> Path:
378
+ """
379
+ Download a file from a URL to a specified destination folder with progress bar.
380
+ """
381
+ local_filename = dest_folder / url.split('/')[-1]
382
+ try:
383
+ with requests.get(url, stream=True) as r:
384
+ r.raise_for_status()
385
+ with open(local_filename, 'wb') as f, tqdm(
386
+ desc=local_filename.name,
387
+ total=file_size,
388
+ unit='iB',
389
+ unit_scale=True,
390
+ unit_divisor=1024,
391
+ ) as progress_bar:
392
+ for chunk in r.iter_content(chunk_size=8192):
393
+ size = f.write(chunk)
394
+ progress_bar.update(size)
395
+ logger.info(f"Successfully downloaded {url} to {local_filename}")
396
+ return local_filename
397
+ except requests.exceptions.RequestException as e:
398
+ logger.error(f"Request failed for {url}: {e}")
399
+ raise
400
+ except Exception as e:
401
+ logger.error(f"Failed to write file {local_filename}: {e}")
402
+ raise
403
+
404
+ @classmethod
405
+ def _convert_size_to_bytes(cls, size_str: str) -> int:
406
+ """
407
+ Convert a human-readable file size to bytes.
408
+ """
409
+ units = {'B': 1, 'KB': 1024, 'MB': 1024**2, 'GB': 1024**3, 'TB': 1024**4}
410
+ size_str = size_str.upper().replace(' ', '')
411
+ if not re.match(r'^\d+(\.\d+)?[BKMGT]B?$', size_str):
412
+ raise ValueError(f"Invalid size string: {size_str}")
413
+
414
+ number, unit = float(re.findall(r'[\d\.]+', size_str)[0]), re.findall(r'[BKMGT]B?', size_str)[0]
395
415
  return int(number * units[unit])
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: ras-commander
3
- Version: 0.56.0
3
+ Version: 0.57.0
4
4
  Summary: A Python library for automating HEC-RAS 6.x operations
5
5
  Home-page: https://github.com/gpt-cmdr/ras-commander
6
- Author: William M. Katzenmeyer
6
+ Author: William M. Katzenmeyer, P.E., C.F.M.
7
7
  Author-email: heccommander@gmail.com
8
8
  Requires-Python: >=3.10
9
9
  Description-Content-Type: text/markdown
@@ -20,6 +20,7 @@ Requires-Dist: matplotlib
20
20
  Requires-Dist: shapely
21
21
  Requires-Dist: pathlib
22
22
  Requires-Dist: rasterstats
23
+ Requires-Dist: rtree
23
24
  Dynamic: author
24
25
  Dynamic: author-email
25
26
  Dynamic: description
@@ -254,7 +255,7 @@ ras_commander
254
255
 
255
256
  ### Accessing HEC Examples through RasExamples
256
257
 
257
- The `RasExamples` class provides functionality for quickly loading and managing HEC-RAS example projects. This is particularly useful for testing and development purposes.
258
+ The `RasExamples` class provides functionality for quickly loading and managing HEC-RAS example projects. This is particularly useful for testing and development purposes. All examples in the ras-commander repository currently utilize HEC example projects to provide fully running scripts and notebooks for end user testing, demonstration and adaption.
258
259
 
259
260
  Key features:
260
261
  - Download and extract HEC-RAS example projects
@@ -17,7 +17,7 @@ ras_commander/HdfUtils.py,sha256=VkIKAXBrLwTlk2VtXSO-W3RU-NHpfHbE1QcZUZgl-t8,152
17
17
  ras_commander/HdfXsec.py,sha256=flREnFFrIZu4SSKGRQeX9w3SS49q0UWPJnq4zO7DbUM,27342
18
18
  ras_commander/LoggingConfig.py,sha256=gWe5K5XTmMQpSczsTysAqpC9my24i_IyM8dvD85fxYg,2704
19
19
  ras_commander/RasCmdr.py,sha256=2i9gR5koFfBLwvhYAbNgQFNKKUKqxD1Hf6T9SN9sx-s,26558
20
- ras_commander/RasExamples.py,sha256=eYlRKryCG88FN5p23TnA1-E2Bxuaz3OxjdHPHJSqdB8,17006
20
+ ras_commander/RasExamples.py,sha256=O4h5WwtewoFZjHqQ4i1hhbUUDSCW6TU4MloXqhQTt8U,17090
21
21
  ras_commander/RasGeo.py,sha256=M0sVNKlWmmbve8iMXLWq25WgbxqLWBo7_1oDg_rALzU,5607
22
22
  ras_commander/RasGpt.py,sha256=N_7p2nucWrBBXdB2k2ZKvOeOdXNmFD9dIY3W7_5i5nw,1206
23
23
  ras_commander/RasMapper.py,sha256=A7xupixCmgXFiSfQs3oWBMBstrO8XcxieMeZWKTcbPQ,3271
@@ -27,8 +27,8 @@ ras_commander/RasToGo.py,sha256=TKujfaV1xQhFaOddF4g2ogGy6ky-CLlfelSMPD2J3Nk,1223
27
27
  ras_commander/RasUnsteady.py,sha256=KfCXAag-_bPwwS3JbPZH-s4hbaoHACO0mlRnGrzbFgA,32092
28
28
  ras_commander/RasUtils.py,sha256=P2-aBL61kdRINsjnBpstZVD6VVc7hI_D3RUXqr6ldmc,34863
29
29
  ras_commander/__init__.py,sha256=mceEWRQJkDBi1o3zVg7DpG2qMrMnKHwwuK3GwyxoVr4,2132
30
- ras_commander-0.56.0.dist-info/LICENSE,sha256=_pbd6qHnlsz1iQ-ozDW_49r86BZT6CRwO2iBtw0iN6M,457
31
- ras_commander-0.56.0.dist-info/METADATA,sha256=Moc5O9hdyNuTXaaPtp_5zjnIIWJ_J3MJdMN9e5g9TD0,22275
32
- ras_commander-0.56.0.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
33
- ras_commander-0.56.0.dist-info/top_level.txt,sha256=i76S7eKLFC8doKcXDl3aiOr9RwT06G8adI6YuKbQDaA,14
34
- ras_commander-0.56.0.dist-info/RECORD,,
30
+ ras_commander-0.57.0.dist-info/LICENSE,sha256=_pbd6qHnlsz1iQ-ozDW_49r86BZT6CRwO2iBtw0iN6M,457
31
+ ras_commander-0.57.0.dist-info/METADATA,sha256=H1Px65L0_yV2AKMfnDgZ_ukH4GicbbJRaSX4g8Xxzd4,22494
32
+ ras_commander-0.57.0.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
33
+ ras_commander-0.57.0.dist-info/top_level.txt,sha256=i76S7eKLFC8doKcXDl3aiOr9RwT06G8adI6YuKbQDaA,14
34
+ ras_commander-0.57.0.dist-info/RECORD,,