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/RasPrj.py CHANGED
@@ -1,4 +1,5 @@
1
- """RasPrj.py
1
+ """
2
+ RasPrj.py - Manages HEC-RAS projects within the ras-commander library
2
3
 
3
4
  This module provides a class for managing HEC-RAS projects.
4
5
 
@@ -14,31 +15,46 @@ This class is used to initialize a RAS project and is used in conjunction with t
14
15
  By default, the RasPrj class is initialized with the global 'ras' object.
15
16
  However, you can create multiple RasPrj instances to manage multiple projects.
16
17
  Do not mix and match global 'ras' object instances and custom instances of RasPrj - it will cause errors.
17
- """
18
18
 
19
- # Example Terminal Output for RasPrj Functions:
20
- # logging.info("----- INSERT TEXT HERE -----")
19
+ This module is part of the ras-commander library and uses a centralized logging configuration.
20
+
21
+ Logging Configuration:
22
+ - The logging is set up in the logging_config.py file.
23
+ - A @log_call decorator is available to automatically log function calls.
24
+ - Log levels: DEBUG, INFO, WARNING, ERROR, CRITICAL
25
+ - Logs are written to both console and a rotating file handler.
26
+ - The default log file is 'ras_commander.log' in the 'logs' directory.
27
+ - The default log level is INFO.
28
+
29
+ To use logging in this module:
30
+ 1. Use the @log_call decorator for automatic function call logging.
31
+ 2. For additional logging, use logger.[level]() calls (e.g., logger.info(), logger.debug()).
32
+
33
+
34
+ Example:
35
+ @log_call
36
+ def my_function():
37
+
38
+ logger.debug("Additional debug information")
39
+ # Function logic here
40
+ """
41
+ import os
21
42
  import re
22
43
  from pathlib import Path
23
44
  import pandas as pd
24
- import logging
25
45
  from typing import Union, Any, List, Dict, Tuple
46
+ import logging
47
+ from ras_commander.logging_config import get_logger, log_call
26
48
 
27
-
28
- # Configure logging
29
- logging.basicConfig(
30
- level=logging.INFO,
31
- format='%(asctime)s - %(levelname)s - %(message)s',
32
- handlers=[
33
- logging.StreamHandler()
34
- ]
35
- )
49
+ logger = get_logger(__name__)
36
50
 
37
51
  class RasPrj:
52
+
38
53
  def __init__(self):
39
54
  self.initialized = False
40
55
  self.boundaries_df = None # New attribute to store boundary conditions
41
56
 
57
+ @log_call
42
58
  def initialize(self, project_folder, ras_exe_path):
43
59
  """
44
60
  Initialize a RasPrj instance.
@@ -60,18 +76,19 @@ class RasPrj:
60
76
  self.project_folder = Path(project_folder)
61
77
  self.prj_file = self.find_ras_prj(self.project_folder)
62
78
  if self.prj_file is None:
63
- logging.error(f"No HEC-RAS project file found in {self.project_folder}")
79
+ logger.error(f"No HEC-RAS project file found in {self.project_folder}")
64
80
  raise ValueError(f"No HEC-RAS project file found in {self.project_folder}")
65
81
  self.project_name = Path(self.prj_file).stem
66
82
  self.ras_exe_path = ras_exe_path
67
83
  self._load_project_data()
68
84
  self.boundaries_df = self.get_boundary_conditions() # Extract boundary conditions
69
85
  self.initialized = True
70
- logging.info(f"Initialization complete for project: {self.project_name}")
71
- logging.info(f"Plan entries: {len(self.plan_df)}, Flow entries: {len(self.flow_df)}, "
86
+ logger.info(f"Initialization complete for project: {self.project_name}")
87
+ logger.info(f"Plan entries: {len(self.plan_df)}, Flow entries: {len(self.flow_df)}, "
72
88
  f"Unsteady entries: {len(self.unsteady_df)}, Geometry entries: {len(self.geom_df)}, "
73
89
  f"Boundary conditions: {len(self.boundaries_df)}")
74
90
 
91
+ @log_call
75
92
  def _load_project_data(self):
76
93
  """
77
94
  Load project data from the HEC-RAS project file.
@@ -85,6 +102,7 @@ class RasPrj:
85
102
  self.unsteady_df = self._get_prj_entries('Unsteady')
86
103
  self.geom_df = self._get_prj_entries('Geom')
87
104
 
105
+ @log_call
88
106
  def _parse_plan_file(self, plan_file_path):
