ras-commander 0.65.0__py3-none-any.whl → 0.67.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/RasPrj.py CHANGED
@@ -160,101 +160,137 @@ class RasPrj:
160
160
  Load project data from the HEC-RAS project file.
161
161
 
162
162
  This method initializes DataFrames for plan, flow, unsteady, and geometry entries
163
- by calling the _get_prj_entries method for each entry type.
164
- Also extracts unsteady_number and geometry_number from plan files and adds them to plan_df.
165
- Now includes Geom Path, Flow Path, and other required columns.
163
+ and ensures all required columns are present with appropriate paths.
166
164
  """
167
- # Load unsteady first to ensure consistent handling of unsteady numbers
168
- self.unsteady_df = self._get_prj_entries('Unsteady')
169
- self.plan_df = self._get_prj_entries('Plan')
170
- self.flow_df = self._get_prj_entries('Flow')
171
- self.geom_df = self.get_geom_entries()
165
+ try:
166
+ # Load data frames
167
+ self.unsteady_df = self._get_prj_entries('Unsteady')
168
+ self.plan_df = self._get_prj_entries('Plan')
169
+ self.flow_df = self._get_prj_entries('Flow')
170
+ self.geom_df = self.get_geom_entries()
171
+
172
+ # Ensure required columns exist
173
+ self._ensure_required_columns()
174
+
175
+ # Set paths for geometry and flow files
176
+ self._set_file_paths()
177
+
178
+ # Make sure all plan paths are properly set
179
+ self._set_plan_paths()
180
+
181
+ except Exception as e:
182
+ logger.error(f"Error loading project data: {e}")
183
+ raise
184
+
185
+ def _ensure_required_columns(self):
186
+ """Ensure all required columns exist in plan_df."""
187
+ required_columns = [
188
+ 'plan_number', 'unsteady_number', 'geometry_number',
189
+ 'Geom File', 'Geom Path', 'Flow File', 'Flow Path', 'full_path'
190
+ ]
191
+
192
+ for col in required_columns:
193
+ if col not in self.plan_df.columns:
194
+ self.plan_df[col] = None
172
195
 
173
- # Set geometry and flow paths
196
+ if not self.plan_df['full_path'].any():
197
+ self.plan_df['full_path'] = self.plan_df['plan_number'].apply(
198
+ lambda x: str(self.project_folder / f"{self.project_name}.p{x}")
199
+ )
200
+
201
+ def _set_file_paths(self):
202
+ """Set geometry and flow paths in plan_df."""
174
203
  for idx, row in self.plan_df.iterrows():
175
204
  try:
176
- # Add Geom Path
177
- if row['Geom File'] is not None:
178
- geom_hdf_path = self.project_folder / f"{self.project_name}.g{row['Geom File']}"
179
- self.plan_df.at[idx, 'Geom Path'] = str(geom_hdf_path)
205
+ self._set_geom_path(idx, row)
206
+ self._set_flow_path(idx, row)
180
207
 
181
- # Add Flow Path
182
- if row['Flow File'] is not None:
183
- # Determine if this is a steady or unsteady flow file
184
- if row['unsteady_number'] is not None:
185
- flow_path = self.project_folder / f"{self.project_name}.u{row['Flow File']}"
186
- else:
187
- flow_path = self.project_folder / f"{self.project_name}.f{row['Flow File']}"
188
- self.plan_df.at[idx, 'Flow Path'] = str(flow_path)
189
-
190
- if not self.suppress_logging:
191
- logger.info(f"Plan {row['plan_number']} paths set up")
208
+ if not self.suppress_logging:
209
+ logger.info(f"Plan {row['plan_number']} paths set up")
192
210
  except Exception as e:
193
211
  logger.error(f"Error processing plan file {row['plan_number']}: {e}")
194
212
 
195
- def _get_geom_file_from_plan(self, plan_file_path):
196
- """
197
- Extract the geometry number from a plan file by finding the Geom File value
198
- and stripping the leading 'g'.
213
+ def _set_geom_path(self, idx: int, row: pd.Series):
214
+ """Set geometry path for a plan entry."""
215
+ if pd.notna(row['Geom File']):
216
+ geom_path = self.project_folder / f"{self.project_name}.g{row['Geom File']}"
217
+ self.plan_df.at[idx, 'Geom Path'] = str(geom_path)
218
+
219
+ def _set_flow_path(self, idx: int, row: pd.Series):
220
+ """Set flow path for a plan entry."""
221
+ if pd.notna(row['Flow File']):
222
+ prefix = 'u' if pd.notna(row['unsteady_number']) else 'f'
223
+ flow_path = self.project_folder / f"{self.project_name}.{prefix}{row['Flow File']}"
224
+ self.plan_df.at[idx, 'Flow Path'] = str(flow_path)
225
+
226
+ def _set_plan_paths(self):
227
+ """Set full path information for plan files and their associated geometry and flow files."""
228
+ if self.plan_df.empty:
229
+ logger.debug("Plan DataFrame is empty, no paths to set")
230
+ return
199
231
 
