ras-commander 0.35.0__py3-none-any.whl → 0.36.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 +360 -332
- ras_commander/RasExamples.py +113 -80
- ras_commander/RasGeo.py +38 -28
- ras_commander/RasGpt.py +142 -0
- ras_commander/RasHdf.py +170 -253
- ras_commander/RasPlan.py +115 -166
- ras_commander/RasPrj.py +212 -141
- ras_commander/RasUnsteady.py +37 -22
- ras_commander/RasUtils.py +98 -82
- ras_commander/__init__.py +11 -13
- ras_commander/logging_config.py +80 -0
- {ras_commander-0.35.0.dist-info → ras_commander-0.36.0.dist-info}/METADATA +15 -11
- ras_commander-0.36.0.dist-info/RECORD +17 -0
- ras_commander-0.35.0.dist-info/RECORD +0 -15
- {ras_commander-0.35.0.dist-info → ras_commander-0.36.0.dist-info}/LICENSE +0 -0
- {ras_commander-0.35.0.dist-info → ras_commander-0.36.0.dist-info}/WHEEL +0 -0
- {ras_commander-0.35.0.dist-info → ras_commander-0.36.0.dist-info}/top_level.txt +0 -0
    
        ras_commander/RasExamples.py
    CHANGED
    
    | @@ -1,3 +1,28 @@ | |
| 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 | 
            +
            """
         | 
| 1 26 | 
             
            import os
         | 
| 2 27 | 
             
            import requests
         | 
| 3 28 | 
             
            import zipfile
         | 
| @@ -10,15 +35,10 @@ from datetime import datetime | |
| 10 35 | 
             
            import logging
         | 
| 11 36 | 
             
            import re
         | 
| 12 37 | 
             
            from tqdm import tqdm
         | 
| 38 | 
            +
            from ras_commander import get_logger
         | 
| 39 | 
            +
            from ras_commander.logging_config import log_call
         | 
| 13 40 |  | 
| 14 | 
            -
             | 
| 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 | 
            -
            )
         | 
| 41 | 
            +
            logger = get_logger(__name__)
         | 
| 22 42 |  | 
| 23 43 | 
             
            class RasExamples:
         | 
| 24 44 | 
             
                """
         | 
| @@ -28,10 +48,8 @@ class RasExamples: | |
| 28 48 | 
             
                It supports both default HEC-RAS example projects and custom projects from user-provided URLs.
         | 
| 29 49 | 
             
                Additionally, it includes functionality to download FEMA's Base Level Engineering (BLE) models
         | 
| 30 50 | 
             
                from CSV files provided by the FEMA Estimated Base Flood Elevation (BFE) Viewer.
         | 
| 31 | 
            -
             | 
| 32 | 
            -
                [Documentation as previously provided]
         | 
| 33 51 | 
             
                """
         | 
| 34 | 
            -
             | 
| 52 | 
            +
                @log_call
         | 
| 35 53 | 
             
                def __init__(self):
         | 
| 36 54 | 
             
                    """
         | 
| 37 55 | 
             
                    Initialize the RasExamples class.
         | 
| @@ -50,9 +68,10 @@ class RasExamples: | |
| 50 68 | 
             
                    self.csv_file_path = self.examples_dir / 'example_projects.csv'
         | 
| 51 69 |  | 
| 52 70 | 
             
                    self.projects_dir.mkdir(parents=True, exist_ok=True)
         | 
| 53 | 
            -
                     | 
| 71 | 
            +
                    logger.info(f"Example projects folder: {self.projects_dir}")
         | 
| 54 72 | 
             
                    self._load_project_data()
         | 
| 55 73 |  | 
| 74 | 
            +
                @log_call
         | 
| 56 75 | 
             
                def _load_project_data(self):
         | 
