ras-commander 0.1.6__py2.py3-none-any.whl → 0.21.0__py2.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.
@@ -0,0 +1,304 @@
1
+ import os
2
+ import requests
3
+ import zipfile
4
+ import pandas as pd
5
+ from pathlib import Path
6
+ import shutil
7
+ from typing import Union, List
8
+ import csv
9
+ from datetime import datetime
10
+
11
+ class RasExamples:
12
+ """
13
+ A class for quickly loading HEC-RAS example projects for testing and development of ras_commander.
14
+
15
+ This class provides functionality to download, extract, and manage HEC-RAS example projects.
16
+ It supports both default HEC-RAS example projects and custom projects from user-provided URLs.
17
+
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
+
47
+ """
48
+
49
+ def __init__(self):
50
+ """
51
+ 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
+ """
60
+ self.base_url = 'https://github.com/HydrologicEngineeringCenter/hec-downloads/releases/download/'
61
+ self.valid_versions = [
62
+ "6.5", "6.4.1", "6.3.1", "6.3", "6.2", "6.1", "6.0",
63
+ "5.0.7", "5.0.6", "5.0.5", "5.0.4", "5.0.3", "5.0.1", "5.0",
64
+ "4.1", "4.0", "3.1.3", "3.1.2", "3.1.1", "3.0", "2.2"
65
+ ]
66
+ self.base_dir = Path.cwd()
67
+ self.examples_dir = self.base_dir
68
+ self.projects_dir = self.examples_dir / 'example_projects'
69
+ self.zip_file_path = None
70
+ self.folder_df = None
71
+ self.csv_file_path = self.examples_dir / 'example_projects.csv'
72
+
73
+ self.projects_dir.mkdir(parents=True, exist_ok=True)
74
+ print(f"Example projects folder: {self.projects_dir}")
75
+ self._load_project_data()
76
+
77
+ def _load_project_data(self):
78
+ """
79
+ 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
+ """
84
+ self._find_zip_file()
85
+
86
+ if not self.zip_file_path:
87
+ print("No example projects zip file found. Downloading...")
88
+ self.get_example_projects()
89
+
90
+ zip_modified_time = os.path.getmtime(self.zip_file_path)
91
+
92
+ if self.csv_file_path.exists():
93
+ csv_modified_time = os.path.getmtime(self.csv_file_path)
94
+
95
+ 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")
99
+ return
100
+
101
+ print("Extracting folder structure from zip file...")
102
+ self._extract_folder_structure()
103
+ self._save_to_csv()
104
+
105
+ def _find_zip_file(self):
106
+ """Locate the example projects zip file in the examples directory."""
107
+ for version in self.valid_versions:
108
+ potential_zip = self.examples_dir / f"Example_Projects_{version.replace('.', '_')}.zip"
109
+ if potential_zip.exists():
110
+ self.zip_file_path = potential_zip
111
+ print(f"Found zip file: {self.zip_file_path}")
112
+ break
113
+
114
+ def _extract_folder_structure(self):
115
+ """
116
+ Extract folder structure from the zip file.
117
+
118
+ Populates folder_df with category and project information.
119
+ """
120
+ folder_data = []
121
+ try:
122
+ with zipfile.ZipFile(self.zip_file_path, 'r') as zip_ref:
123
+ for file in zip_ref.namelist():
124
+ parts = Path(file).parts
125
+ if len(parts) > 2:
126
+ folder_data.append({
127
+ 'Category': parts[1],
128
+ 'Project': parts[2]
129
+ })
130
+
131
+ 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)
135
+ except Exception as e:
136
+ print(f"An error occurred while extracting the folder structure: {str(e)}")
137
+ self.folder_df = pd.DataFrame(columns=['Category', 'Project'])
138
+
139
+ def _save_to_csv(self):
140
+ """Save the extracted folder structure to CSV file."""
141
+ 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}")
144
+
145
+ def get_example_projects(self, version_number='6.5'):
146
+ """
147
+ 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
+ """
158
+ print(f"Getting example projects for version {version_number}")
159
+ if version_number not in self.valid_versions:
160
+ raise ValueError(f"Invalid version number. Valid versions are: {', '.join(self.valid_versions)}")
161
+
162
+ zip_url = f"{self.base_url}1.0.31/Example_Projects_{version_number.replace('.', '_')}.zip"
163
+
164
+ self.examples_dir.mkdir(parents=True, exist_ok=True)
165
+
166
+ self.zip_file_path = self.examples_dir / f"Example_Projects_{version_number.replace('.', '_')}.zip"
167
+
168
+ 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}")
174
+ else:
175
+ print("HEC-RAS Example Projects zip file already exists. Skipping download.")
176
+
177
+ self._load_project_data()
178
+ return self.projects_dir
179
+
180
+ def list_categories(self):
181
+ """
182
+ List all categories of example projects.
183
+
184
+ Returns:
185
+ list: Available categories.
186
+ """
187
+ 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.")
189
+ return []
190
+ categories = self.folder_df['Category'].unique()
191
+ print(f"Available categories: {', '.join(categories)}")
192
+ return categories.tolist()
193
+
194
+ def list_projects(self, category=None):
195
+ """
196
+ 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
+ """
204
+ if self.folder_df is None:
205
+ print("No projects available. Make sure the zip file is properly loaded.")
206
+ return []
207
+ if category:
208
+ projects = self.folder_df[self.folder_df['Category'] == category]['Project'].unique()
209
+ else:
210
+ projects = self.folder_df['Project'].unique()
211
+ return projects.tolist()
212
+
213
+ def extract_project(self, project_names: Union[str, List[str]]):
214
+ """
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.
225
+ """
226
+ if isinstance(project_names, str):
227
+ project_names = [project_names]
228
+
229
+ extracted_paths = []
230
+
231
+ for project_name in project_names:
232
+ print("----- RasExamples Extracting Project -----")
233
+ print(f"Extracting project '{project_name}'")
234
+ project_path = self.projects_dir / project_name
235
+
236
+ 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.")
240
+
241
+ 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.")
243
+
244
+ project_info = self.folder_df[self.folder_df['Project'] == project_name]
245
+ if project_info.empty:
246
+ raise ValueError(f"Project '{project_name}' not found in the zip file.")
247
+
248
+ category = project_info['Category'].iloc[0]
249
+
250
+ # Ensure the project directory exists
251
+ project_path.mkdir(parents=True, exist_ok=True)
252
+
253
+ try:
254
+ with zipfile.ZipFile(self.zip_file_path, 'r') as zip_ref:
255
+ for file in zip_ref.namelist():
256
+ parts = Path(file).parts
257
+ if len(parts) > 2 and parts[2] == project_name:
258
+ # Remove the first two levels (category and project name)
259
+ relative_path = Path(*parts[3:])
260
+ extract_path = project_path / relative_path
261
+ if file.endswith('/'):
262
+ extract_path.mkdir(parents=True, exist_ok=True)
263
+ else:
264
+ extract_path.parent.mkdir(parents=True, exist_ok=True)
265
+ with zip_ref.open(file) as source, open(extract_path, "wb") as target:
266
+ shutil.copyfileobj(source, target)
267
+
268
+ print(f"Successfully extracted project '{project_name}' to {project_path}")
269
+ extracted_paths.append(project_path)
270
+ except zipfile.BadZipFile:
271
+ print(f"Error: The file {self.zip_file_path} is not a valid zip file.")
272
+ except FileNotFoundError:
273
+ print(f"Error: The file {self.zip_file_path} was not found.")
274
+ except Exception as e:
275
+ print(f"An unexpected error occurred while extracting the project: {str(e)}")
276
+ #print("----- RasExamples Extraction Complete -----")
277
+ return extracted_paths
278
+
279
+ def is_project_extracted(self, project_name):
280
+ """
281
+ 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
+ """
289
+ project_path = self.projects_dir / project_name
290
+ return project_path.exists()
291
+
292
+ def clean_projects_directory(self):
293
+ """Remove all extracted projects from the example_projects directory."""
294
+ print(f"Cleaning projects directory: {self.projects_dir}")
295
+ if self.projects_dir.exists():
296
+ shutil.rmtree(self.projects_dir)
297
+ self.projects_dir.mkdir(parents=True, exist_ok=True)
298
+ print("Projects directory cleaned.")
299
+
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}")
@@ -0,0 +1,83 @@
1
+ """
2
+ Operations for handling geometry files in HEC-RAS projects.
3
+ """
4
+ from pathlib import Path
5
+ from typing import List, Union
6
+ from .RasPlan import RasPlan
7
+ from .RasPrj import ras
8
+ import re
9
+
10
+ class RasGeo:
11
+ """
12
+ A class for operations on HEC-RAS geometry files.
13
+ """
14
+
15
+ @staticmethod
16
+ def clear_geompre_files(
17
+ plan_files: Union[str, Path, List[Union[str, Path]]] = None,
18
+ ras_object = None
19
+ ) -> None:
20
+ """
21
+ Clear HEC-RAS geometry preprocessor files for specified plan files or all plan files in the project directory.
22
+
23
+ Limitations/Future Work:
24
+ - This function only deletes the geometry preprocessor file.
25
+ - It does not clear the IB tables.
26
+ - It also does not clear geometry preprocessor tables from the geometry HDF.
27
+ - All of these features will need to be added to reliably remove geometry preprocessor files for 1D and 2D projects.
28
+
29
+ Parameters:
30
+ plan_files (Union[str, Path, List[Union[str, Path]]], optional):
31
+ Full path(s) to the HEC-RAS plan file(s) (.p*).
32
+ If None, clears all plan files in the project directory.
33
+ ras_object: An optional RAS object instance.
34
+
35
+ Returns:
36
+ None
37
+
38
+ Examples:
39
+ # Clear all geometry preprocessor files in the project directory
40
+ RasGeo.clear_geompre_files()
41
+
42
+ # Clear a single plan file
43
+ RasGeo.clear_geompre_files(r'path/to/plan.p01')
44
+
45
+ # Clear multiple plan files
46
+ RasGeo.clear_geompre_files([r'path/to/plan1.p01', r'path/to/plan2.p02'])
47
+
48
+ Note:
49
+ This function updates the ras object's geometry dataframe after clearing the preprocessor files.
50
+ """
51
+ ## Explicit Function Steps
52
+ # 1. Initialize the ras_object, defaulting to the global ras if not provided.
53
+ # 2. Define a helper function to clear a single geometry preprocessor file.
54
+ # 3. Determine the list of plan files to process based on the input.
55
+ # 4. Iterate over each plan file and clear its geometry preprocessor file.
56
+ ras_obj = ras_object or ras
57
+ ras_obj.check_initialized()
58
+
59
+ def clear_single_file(plan_file: Union[str, Path], ras_obj) -> None:
60
+ plan_path = Path(plan_file)
61
+ geom_preprocessor_suffix = '.c' + ''.join(plan_path.suffixes[1:]) if plan_path.suffixes else '.c'
62
+ geom_preprocessor_file = plan_path.with_suffix(geom_preprocessor_suffix)
63
+ if geom_preprocessor_file.exists():
64
+ print(f"Deleting geometry preprocessor file: {geom_preprocessor_file}")
65
+ geom_preprocessor_file.unlink()
66
+ print("File deletion completed successfully.")
67
+ else:
68
+ print(f"No geometry preprocessor file found for: {plan_file}")
69
+
70
+ if plan_files is None:
71
+ print("Clearing all geometry preprocessor files in the project directory.")
72
+ plan_files_to_clear = list(ras_obj.project_folder.glob(r'*.p*'))
73
+ elif isinstance(plan_files, (str, Path)):
74
+ plan_files_to_clear = [plan_files]
75
+ elif isinstance(plan_files, list):
76
+ plan_files_to_clear = plan_files
77
+ else:
78
+ raise ValueError("Invalid input. Please provide a string, Path, list of paths, or None.")
79
+
80
+ for plan_file in plan_files_to_clear:
81
+ clear_single_file(plan_file, ras_obj)
82
+ ras_obj.geom_df = ras_obj.get_geom_entries()
83
+