200
- Parameters:
201
- -----------
202
- plan_file_path : str or Path
203
- Path to the plan file.
204
-
205
- Returns:
206
- --------
207
- tuple: (str, str)
208
- A tuple containing (geometry_number, full_hdf_path) or (None, None) if not found.
209
- geometry_number is the number after 'g' in the Geom File value
210
- full_hdf_path is the path to the geometry HDF file
211
- """
212
- content, encoding = read_file_with_fallback_encoding(plan_file_path)
213
-
214
- if content is None:
215
- return None, None
232
+ # Ensure full path is set for all plan entries
233
+ if 'full_path' not in self.plan_df.columns or self.plan_df['full_path'].isna().any():
234
+ self.plan_df['full_path'] = self.plan_df['plan_number'].apply(
235
+ lambda x: str(self.project_folder / f"{self.project_name}.p{x}")
236
+ )
216
237
 
217
- try:
218
- match = re.search(r'Geom File=g(\d+)', content)
219
- if match:
220
- geom_number = match.group(1) # This gets just the number after 'g'
221
- geom_file = f"g{geom_number}"
222
- geom_hdf_path = self.project_folder / f"{self.project_name}.{geom_file}.hdf"
223
- if geom_hdf_path.exists():
224
- return geom_number, str(geom_hdf_path)
225
- except Exception as e:
226
- logger.error(f"Error extracting geometry number from {plan_file_path}: {e}")
238
+ # Create the Geom Path and Flow Path columns if they don't exist
239
+ if 'Geom Path' not in self.plan_df.columns:
240
+ self.plan_df['Geom Path'] = None
241
+ if 'Flow Path' not in self.plan_df.columns:
242
+ self.plan_df['Flow Path'] = None
227
243
 
228
- return None, None
244
+ # Update paths for each plan entry
245
+ for idx, row in self.plan_df.iterrows():
246
+ try:
247
+ # Set geometry path if Geom File exists and Geom Path is missing or invalid
248
+ if pd.notna(row['Geom File']):
249
+ geom_path = self.project_folder / f"{self.project_name}.g{row['Geom File']}"
250
+ self.plan_df.at[idx, 'Geom Path'] = str(geom_path)
251
+
252
+ # Set flow path if Flow File exists and Flow Path is missing or invalid
253
+ if pd.notna(row['Flow File']):
254
+ # Determine the prefix (u for unsteady, f for steady flow)
255
+ prefix = 'u' if pd.notna(row['unsteady_number']) else 'f'
256
+ flow_path = self.project_folder / f"{self.project_name}.{prefix}{row['Flow File']}"
257
+ self.plan_df.at[idx, 'Flow Path'] = str(flow_path)
258
+
259
+ if not self.suppress_logging:
260
+ logger.debug(f"Plan {row['plan_number']} paths set up")
261
+ except Exception as e:
262
+ logger.error(f"Error setting paths for plan {row.get('plan_number', idx)}: {e}")
229
263
 
230
- def _get_flow_file_from_plan(self, plan_file_path):
264
+ def _get_geom_file_for_plan(self, plan_number):
231
265
  """
232
- Extract the Flow File value from a plan file.
266
+ Get the geometry file path for a given plan number.
233
267
 