| 57 76 | 
             
                    """
         | 
| 58 77 | 
             
                    Load project data from CSV if up-to-date, otherwise extract from zip.
         | 
| @@ -60,43 +79,45 @@ class RasExamples: | |
| 60 79 | 
             
                    self._find_zip_file()
         | 
| 61 80 |  | 
| 62 81 | 
             
                    if not self.zip_file_path:
         | 
| 63 | 
            -
                         | 
| 82 | 
            +
                        logger.info("No example projects zip file found. Downloading...")
         | 
| 64 83 | 
             
                        self.get_example_projects()
         | 
| 65 84 |  | 
| 66 85 | 
             
                    try:
         | 
| 67 86 | 
             
                        zip_modified_time = os.path.getmtime(self.zip_file_path)
         | 
| 68 87 | 
             
                    except FileNotFoundError:
         | 
| 69 | 
            -
                         | 
| 88 | 
            +
                        logger.error(f"Zip file not found at {self.zip_file_path}.")
         | 
| 70 89 | 
             
                        return
         | 
| 71 90 |  | 
| 72 91 | 
             
                    if self.csv_file_path.exists():
         | 
| 73 92 | 
             
                        csv_modified_time = os.path.getmtime(self.csv_file_path)
         | 
| 74 93 |  | 
| 75 94 | 
             
                        if csv_modified_time >= zip_modified_time:
         | 
| 76 | 
            -
                             | 
| 95 | 
            +
                            logger.info("Loading project data from CSV...")
         | 
| 77 96 | 
             
                            try:
         | 
| 78 97 | 
             
                                self.folder_df = pd.read_csv(self.csv_file_path)
         | 
| 79 | 
            -
                                 | 
| 98 | 
            +
                                logger.info(f"Loaded {len(self.folder_df)} projects from CSV. Use list_categories() and list_projects() to explore them.")
         | 
| 80 99 | 
             
                            except Exception as e:
         | 
| 81 | 
            -
                                 | 
| 100 | 
            +
                                logger.error(f"Failed to read CSV file: {e}")
         | 
| 82 101 | 
             
                                self.folder_df = None
         | 
| 83 102 | 
             
                            return
         | 
| 84 103 |  | 
| 85 | 
            -
                     | 
| 104 | 
            +
                    logger.info("Extracting folder structure from zip file...")
         | 
| 86 105 | 
             
                    self._extract_folder_structure()
         | 
| 87 106 | 
             
                    self._save_to_csv()
         | 
| 88 107 |  | 
| 108 | 
            +
                @log_call
         | 
| 89 109 | 
             
                def _find_zip_file(self):
         | 
| 90 110 | 
             
                    """Locate the example projects zip file in the examples directory."""
         | 
| 91 111 | 
             
                    for version in self.valid_versions:
         | 
| 92 112 | 
             
                        potential_zip = self.examples_dir / f"Example_Projects_{version.replace('.', '_')}.zip"
         | 
| 93 113 | 
             
                        if potential_zip.exists():
         | 
| 94 114 | 
             
                            self.zip_file_path = potential_zip
         | 
| 95 | 
            -
                             | 
| 115 | 
            +
                            logger.info(f"Found zip file: {self.zip_file_path}")
         | 
| 96 116 | 
             
                            break
         | 
| 97 117 | 
             
                    else:
         | 
| 98 | 
            -
                         | 
| 118 | 
            +
                        logger.warning("No existing example projects zip file found.")
         | 
| 99 119 |  | 
| 120 | 
            +
                @log_call
         | 
| 100 121 | 
             
                def _extract_folder_structure(self):
         | 
| 101 122 | 
             
                    """
         | 
| 102 123 | 
             
                    Extract folder structure from the zip file.
         | 
| @@ -115,34 +136,36 @@ class RasExamples: | |
| 115 136 | 
             
                                    })
         | 
| 116 137 |  | 
| 117 138 | 
             
                        self.folder_df = pd.DataFrame(folder_data).drop_duplicates()
         | 
| 118 | 
            -
                         | 
| 119 | 
            -
                         | 
| 139 | 
            +
                        logger.info(f"Extracted {len(self.folder_df)} projects.")
         | 
| 140 | 
            +
                        logger.debug(f"folder_df:\n{self.folder_df}")
         | 
| 120 141 | 
             
                    except zipfile.BadZipFile:
         | 
| 121 | 
            -
                         | 
| 142 | 
            +
                        logger.error(f"The file {self.zip_file_path} is not a valid zip file.")
         | 
| 122 143 | 
             
                        self.folder_df = pd.DataFrame(columns=['Category', 'Project'])
         | 
| 123 144 | 
             
                    except Exception as e:
         | 
| 124 | 
            -
                         | 
| 145 | 
            +
                        logger.error(f"An error occurred while extracting the folder structure: {str(e)}")
         | 
| 125 146 | 
             
                        self.folder_df = pd.DataFrame(columns=['Category', 'Project'])
         | 
| 126 147 |  | 
| 148 | 
            +
                @log_call
         | 
| 127 149 | 
             
                def _save_to_csv(self):
         | 
| 128 150 | 
             
                    """Save the extracted folder structure to CSV file."""
         | 
| 129 151 | 
             
                    if self.folder_df is not None and not self.folder_df.empty:
         | 
| 130 152 | 
             
                        try:
         | 
| 131 153 | 
             
                            self.folder_df.to_csv(self.csv_file_path, index=False)
         | 
| 132 | 
            -
                             | 
| 154 | 
            +
                            logger.info(f"Saved project data to {self.csv_file_path}")
         | 
| 133 155 | 
             
                        except Exception as e:
         | 
| 134 | 
            -
                             | 
