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/RasUnsteady.py
    CHANGED
    
    | @@ -1,21 +1,35 @@ | |
| 1 1 | 
             
            """
         | 
| 2 | 
            -
            Operations for handling unsteady flow files in HEC-RAS projects.
         | 
| 2 | 
            +
            RasUnsteady - Operations for handling unsteady flow 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 | 
            +
             | 
| 18 | 
            +
             | 
| 19 | 
            +
            Example:
         | 
| 20 | 
            +
                @log_call
         | 
| 21 | 
            +
                def my_function():
         | 
| 22 | 
            +
                    logger.debug("Additional debug information")
         | 
| 23 | 
            +
                    # Function logic here
         | 
| 3 24 | 
             
            """
         | 
| 25 | 
            +
            import os
         | 
| 4 26 | 
             
            from pathlib import Path
         | 
| 5 27 | 
             
            from .RasPrj import ras
         | 
| 6 | 
            -
            import  | 
| 7 | 
            -
             | 
| 28 | 
            +
            from ras_commander.logging_config import get_logger, log_call
         | 
| 29 | 
            +
             | 
| 30 | 
            +
            logger = get_logger(__name__)
         | 
| 8 31 |  | 
| 9 | 
            -
            #  | 
| 10 | 
            -
            logging.basicConfig(
         | 
| 11 | 
            -
                level=logging.INFO,  # Set to DEBUG for more detailed output
         | 
| 12 | 
            -
                format='%(asctime)s - %(levelname)s - %(message)s',
         | 
| 13 | 
            -
                handlers=[
         | 
| 14 | 
            -
                    logging.StreamHandler(),  # Logs to console
         | 
| 15 | 
            -
                    # Uncomment the next line to enable logging to a file
         | 
| 16 | 
            -
                    # logging.FileHandler('ras_unsteady.log')
         | 
| 17 | 
            -
                ]
         | 
| 18 | 
            -
            )
         | 
| 32 | 
            +
            # Module code starts here
         | 
| 19 33 |  | 
| 20 34 | 
             
            class RasUnsteady:
         | 
| 21 35 | 
             
                """
         | 
| @@ -23,6 +37,7 @@ class RasUnsteady: | |
| 23 37 | 
             
                """
         | 
| 24 38 |  | 
| 25 39 | 
             
                @staticmethod
         | 
| 40 | 
            +
                @log_call
         | 
| 26 41 | 
             
                def update_unsteady_parameters(unsteady_file, modifications, ras_object=None):
         | 
| 27 42 | 
             
                    """
         | 
| 28 43 | 
             
                    Modify parameters in an unsteady flow file.
         | 
| @@ -58,12 +73,12 @@ class RasUnsteady: | |
| 58 73 | 
             
                    try:
         | 
| 59 74 | 
             
                        with open(unsteady_path, 'r') as f:
         | 
| 60 75 | 
             
                            lines = f.readlines()
         | 
| 61 | 
            -
                         | 
| 76 | 
            +
                        logger.debug(f"Successfully read unsteady flow file: {unsteady_path}")
         | 
| 62 77 | 
             
                    except FileNotFoundError:
         | 
| 63 | 
            -
                         | 
| 78 | 
            +
                        logger.error(f"Unsteady flow file not found: {unsteady_path}")
         | 
| 64 79 | 
             
                        raise FileNotFoundError(f"Unsteady flow file not found: {unsteady_path}")
         | 
| 65 80 | 
             
                    except PermissionError:
         | 
| 66 | 
            -
                         | 
| 81 | 
            +
                        logger.error(f"Permission denied when reading unsteady flow file: {unsteady_path}")
         | 
| 67 82 | 
             
                        raise PermissionError(f"Permission denied when reading unsteady flow file: {unsteady_path}")
         | 
| 68 83 |  | 
| 69 84 | 
             
                    updated = False
         | 
| @@ -73,21 +88,21 @@ class RasUnsteady: | |
| 73 88 | 
             
                                old_value = line.strip().split('=')[1]
         | 
| 74 89 | 
             
                                lines[i] = f"{param}={new_value}\n"
         | 
| 75 90 | 
             
                                updated = True
         | 
| 76 | 
            -
                                 | 
| 91 | 
            +
                                logger.info(f"Updated {param} from {old_value} to {new_value}")
         | 
| 77 92 |  | 
| 78 93 | 
             
                    if updated:
         | 
| 79 94 | 
             
                        try:
         | 
| 80 95 | 
             
                            with open(unsteady_path, 'w') as f:
         | 
| 81 96 | 
             
                                f.writelines(lines)
         | 
| 82 | 
            -
                             | 
| 97 | 
            +
                            logger.debug(f"Successfully wrote modifications to unsteady flow file: {unsteady_path}")
         | 
| 83 98 | 
             
                        except PermissionError:
         | 
| 84 | 
            -
                             | 
| 99 | 
            +
                            logger.error(f"Permission denied when writing to unsteady flow file: {unsteady_path}")
         | 
| 85 100 | 
             
                            raise PermissionError(f"Permission denied when writing to unsteady flow file: {unsteady_path}")
         | 
| 86 101 | 
             
                        except IOError as e:
         | 
| 87 | 
            -
                             | 
| 102 | 
            +
                            logger.error(f"Error writing to unsteady flow file: {unsteady_path}. {str(e)}")
         | 
| 88 103 | 
             
                            raise IOError(f"Error writing to unsteady flow file: {unsteady_path}. {str(e)}")
         | 
| 89 | 
            -
                         | 
| 104 | 
            +
                        logger.info(f"Applied modifications to {unsteady_file}")
         | 
| 90 105 | 
             
                    else:
         | 
| 91 | 
            -
                         | 
| 106 | 
            +
                        logger.warning(f"No matching parameters found in {unsteady_file}")
         | 
| 92 107 |  | 
| 93 108 | 
             
                    ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
         | 
    
        ras_commander/RasUtils.py
    CHANGED
    
    | @@ -1,21 +1,38 @@ | |
| 1 1 | 
             
            """
         | 