234
- Parameters:
235
- -----------
236
- plan_file_path : str or Path
237
- Path to the plan file.
268
+ Args:
269
+ plan_number (str): The plan number to find the geometry file for.
238
270
 
239
271
  Returns:
240
- --------
241
- str or None
242
- The Flow File value or None if not found.
272
+ str: The full path to the geometry HDF file, or None if not found.
243
273
  """
274
+ plan_file_path = self.project_folder / f"{self.project_name}.p{plan_number}"
244
275
  content, encoding = read_file_with_fallback_encoding(plan_file_path)
245
276
 
246
277
  if content is None:
247
278
  return None
248
279
 
249
280
  try:
250
- match = re.search(r'Flow File=([^\s]+)', content)
251
- if match:
252
- return match.group(1)
281
+ for line in content.splitlines():
282
+ if line.startswith("Geom File="):
283
+ geom_file = line.strip().split('=')[1]
284
+ geom_hdf_path = self.project_folder / f"{self.project_name}.{geom_file}.hdf"
285
+ if geom_hdf_path.exists():
286
+ return str(geom_hdf_path)
287
+ else:
288
+ return None
253
289
  except Exception as e:
254
- logger.error(f"Error extracting Flow File from {plan_file_path}: {e}")
255
-
290
+ logger.error(f"Error reading plan file for geometry: {e}")
256
291
  return None
257
292
 
293
+
258
294
  @staticmethod
259
295
  @log_call
260
296
  def get_plan_value(
@@ -439,112 +475,106 @@ class RasPrj:
439
475
 
440
476
  Returns:
441
477
  pd.DataFrame: A DataFrame containing the extracted entries.
478
+
479
+ Raises:
480
+ Exception: If there's an error reading or processing the project file.
442
481
  """
443
482
  entries = []
444
483
  pattern = re.compile(rf"{entry_type} File=(\w+)")
445
484
 
446
485
  try:
447
- with open(self.prj_file, 'r') as file:
486
+ with open(self.prj_file, 'r', encoding='utf-8') as file:
448
487
  for line in file:
449
488
  match = pattern.match(line.strip())
450
489
  if match:
451
490
  file_name = match.group(1)
452
491
  full_path = str(self.project_folder / f"{self.project_name}.{file_name}")
453
- entry_number = file_name[1:] # Extract number portion without prefix
492
+ entry_number = file_name[1:]
454
493
 
455
494
  entry = {
456
495
  f'{entry_type.lower()}_number': entry_number,
457
496
  'full_path': full_path
458
497
  }
459
498
 
499
+ # Handle Unsteady entries
460
500
  if entry_type == 'Unsteady':
461
- entry['unsteady_number'] = entry_number
462
- unsteady_info = self._parse_unsteady_file(Path(full_path))
463
- entry.update(unsteady_info)
501
+ entry.update(self._process_unsteady_entry(entry_number, full_path))
464
502
  else:
465
- entry.update({
466
- 'unsteady_number': None,
467
- 'geometry_number': None
468
- })
503
+ entry.update(self._process_default_entry())
469
504
 
505
+ # Handle Plan entries
470
506
  if entry_type == 'Plan':
471
- plan_info = self._parse_plan_file(Path(full_path))
472
- if plan_info:
473
- # Handle Flow File (unsteady) number
474
- flow_file = plan_info.get('Flow File')
475
- if flow_file and flow_file.startswith('u'):
476
- entry['unsteady_number'] = flow_file[1:]
477
- entry['Flow File'] = flow_file[1:]
478
- else:
479
- entry['unsteady_number'] = None
480
- entry['Flow File'] = flow_file[1:] if flow_file and flow_file.startswith('f') else None
481
-
482
- # Handle Geom File number
483
- geom_file = plan_info.get('Geom File')
484
- if geom_file and geom_file.startswith('g'):
485
- entry['geometry_number'] = geom_file[1:]
486
- entry['Geom File'] = geom_file[1:]
487
- else:
488
- entry['geometry_number'] = None
489
- entry['Geom File'] = None
490
-
491
- # Add all plan key information with exact same names
492
- for key, value in plan_info.items():
493
- if key not in ['Flow File', 'Geom File']:
494
- entry[key] = value
507
+ entry.update(self._process_plan_entry(entry_number, full_path))
495
508
 