| 156 | 
            +
                            logger.error(f"Failed to save project data to CSV: {e}")
         | 
| 135 157 | 
             
                    else:
         | 
| 136 | 
            -
                         | 
| 158 | 
            +
                        logger.warning("No folder data to save to CSV.")
         | 
| 137 159 |  | 
| 160 | 
            +
                @log_call
         | 
| 138 161 | 
             
                def get_example_projects(self, version_number='6.5'):
         | 
| 139 162 | 
             
                    """
         | 
| 140 163 | 
             
                    Download and extract HEC-RAS example projects for a specified version.
         | 
| 141 164 | 
             
                    """
         | 
| 142 | 
            -
                     | 
| 165 | 
            +
                    logger.info(f"Getting example projects for version {version_number}")
         | 
| 143 166 | 
             
                    if version_number not in self.valid_versions:
         | 
| 144 167 | 
             
                        error_msg = f"Invalid version number. Valid versions are: {', '.join(self.valid_versions)}"
         | 
| 145 | 
            -
                         | 
| 168 | 
            +
                        logger.error(error_msg)
         | 
| 146 169 | 
             
                        raise ValueError(error_msg)
         | 
| 147 170 |  | 
| 148 171 | 
             
                    zip_url = f"{self.base_url}1.0.31/Example_Projects_{version_number.replace('.', '_')}.zip"
         | 
| @@ -152,48 +175,51 @@ class RasExamples: | |
| 152 175 | 
             
                    self.zip_file_path = self.examples_dir / f"Example_Projects_{version_number.replace('.', '_')}.zip"
         | 
| 153 176 |  | 
| 154 177 | 
             
                    if not self.zip_file_path.exists():
         | 
| 155 | 
            -
                         | 
| 178 | 
            +
                        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....")
         | 
| 156 179 | 
             
                        try:
         | 
| 157 180 | 
             
                            response = requests.get(zip_url, stream=True)
         | 
| 158 181 | 
             
                            response.raise_for_status()
         | 
| 159 182 | 
             
                            with open(self.zip_file_path, 'wb') as file:
         | 
| 160 183 | 
             
                                shutil.copyfileobj(response.raw, file)
         | 
| 161 | 
            -
                             | 
| 184 | 
            +
                            logger.info(f"Downloaded to {self.zip_file_path}")
         | 
| 162 185 | 
             
                        except requests.exceptions.RequestException as e:
         | 
| 163 | 
            -
                             | 
| 186 | 
            +
                            logger.error(f"Failed to download the zip file: {e}")
         | 
| 164 187 | 
             
                            raise
         | 
| 165 188 | 
             
                    else:
         | 
| 166 | 
            -
                         | 
| 189 | 
            +
                        logger.info("HEC-RAS Example Projects zip file already exists. Skipping download.")
         | 
| 167 190 |  | 
| 168 191 | 
             
                    self._load_project_data()
         | 
| 169 192 | 
             
                    return self.projects_dir
         | 
| 170 193 |  | 
| 194 | 
            +
                @log_call
         | 
| 171 195 | 
             
                def list_categories(self):
         | 
| 172 196 | 
             
                    """
         | 
| 173 197 | 
             
                    List all categories of example projects.
         | 
| 174 198 | 
             
                    """
         | 
| 175 199 | 
             
                    if self.folder_df is None or 'Category' not in self.folder_df.columns:
         | 
| 176 | 
            -
                         | 
| 200 | 
            +
                        logger.warning("No categories available. Make sure the zip file is properly loaded.")
         | 
| 177 201 | 
             
                        return []
         | 
| 178 202 | 
             
                    categories = self.folder_df['Category'].unique()
         | 
| 179 | 
            -
                     | 
| 203 | 
            +
                    logger.info(f"Available categories: {', '.join(categories)}")
         | 
| 180 204 | 
             
                    return categories.tolist()
         | 
| 181 205 |  | 
| 206 | 
            +
                @log_call
         | 
| 182 207 | 
             
                def list_projects(self, category=None):
         | 
| 183 208 | 
             
                    """
         | 
| 184 209 | 
             
                    List all projects or projects in a specific category.
         | 
| 185 210 | 
             
                    """
         | 
| 186 211 | 
             
                    if self.folder_df is None:
         | 
| 187 | 
            -
                         | 
| 212 | 
            +
                        logger.warning("No projects available. Make sure the zip file is properly loaded.")
         | 
| 188 213 | 
             
                        return []
         | 
| 189 214 | 
             
                    if category:
         | 
| 190 215 | 
             
                        projects = self.folder_df[self.folder_df['Category'] == category]['Project'].unique()
         | 
| 191 | 
            -
                         | 
| 216 | 
            +
                        logger.info(f"Projects in category '{category}': {', '.join(projects)}")
         | 