| 2 | 
            -
            Utility functions for the ras-commander library | 
| 2 | 
            +
            RasUtils - Utility functions for the ras-commander library
         | 
| 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 | 
            +
             | 
| 18 | 
            +
            Example:
         | 
| 19 | 
            +
                @log_call
         | 
| 20 | 
            +
                def my_function():
         | 
| 21 | 
            +
                    logger.debug("Additional debug information")
         | 
| 22 | 
            +
                    # Function logic here
         | 
| 3 23 | 
             
            """
         | 
| 4 24 | 
             
            import os
         | 
| 5 | 
            -
            import shutil
         | 
| 6 | 
            -
            import logging
         | 
| 7 | 
            -
            import time
         | 
| 8 25 | 
             
            from pathlib import Path
         | 
| 9 26 | 
             
            from .RasPrj import ras
         | 
| 10 27 | 
             
            from typing import Union, Optional, Dict
         | 
| 11 28 | 
             
            import pandas as pd
         | 
| 12 29 | 
             
            import numpy as np
         | 
| 30 | 
            +
            import shutil
         | 
| 31 | 
            +
            from ras_commander import get_logger
         | 
| 32 | 
            +
            from ras_commander.logging_config import get_logger, log_call
         | 
| 13 33 |  | 
| 14 | 
            -
             | 
| 15 | 
            -
             | 
| 16 | 
            -
                level=logging.INFO,
         | 
| 17 | 
            -
                format='%(asctime)s - %(levelname)s - %(message)s'
         | 
| 18 | 
            -
            )
         | 
| 34 | 
            +
            logger = get_logger(__name__)
         | 
| 35 | 
            +
            # Module code starts here
         | 
| 19 36 |  | 
| 20 37 | 
             
            class RasUtils:
         | 
| 21 38 | 
             
                """
         | 
| @@ -24,6 +41,7 @@ class RasUtils: | |
| 24 41 | 
             
                """
         | 
| 25 42 |  | 
| 26 43 | 
             
                @staticmethod
         | 
| 44 | 
            +
                @log_call
         | 
| 27 45 | 
             
                def create_backup(file_path: Path, backup_suffix: str = "_backup", ras_object=None) -> Path:
         | 
| 28 46 | 
             
                    """
         | 
| 29 47 | 
             
                    Create a backup of the specified file.
         | 
| @@ -47,13 +65,14 @@ class RasUtils: | |
| 47 65 | 
             
                    backup_path = original_path.with_name(f"{original_path.stem}{backup_suffix}{original_path.suffix}")
         | 
| 48 66 | 
             
                    try:
         | 
| 49 67 | 
             
                        shutil.copy2(original_path, backup_path)
         | 
| 50 | 
            -
                         | 
| 68 | 
            +
                        logger.info(f"Backup created: {backup_path}")
         | 
| 51 69 | 
             
                    except Exception as e:
         | 
| 52 | 
            -
                         | 
| 70 | 
            +
                        logger.error(f"Failed to create backup for {original_path}: {e}")
         | 
| 53 71 | 
             
                        raise
         | 
| 54 72 | 
             
                    return backup_path
         | 
| 55 73 |  | 
| 56 74 | 
             
                @staticmethod
         | 
| 75 | 
            +
                @log_call
         | 
| 57 76 | 
             
                def restore_from_backup(backup_path: Path, remove_backup: bool = True, ras_object=None) -> Path:
         | 
| 58 77 | 
             
                    """
         | 
| 59 78 | 
             
                    Restore a file from its backup.
         | 
| @@ -75,23 +94,24 @@ class RasUtils: | |
| 75 94 |  | 
| 76 95 | 
             
                    backup_path = Path(backup_path)
         | 
| 77 96 | 
             
                    if '_backup' not in backup_path.stem:
         | 
| 78 | 
            -
                         | 
| 97 | 
            +
                        logger.error(f"Backup suffix '_backup' not found in {backup_path.name}")
         | 
| 79 98 | 
             
                        raise ValueError(f"Backup suffix '_backup' not found in {backup_path.name}")
         | 
| 80 99 |  | 
| 81 100 | 
             
                    original_stem = backup_path.stem.rsplit('_backup', 1)[0]
         | 
| 82 101 | 
             
                    original_path = backup_path.with_name(f"{original_stem}{backup_path.suffix}")
         | 
| 83 102 | 
             
                    try:
         | 
| 84 103 | 
             
                        shutil.copy2(backup_path, original_path)
         | 
| 85 | 
            -
                         | 
| 104 | 
            +
                        logger.info(f"File restored: {original_path}")
         | 
| 86 105 | 
             
                        if remove_backup:
         | 
| 87 106 | 
             
                            backup_path.unlink()
         | 
| 88 | 
            -
                             | 
| 107 | 
            +
                            logger.info(f"Backup removed: {backup_path}")
         | 
| 89 108 | 
             
                    except Exception as e:
         | 
| 90 | 
            -
                         | 
| 109 | 
            +
                        logger.error(f"Failed to restore from backup {backup_path}: {e}")
         | 
| 91 110 | 
             
                        raise
         | 
| 92 111 | 
             
                    return original_path
         | 
| 93 112 |  | 
| 94 113 | 
             
                @staticmethod
         | 
| 114 | 
            +
                @log_call
         | 
| 95 115 | 
             
                def create_directory(directory_path: Path, ras_object=None) -> Path:
         | 
| 96 116 | 
             
                    """
         | 