496
- # Add HDF results path
497
- hdf_results_path = self.project_folder / f"{self.project_name}.p{entry_number}.hdf"
498
- entry['HDF_Results_Path'] = str(hdf_results_path) if hdf_results_path.exists() else None
499
-
500
509
  entries.append(entry)
501
-
502
- df = pd.DataFrame(entries)
503
-
504
- if not df.empty and entry_type == 'Plan':
505
- # Set required column order
506
- first_cols = ['plan_number', 'unsteady_number', 'geometry_number']
507
-
508
- # Standard plan key columns in the exact order specified
509
- plan_key_cols = [
510
- 'Plan Title', 'Program Version', 'Short Identifier', 'Simulation Date',
511
- 'Std Step Tol', 'Computation Interval', 'Output Interval', 'Instantaneous Interval',
512
- 'Mapping Interval', 'Run HTab', 'Run UNet', 'Run Sediment', 'Run PostProcess',
513
- 'Run WQNet', 'Run RASMapper', 'UNET Use Existing IB Tables', 'HDF_Results_Path',
514
- 'UNET 1D Methodology', 'Write IC File', 'Write IC File at Fixed DateTime',
515
- 'IC Time', 'Write IC File Reoccurance', 'Write IC File at Sim End'
516
- ]
517
-
518
- # Additional convenience columns
519
- file_path_cols = ['Geom File', 'Geom Path', 'Flow File', 'Flow Path']
520
-
521
- # Build the final column list
522
- all_cols = first_cols.copy()
523
- for col in plan_key_cols:
524
- if col in df.columns:
525
- all_cols.append(col)
526
-
527
- # Add any remaining columns not explicitly specified
528
- other_cols = [col for col in df.columns if col not in all_cols + file_path_cols + ['full_path']]
529
- all_cols.extend(other_cols)
530
- all_cols.extend(file_path_cols)
531
-
532
- # Rename plan_number column
533
- df = df.rename(columns={f'{entry_type.lower()}_number': 'plan_number'})
534
-
535
- # Fill in missing columns with None
536
- for col in all_cols:
537
- if col not in df.columns:
538
- df[col] = None
539
-
540
- # Return DataFrame with specified column order
541
- return df[all_cols]
542
510
 
543
- return df
511
+ df = pd.DataFrame(entries)
512
+ return self._format_dataframe(df, entry_type)
513
+
544
514
  except Exception as e:
545
515
  logger.error(f"Error in _get_prj_entries for {entry_type}: {e}")
546
516
  raise
547
517
 
518
+ def _process_unsteady_entry(self, entry_number: str, full_path: str) -> dict:
519
+ """Process unsteady entry data."""
520
+ entry = {'unsteady_number': entry_number}
521
+ unsteady_info = self._parse_unsteady_file(Path(full_path))
522
+ entry.update(unsteady_info)
523
+ return entry
524
+
525
+ def _process_default_entry(self) -> dict:
526
+ """Process default entry data."""
527
+ return {
528
+ 'unsteady_number': None,
529
+ 'geometry_number': None
530
+ }
531
+
532
+ def _process_plan_entry(self, entry_number: str, full_path: str) -> dict:
533
+ """Process plan entry data."""
534
+ entry = {}
535
+ plan_info = self._parse_plan_file(Path(full_path))
536
+
537
+ if plan_info:
538
+ entry.update(self._process_flow_file(plan_info))
539
+ entry.update(self._process_geom_file(plan_info))
540
+
541
+ # Add remaining plan info
542
+ for key, value in plan_info.items():
543
+ if key not in ['Flow File', 'Geom File']:
544
+ entry[key] = value
545
+
546
+ # Add HDF results path
547
+ hdf_results_path = self.project_folder / f"{self.project_name}.p{entry_number}.hdf"
548
+ entry['HDF_Results_Path'] = str(hdf_results_path) if hdf_results_path.exists() else None
549
+
550
+ return entry
551
+
552
+ def _process_flow_file(self, plan_info: dict) -> dict:
553
+ """Process flow file information from plan info."""
554
+ flow_file = plan_info.get('Flow File')
555
+ if flow_file and flow_file.startswith('u'):
556
+ return {
557
+ 'unsteady_number': flow_file[1:],
558
+ 'Flow File': flow_file[1:]
559
+ }
560
+ return {
561
+ 'unsteady_number': None,
562
+ 'Flow File': flow_file[1:] if flow_file and flow_file.startswith('f') else None
563
+ }
564
+
565
+ def _process_geom_file(self, plan_info: dict) -> dict:
566
+ """Process geometry file information from plan info."""
567
+ geom_file = plan_info.get('Geom File')
568
+ if geom_file and geom_file.startswith('g'):
569
+ return {
570
+ 'geometry_number': geom_file[1:],
571
+ 'Geom File': geom_file[1:]
572
+ }
573
+ return {
574
+ 'geometry_number': None,
575
+ 'Geom File': None
576
+ }
577
+
548
578
  def _parse_unsteady_file(self, unsteady_file_path):