| 192 217 | 
             
                    else:
         | 
| 193 218 | 
             
                        projects = self.folder_df['Project'].unique()
         | 
| 194 | 
            -
                         | 
| 219 | 
            +
                        logger.info(f"All available projects: {', '.join(projects)}")
         | 
| 195 220 | 
             
                    return projects.tolist()
         | 
| 196 221 |  | 
| 222 | 
            +
                @log_call
         | 
| 197 223 | 
             
                def extract_project(self, project_names: Union[str, List[str]]):
         | 
| 198 224 | 
             
                    """
         | 
| 199 225 | 
             
                    Extract one or more specific HEC-RAS projects from the zip file.
         | 
| @@ -204,28 +230,28 @@ class RasExamples: | |
| 204 230 | 
             
                    extracted_paths = []
         | 
| 205 231 |  | 
| 206 232 | 
             
                    for project_name in project_names:
         | 
| 207 | 
            -
                         | 
| 208 | 
            -
                         | 
| 233 | 
            +
                        logger.info("----- RasExamples Extracting Project -----")
         | 
| 234 | 
            +
                        logger.info(f"Extracting project '{project_name}'")
         | 
| 209 235 | 
             
                        project_path = self.projects_dir / project_name
         | 
| 210 236 |  | 
| 211 237 | 
             
                        if project_path.exists():
         | 
| 212 | 
            -
                             | 
| 238 | 
            +
                            logger.info(f"Project '{project_name}' already exists. Deleting existing folder...")
         | 
| 213 239 | 
             
                            try:
         | 
| 214 240 | 
             
                                shutil.rmtree(project_path)
         | 
| 215 | 
            -
                                 | 
| 241 | 
            +
                                logger.info(f"Existing folder for project '{project_name}' has been deleted.")
         | 
| 216 242 | 
             
                            except Exception as e:
         | 
| 217 | 
            -
                                 | 
| 243 | 
            +
                                logger.error(f"Failed to delete existing project folder '{project_name}': {e}")
         | 
| 218 244 | 
             
                                continue
         | 
| 219 245 |  | 
| 220 246 | 
             
                        if self.folder_df is None or self.folder_df.empty:
         | 
| 221 247 | 
             
                            error_msg = "No project information available. Make sure the zip file is properly loaded."
         | 
| 222 | 
            -
                             | 
| 248 | 
            +
                            logger.error(error_msg)
         | 
| 223 249 | 
             
                            raise ValueError(error_msg)
         | 
| 224 250 |  | 
| 225 251 | 
             
                        project_info = self.folder_df[self.folder_df['Project'] == project_name]
         | 
| 226 252 | 
             
                        if project_info.empty:
         | 
| 227 253 | 
             
                            error_msg = f"Project '{project_name}' not found in the zip file."
         | 
| 228 | 
            -
                             | 
| 254 | 
            +
                            logger.error(error_msg)
         | 
| 229 255 | 
             
                            raise ValueError(error_msg)
         | 
| 230 256 |  | 
| 231 257 | 
             
                        category = project_info['Category'].iloc[0]
         | 
| @@ -248,40 +274,44 @@ class RasExamples: | |
| 248 274 | 
             
                                            with zip_ref.open(file) as source, open(extract_path, "wb") as target:
         | 
| 249 275 | 
             
                                                shutil.copyfileobj(source, target)
         | 
| 250 276 |  | 
| 251 | 
            -
                             | 
| 277 | 
            +
                            logger.info(f"Successfully extracted project '{project_name}' to {project_path}")
         | 
| 252 278 | 
             
                            extracted_paths.append(project_path)
         | 
| 253 279 | 
             
                        except zipfile.BadZipFile:
         | 
| 254 | 
            -
                             | 
| 280 | 
            +
                            logger.error(f"Error: The file {self.zip_file_path} is not a valid zip file.")
         | 
| 255 281 | 
             
                        except FileNotFoundError:
         | 
| 256 | 
            -
                             | 
| 282 | 
            +
                            logger.error(f"Error: The file {self.zip_file_path} was not found.")
         | 
| 257 283 | 
             
                        except Exception as e:
         | 
| 258 | 
            -
                             | 
| 259 | 
            -
                         | 
| 284 | 
            +
                            logger.error(f"An unexpected error occurred while extracting the project: {str(e)}")
         | 
| 285 | 
            +
                        logger.info("----- RasExamples Extraction Complete -----")
         | 
| 260 286 | 
             
                    return extracted_paths
         | 
| 261 287 |  | 
| 288 | 
            +
                @log_call
         | 
| 262 289 | 
             
                def is_project_extracted(self, project_name):
         | 
| 263 290 | 
             
                    """
         | 