| 97 117 | 
             
                    Ensure that a directory exists, creating it if necessary.
         | 
| @@ -113,13 +133,14 @@ class RasUtils: | |
| 113 133 | 
             
                    path = Path(directory_path)
         | 
| 114 134 | 
             
                    try:
         | 
| 115 135 | 
             
                        path.mkdir(parents=True, exist_ok=True)
         | 
| 116 | 
            -
                         | 
| 136 | 
            +
                        logger.info(f"Directory ensured: {path}")
         | 
| 117 137 | 
             
                    except Exception as e:
         | 
| 118 | 
            -
                         | 
| 138 | 
            +
                        logger.error(f"Failed to create directory {path}: {e}")
         | 
| 119 139 | 
             
                        raise
         | 
| 120 140 | 
             
                    return path
         | 
| 121 141 |  | 
| 122 142 | 
             
                @staticmethod
         | 
| 143 | 
            +
                @log_call
         | 
| 123 144 | 
             
                def find_files_by_extension(extension: str, ras_object=None) -> list:
         | 
| 124 145 | 
             
                    """
         | 
| 125 146 | 
             
                    List all files in the project directory with a specific extension.
         | 
| @@ -141,13 +162,14 @@ class RasUtils: | |
| 141 162 | 
             
                    try:
         | 
| 142 163 | 
             
                        files = list(ras_obj.project_folder.glob(f"*{extension}"))
         | 
| 143 164 | 
             
                        file_list = [str(file) for file in files]
         | 
| 144 | 
            -
                         | 
| 165 | 
            +
                        logger.info(f"Found {len(file_list)} files with extension '{extension}' in {ras_obj.project_folder}")
         | 
| 145 166 | 
             
                        return file_list
         | 
| 146 167 | 
             
                    except Exception as e:
         | 
| 147 | 
            -
                         | 
| 168 | 
            +
                        logger.error(f"Failed to find files with extension '{extension}': {e}")
         | 
| 148 169 | 
             
                        raise
         | 
| 149 170 |  | 
| 150 171 | 
             
                @staticmethod
         | 
| 172 | 
            +
                @log_call
         | 
| 151 173 | 
             
                def get_file_size(file_path: Path, ras_object=None) -> Optional[int]:
         | 
| 152 174 | 
             
                    """
         | 
| 153 175 | 
             
                    Get the size of a file in bytes.
         | 
| @@ -170,16 +192,17 @@ class RasUtils: | |
| 170 192 | 
             
                    if path.exists():
         | 
| 171 193 | 
             
                        try:
         | 
| 172 194 | 
             
                            size = path.stat().st_size
         | 
| 173 | 
            -
                             | 
| 195 | 
            +
                            logger.info(f"Size of {path}: {size} bytes")
         | 
| 174 196 | 
             
                            return size
         | 
| 175 197 | 
             
                        except Exception as e:
         | 
| 176 | 
            -
                             | 
| 198 | 
            +
                            logger.error(f"Failed to get size for {path}: {e}")
         | 
| 177 199 | 
             
                            raise
         | 
| 178 200 | 
             
                    else:
         | 
| 179 | 
            -
                         | 
| 201 | 
            +
                        logger.warning(f"File not found: {path}")
         | 
| 180 202 | 
             
                        return None
         | 
| 181 203 |  | 
| 182 204 | 
             
                @staticmethod
         | 
| 205 | 
            +
                @log_call
         | 
| 183 206 | 
             
                def get_file_modification_time(file_path: Path, ras_object=None) -> Optional[float]:
         | 
| 184 207 | 
             
                    """
         | 
| 185 208 | 
             
                    Get the last modification time of a file.
         | 
| @@ -195,6 +218,7 @@ class RasUtils: | |
| 195 218 | 
             
                    >>> mtime = RasUtils.get_file_modification_time(Path("project.prj"))
         | 
| 196 219 | 
             
                    >>> print(f"Last modified: {mtime}")
         | 
| 197 220 | 
             
                    """
         | 
| 221 | 
            +
                    
         | 
| 198 222 | 
             
                    ras_obj = ras_object or ras
         | 
| 199 223 | 
             
                    ras_obj.check_initialized()
         | 
| 200 224 |  | 
| @@ -202,16 +226,17 @@ class RasUtils: | |
| 202 226 | 
             
                    if path.exists():
         | 
| 203 227 | 
             
                        try:
         | 
| 204 228 | 
             
                            mtime = path.stat().st_mtime
         | 
| 205 | 
            -
                             | 
| 229 | 
            +
                            logger.info(f"Last modification time of {path}: {mtime}")
         | 
| 206 230 | 
             
                            return mtime
         | 
| 207 231 | 
             
                        except Exception as e:
         | 
| 208 | 
            -
                             | 
| 232 | 
            +
                            logger.exception(f"Failed to get modification time for {path}")
         | 
| 209 233 | 
             
                            raise
         | 
| 210 234 | 
             
                    else:
         | 
| 211 | 
            -
                         | 
| 235 | 
            +
                        logger.warning(f"File not found: {path}")
         | 