89
107
  """
90
108
  Parse a plan file and extract critical information.
@@ -96,45 +114,57 @@ class RasPrj:
96
114
  dict: Dictionary containing extracted plan information.
97
115
  """
98
116
  plan_info = {}
99
- with open(plan_file_path, 'r') as file:
100
- content = file.read()
101
-
102
- # Extract description
103
- description_match = re.search(r'Begin DESCRIPTION(.*?)END DESCRIPTION', content, re.DOTALL)
104
- if description_match:
105
- plan_info['description'] = description_match.group(1).strip()
106
-
107
- # Extract other critical information
108
- patterns = {
109
- 'computation_interval': r'Computation Interval=(.+)',
110
- 'dss_file': r'DSS File=(.+)',
111
- 'flow_file': r'Flow File=(.+)',
112
- 'friction_slope_method': r'Friction Slope Method=(.+)',
113
- 'geom_file': r'Geom File=(.+)',
114
- 'mapping_interval': r'Mapping Interval=(.+)',
115
- 'plan_title': r'Plan Title=(.+)',
116
- 'program_version': r'Program Version=(.+)',
117
- 'run_htab': r'Run HTab=(.+)',
118
- 'run_post_process': r'Run PostProcess=(.+)',
119
- 'run_sediment': r'Run Sediment=(.+)',
120
- 'run_unet': r'Run UNet=(.+)',
121
- 'run_wqnet': r'Run WQNet=(.+)',
122
- 'short_identifier': r'Short Identifier=(.+)',
123
- 'simulation_date': r'Simulation Date=(.+)',
124
- 'unet_d1_cores': r'UNET D1 Cores=(.+)',
125
- 'unet_use_existing_ib_tables': r'UNET Use Existing IB Tables=(.+)',
126
- 'unet_1d_methodology': r'UNET 1D Methodology=(.+)',
127
- 'unet_d2_solver_type': r'UNET D2 SolverType=(.+)',
128
- 'unet_d2_name': r'UNET D2 Name=(.+)'
129
- }
117
+ try:
118
+ with open(plan_file_path, 'r') as file:
119
+ content = file.read()
120
+
121
+ # Extract description
122
+ description_match = re.search(r'Begin DESCRIPTION(.*?)END DESCRIPTION', content, re.DOTALL)
123
+ if description_match:
124
+ plan_info['description'] = description_match.group(1).strip()
125
+
126
+ # BEGIN Exception to Style Guide, this is needed to keep the key names consistent with the plan file keys.
127
+
128
+ # Extract other critical information
129
+ supported_plan_keys = {
130
+ 'Computation Interval': r'Computation Interval=(.+)',
131
+ 'DSS File': r'DSS File=(.+)',
132
+ 'Flow File': r'Flow File=(.+)',
133
+ 'Friction Slope Method': r'Friction Slope Method=(.+)',
134
+ 'Geom File': r'Geom File=(.+)',
135
+ 'Mapping Interval': r'Mapping Interval=(.+)',
136
+ 'Plan Title': r'Plan Title=(.+)',
137
+ 'Program Version': r'Program Version=(.+)',
138
+ 'Run HTab': r'Run HTab=(.+)',
139
+ 'Run PostProcess': r'Run PostProcess=(.+)',
140
+ 'Run Sediment': r'Run Sediment=(.+)',
141
+ 'Run UNet': r'Run UNet=(.+)',
142
+ 'Run WQNet': r'Run WQNet=(.+)',
143
+ 'Short Identifier': r'Short Identifier=(.+)',
144
+ 'Simulation Date': r'Simulation Date=(.+)',
145
+ 'UNET D1 Cores': r'UNET D1 Cores=(.+)',
146
+ 'UNET Use Existing IB Tables': r'UNET Use Existing IB Tables=(.+)',
147
+ 'UNET 1D Methodology': r'UNET 1D Methodology=(.+)',
148
+ 'UNET D2 SolverType': r'UNET D2 SolverType=(.+)',
149
+ 'UNET D2 Name': r'UNET D2 Name=(.+)'
150
+ }
151
+
152
+ # END Exception to Style Guide
153
+
154
+ for key, pattern in supported_plan_keys.items():
155
+ match = re.search(pattern, content)
156
+ if match:
157
+ plan_info[key] = match.group(1).strip()
130
158
 