| 264 291 | 
             
                    Check if a specific project is already extracted.
         | 
| 265 292 | 
             
                    """
         | 
| 266 293 | 
             
                    project_path = self.projects_dir / project_name
         | 
| 267 294 | 
             
                    is_extracted = project_path.exists()
         | 
| 268 | 
            -
                     | 
| 295 | 
            +
                    logger.info(f"Project '{project_name}' extracted: {is_extracted}")
         | 
| 269 296 | 
             
                    return is_extracted
         | 
| 270 297 |  | 
| 298 | 
            +
                @log_call
         | 
| 271 299 | 
             
                def clean_projects_directory(self):
         | 
| 272 300 | 
             
                    """Remove all extracted projects from the example_projects directory."""
         | 
| 273 | 
            -
                     | 
| 301 | 
            +
                    logger.info(f"Cleaning projects directory: {self.projects_dir}")
         | 
| 274 302 | 
             
                    if self.projects_dir.exists():
         | 
| 275 303 | 
             
                        try:
         | 
| 276 304 | 
             
                            shutil.rmtree(self.projects_dir)
         | 
| 277 | 
            -
                             | 
| 305 | 
            +
                            logger.info("All projects have been removed.")
         | 
| 278 306 | 
             
                        except Exception as e:
         | 
| 279 | 
            -
                             | 
| 307 | 
            +
                            logger.error(f"Failed to remove projects directory: {e}")
         | 
| 280 308 | 
             
                    else:
         | 
| 281 | 
            -
                         | 
| 309 | 
            +
                        logger.warning("Projects directory does not exist.")
         | 
| 282 310 | 
             
                    self.projects_dir.mkdir(parents=True, exist_ok=True)
         | 
| 283 | 
            -
                     | 
| 284 | 
            -
             | 
| 311 | 
            +
                    logger.info("Projects directory cleaned and recreated.")
         | 
| 312 | 
            +
             | 
| 313 | 
            +
             | 
| 314 | 
            +
                @log_call
         | 
| 285 315 | 
             
                def download_fema_ble_model(self, csv_file: Union[str, Path], output_base_dir: Union[str, Path] = None):
         | 
| 286 316 | 
             
                    """
         | 
| 287 317 | 
             
                    Download a single FEMA Base Level Engineering (BLE) model from a CSV file and organize it into folders.
         | 
| @@ -316,37 +346,37 @@ class RasExamples: | |
| 316 346 | 
             
                        output_base_dir = Path(output_base_dir)
         | 
| 317 347 |  | 
| 318 348 | 
             
                    if not csv_file.exists() or not csv_file.is_file():
         | 
| 319 | 
            -
                         | 
| 349 | 
            +
                        logger.error(f"The specified CSV file does not exist: {csv_file}")
         | 
| 320 350 | 
             
                        raise FileNotFoundError(f"The specified CSV file does not exist: {csv_file}")
         | 
| 321 351 |  | 
| 322 352 | 
             
                    output_base_dir.mkdir(parents=True, exist_ok=True)
         | 
| 323 | 
            -
                     | 
| 353 | 
            +
                    logger.info(f"BLE model will be organized in: {output_base_dir}")
         | 
| 324 354 |  | 
| 325 355 | 
             
                    try:
         | 
| 326 356 | 
             
                        # Extract region name from the filename (assuming format <AnyCharacters>_<Region>_DownloadIndex.csv)
         | 
| 327 357 | 
             
                        match = re.match(r'.+?_(.+?)_DownloadIndex\.csv', csv_file.name)
         | 
| 328 358 | 
             
                        if not match:
         | 
| 329 | 
            -
                             | 
| 359 | 
            +
                            logger.warning(f"Filename does not match expected pattern and will be skipped: {csv_file.name}")
         | 
| 330 360 | 
             
                            return
         | 
| 331 361 | 
             
                        region = match.group(1)
         | 
| 332 | 
            -
                         | 
| 362 | 
            +
                        logger.info(f"Processing region: {region}")
         | 
| 333 363 |  | 
| 334 364 | 
             
                        # Create folder for this region
         | 
| 335 365 | 
             
                        region_folder = output_base_dir / region
         | 
| 336 366 | 
             
                        region_folder.mkdir(parents=True, exist_ok=True)
         | 
| 337 | 
            -
                         | 
| 367 | 
            +
                        logger.info(f"Created/verified region folder: {region_folder}")
         | 
| 338 368 |  | 
| 339 369 | 
             
                        # Read the CSV file
         | 
| 340 370 | 
             
                        try:
         | 
| 341 371 | 
             
                            df = pd.read_csv(csv_file, comment='#')
         | 
| 342 372 | 
             
                        except pd.errors.ParserError as e:
         | 
| 343 | 
            -
                             | 
