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.
- ras_commander/RasCmdr.py +171 -138
- ras_commander/RasExamples.py +334 -120
- ras_commander/RasGeo.py +27 -6
- ras_commander/RasHdf.py +1702 -0
- ras_commander/RasPlan.py +398 -437
- ras_commander/RasPrj.py +403 -65
- ras_commander/RasUnsteady.py +24 -4
- ras_commander/RasUtils.py +352 -51
- ras_commander/__init__.py +4 -1
- ras_commander-0.35.0.dist-info/METADATA +319 -0
- ras_commander-0.35.0.dist-info/RECORD +15 -0
- ras_commander-0.33.0.dist-info/METADATA +0 -5
- ras_commander-0.33.0.dist-info/RECORD +0 -14
- {ras_commander-0.33.0.dist-info → ras_commander-0.35.0.dist-info}/LICENSE +0 -0
- {ras_commander-0.33.0.dist-info → ras_commander-0.35.0.dist-info}/WHEEL +0 -0
- {ras_commander-0.33.0.dist-info → ras_commander-0.35.0.dist-info}/top_level.txt +0 -0
ras_commander/RasExamples.py
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
63
|
+
logging.info("No example projects zip file found. Downloading...")
|
88
64
|
self.get_example_projects()
|
89
65
|
|
90
|
-
|
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
|
-
|
97
|
-
|
98
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
133
|
-
|
134
|
-
|
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
|
-
|
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
|
-
|
143
|
-
|
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
|
-
|
142
|
+
logging.info(f"Getting example projects for version {version_number}")
|
159
143
|
if version_number not in self.valid_versions:
|
160
|
-
|
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
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
233
|
-
|
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
|
-
|
238
|
-
|
239
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
254
|
+
logging.error(f"Error: The file {self.zip_file_path} is not a valid zip file.")
|
272
255
|
except FileNotFoundError:
|
273
|
-
|
256
|
+
logging.error(f"Error: The file {self.zip_file_path} was not found.")
|
274
257
|
except Exception as e:
|
275
|
-
|
276
|
-
|
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
|
-
|
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
|
-
|
273
|
+
logging.info(f"Cleaning projects directory: {self.projects_dir}")
|
295
274
|
if self.projects_dir.exists():
|
296
|
-
|
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
|
-
|
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
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
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
|
-
|
75
|
+
logging.info(f"Deleting geometry preprocessor file: {geom_preprocessor_file}")
|
66
76
|
geom_preprocessor_file.unlink()
|
67
|
-
|
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
|
-
|
85
|
+
logging.warning(f"No geometry preprocessor file found for: {plan_file}")
|
74
86
|
|
75
87
|
if plan_files is None:
|
76
|
-
|
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
|
-
|
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
|