131
- for key, pattern in patterns.items():
132
- match = re.search(pattern, content)
133
- if match:
134
- plan_info[key] = match.group(1).strip()
159
+ logger.debug(f"Parsed plan file: {plan_file_path}")
160
+ except Exception as e:
161
+ logger.exception(f"Error parsing plan file {plan_file_path}: {e}")
135
162
 
136
163
  return plan_info
137
164
 
165
+
166
+
167
+ @log_call
138
168
  def _get_prj_entries(self, entry_type):
139
169
  """
140
170
  Extract entries of a specific type from the HEC-RAS project file.
@@ -168,7 +198,6 @@ class RasPrj:
168
198
  plan_info = self._parse_plan_file(Path(full_path))
169
199
  entry.update(plan_info)
170
200
 
171
- # Add HDF results path if it exists
172
201
  hdf_results_path = self.project_folder / f"{self.project_name}.p{file_name[1:]}.hdf"
173
202
  entry['HDF_Results_Path'] = str(hdf_results_path) if hdf_results_path.exists() else None
174
203
 
@@ -178,11 +207,11 @@ class RasPrj:
178
207
 
179
208
  entries.append(entry)
180
209
  except Exception as e:
181
- logging.exception(f"Failed to read project file {self.prj_file}: {e}")
182
210
  raise
183
211
 
184
212
  return pd.DataFrame(entries)
185
213
 
214
+ @log_call
186
215
  def _parse_unsteady_file(self, unsteady_file_path):
187
216
  """
188
217
  Parse an unsteady flow file and extract critical information.
@@ -197,21 +226,24 @@ class RasPrj:
197
226
  with open(unsteady_file_path, 'r') as file:
198
227
  content = file.read()
199
228
 