549
579
  """
550
580
  Parse an unsteady flow file and extract critical information.
@@ -935,27 +965,144 @@ class RasPrj:
935
965
  return bc_info, unparsed_lines
936
966
 
937
967
  @log_call
938
- def get_unsteady_numbers_from_plans(self):
968
+ def _format_dataframe(self, df, entry_type):
939
969
  """
940
- Get all plans that use unsteady flow files.
970
+ Format the DataFrame according to the desired column structure.
971
+
972
+ Args:
973
+ df (pd.DataFrame): The DataFrame to format.
974
+ entry_type (str): The type of entry (e.g., 'Plan', 'Flow', 'Unsteady', 'Geom').
941
975
 
942
976
  Returns:
943
- --------
944
- pd.DataFrame
945
- A DataFrame containing only plan entries that use unsteady flow files.
977
+ pd.DataFrame: The formatted DataFrame.
978
+ """
979
+ if df.empty:
980
+ return df
946
981
 
947
- Raises:
948
- -------
949
- RuntimeError: If the project has not been initialized.
982
+ if entry_type == 'Plan':
983
+ # Set required column order
984
+ first_cols = ['plan_number', 'unsteady_number', 'geometry_number']
985
+
986
+ # Standard plan key columns in the exact order specified
987
+ plan_key_cols = [
988
+ 'Plan Title', 'Program Version', 'Short Identifier', 'Simulation Date',
989
+ 'Std Step Tol', 'Computation Interval', 'Output Interval', 'Instantaneous Interval',
990
+ 'Mapping Interval', 'Run HTab', 'Run UNet', 'Run Sediment', 'Run PostProcess',
991
+ 'Run WQNet', 'Run RASMapper', 'UNET Use Existing IB Tables', 'HDF_Results_Path',
992
+ 'UNET 1D Methodology', 'Write IC File', 'Write IC File at Fixed DateTime',
993
+ 'IC Time', 'Write IC File Reoccurance', 'Write IC File at Sim End'
994
+ ]
995
+
996
+ # Additional convenience columns
997
+ file_path_cols = ['Geom File', 'Geom Path', 'Flow File', 'Flow Path']
998
+
999
+ # Special columns that must be preserved
1000
+ special_cols = ['HDF_Results_Path']
1001
+
1002
+ # Build the final column list
1003
+ all_cols = first_cols.copy()
1004
+
1005
+ # Add plan key columns if they exist
1006
+ for col in plan_key_cols:
1007
+ if col in df.columns and col not in all_cols and col not in special_cols:
1008
+ all_cols.append(col)
1009
+
1010
+ # Add any remaining columns not explicitly specified
1011
+ other_cols = [col for col in df.columns if col not in all_cols + file_path_cols + special_cols + ['full_path']]
1012
+ all_cols.extend(other_cols)
1013
+
1014
+ # Add HDF_Results_Path if it exists (ensure it comes before file paths)
1015
+ for special_col in special_cols:
1016
+ if special_col in df.columns and special_col not in all_cols:
1017
+ all_cols.append(special_col)
1018
+
1019
+ # Add file path columns at the end
1020
+ all_cols.extend(file_path_cols)
1021
+
1022
+ # Rename plan_number column
1023
+ df = df.rename(columns={f'{entry_type.lower()}_number': 'plan_number'})
1024
+
1025
+ # Fill in missing columns with None
1026
+ for col in all_cols:
1027
+ if col not in df.columns:
1028
+ df[col] = None
1029
+
1030
+ # Make sure full_path column is preserved and included
1031
+ if 'full_path' in df.columns and 'full_path' not in all_cols:
1032
+ all_cols.append('full_path')
1033
+
1034
+ # Return DataFrame with specified column order
1035
+ cols_to_return = [col for col in all_cols if col in df.columns]
1036
+ return df[cols_to_return]
1037
+
1038
+ return df
1039
+
1040
+ @log_call
1041
+ def _get_prj_entries(self, entry_type):
950
1042
  """