| 373 | 
            +
                            logger.error(f"Error parsing CSV file {csv_file.name}: {e}")
         | 
| 344 374 | 
             
                            return
         | 
| 345 375 |  | 
| 346 376 | 
             
                        # Verify required columns exist
         | 
| 347 377 | 
             
                        required_columns = {'URL', 'FileName', 'FileSize', 'Description', 'Details'}
         | 
| 348 378 | 
             
                        if not required_columns.issubset(df.columns):
         | 
| 349 | 
            -
                             | 
| 379 | 
            +
                            logger.warning(f"CSV file {csv_file.name} is missing required columns and will be skipped.")
         | 
| 350 380 | 
             
                            return
         | 
| 351 381 |  | 
| 352 382 | 
             
                        # Process each row in the CSV
         | 
| @@ -360,7 +390,7 @@ class RasExamples: | |
| 360 390 | 
             
                            try:
         | 
| 361 391 | 
             
                                file_size = self._convert_size_to_bytes(file_size_str)
         | 
| 362 392 | 
             
                            except ValueError as e:
         | 
| 363 | 
            -
                                 | 
| 393 | 
            +
                                logger.error(f"Error converting file size '{file_size_str}' to bytes: {e}")
         | 
| 364 394 | 
             
                                continue
         | 
| 365 395 |  | 
| 366 396 | 
             
                            # Create a subfolder based on the safe description name
         | 
| @@ -372,43 +402,45 @@ class RasExamples: | |
| 372 402 | 
             
                            downloaded_file = csv_folder / file_name
         | 
| 373 403 | 
             
                            if not downloaded_file.exists():
         | 
| 374 404 | 
             
                                try:
         | 
| 375 | 
            -
                                     | 
| 405 | 
            +
                                    logger.info(f"Downloading {file_name} from {download_url} to {csv_folder}")
         | 
| 376 406 | 
             
                                    downloaded_file = self._download_file_with_progress(download_url, csv_folder, file_size)
         | 
| 377 | 
            -
                                     | 
| 407 | 
            +
                                    logger.info(f"Downloaded file to: {downloaded_file}")
         | 
| 378 408 | 
             
                                except Exception as e:
         | 
| 379 | 
            -
                                     | 
| 409 | 
            +
                                    logger.error(f"Failed to download {download_url}: {e}")
         | 
| 380 410 | 
             
                                    continue
         | 
| 381 411 | 
             
                            else:
         | 
| 382 | 
            -
                                 | 
| 412 | 
            +
                                logger.info(f"File {file_name} already exists in {csv_folder}, skipping download.")
         | 
| 383 413 |  | 
| 384 414 | 
             
                            # If it's a zip file, unzip it to the description folder
         | 
| 385 415 | 
             
                            if downloaded_file.suffix == '.zip':
         | 
| 386 416 | 
             
                                # If the folder exists, delete it
         | 
| 387 417 | 
             
                                if description_folder.exists():
         | 
| 388 | 
            -
                                     | 
| 418 | 
            +
                                    logger.info(f"Folder {description_folder} already exists. Deleting it.")
         | 
| 389 419 | 
             
                                    shutil.rmtree(description_folder)
         | 
| 390 420 |  | 
| 391 421 | 
             
                                description_folder.mkdir(parents=True, exist_ok=True)
         | 
| 392 | 
            -
                                 | 
| 422 | 
            +
                                logger.info(f"Created/verified description folder: {description_folder}")
         | 
| 393 423 |  | 
| 394 | 
            -
                                 | 
| 424 | 
            +
                                logger.info(f"Unzipping {downloaded_file} into {description_folder}")
         | 
| 395 425 | 
             
                                try:
         | 
| 396 426 | 
             
                                    with zipfile.ZipFile(downloaded_file, 'r') as zip_ref:
         | 
| 397 427 | 
             
                                        zip_ref.extractall(description_folder)
         | 
| 398 | 
            -
                                     | 
| 428 | 
            +
                                    logger.info(f"Unzipped {downloaded_file} successfully.")
         | 
| 399 429 | 
             
                                except Exception as e:
         | 
| 400 | 
            -
                                     | 
| 430 | 
            +
                                    logger.error(f"Failed to extract {downloaded_file}: {e}")
         | 
| 401 431 | 
             
                    except Exception as e:
         | 
| 402 | 
            -
                         | 
| 432 | 
            +
                        logger.error(f"An error occurred while processing {csv_file.name}: {e}")
         | 
| 403 433 |  | 
| 434 | 
            +
                @log_call
         | 
| 404 435 | 
             
                def _make_safe_folder_name(self, name: str) -> str:
         | 
| 405 436 | 
             
                    """
         | 
| 406 437 | 
             
                    Convert a string to a safe folder name by replacing unsafe characters with underscores.
         | 
| 407 438 | 
             
                    """
         | 