200
- # Extract critical information
201
- patterns = {
202
- 'flow_title': r'Flow Title=(.+)',
203
- 'program_version': r'Program Version=(.+)',
204
- 'use_restart': r'Use Restart=(.+)',
205
- 'precipitation_mode': r'Precipitation Mode=(.+)',
206
- 'wind_mode': r'Wind Mode=(.+)',
207
- 'precipitation_bc_mode': r'Met BC=Precipitation\|Mode=(.+)',
208
- 'evapotranspiration_bc_mode': r'Met BC=Evapotranspiration\|Mode=(.+)',
209
- 'precipitation_expanded_view': r'Met BC=Precipitation\|Expanded View=(.+)',
210
- 'precipitation_constant_units': r'Met BC=Precipitation\|Constant Units=(.+)',
211
- 'precipitation_gridded_source': r'Met BC=Precipitation\|Gridded Source=(.+)'
229
+ # BEGIN Exception to Style Guide, this is needed to keep the key names consistent with the unsteady file keys.
230
+
231
+ supported_unsteady_keys = {
232
+ 'Flow Title': r'Flow Title=(.+)',
233
+ 'Program Version': r'Program Version=(.+)',
234
+ 'Use Restart': r'Use Restart=(.+)',
235
+ 'Precipitation Mode': r'Precipitation Mode=(.+)',
236
+ 'Wind Mode': r'Wind Mode=(.+)',
237
+ 'Met BC=Precipitation|Mode': r'Met BC=Precipitation\|Mode=(.+)',
238
+ 'Met BC=Evapotranspiration|Mode': r'Met BC=Evapotranspiration\|Mode=(.+)',
239
+ 'Met BC=Precipitation|Expanded View': r'Met BC=Precipitation\|Expanded View=(.+)',
240
+ 'Met BC=Precipitation|Constant Units': r'Met BC=Precipitation\|Constant Units=(.+)',
241
+ 'Met BC=Precipitation|Gridded Source': r'Met BC=Precipitation\|Gridded Source=(.+)'
212
242
  }
213
243
 
214
- for key, pattern in patterns.items():
244
+ # END Exception to Style Guide
245
+
246
+ for key, pattern in supported_unsteady_keys.items():
215
247
  match = re.search(pattern, content)
216
248
  if match:
217
249
  unsteady_info[key] = match.group(1).strip()
@@ -228,6 +260,7 @@ class RasPrj:
228
260
  """
229
261
  return self.initialized
230
262
 
263
+ @log_call
231
264
  def check_initialized(self):
232
265
  """
233
266
  Ensure that the RasPrj instance has been initialized.
@@ -236,10 +269,10 @@ class RasPrj:
236
269
  RuntimeError: If the project has not been initialized.
237
270
  """
238
271
  if not self.initialized:
239
- logging.error("Project not initialized. Call init_ras_project() first.")
240
272
  raise RuntimeError("Project not initialized. Call init_ras_project() first.")
241
273
 
242
274
  @staticmethod
275
+ @log_call
243
276
  def find_ras_prj(folder_path):
244
277
  """
245
278
  Find the appropriate HEC-RAS project file (.prj) in the given folder.
@@ -254,28 +287,25 @@ class RasPrj:
254
287
  prj_files = list(folder_path.glob("*.prj"))
255
288
  rasmap_files = list(folder_path.glob("*.rasmap"))
256
289
  if len(prj_files) == 1:
257
- logging.info(f"Single .prj file found: {prj_files[0]}")
258
290
  return prj_files[0].resolve()
259
291
  if len(prj_files) > 1:
260
292
  if len(rasmap_files) == 1:
261
293
  base_filename = rasmap_files[0].stem
262
294
  prj_file = folder_path / f"{base_filename}.prj"
263
295
  if prj_file.exists():
264
- logging.info(f"Matched .prj file based on .rasmap: {prj_file}")
265
296
  return prj_file.resolve()
266
297
  for prj_file in prj_files:
267
298
  try:
268
299
  with open(prj_file, 'r') as file:
269
300
  content = file.read()
270
301
  if "Proj Title=" in content:
271
- logging.info(f".prj file with 'Proj Title=' found: {prj_file}")
272
302
  return prj_file.resolve()
273
- except Exception as e:
274
- logging.warning(f"Failed to read .prj file {prj_file}: {e}")
303
+ except Exception:
275
304
  continue
276
- logging.warning("No suitable .prj file found after all checks.")
277
305
  return None
278
306
 
307
+
308
+ @log_call
279
309
  def get_project_name(self):
280
310
  """
281
311
  Get the name of the HEC-RAS project.
@@ -289,6 +319,7 @@ class RasPrj:
289
319
  self.check_initialized()
290
320
  return self.project_name
291
321
 
322
+ @log_call
292
323
  def get_prj_entries(self, entry_type):
293
324
  """
294
325
  Get entries of a specific type from the HEC-RAS project.
@@ -305,6 +336,7 @@ class RasPrj:
305
336
  self.check_initialized()
306
337
  return self._get_prj_entries(entry_type)
307
338
 
339
+ @log_call
308
340
  def get_plan_entries(self):
309
341
  """
310
342
  Get all plan entries from the HEC-RAS project.
@@ -318,6 +350,7 @@ class RasPrj:
318
350
  self.check_initialized()
319
351
  return self._get_prj_entries('Plan')
320
352
 
353
+ @log_call
321
354
  def get_flow_entries(self):
322
355
  """
323
356
  Get all flow entries from the HEC-RAS project.
@@ -331,6 +364,7 @@ class RasPrj:
331
364
  self.check_initialized()
332
365
  return self._get_prj_entries('Flow')
333
366
 
367
+ @log_call
334
368
  def get_unsteady_entries(self):
335
369
  """
336
370
  Get all unsteady flow entries from the HEC-RAS project.
@@ -344,6 +378,7 @@ class RasPrj:
344
378
  self.check_initialized()
345
379
  return self._get_prj_entries('Unsteady')
346
380
 
381
+ @log_call
347
382
  def get_geom_entries(self):
348
383
  """
349
384
  Get all geometry entries from the HEC-RAS project.
@@ -357,6 +392,7 @@ class RasPrj:
357
392
  self.check_initialized()
358
393
  return self._get_prj_entries('Geom')
359
394
 
395
+ @log_call
360
396
  def get_hdf_entries(self):
361
397
  """
362
398
  Get HDF entries for plans that have results.
@@ -367,40 +403,38 @@ class RasPrj:
367
403
  """
368
404
  self.check_initialized()
369
405
 
370
- # Filter the plan_df to include only entries with existing HDF results
371
406
  hdf_entries = self.plan_df[self.plan_df['HDF_Results_Path'].notna()].copy()
372
407
 
373
- # If no HDF entries are found, log the information
374
408
  if hdf_entries.empty:
375
- logging.info("No HDF entries found.")
376
409
  return pd.DataFrame(columns=self.plan_df.columns)
377
410
 
378
- logging.info(f"Found {len(hdf_entries)} HDF entries.")
379
411
  return hdf_entries
380
412
 
413
+
414
+ @log_call
381
415
  def print_data(self):
382
416
  """Print all RAS Object data for this instance."""
383
417
  self.check_initialized()
384
- logging.info(f"--- Data for {self.project_name} ---")
385
- logging.info(f"Project folder: {self.project_folder}")
386
- logging.info(f"PRJ file: {self.prj_file}")
387
- logging.info(f"HEC-RAS executable: {self.ras_exe_path}")
388
- logging.info("Plan files:")
389
- logging.info(f"\n{self.plan_df}")
390
- logging.info("Flow files:")
391
- logging.info(f"\n{self.flow_df}")
392
- logging.info("Unsteady flow files:")
393
- logging.info(f"\n{self.unsteady_df}")
394
- logging.info("Geometry files:")
395
- logging.info(f"\n{self.geom_df}")
396
- logging.info("HDF entries:")
397
- logging.info(f"\n{self.get_hdf_entries()}")
398
- logging.info("Boundary conditions:")
399
- logging.info(f"\n{self.boundaries_df}")
400
- logging.info("----------------------------")
401
-
418
+ logger.info(f"--- Data for {self.project_name} ---")
419
+ logger.info(f"Project folder: {self.project_folder}")
420
+ logger.info(f"PRJ file: {self.prj_file}")
421
+ logger.info(f"HEC-RAS executable: {self.ras_exe_path}")
422
+ logger.info("Plan files:")
423
+ logger.info(f"\n{self.plan_df}")
424
+ logger.info("Flow files:")
425
+ logger.info(f"\n{self.flow_df}")
426
+ logger.info("Unsteady flow files:")
427
+ logger.info(f"\n{self.unsteady_df}")
428
+ logger.info("Geometry files:")
429
+ logger.info(f"\n{self.geom_df}")
430
+ logger.info("HDF entries:")
431
+ logger.info(f"\n{self.get_hdf_entries()}")
432
+ logger.info("Boundary conditions:")
433
+ logger.info(f"\n{self.boundaries_df}")
434
+ logger.info("----------------------------")
402
435
 
403
436
  @staticmethod
437
+ @log_call
404
438
  def get_plan_value(
405
439
  plan_number_or_path: Union[str, Path],
406
440
  key: str,
@@ -427,6 +461,7 @@ class RasPrj:
427
461
  >>> computation_interval = RasUtils.get_plan_value("01", "computation_interval")
428
462
  >>> print(f"Computation interval: {computation_interval}")
429
463
  """
464
+ logger = getLogger(__name__)
430
465
  ras_obj = ras_object or ras
431
466
  ras_obj.check_initialized()
432
467
 
@@ -439,19 +474,21 @@ class RasPrj:
439
474
  }
440
475
 
441
476
  if key not in valid_keys:
477
+ logger.error(f"Invalid key: {key}")
442
478
  raise ValueError(f"Invalid key: {key}. Valid keys are: {', '.join(valid_keys)}")
443
479
 
444
480
  plan_file_path = Path(plan_number_or_path)
445
481
  if not plan_file_path.is_file():
446
482
  plan_file_path = RasUtils.get_plan_path(plan_number_or_path, ras_object)
447
483
  if not plan_file_path.exists():
484
+ logger.error(f"Plan file not found: {plan_file_path}")
448
485
  raise ValueError(f"Plan file not found: {plan_file_path}")
449
486
 
450
487
  try:
451
488
  with open(plan_file_path, 'r') as file:
452
489
  content = file.read()
453
490
  except IOError as e:
454
- logging.error(f"Error reading plan file {plan_file_path}: {e}")
491
+ logger.error(f"Error reading plan file {plan_file_path}: {e}")
455
492
  raise
456
493
 
457
494
  if key == 'description':
@@ -464,6 +501,7 @@ class RasPrj:
464
501
  match = re.search(pattern, content)
465
502
  return match.group(1).strip() if match else None
466
503
 
504
+ @log_call
467
505
  def get_boundary_conditions(self) -> pd.DataFrame:
468
506
  """
469
507
  Extract boundary conditions from unsteady flow files and create a DataFrame.
@@ -478,27 +516,39 @@ class RasPrj:
478
516
  is logged at the DEBUG level for each boundary condition. This feature is crucial
479
517
  for developers incorporating new boundary condition types or parameters, as it
480
518
  allows them to see what information might be missing from the current parsing logic.
519
+ If no unsteady flow files are present, it returns an empty DataFrame.
481
520
 
482
521
  Returns:
483
522
  pd.DataFrame: A DataFrame containing detailed boundary condition information,
484
- linked to the unsteady flow files.
523
+ linked to the unsteady flow files.
485
524
 
486
525
  Usage:
487
526
  To see the unparsed lines, set the logging level to DEBUG before calling this method:
488
527
 
489
528
  import logging
490
- logging.getLogger().setLevel(logging.DEBUG)
529
+ getLogger().setLevel(logging.DEBUG)
491
530
 
492
531
  boundaries_df = ras_project.get_boundary_conditions()
532
+ linked to the unsteady flow files. Returns an empty DataFrame if
533
+ no unsteady flow files are present.
493
534
  """
494
535
  boundary_data = []
495
536
 
537
+ # Check if unsteady_df is empty
538
+ if self.unsteady_df.empty:
539
+ logger.info("No unsteady flow files found in the project.")
540
+ return pd.DataFrame() # Return an empty DataFrame
541
+
496
542
  for _, row in self.unsteady_df.iterrows():
497
543
  unsteady_file_path = row['full_path']
498
544
  unsteady_number = row['unsteady_number']
499
545
 
500
- with open(unsteady_file_path, 'r') as file:
501
- content = file.read()
546
+ try:
547
+ with open(unsteady_file_path, 'r') as file:
548
+ content = file.read()
549
+ except IOError as e:
550
+ logger.error(f"Error reading unsteady file {unsteady_file_path}: {e}")
551
+ continue
502
552
 
503
553
  bc_blocks = re.split(r'(?=Boundary Location=)', content)[1:]
504
554
 
@@ -507,7 +557,11 @@ class RasPrj:
507
557
  boundary_data.append(bc_info)
508
558
 
509
559
  if unparsed_lines:
510
- logging.debug(f"Unparsed lines for boundary condition {i} in unsteady file {unsteady_number}:\n{unparsed_lines}")
560
+ logger.debug(f"Unparsed lines for boundary condition {i} in unsteady file {unsteady_number}:\n{unparsed_lines}")
561
+
562
+ if not boundary_data:
563
+ logger.info("No boundary conditions found in unsteady flow files.")
564
+ return pd.DataFrame() # Return an empty DataFrame if no boundary conditions were found
511
565
 
512
566
  boundaries_df = pd.DataFrame(boundary_data)
513
567
 
@@ -517,6 +571,7 @@ class RasPrj:
517
571
 
518
572
  return merged_df
519
573
 
574
+ @log_call
520
575
  def _parse_boundary_condition(self, block: str, unsteady_number: str, bc_number: int) -> Tuple[Dict, str]:
521
576
  lines = block.split('\n')
522
577
  bc_info = {
@@ -588,21 +643,25 @@ class RasPrj:
588
643
  # Collect unparsed lines
589
644
  unparsed_lines = '\n'.join(line for i, line in enumerate(lines) if i not in parsed_lines and line.strip())
590
645
 
646
+ if unparsed_lines:
647
+ logger.debug(f"Unparsed lines for boundary condition {bc_number} in unsteady file {unsteady_number}:\n{unparsed_lines}")
648
+
591
649
  return bc_info, unparsed_lines
592
650
 
593
651
 
594
652
  # Create a global instance named 'ras'
653
+ # Defining the global instance allows the init_ras_project function to initialize the project.
654
+ # This only happens on the library initialization, not when the user calls init_ras_project.
595
655
  ras = RasPrj()
596
656
 
597
657
  # END OF CLASS DEFINITION
598
658
 
599
659
 
600
-
601
-
602
660
  # START OF FUNCTION DEFINITIONS
603
661
 
604
662
 
605
- def init_ras_project(ras_project_folder, ras_version, ras_instance=None):
663
+ @log_call
664
+ def init_ras_project(ras_project_folder, ras_version=None, ras_instance=None):
606
665
  """
607
666
  Initialize a RAS project.
608
667
 
@@ -616,9 +675,13 @@ def init_ras_project(ras_project_folder, ras_version, ras_instance=None):
616
675
  -----------
617
676
  ras_project_folder : str
618
677
  The path to the RAS project folder.
619
- ras_version : str
678
+ ras_version : str, optional
620
679
  The version of RAS to use (e.g., "6.5").
621
680
  The version can also be a full path to the Ras.exe file. (Useful when calling ras objects for folder copies.)
681
+ If None, the function will attempt to use the version from the global 'ras' object or a default path.
682
+ You MUST specify a version number via init at some point or ras will not run.
683
+ Once the ras_version is specified once it should auto-fill from the global 'ras' object.
684
+ The RAS Commander Library Assistant can ignore this argument since it does not have Ras.exe present, but all of other operations are fully working.
622
685
  ras_instance : RasPrj, optional
623
686
  An instance of RasPrj to initialize. If None, the global 'ras' instance is used.
624
687
 
@@ -630,12 +693,12 @@ def init_ras_project(ras_project_folder, ras_version, ras_instance=None):
630
693
  Usage:
631
694
  ------
632
695
  1. For general use with a single project:
633
- init_ras_project("/path/to/project", "6.5")
696
+ init_ras_project("/path/to/project")
634
697
  # Use the global 'ras' object after initialization
635
698
 
636
699
  2. For managing multiple projects:
637
700
  project1 = init_ras_project("/path/to/project1", "6.5", ras_instance=RasPrj())
638
- project2 = init_ras_project("/path/to/project2", "6.5", ras_instance=RasPrj())
701
+ project2 = init_ras_project("/path/to/project2", ras_instance=RasPrj())
639
702
 
640
703
  Notes:
641
704
  ------
@@ -644,51 +707,70 @@ def init_ras_project(ras_project_folder, ras_version, ras_instance=None):
644
707
  - Be consistent in your approach: stick to either the global 'ras' object
645
708
  or custom instances throughout your script or application.
646
709
  - Document your choice of approach clearly in your code.
710
+ - If ras_version is not provided, the function will attempt to use the version
711
+ from the global 'ras' object or a default path.
647
712
 
648
713
  Warnings:
649
714
  ---------
650
715
  Avoid mixing use of the global 'ras' object and custom instances to prevent
651
716
  confusion and potential bugs.
652
717
  """
653
- logging.info(f"Initializing project in folder: {ras_project_folder}")
654
- logging.info(f"Using ras_instance with id: {id(ras_instance)}")
655
-
656
-
657
-
658
718
  if not Path(ras_project_folder).exists():
659
- logging.error(f"The specified RAS project folder does not exist: {ras_project_folder}. Please check the path and try again.")
719
+ logger.error(f"The specified RAS project folder does not exist: {ras_project_folder}")
660
720
  raise FileNotFoundError(f"The specified RAS project folder does not exist: {ras_project_folder}. Please check the path and try again.")
661
721
 
662
722
  ras_exe_path = get_ras_exe(ras_version)
663
723
 
664
724
  if ras_instance is None:
665
- logging.info("Initializing global 'ras' object via init_ras_project function.")
725
+ logger.info("Initializing global 'ras' object via init_ras_project function.")
666
726
  ras_instance = ras
667
727
  elif not isinstance(ras_instance, RasPrj):
668
- logging.error("Provided ras_instance is not an instance of RasPrj.")
728
+ logger.error("Provided ras_instance is not an instance of RasPrj.")
669
729
  raise TypeError("ras_instance must be an instance of RasPrj or None.")
670
730
 
671
731
  # Initialize the RasPrj instance
672
732
  ras_instance.initialize(ras_project_folder, ras_exe_path)
673
733
 
674
- logging.info(f"Project initialized. ras_instance project folder: {ras_instance.project_folder}")
734
+ logger.info(f"Project initialized. ras_instance project folder: {ras_instance.project_folder}")
675
735
  return ras_instance
676
736
 
677
-
678
- def get_ras_exe(ras_version):
737
+ @log_call
738
+ def get_ras_exe(ras_version=None):
679
739
  """
680
740
  Determine the HEC-RAS executable path based on the input.
681
741
 
682
742
  Args:
683
- ras_version (str): Either a version number or a full path to the HEC-RAS executable.
743
+ ras_version (str, optional): Either a version number or a full path to the HEC-RAS executable.
744
+ If None, the function will attempt to use the version from the global 'ras' object
745
+ or a default path.
684
746
 
685
747
  Returns:
686
748
  str: The full path to the HEC-RAS executable.
687
749
 
688
750
  Raises:
689
751
  ValueError: If the input is neither a valid version number nor a valid file path.
690
- FileNotFoundError: If the executable file does not exist at the specified or constructed path.
752
+
753
+ Notes:
754
+ - If ras_version is not provided, the function will first check the global 'ras' object for a path.
755
+ - If the global 'ras' object is not initialized or doesn't have a path, a default path will be used.
756
+ - The default path allows the library to function in environments without HEC-RAS installed.
757
+ - This enables the HEC-Commander GPT to operate without stopping, even if HEC-RAS is not present.
758
+ - End users MUST use logging to check for operational errors, as full code stops prevent the GPT from running.
691
759
  """
760
+ # If ras_version is not provided, use the version of the global 'ras' object.
761
+ # If the global 'ras' object is not initialized, use the default path: default_path = Path("/path/to/Ras.exe")
762
+ # This default path allows the library to function in environments without HEC-RAS installed.
763
+ # It enables our HEC-Commander GPT to operate without stopping, even though HEC-RAS is not present.
764
+ # End users MUST use logging to check for operational errors, as full code stops prevent the GPT from running.
765
+ if ras_version is None:
766
+ if hasattr(ras, 'ras_exe_path') and ras.ras_exe_path:
767
+ logger.info(f"Using HEC-RAS executable from global 'ras' object: {ras.ras_exe_path}")
768
+ return ras.ras_exe_path
769
+ else:
770
+ default_path = Path("Ras.exe")
771
+ logger.warning(f"No HEC-RAS version specified and global 'ras' object not initialized or missing ras_exe_path. Using default path: {default_path}. The RAS Commander (ras-commander) Library Assistant can ignore this error since it does not have HEC-RAS installed.")
772
+ return str(default_path)
773
+
692
774
  ras_version_numbers = [
693
775
  "6.5", "6.4.1", "6.3.1", "6.3", "6.2", "6.1", "6.0",
694
776
  "5.0.7", "5.0.6", "5.0.5", "5.0.4", "5.0.3", "5.0.1", "5.0",
@@ -698,41 +780,30 @@ def get_ras_exe(ras_version):
698
780
  hecras_path = Path(ras_version)
699
781
 
700
782
  if hecras_path.is_file() and hecras_path.suffix.lower() == '.exe':
701
- logging.info(f"HEC-RAS executable found at specified path: {hecras_path}")
783
+ logger.info(f"HEC-RAS executable found at specified path: {hecras_path}")
702
784
  return str(hecras_path)
703
785
 
704
786
  if ras_version in ras_version_numbers:
705
787
  default_path = Path(f"C:/Program Files (x86)/HEC/HEC-RAS/{ras_version}/Ras.exe")
706
788
  if default_path.is_file():
707
- logging.info(f"HEC-RAS executable found at default path: {default_path}")
789
+ logger.info(f"HEC-RAS executable found at default path: {default_path}")
708
790
  return str(default_path)
709
791
  else:
710
- logging.error(f"HEC-RAS executable not found at the expected path: {default_path}")
711
- raise FileNotFoundError(f"HEC-RAS executable not found at the expected path: {default_path}")
792
+ logger.critical(f"HEC-RAS executable not found at the expected path: {default_path}")
712
793
 
713
794
  try:
714
795
  version_float = float(ras_version)
715
796
  if version_float > max(float(v) for v in ras_version_numbers):
716
797
  newer_version_path = Path(f"C:/Program Files (x86)/HEC/HEC-RAS/{ras_version}/Ras.exe")
717
798
  if newer_version_path.is_file():
718
- logging.info(f"Newer version of HEC-RAS executable found at: {newer_version_path}")
799
+ logger.info(f"Newer version of HEC-RAS executable found at: {newer_version_path}")
719
800
  return str(newer_version_path)
720
801
  else:
721
- logging.error("Newer version of HEC-RAS was specified, but the executable was not found.")
722
- raise FileNotFoundError(
723
- f"Newer version of HEC-RAS was specified. Check the version number or pass the full Ras.exe path as the function argument instead of the version number. The script looked for the executable at: {newer_version_path}"
724
- )
802
+ logger.critical("Newer version of HEC-RAS was specified, but the executable was not found.")
725
803
  except ValueError:
726
804
  pass
727
805
 
728
- logging.error(
729
- f"Invalid HEC-RAS version or path: {ras_version}. "
730
- f"Please provide a valid version number from {ras_version_numbers} "
731
- "or a full path to the HEC-RAS executable."
732
- )
733
- raise ValueError(
734
- f"Invalid HEC-RAS version or path: {ras_version}. "
735
- f"Please provide a valid version number from {ras_version_numbers} "
736
- "or a full path to the HEC-RAS executable."
737
- )
738
-
806
+ logger.error(f"Invalid HEC-RAS version or path: {ras_version}, returning default path: {default_path}")
807
+ #raise ValueError(f"Invalid HEC-RAS version or path: {ras_version}") # don't raise an error here, just return the default path
808
+ return str(default_path)
809
+