| 212 236 | 
             
                        return None
         | 
| 213 237 |  | 
| 214 238 | 
             
                @staticmethod
         | 
| 239 | 
            +
                @log_call
         | 
| 215 240 | 
             
                def get_plan_path(current_plan_number_or_path: Union[str, Path], ras_object=None) -> Path:
         | 
| 216 241 | 
             
                    """
         | 
| 217 242 | 
             
                    Get the path for a plan file with a given plan number or path.
         | 
| @@ -229,27 +254,29 @@ class RasUtils: | |
| 229 254 | 
             
                    >>> plan_path = RasUtils.get_plan_path("path/to/plan.p01")
         | 
| 230 255 | 
             
                    >>> print(f"Plan file path: {plan_path}")
         | 
| 231 256 | 
             
                    """
         | 
| 257 | 
            +
                    
         | 
| 232 258 | 
             
                    ras_obj = ras_object or ras
         | 
| 233 259 | 
             
                    ras_obj.check_initialized()
         | 
| 234 260 |  | 
| 235 261 | 
             
                    plan_path = Path(current_plan_number_or_path)
         | 
| 236 262 | 
             
                    if plan_path.is_file():
         | 
| 237 | 
            -
                         | 
| 263 | 
            +
                        logger.info(f"Using provided plan file path: {plan_path}")
         | 
| 238 264 | 
             
                        return plan_path
         | 
| 239 265 |  | 
| 240 266 | 
             
                    try:
         | 
| 241 267 | 
             
                        current_plan_number = f"{int(current_plan_number_or_path):02d}"  # Ensure two-digit format
         | 
| 242 | 
            -
                         | 
| 268 | 
            +
                        logger.debug(f"Converted plan number to two-digit format: {current_plan_number}")
         | 
| 243 269 | 
             
                    except ValueError:
         | 
| 244 | 
            -
                         | 
| 270 | 
            +
                        logger.error(f"Invalid plan number: {current_plan_number_or_path}. Expected a number from 1 to 99.")
         | 
| 245 271 | 
             
                        raise ValueError(f"Invalid plan number: {current_plan_number_or_path}. Expected a number from 1 to 99.")
         | 
| 246 272 |  | 
| 247 273 | 
             
                    plan_name = f"{ras_obj.project_name}.p{current_plan_number}"
         | 
| 248 274 | 
             
                    full_plan_path = ras_obj.project_folder / plan_name
         | 
| 249 | 
            -
                     | 
| 275 | 
            +
                    logger.info(f"Constructed plan file path: {full_plan_path}")
         | 
| 250 276 | 
             
                    return full_plan_path
         | 
| 251 277 |  | 
| 252 278 | 
             
                @staticmethod
         | 
| 279 | 
            +
                @log_call
         | 