| 408 439 | 
             
                    safe_name = re.sub(r'[^a-zA-Z0-9_\-]', '_', name)
         | 
| 409 | 
            -
                     | 
| 440 | 
            +
                    logger.debug(f"Converted '{name}' to safe folder name '{safe_name}'")
         | 
| 410 441 | 
             
                    return safe_name
         | 
| 411 442 |  | 
| 443 | 
            +
                @log_call
         | 
| 412 444 | 
             
                def _download_file_with_progress(self, url: str, dest_folder: Path, file_size: int) -> Path:
         | 
| 413 445 | 
             
                    """
         | 
| 414 446 | 
             
                    Download a file from a URL to a specified destination folder with progress bar.
         | 
| @@ -427,15 +459,16 @@ class RasExamples: | |
| 427 459 | 
             
                                for chunk in r.iter_content(chunk_size=8192):
         | 
| 428 460 | 
             
                                    size = f.write(chunk)
         | 
| 429 461 | 
             
                                    progress_bar.update(size)
         | 
| 430 | 
            -
                         | 
| 462 | 
            +
                        logger.info(f"Successfully downloaded {url} to {local_filename}")
         | 
| 431 463 | 
             
                        return local_filename
         | 
| 432 464 | 
             
                    except requests.exceptions.RequestException as e:
         | 
| 433 | 
            -
                         | 
| 465 | 
            +
                        logger.error(f"Request failed for {url}: {e}")
         | 
| 434 466 | 
             
                        raise
         | 
| 435 467 | 
             
                    except Exception as e:
         | 
| 436 | 
            -
                         | 
| 468 | 
            +
                        logger.error(f"Failed to write file {local_filename}: {e}")
         | 
| 437 469 | 
             
                        raise
         | 
| 438 470 |  | 
| 471 | 
            +
                @log_call
         | 
| 439 472 | 
             
                def _convert_size_to_bytes(self, size_str: str) -> int:
         | 
| 440 473 | 
             
                    """
         | 
| 441 474 | 
             
                    Convert a human-readable file size to bytes.
         | 
| @@ -453,7 +486,7 @@ class RasExamples: | |
| 453 486 | 
             
                # ras_examples.download_fema_ble_models('/path/to/csv/files', '/path/to/output/folder')
         | 
| 454 487 | 
             
                # extracted_paths = ras_examples.extract_project(["Bald Eagle Creek", "BaldEagleCrkMulti2D", "Muncie"])
         | 
| 455 488 | 
             
                # for path in extracted_paths:
         | 
| 456 | 
            -
                #      | 
| 489 | 
            +
                #     logger.info(f"Extracted to: {path}")
         | 
| 457 490 |  | 
| 458 491 |  | 
| 459 492 | 
             
            """
         | 
    
        ras_commander/RasGeo.py
    CHANGED
    
    | @@ -1,21 +1,37 @@ | |
| 1 1 | 
             
            """
         | 
| 2 | 
            -
            Operations for handling geometry files in HEC-RAS projects | 
| 2 | 
            +
            RasGeo - Operations for handling geometry files in HEC-RAS projects
         | 
| 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
         | 
| 3 25 | 
             
            """
         | 
| 26 | 
            +
            import os
         | 
| 4 27 | 
             
            from pathlib import Path
         | 
| 5 28 | 
             
            from typing import List, Union
         | 
| 6 29 | 
             
            from .RasPlan import RasPlan
         | 
| 7 30 | 
             
            from .RasPrj import ras
         | 
| 8 | 
            -
            import  | 
| 9 | 
            -
            import  | 
| 31 | 
            +
            from ras_commander import get_logger
         | 
| 32 | 
            +
            from ras_commander.logging_config import log_call
         | 
| 10 33 |  | 
| 11 | 
            -
             | 
| 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 | 
            -
            )
         | 
| 34 | 
            +
            logger = get_logger(__name__)
         | 
| 19 35 |  | 
| 20 36 | 
             
            class RasGeo:
         | 
| 21 37 | 
             
                """
         | 
| @@ -23,6 +39,7 @@ class RasGeo: | |
| 23 39 | 
             
                """
         | 
| 24 40 |  | 
| 25 41 | 
             
                @staticmethod
         | 
| 42 | 
            +
                @log_call
         | 
| 26 43 | 
             
                def clear_geompre_files(
         | 
| 27 44 | 
             
                    plan_files: Union[str, Path, List[Union[str, Path]]] = None,
         | 
| 28 45 | 
             
                    ras_object = None
         | 
| @@ -58,11 +75,6 @@ class RasGeo: | |
| 58 75 | 
             
                    Note:
         | 
| 59 76 | 
             
                        This function updates the ras object's geometry dataframe after clearing the preprocessor files.
         | 
| 60 77 | 
             
                    """
         | 