951
- self.check_initialized()
1043
+ Extract entries of a specific type from the HEC-RAS project file.
1044
+ """
1045
+ entries = []
1046
+ pattern = re.compile(rf"{entry_type} File=(\w+)")
1047
+
1048
+ try:
1049
+ with open(self.prj_file, 'r') as file:
1050
+ for line in file:
1051
+ match = pattern.match(line.strip())
1052
+ if match:
1053
+ file_name = match.group(1)
1054
+ full_path = str(self.project_folder / f"{self.project_name}.{file_name}")
1055
+ entry = self._create_entry(entry_type, file_name, full_path)
1056
+ entries.append(entry)
952
1057
 
953
- # Filter plan_df to only include plans with unsteady_number not None
954
- unsteady_plans = self.plan_df[self.plan_df['unsteady_number'].notna()].copy()
1058
+ return self._format_dataframe(pd.DataFrame(entries), entry_type)
955
1059
 
956
- logger.info(f"Found {len(unsteady_plans)} plans using unsteady flow files")
1060
+ except Exception as e:
1061
+ logger.error(f"Error in _get_prj_entries for {entry_type}: {e}")
1062
+ raise
1063
+
1064
+ def _create_entry(self, entry_type, file_name, full_path):
1065
+ """Helper method to create entry dictionary."""
1066
+ entry_number = file_name[1:]
1067
+ entry = {
1068
+ f'{entry_type.lower()}_number': entry_number,
1069
+ 'full_path': full_path,
1070
+ 'unsteady_number': None,
1071
+ 'geometry_number': None
1072
+ }
1073
+
1074
+ if entry_type == 'Unsteady':
1075
+ entry['unsteady_number'] = entry_number
1076
+ entry.update(self._parse_unsteady_file(Path(full_path)))
1077
+ elif entry_type == 'Plan':
1078
+ self._update_plan_entry(entry, entry_number, full_path)
957
1079
 
958
- return unsteady_plans
1080
+ return entry
1081
+
1082
+ def _update_plan_entry(self, entry, entry_number, full_path):
1083
+ """Helper method to update plan entry with additional information."""
1084
+ plan_info = self._parse_plan_file(Path(full_path))
1085
+ if plan_info:
1086
+ # Handle Flow File
1087
+ flow_file = plan_info.get('Flow File')
1088
+ if flow_file:
1089
+ if flow_file.startswith('u'):
1090
+ entry.update({'unsteady_number': flow_file[1:], 'Flow File': flow_file[1:]})
1091
+ else:
1092
+ entry['Flow File'] = flow_file[1:] if flow_file.startswith('f') else None
1093
+
1094
+ # Handle Geom File
1095
+ geom_file = plan_info.get('Geom File')
1096
+ if geom_file and geom_file.startswith('g'):
1097
+ entry.update({'geometry_number': geom_file[1:], 'Geom File': geom_file[1:]})
1098
+
1099
+ # Add remaining plan info
1100
+ entry.update({k: v for k, v in plan_info.items() if k not in ['Flow File', 'Geom File']})
1101
+
1102
+ # Add HDF results path
1103
+ hdf_path = self.project_folder / f"{self.project_name}.p{entry_number}.hdf"
1104
+ entry['HDF_Results_Path'] = str(hdf_path) if hdf_path.exists() else None
1105
+
959
1106
 
960
1107
  # Create a global instance named 'ras'
961
1108
  # Defining the global instance allows the init_ras_project function to initialize the project.
@@ -1027,7 +1174,7 @@ def get_ras_exe(ras_version=None):
1027
1174
  Args:
1028
1175
  ras_version (str, optional): Either a version number or a full path to the HEC-RAS executable.
1029
1176
  If None, the function will first check the global 'ras' object for a path.
1030
- If the global 'ras' object is not initialized or doesn't have a path, a default path will be used.
1177
+ or a default path.
1031
1178
 
1032
1179
  Returns:
1033
1180
  str: The full path to the HEC-RAS executable.
ras_commander/RasUtils.py CHANGED
@@ -216,30 +216,49 @@ class RasUtils:
216
216
  Returns:
217
217
  Path: Full path to the plan file
218
218
 
219
+ Raises:
220
+ ValueError: If plan number is not between 1 and 99
221
+ TypeError: If input type is invalid
222
+ FileNotFoundError: If the plan file does not exist
223
+
219
224
  Example:
220
225
  >>> plan_path = RasUtils.get_plan_path(1)
221
226
  >>> print(f"Plan file path: {plan_path}")
222
227
  >>> plan_path = RasUtils.get_plan_path("path/to/plan.p01")
223
228
  >>> print(f"Plan file path: {plan_path}")
224
229
  """