| 253 280 | 
             
                def remove_with_retry(
         | 
| 254 281 | 
             
                    path: Path,
         | 
| 255 282 | 
             
                    max_attempts: int = 5,
         | 
| @@ -274,6 +301,7 @@ class RasUtils: | |
| 274 301 | 
             
                    >>> success = RasUtils.remove_with_retry(Path("temp_folder"), is_folder=True)
         | 
| 275 302 | 
             
                    >>> print(f"Removal successful: {success}")
         | 
| 276 303 | 
             
                    """
         | 
| 304 | 
            +
                    
         | 
| 277 305 | 
             
                    ras_obj = ras_object or ras
         | 
| 278 306 | 
             
                    ras_obj.check_initialized()
         | 
| 279 307 |  | 
| @@ -283,32 +311,33 @@ class RasUtils: | |
| 283 311 | 
             
                            if path.exists():
         | 
| 284 312 | 
             
                                if is_folder:
         | 
| 285 313 | 
             
                                    shutil.rmtree(path)
         | 
| 286 | 
            -
                                     | 
| 314 | 
            +
                                    logger.info(f"Folder removed: {path}")
         | 
| 287 315 | 
             
                                else:
         | 
| 288 316 | 
             
                                    path.unlink()
         | 
| 289 | 
            -
                                     | 
| 317 | 
            +
                                    logger.info(f"File removed: {path}")
         | 
| 290 318 | 
             
                            else:
         | 
| 291 | 
            -
                                 | 
| 319 | 
            +
                                logger.info(f"Path does not exist, nothing to remove: {path}")
         | 
| 292 320 | 
             
                            return True
         | 
| 293 321 | 
             
                        except PermissionError as pe:
         | 
| 294 322 | 
             
                            if attempt < max_attempts:
         | 
| 295 323 | 
             
                                delay = initial_delay * (2 ** (attempt - 1))  # Exponential backoff
         | 
| 296 | 
            -
                                 | 
| 324 | 
            +
                                logger.warning(
         | 
| 297 325 | 
             
                                    f"PermissionError on attempt {attempt} to remove {path}: {pe}. "
         | 
| 298 326 | 
             
                                    f"Retrying in {delay} seconds..."
         | 
| 299 327 | 
             
                                )
         | 
| 300 328 | 
             
                                time.sleep(delay)
         | 
| 301 329 | 
             
                            else:
         | 
| 302 | 
            -
                                 | 
| 330 | 
            +
                                logger.error(
         | 
| 303 331 | 
             
                                    f"Failed to remove {path} after {max_attempts} attempts due to PermissionError: {pe}. Skipping."
         | 
| 304 332 | 
             
                                )
         | 
| 305 333 | 
             
                                return False
         | 
| 306 334 | 
             
                        except Exception as e:
         | 
| 307 | 
            -
                             | 
| 335 | 
            +
                            logger.exception(f"Failed to remove {path} on attempt {attempt}")
         | 
| 308 336 | 
             
                            return False
         | 
| 309 337 | 
             
                    return False
         | 
| 310 338 |  | 
| 311 339 | 
             
                @staticmethod
         | 
| 340 | 
            +
                @log_call
         | 
| 312 341 | 
             
                def update_plan_file(
         | 
| 313 342 | 
             
                    plan_number_or_path: Union[str, Path],
         | 
| 314 343 | 
             
                    file_type: str,
         | 
| @@ -332,12 +361,13 @@ class RasUtils: | |
| 332 361 | 
             
                    >>> RasUtils.update_plan_file(1, "Geom", 2)
         | 
| 333 362 | 
             
                    >>> RasUtils.update_plan_file("path/to/plan.p01", "Geom", 2)
         | 
| 334 363 | 
             
                    """
         | 
| 364 | 
            +
                    
         | 
| 335 365 | 
             
                    ras_obj = ras_object or ras
         | 
| 336 366 | 
             
                    ras_obj.check_initialized()
         | 
| 337 367 |  | 
| 338 368 | 
             
                    valid_file_types = {'Geom': 'g', 'Flow': 'f', 'Unsteady': 'u'}
         | 
| 339 369 | 
             
                    if file_type not in valid_file_types:
         | 
| 340 | 
            -
                         | 
| 370 | 
            +
                        logger.error(
         | 
| 341 371 | 
             
                            f"Invalid file_type '{file_type}'. Expected one of: {', '.join(valid_file_types.keys())}"
         | 
| 342 372 | 
             
                        )
         | 
| 343 373 | 
             
                        raise ValueError(
         | 
| @@ -348,7 +378,7 @@ class RasUtils: | |
| 348 378 | 
             
                    if not plan_file_path.is_file():
         | 
| 349 379 | 
             
                        plan_file_path = RasUtils.get_plan_path(plan_number_or_path, ras_object)
         | 
| 350 380 | 
             
                        if not plan_file_path.exists():
         | 
| 351 | 
            -
                             | 
| 381 | 
            +
                            logger.error(f"Plan file not found: {plan_file_path}")
         | 
| 352 382 | 
             
                            raise FileNotFoundError(f"Plan file not found: {plan_file_path}")
         | 
| 353 383 |  | 
| 354 384 | 
             
                    file_prefix = valid_file_types[file_type]
         | 
| @@ -360,30 +390,30 @@ class RasUtils: | |
| 360 390 | 
             
                        with plan_file_path.open('r') as file:
         | 
| 361 391 | 
             
                            lines = file.readlines()
         | 
| 362 392 | 
             
                    except Exception as e:
         | 
| 363 | 
            -
                         | 
| 393 | 
            +
                        logger.exception(f"Failed to read plan file {plan_file_path}")
         | 
| 364 394 | 
             
                        raise
         | 
| 365 395 |  | 
| 366 396 | 
             
                    updated = False
         | 
| 367 397 | 
             
                    for i, line in enumerate(lines):
         | 
| 368 398 | 
             
                        if line.startswith(search_pattern):
         | 
| 369 399 | 
             
                            lines[i] = f"{search_pattern}{file_prefix}{formatted_entry_number}\n"
         | 
| 370 | 
            -
                             | 
| 400 | 
            +
                            logger.info(
         | 
| 371 401 | 
             
                                f"Updated {file_type} File in {plan_file_path} to {file_prefix}{formatted_entry_number}"
         | 
| 372 402 | 
             
                            )
         | 
| 373 403 | 
             
                            updated = True
         | 
| 374 404 | 
             
                            break
         | 
| 375 405 |  | 
| 376 406 | 
             
                    if not updated:
         | 
| 377 | 
            -
                         | 
| 407 | 
            +
                        logger.warning(
         | 
| 378 408 | 
             
                            f"Search pattern '{search_pattern}' not found in {plan_file_path}. No update performed."
         | 
| 379 409 | 
             
                        )
         | 
| 380 410 |  | 
| 381 411 | 
             
                    try:
         | 
| 382 412 | 
             
                        with plan_file_path.open('w') as file:
         | 
| 383 413 | 
             
                            file.writelines(lines)
         | 
| 384 | 
            -
                         | 
| 414 | 
            +
                        logger.info(f"Successfully updated plan file: {plan_file_path}")
         | 
| 385 415 | 
             
                    except Exception as e:
         | 
| 386 | 
            -
                         | 
| 416 | 
            +
                        logger.exception(f"Failed to write updates to plan file {plan_file_path}")
         | 
| 387 417 | 
             
                        raise
         | 
| 388 418 |  | 
| 389 419 | 
             
                    # Refresh RasPrj dataframes
         | 
| @@ -392,12 +422,13 @@ class RasUtils: | |
| 392 422 | 
             
                        ras_obj.geom_df = ras_obj.get_geom_entries()
         | 
| 393 423 | 
             
                        ras_obj.flow_df = ras_obj.get_flow_entries()
         | 
| 394 424 | 
             
                        ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
         | 
| 395 | 
            -
                         | 
| 425 | 
            +
                        logger.info("RAS object dataframes have been refreshed.")
         | 
| 396 426 | 
             
                    except Exception as e:
         | 
| 397 | 
            -
                         | 
| 427 | 
            +
                        logger.exception("Failed to refresh RasPrj dataframes")
         | 
| 398 428 | 
             
                        raise
         | 
| 399 429 |  | 
| 400 430 | 
             
                @staticmethod
         | 
| 431 | 
            +
                @log_call
         | 
| 401 432 | 
             
                def check_file_access(file_path: Path, mode: str = 'r') -> None:
         | 
| 402 433 | 
             
                    """
         | 
| 403 434 | 
             
                    Check if the file can be accessed with the specified mode.
         | 
| @@ -410,36 +441,30 @@ class RasUtils: | |
| 410 441 | 
             
                    FileNotFoundError: If the file does not exist
         | 
| 411 442 | 
             
                    PermissionError: If the required permissions are not met
         | 
| 412 443 | 
             
                    """
         | 
| 444 | 
            +
                    
         | 
| 413 445 | 
             
                    path = Path(file_path)
         | 
| 414 446 | 
             
                    if not path.exists():
         | 
| 415 | 
            -
                         | 
| 447 | 
            +
                        logger.error(f"File not found: {file_path}")
         | 
| 416 448 | 
             
                        raise FileNotFoundError(f"File not found: {file_path}")
         | 
| 417 449 |  | 
| 418 450 | 
             
                    if mode in ('r', 'rb'):
         | 
| 419 451 | 
             
                        if not os.access(path, os.R_OK):
         | 
| 420 | 
            -
                             | 
| 452 | 
            +
                            logger.error(f"Read permission denied for file: {file_path}")
         | 
| 421 453 | 
             
                            raise PermissionError(f"Read permission denied for file: {file_path}")
         | 
| 422 454 | 
             
                        else:
         | 
| 423 | 
            -
                             | 
| 455 | 
            +
                            logger.debug(f"Read access granted for file: {file_path}")
         | 
| 424 456 |  | 
| 425 457 | 
             
                    if mode in ('w', 'wb', 'a', 'ab'):
         | 
| 426 458 | 
             
                        parent_dir = path.parent
         | 
| 427 459 | 
             
                        if not os.access(parent_dir, os.W_OK):
         | 
| 428 | 
            -
                             | 
| 460 | 
            +
                            logger.error(f"Write permission denied for directory: {parent_dir}")
         | 
| 429 461 | 
             
                            raise PermissionError(f"Write permission denied for directory: {parent_dir}")
         | 
| 430 462 | 
             
                        else:
         | 
| 431 | 
            -
                             | 
| 432 | 
            -
             | 
| 433 | 
            -
             | 
| 434 | 
            -
             | 
| 435 | 
            -
             | 
| 436 | 
            -
            #  --------------------------   Functions below were imported from funkshuns.py  --------------------------
         | 
| 437 | 
            -
            #  --------------------------   Converted to ras-commander style guide   ----------------------------------
         | 
| 438 | 
            -
             | 
| 439 | 
            -
             | 
| 463 | 
            +
                            logger.debug(f"Write access granted for directory: {parent_dir}")
         | 
| 440 464 |  | 
| 441 465 |  | 
| 442 466 | 
             
                @staticmethod
         | 
| 467 | 
            +
                @log_call
         | 
| 443 468 | 
             
                def convert_to_dataframe(data_source: Union[pd.DataFrame, Path], **kwargs) -> pd.DataFrame:
         | 
| 444 469 | 
             
                    """
         | 
| 445 470 | 
             
                    Converts input to a pandas DataFrame. Supports existing DataFrames or file paths (CSV, Excel, TSV, Parquet).
         | 
| @@ -458,13 +483,13 @@ class RasUtils: | |
| 458 483 | 
             
                        >>> df = RasUtils.convert_to_dataframe(Path("data.csv"))
         | 
| 459 484 | 
             
                        >>> print(type(df))
         | 
| 460 485 | 
             
                        <class 'pandas.core.frame.DataFrame'>
         | 
| 461 | 
            -
                        
         | 
| 462 | 
            -
                    Attribution Note: This function is sourced from funkshuns.py by Sean Micek, and converted to the ras-commander style guide.
         | 
| 463 486 | 
             
                    """
         | 
| 464 487 | 
             
                    if isinstance(data_source, pd.DataFrame):
         | 
| 488 | 
            +
                        logger.debug("Input is already a DataFrame, returning a copy.")
         | 
| 465 489 | 
             
                        return data_source.copy()
         | 
| 466 490 | 
             
                    elif isinstance(data_source, Path):
         | 
| 467 491 | 
             
                        ext = data_source.suffix.replace('.', '', 1)
         | 
| 492 | 
            +
                        logger.info(f"Converting file with extension '{ext}' to DataFrame.")
         | 
| 468 493 | 
             
                        if ext == 'csv':
         | 
| 469 494 | 
             
                            return pd.read_csv(data_source, **kwargs)
         | 
| 470 495 | 
             
                        elif ext.startswith('x'):
         | 
| @@ -474,11 +499,14 @@ class RasUtils: | |
| 474 499 | 
             
                        elif ext in ["parquet", "pq", "parq"]:
         | 
| 475 500 | 
             
                            return pd.read_parquet(data_source, **kwargs)
         | 
| 476 501 | 
             
                        else:
         | 
| 502 | 
            +
                            logger.error(f"Unsupported file type: {ext}")
         | 
| 477 503 | 
             
                            raise NotImplementedError(f"Unsupported file type {ext}. Should be one of csv, tsv, parquet, or xlsx.")
         | 
| 478 504 | 
             
                    else:
         | 
| 505 | 
            +
                        logger.error(f"Unsupported input type: {type(data_source)}")
         | 
| 479 506 | 
             
                        raise NotImplementedError(f"Unsupported type {type(data_source)}. Only file path / existing DataFrame supported at this time")
         | 
| 480 507 |  | 
| 481 508 | 
             
                @staticmethod
         | 
| 509 | 
            +
                @log_call
         | 
| 482 510 | 
             
                def save_to_excel(dataframe: pd.DataFrame, excel_path: Path, **kwargs) -> None:
         | 
| 483 511 | 
             
                    """
         | 
| 484 512 | 
             
                    Saves a pandas DataFrame to an Excel file with retry functionality.
         | 
| @@ -494,8 +522,6 @@ class RasUtils: | |
| 494 522 | 
             
                    Example:
         | 
| 495 523 | 
             
                        >>> df = pd.DataFrame({'A': [1, 2, 3], 'B': [4, 5, 6]})
         | 
| 496 524 | 
             
                        >>> RasUtils.save_to_excel(df, Path('output.xlsx'))
         | 
| 497 | 
            -
                        
         | 
| 498 | 
            -
                    Attribution Note: This function is sourced from funkshuns.py by Sean Micek, and converted to the ras-commander style guide.
         | 
| 499 525 | 
             
                    """
         | 
| 500 526 | 
             
                    saved = False
         | 
| 501 527 | 
             
                    max_attempts = 3
         | 
| @@ -504,28 +530,18 @@ class RasUtils: | |
| 504 530 | 
             
                    while not saved and attempt < max_attempts:
         | 
| 505 531 | 
             
                        try:
         | 
| 506 532 | 
             
                            dataframe.to_excel(excel_path, **kwargs)
         | 
| 507 | 
            -
                             | 
| 533 | 
            +
                            logger.info(f'DataFrame successfully saved to {excel_path}')
         | 
| 508 534 | 
             
                            saved = True
         | 
| 509 535 | 
             
                        except IOError as e:
         | 
| 510 536 | 
             
                            attempt += 1
         | 
| 511 537 | 
             
                            if attempt < max_attempts:
         | 
| 512 | 
            -
                                 | 
| 538 | 
            +
                                logger.warning(f"Error saving file. Attempt {attempt} of {max_attempts}. Please close the Excel document if it's open.")
         | 
| 513 539 | 
             
                            else:
         | 
| 540 | 
            +
                                logger.error(f"Failed to save {excel_path} after {max_attempts} attempts.")
         | 
| 514 541 | 
             
                                raise IOError(f"Failed to save {excel_path} after {max_attempts} attempts. Last error: {str(e)}")
         | 
| 515 542 |  | 
| 516 | 
            -
             | 
| 517 | 
            -
             | 
| 518 | 
            -
             | 
| 519 | 
            -
             | 
| 520 | 
            -
             | 
| 521 | 
            -
             | 
| 522 | 
            -
             | 
| 523 | 
            -
             | 
| 524 | 
            -
             | 
| 525 | 
            -
            #####  Statistical Metrics #####
         | 
| 526 | 
            -
             | 
| 527 | 
            -
             | 
| 528 543 | 
             
                @staticmethod
         | 
| 544 | 
            +
                @log_call
         | 
| 529 545 | 
             
                def calculate_rmse(observed_values: np.ndarray, predicted_values: np.ndarray, normalized: bool = True) -> float:
         | 
| 530 546 | 
             
                    """
         | 
| 531 547 | 
             
                    Calculate the Root Mean Squared Error (RMSE) between observed and predicted values.
         | 
| @@ -543,17 +559,17 @@ class RasUtils: | |
| 543 559 | 
             
                        >>> predicted = np.array([1.1, 2.2, 2.9])
         | 
| 544 560 | 
             
                        >>> RasUtils.calculate_rmse(observed, predicted)
         | 
| 545 561 | 
             
                        0.06396394
         | 
| 546 | 
            -
                        
         | 
| 547 | 
            -
                    Attribution Note: This function is sourced from funkshuns.py by Sean Micek, and converted to the ras-commander style guide.
         | 
| 548 562 | 
             
                    """
         | 
| 549 563 | 
             
                    rmse = np.sqrt(np.mean((predicted_values - observed_values) ** 2))
         | 
| 550 564 |  | 
| 551 565 | 
             
                    if normalized:
         | 
| 552 566 | 
             
                        rmse = rmse / np.abs(np.mean(observed_values))
         | 
| 553 567 |  | 
| 568 | 
            +
                    logger.debug(f"Calculated RMSE: {rmse}")
         | 
| 554 569 | 
             
                    return rmse
         | 
| 555 570 |  | 
| 556 571 | 
             
                @staticmethod
         | 
| 572 | 
            +
                @log_call
         | 
| 557 573 | 
             
                def calculate_percent_bias(observed_values: np.ndarray, predicted_values: np.ndarray, as_percentage: bool = False) -> float:
         | 
| 558 574 | 
             
                    """
         | 
| 559 575 | 
             
                    Calculate the Percent Bias between observed and predicted values.
         | 
| @@ -571,16 +587,16 @@ class RasUtils: | |
| 571 587 | 
             
                        >>> predicted = np.array([1.1, 2.2, 2.9])
         | 
| 572 588 | 
             
                        >>> RasUtils.calculate_percent_bias(observed, predicted, as_percentage=True)
         | 
| 573 589 | 
             
                        3.33333333
         | 
| 574 | 
            -
                        
         | 
| 575 | 
            -
                    Attribution Note: This function is sourced from funkshuns.py by Sean Micek, and converted to the ras-commander style guide.
         | 
| 576 590 | 
             
                    """
         | 
| 577 591 | 
             
                    multiplier = 100 if as_percentage else 1
         | 
| 578 592 |  | 
| 579 593 | 
             
                    percent_bias = multiplier * (np.mean(predicted_values) - np.mean(observed_values)) / np.mean(observed_values)
         | 
| 580 594 |  | 
| 595 | 
            +
                    logger.debug(f"Calculated Percent Bias: {percent_bias}")
         | 
| 581 596 | 
             
                    return percent_bias
         | 
| 582 597 |  | 
| 583 598 | 
             
                @staticmethod
         | 
| 599 | 
            +
                @log_call
         | 
| 584 600 | 
             
                def calculate_error_metrics(observed_values: np.ndarray, predicted_values: np.ndarray) -> Dict[str, float]:
         | 
| 585 601 | 
             
                    """
         | 
| 586 602 | 
             
                    Compute a trio of error metrics: correlation, RMSE, and Percent Bias.
         | 
| @@ -597,14 +613,14 @@ class RasUtils: | |
| 597 613 | 
             
                        >>> predicted = np.array([1.1, 2.2, 2.9])
         | 
| 598 614 | 
             
                        >>> RasUtils.calculate_error_metrics(observed, predicted)
         | 
| 599 615 | 
             
                        {'cor': 0.9993, 'rmse': 0.06396, 'pb': 0.03333}
         | 
| 600 | 
            -
                        
         | 
| 601 | 
            -
                    Attribution Note: This function is sourced from funkshuns.py by Sean Micek, and converted to the ras-commander style guide.
         | 
| 602 616 | 
             
                    """
         | 
| 603 617 | 
             
                    correlation = np.corrcoef(observed_values, predicted_values)[0, 1]
         | 
| 604 618 | 
             
                    rmse = RasUtils.calculate_rmse(observed_values, predicted_values)
         | 
| 605 619 | 
             
                    percent_bias = RasUtils.calculate_percent_bias(observed_values, predicted_values)
         | 
| 606 620 |  | 
| 607 | 
            -
                     | 
| 621 | 
            +
                    metrics = {'cor': correlation, 'rmse': rmse, 'pb': percent_bias}
         | 
| 622 | 
            +
                    logger.info(f"Calculated error metrics: {metrics}")
         | 
| 623 | 
            +
                    return metrics
         | 
| 608 624 |  | 
| 609 625 |  | 
| 610 626 |  | 
    
        ras_commander/__init__.py
    CHANGED
    
    | @@ -1,4 +1,5 @@ | |
| 1 1 | 
             
            from importlib.metadata import version, PackageNotFoundError
         | 
| 2 | 
            +
            from .logging_config import setup_logging, get_logger, log_call
         | 
| 2 3 |  | 
| 3 4 | 
             
            try:
         | 
| 4 5 | 
             
                __version__ = version("ras-commander")
         | 
| @@ -6,6 +7,9 @@ except PackageNotFoundError: | |
| 6 7 | 
             
                # package is not installed
         | 
| 7 8 | 
             
                __version__ = "unknown"
         | 
| 8 9 |  | 
| 10 | 
            +
            # Set up logging
         | 
| 11 | 
            +
            setup_logging()
         | 
| 12 | 
            +
             | 
| 9 13 | 
             
            # Import all necessary functions and classes directly
         | 
| 10 14 | 
             
            from .RasPrj import ras, init_ras_project, get_ras_exe
         | 
| 11 15 | 
             
            from .RasPrj import RasPrj
         | 
| @@ -15,17 +19,8 @@ from .RasUnsteady import RasUnsteady | |
| 15 19 | 
             
            from .RasCmdr import RasCmdr
         | 
| 16 20 | 
             
            from .RasUtils import RasUtils
         | 
| 17 21 | 
             
            from .RasExamples import RasExamples
         | 
| 18 | 
            -
            from .RasHdf import RasHdf | 
| 19 | 
            -
             | 
| 20 | 
            -
            # Import all attributes from these modules
         | 
| 21 | 
            -
            from .RasPrj import *
         | 
| 22 | 
            -
            from .RasPlan import *
         | 
| 23 | 
            -
            from .RasGeo import *
         | 
| 24 | 
            -
            from .RasUnsteady import *
         | 
| 25 | 
            -
            from .RasCmdr import *
         | 
| 26 | 
            -
            from .RasUtils import *
         | 
| 27 | 
            -
            from .RasExamples import *
         | 
| 28 | 
            -
            from .RasHdf import *  # Add this line
         | 
| 22 | 
            +
            from .RasHdf import RasHdf
         | 
| 23 | 
            +
            from .RasGpt import RasGpt
         | 
| 29 24 |  | 
| 30 25 | 
             
            # Define __all__ to specify what should be imported when using "from ras_commander import *"
         | 
| 31 26 | 
             
            __all__ = [
         | 
| @@ -39,5 +34,8 @@ __all__ = [ | |
| 39 34 | 
             
                "RasCmdr",
         | 
| 40 35 | 
             
                "RasUtils",
         | 
| 41 36 | 
             
                "RasExamples",
         | 
| 42 | 
            -
                "RasHdf" | 
| 43 | 
            -
             | 
| 37 | 
            +
                "RasHdf",
         | 
| 38 | 
            +
                "RasGpt",
         | 
| 39 | 
            +
                "get_logger",
         | 
| 40 | 
            +
                "log_call"
         | 
| 41 | 
            +
            ]
         |