| 61 | 
            -
                    ## Explicit Function Steps
         | 
| 62 | 
            -
                    # 1. Initialize the ras_object, defaulting to the global ras if not provided.
         | 
| 63 | 
            -
                    # 2. Define a helper function to clear a single geometry preprocessor file.
         | 
| 64 | 
            -
                    # 3. Determine the list of plan files to process based on the input.
         | 
| 65 | 
            -
                    # 4. Iterate over each plan file and clear its geometry preprocessor file.
         | 
| 66 78 | 
             
                    ras_obj = ras_object or ras
         | 
| 67 79 | 
             
                    ras_obj.check_initialized()
         | 
| 68 80 |  | 
| @@ -72,38 +84,36 @@ class RasGeo: | |
| 72 84 | 
             
                        geom_preprocessor_file = plan_path.with_suffix(geom_preprocessor_suffix)
         | 
| 73 85 | 
             
                        if geom_preprocessor_file.exists():
         | 
| 74 86 | 
             
                            try:
         | 
| 75 | 
            -
                                logging.info(f"Deleting geometry preprocessor file: {geom_preprocessor_file}")
         | 
| 76 87 | 
             
                                geom_preprocessor_file.unlink()
         | 
| 77 | 
            -
                                 | 
| 88 | 
            +
                                logger.info(f"Deleted geometry preprocessor file: {geom_preprocessor_file}")
         | 
| 78 89 | 
             
                            except PermissionError:
         | 
| 79 | 
            -
                                 | 
| 90 | 
            +
                                logger.error(f"Permission denied: Unable to delete geometry preprocessor file: {geom_preprocessor_file}")
         | 
| 80 91 | 
             
                                raise PermissionError(f"Unable to delete geometry preprocessor file: {geom_preprocessor_file}. Permission denied.")
         | 
| 81 92 | 
             
                            except OSError as e:
         | 
| 82 | 
            -
                                 | 
| 93 | 
            +
                                logger.error(f"Error deleting geometry preprocessor file: {geom_preprocessor_file}. {str(e)}")
         | 
| 83 94 | 
             
                                raise OSError(f"Error deleting geometry preprocessor file: {geom_preprocessor_file}. {str(e)}")
         | 
| 84 95 | 
             
                        else:
         | 
| 85 | 
            -
                             | 
| 96 | 
            +
                            logger.warning(f"No geometry preprocessor file found for: {plan_file}")
         | 
| 86 97 |  | 
| 87 98 | 
             
                    if plan_files is None:
         | 
| 88 | 
            -
                         | 
| 99 | 
            +
                        logger.info("Clearing all geometry preprocessor files in the project directory.")
         | 
| 89 100 | 
             
                        plan_files_to_clear = list(ras_obj.project_folder.glob(r'*.p*'))
         | 
| 90 101 | 
             
                    elif isinstance(plan_files, (str, Path)):
         | 
| 91 102 | 
             
                        plan_files_to_clear = [plan_files]
         | 
| 92 | 
            -
                         | 
| 103 | 
            +
                        logger.info(f"Clearing geometry preprocessor file for single plan: {plan_files}")
         | 
| 93 104 | 
             
                    elif isinstance(plan_files, list):
         | 
| 94 105 | 
             
                        plan_files_to_clear = plan_files
         | 
| 95 | 
            -
                         | 
| 106 | 
            +
                        logger.info(f"Clearing geometry preprocessor files for multiple plans: {plan_files}")
         | 
| 96 107 | 
             
                    else:
         | 
| 97 | 
            -
                         | 
| 108 | 
            +
                        logger.error("Invalid input type for plan_files.")
         | 
| 98 109 | 
             
                        raise ValueError("Invalid input. Please provide a string, Path, list of paths, or None.")
         | 
| 99 110 |  | 
| 100 111 | 
             
                    for plan_file in plan_files_to_clear:
         | 
| 101 112 | 
             
                        clear_single_file(plan_file, ras_obj)
         | 
| 102 113 |  | 
| 103 | 
            -
                    # Update the geometry dataframe
         | 
| 104 114 | 
             
                    try:
         | 
| 105 115 | 
             
                        ras_obj.geom_df = ras_obj.get_geom_entries()
         | 
| 106 | 
            -
                         | 
| 116 | 
            +
                        logger.info("Geometry dataframe updated successfully.")
         | 
| 107 117 | 
             
                    except Exception as e:
         | 
| 108 | 
            -
                         | 
| 118 | 
            +
                        logger.error(f"Failed to update geometry dataframe: {str(e)}")
         | 
| 109 119 | 
             
                        raise
         |