225
-
230
+ # Validate RAS object
226
231
  ras_obj = ras_object or ras
227
232
  ras_obj.check_initialized()
228
233
 
234
+ # Handle direct file path input
229
235
  plan_path = Path(current_plan_number_or_path)
230
236
  if plan_path.is_file():
231
237
  logger.info(f"Using provided plan file path: {plan_path}")
232
238
  return plan_path
233
239
 
240
+ # Handle plan number input
234
241
  try:
235
- current_plan_number = f"{int(current_plan_number_or_path):02d}" # Ensure two-digit format
242
+ plan_num = int(current_plan_number_or_path)
243
+ if not 1 <= plan_num <= 99:
244
+ raise ValueError(f"Plan number must be between 1 and 99, got: {plan_num}")
245
+ current_plan_number = f"{plan_num:02d}" # Ensure two-digit format
236
246
  logger.debug(f"Converted plan number to two-digit format: {current_plan_number}")
237
- except ValueError:
247
+ except (ValueError, TypeError) as e:
248
+ if isinstance(e, TypeError):
249
+ logger.error(f"Invalid input type: {type(current_plan_number_or_path)}. Expected string, number, or Path.")
250
+ raise TypeError(f"Invalid input type: {type(current_plan_number_or_path)}. Expected string, number, or Path.")
238
251
  logger.error(f"Invalid plan number: {current_plan_number_or_path}. Expected a number from 1 to 99.")
239
252
  raise ValueError(f"Invalid plan number: {current_plan_number_or_path}. Expected a number from 1 to 99.")
240
253
 
254
+ # Construct and validate plan path
241
255
  plan_name = f"{ras_obj.project_name}.p{current_plan_number}"
242
256
  full_plan_path = ras_obj.project_folder / plan_name
257
+
258
+ if not full_plan_path.exists():
259
+ logger.error(f"Plan file does not exist: {full_plan_path}")
260
+ raise FileNotFoundError(f"Plan file does not exist: {full_plan_path}")
261
+
243
262
  logger.info(f"Constructed plan file path: {full_plan_path}")
244
263
  return full_plan_path
245
264
 
ras_commander/__init__.py CHANGED
@@ -10,7 +10,7 @@ try:
10
10
  __version__ = version("ras-commander")
11
11
  except PackageNotFoundError:
12
12
  # package is not installed
13
- __version__ = "0.65.0"
13
+ __version__ = "0.67.0"
14
14
 
15
15
  # Set up logging
16
16
  setup_logging()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: ras-commander
3
- Version: 0.65.0
3
+ Version: 0.67.0
4
4
  Summary: A Python library for automating HEC-RAS 6.x operations
5
5
  Home-page: https://github.com/gpt-cmdr/ras-commander
6
6
  Author: William M. Katzenmeyer, P.E., C.F.M.