ras-commander 0.33.0__py3-none-any.whl → 0.35.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/RasUtils.py CHANGED
@@ -7,7 +7,15 @@ import logging
7
7
  import time
8
8
  from pathlib import Path
9
9
  from .RasPrj import ras
10
- from typing import Union
10
+ from typing import Union, Optional, Dict
11
+ import pandas as pd
12
+ import numpy as np
13
+
14
+ # Configure logging
15
+ logging.basicConfig(
16
+ level=logging.INFO,
17
+ format='%(asctime)s - %(levelname)s - %(message)s'
18
+ )
11
19
 
12
20
  class RasUtils:
13
21
  """
@@ -37,8 +45,12 @@ class RasUtils:
37
45
 
38
46
  original_path = Path(file_path)
39
47
  backup_path = original_path.with_name(f"{original_path.stem}{backup_suffix}{original_path.suffix}")
40
- shutil.copy2(original_path, backup_path)
41
- logging.info(f"Backup created: {backup_path}")
48
+ try:
49
+ shutil.copy2(original_path, backup_path)
50
+ logging.info(f"Backup created: {backup_path}")
51
+ except Exception as e:
52
+ logging.error(f"Failed to create backup for {original_path}: {e}")
53
+ raise
42
54
  return backup_path
43
55
 
44
56
  @staticmethod
@@ -62,12 +74,21 @@ class RasUtils:
62
74
  ras_obj.check_initialized()
63
75
 
64
76
  backup_path = Path(backup_path)
65
- original_path = backup_path.with_name(backup_path.stem.rsplit('_backup', 1)[0] + backup_path.suffix)
66
- shutil.copy2(backup_path, original_path)
67
- logging.info(f"File restored: {original_path}")
68
- if remove_backup:
69
- backup_path.unlink()
70
- logging.info(f"Backup removed: {backup_path}")
77
+ if '_backup' not in backup_path.stem:
78
+ logging.error(f"Backup suffix '_backup' not found in {backup_path.name}")
79
+ raise ValueError(f"Backup suffix '_backup' not found in {backup_path.name}")
80
+
81
+ original_stem = backup_path.stem.rsplit('_backup', 1)[0]
82
+ original_path = backup_path.with_name(f"{original_stem}{backup_path.suffix}")
83
+ try:
84
+ shutil.copy2(backup_path, original_path)
85
+ logging.info(f"File restored: {original_path}")
86
+ if remove_backup:
87
+ backup_path.unlink()
88
+ logging.info(f"Backup removed: {backup_path}")
89
+ except Exception as e:
90
+ logging.error(f"Failed to restore from backup {backup_path}: {e}")
91
+ raise
71
92
  return original_path
72
93
 
73
94
  @staticmethod
@@ -90,8 +111,12 @@ class RasUtils:
90
111
  ras_obj.check_initialized()
91
112
 
92
113
  path = Path(directory_path)
93
- path.mkdir(parents=True, exist_ok=True)
94
- logging.info(f"Directory ensured: {path}")
114
+ try:
115
+ path.mkdir(parents=True, exist_ok=True)
116
+ logging.info(f"Directory ensured: {path}")
117
+ except Exception as e:
118
+ logging.error(f"Failed to create directory {path}: {e}")
119
+ raise
95
120
  return path
96
121
 
97
122
  @staticmethod
@@ -113,11 +138,17 @@ class RasUtils:
113
138
  ras_obj = ras_object or ras
114
139
  ras_obj.check_initialized()
115
140
 
116
- files = list(ras_obj.project_folder.glob(f"*{extension}"))
117
- return [str(file) for file in files]
141
+ try:
142
+ files = list(ras_obj.project_folder.glob(f"*{extension}"))
143
+ file_list = [str(file) for file in files]
144
+ logging.info(f"Found {len(file_list)} files with extension '{extension}' in {ras_obj.project_folder}")
145
+ return file_list
146
+ except Exception as e:
147
+ logging.error(f"Failed to find files with extension '{extension}': {e}")
148
+ raise
118
149
 
119
150
  @staticmethod
120
- def get_file_size(file_path: Path, ras_object=None) -> int:
151
+ def get_file_size(file_path: Path, ras_object=None) -> Optional[int]:
121
152
  """
122
153
  Get the size of a file in bytes.
123
154
 
@@ -126,7 +157,7 @@ class RasUtils:
126
157
  ras_object (RasPrj, optional): RAS object to use. If None, uses the default ras object.
127
158
 
128
159
  Returns:
129
- int: Size of the file in bytes
160
+ Optional[int]: Size of the file in bytes, or None if the file does not exist
130
161
 
131
162
  Example:
132
163
  >>> size = RasUtils.get_file_size(Path("project.prj"))
@@ -137,13 +168,19 @@ class RasUtils:
137
168
 
138
169
  path = Path(file_path)
139
170
  if path.exists():
140
- return path.stat().st_size
171
+ try:
172
+ size = path.stat().st_size
173
+ logging.info(f"Size of {path}: {size} bytes")
174
+ return size
175
+ except Exception as e:
176
+ logging.error(f"Failed to get size for {path}: {e}")
177
+ raise
141
178
  else:
142
179
  logging.warning(f"File not found: {path}")
143
180
  return None
144
181
 
145
182
  @staticmethod
146
- def get_file_modification_time(file_path: Path, ras_object=None) -> float:
183
+ def get_file_modification_time(file_path: Path, ras_object=None) -> Optional[float]:
147
184
  """
148
185
  Get the last modification time of a file.
149
186
 
@@ -152,7 +189,7 @@ class RasUtils:
152
189
  ras_object (RasPrj, optional): RAS object to use. If None, uses the default ras object.
153
190
 
154
191
  Returns:
155
- float: Last modification time as a timestamp
192
+ Optional[float]: Last modification time as a timestamp, or None if the file does not exist
156
193
 
157
194
  Example:
158
195
  >>> mtime = RasUtils.get_file_modification_time(Path("project.prj"))
@@ -163,7 +200,13 @@ class RasUtils:
163
200
 
164
201
  path = Path(file_path)
165
202
  if path.exists():
166
- return path.stat().st_mtime
203
+ try:
204
+ mtime = path.stat().st_mtime
205
+ logging.info(f"Last modification time of {path}: {mtime}")
206
+ return mtime
207
+ except Exception as e:
208
+ logging.error(f"Failed to get modification time for {path}: {e}")
209
+ raise
167
210
  else:
168
211
  logging.warning(f"File not found: {path}")
169
212
  return None
@@ -191,18 +234,29 @@ class RasUtils:
191
234
 
192
235
  plan_path = Path(current_plan_number_or_path)
193
236
  if plan_path.is_file():
237
+ logging.info(f"Using provided plan file path: {plan_path}")
194
238
  return plan_path
195
239
 
196
240
  try:
197
241
  current_plan_number = f"{int(current_plan_number_or_path):02d}" # Ensure two-digit format
242
+ logging.info(f"Converted plan number to two-digit format: {current_plan_number}")
198
243
  except ValueError:
244
+ logging.error(f"Invalid plan number: {current_plan_number_or_path}. Expected a number from 1 to 99.")
199
245
  raise ValueError(f"Invalid plan number: {current_plan_number_or_path}. Expected a number from 1 to 99.")
200
246
 
201
247
  plan_name = f"{ras_obj.project_name}.p{current_plan_number}"
202
- return ras_obj.project_folder / plan_name
248
+ full_plan_path = ras_obj.project_folder / plan_name
249
+ logging.info(f"Constructed plan file path: {full_plan_path}")
250
+ return full_plan_path
203
251
 
204
252
  @staticmethod
205
- def remove_with_retry(path: Path, max_attempts: int = 5, initial_delay: float = 1.0, is_folder: bool = True, ras_object=None) -> bool:
253
+ def remove_with_retry(
254
+ path: Path,
255
+ max_attempts: int = 5,
256
+ initial_delay: float = 1.0,
257
+ is_folder: bool = True,
258
+ ras_object=None
259
+ ) -> bool:
206
260
  """
207
261
  Attempts to remove a file or folder with retry logic and exponential backoff.
208
262
 
@@ -224,26 +278,43 @@ class RasUtils:
224
278
  ras_obj.check_initialized()
225
279
 
226
280
  path = Path(path)
227
- for attempt in range(max_attempts):
281
+ for attempt in range(1, max_attempts + 1):
228
282
  try:
229
283
  if path.exists():
230
284
  if is_folder:
231
285
  shutil.rmtree(path)
286
+ logging.info(f"Folder removed: {path}")
232
287
  else:
233
288
  path.unlink()
289
+ logging.info(f"File removed: {path}")
290
+ else:
291
+ logging.info(f"Path does not exist, nothing to remove: {path}")
234
292
  return True
235
- except PermissionError:
236
- if attempt < max_attempts - 1:
237
- delay = initial_delay * (2 ** attempt) # Exponential backoff
238
- logging.warning(f"Failed to remove {path}. Retrying in {delay} seconds...")
293
+ except PermissionError as pe:
294
+ if attempt < max_attempts:
295
+ delay = initial_delay * (2 ** (attempt - 1)) # Exponential backoff
296
+ logging.warning(
297
+ f"PermissionError on attempt {attempt} to remove {path}: {pe}. "
298
+ f"Retrying in {delay} seconds..."
299
+ )
239
300
  time.sleep(delay)
240
301
  else:
241
- logging.error(f"Failed to remove {path} after {max_attempts} attempts. Skipping.")
302
+ logging.error(
303
+ f"Failed to remove {path} after {max_attempts} attempts due to PermissionError: {pe}. Skipping."
304
+ )
242
305
  return False
306
+ except Exception as e:
307
+ logging.error(f"Failed to remove {path} on attempt {attempt}: {e}")
308
+ return False
243
309
  return False
244
310
 
245
311
  @staticmethod
246
- def update_plan_file(plan_number_or_path: Union[str, Path], file_type: str, entry_number: int, ras_object=None) -> None:
312
+ def update_plan_file(
313
+ plan_number_or_path: Union[str, Path],
314
+ file_type: str,
315
+ entry_number: int,
316
+ ras_object=None
317
+ ) -> None:
247
318
  """
248
319
  Update a plan file with a new file reference.
249
320
 
@@ -266,45 +337,275 @@ class RasUtils:
266
337
 
267
338
  valid_file_types = {'Geom': 'g', 'Flow': 'f', 'Unsteady': 'u'}
268
339
  if file_type not in valid_file_types:
269
- raise ValueError(f"Invalid file_type. Expected one of: {', '.join(valid_file_types.keys())}")
340
+ logging.error(
341
+ f"Invalid file_type '{file_type}'. Expected one of: {', '.join(valid_file_types.keys())}"
342
+ )
343
+ raise ValueError(
344
+ f"Invalid file_type. Expected one of: {', '.join(valid_file_types.keys())}"
345
+ )
270
346
 
271
347
  plan_file_path = Path(plan_number_or_path)
272
348
  if not plan_file_path.is_file():
273
349
  plan_file_path = RasUtils.get_plan_path(plan_number_or_path, ras_object)
350
+ if not plan_file_path.exists():
351
+ logging.error(f"Plan file not found: {plan_file_path}")
352
+ raise FileNotFoundError(f"Plan file not found: {plan_file_path}")
274
353
 
275
- if not plan_file_path.exists():
276
- raise FileNotFoundError(f"Plan file not found: {plan_file_path}")
277
-
278
354
  file_prefix = valid_file_types[file_type]
279
355
  search_pattern = f"{file_type} File="
280
- entry_number = f"{int(entry_number):02d}" # Ensure two-digit format
281
-
282
- RasUtils.check_file_access(plan_file_path, 'r')
283
- with open(plan_file_path, 'r') as file:
284
- lines = file.readlines()
356
+ formatted_entry_number = f"{int(entry_number):02d}" # Ensure two-digit format
285
357
 
358
+ try:
359
+ RasUtils.check_file_access(plan_file_path, 'r')
360
+ with plan_file_path.open('r') as file:
361
+ lines = file.readlines()
362
+ except Exception as e:
363
+ logging.error(f"Failed to read plan file {plan_file_path}: {e}")
364
+ raise
365
+
366
+ updated = False
286
367
  for i, line in enumerate(lines):
287
368
  if line.startswith(search_pattern):
288
- lines[i] = f"{search_pattern}{file_prefix}{entry_number}\n"
289
- logging.info(f"Updated {file_type} File in {plan_file_path} to {file_prefix}{entry_number}")
369
+ lines[i] = f"{search_pattern}{file_prefix}{formatted_entry_number}\n"
370
+ logging.info(
371
+ f"Updated {file_type} File in {plan_file_path} to {file_prefix}{formatted_entry_number}"
372
+ )
373
+ updated = True
290
374
  break
291
375
 
292
- with plan_file_path.open('w') as file:
293
- file.writelines(lines)
376
+ if not updated:
377
+ logging.warning(
378
+ f"Search pattern '{search_pattern}' not found in {plan_file_path}. No update performed."
379
+ )
294
380
 
295
- logging.info(f"Successfully updated plan file: {plan_file_path}")
296
- ras_obj.plan_df = ras_obj.get_plan_entries()
297
- ras_obj.geom_df = ras_obj.get_geom_entries()
298
- ras_obj.flow_df = ras_obj.get_flow_entries()
299
- ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
381
+ try:
382
+ with plan_file_path.open('w') as file:
383
+ file.writelines(lines)
384
+ logging.info(f"Successfully updated plan file: {plan_file_path}")
385
+ except Exception as e:
386
+ logging.error(f"Failed to write updates to plan file {plan_file_path}: {e}")
387
+ raise
388
+
389
+ # Refresh RasPrj dataframes
390
+ try:
391
+ ras_obj.plan_df = ras_obj.get_plan_entries()
392
+ ras_obj.geom_df = ras_obj.get_geom_entries()
393
+ ras_obj.flow_df = ras_obj.get_flow_entries()
394
+ ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
395
+ logging.info("RAS object dataframes have been refreshed.")
396
+ except Exception as e:
397
+ logging.error(f"Failed to refresh RasPrj dataframes: {e}")
398
+ raise
300
399
 
301
400
  @staticmethod
302
- def check_file_access(file_path, mode='r'):
401
+ def check_file_access(file_path: Path, mode: str = 'r') -> None:
402
+ """
403
+ Check if the file can be accessed with the specified mode.
404
+
405
+ Parameters:
406
+ file_path (Path): Path to the file
407
+ mode (str): Mode to check ('r' for read, 'w' for write, etc.)
408
+
409
+ Raises:
410
+ FileNotFoundError: If the file does not exist
411
+ PermissionError: If the required permissions are not met
412
+ """
303
413
  path = Path(file_path)
304
414
  if not path.exists():
415
+ logging.error(f"File not found: {file_path}")
305
416
  raise FileNotFoundError(f"File not found: {file_path}")
306
- if mode in ('r', 'rb') and not os.access(path, os.R_OK):
307
- raise PermissionError(f"Read permission denied for file: {file_path}")
308
- if mode in ('w', 'wb', 'a', 'ab') and not os.access(path.parent, os.W_OK):
309
- raise PermissionError(f"Write permission denied for directory: {path.parent}")
417
+
418
+ if mode in ('r', 'rb'):
419
+ if not os.access(path, os.R_OK):
420
+ logging.error(f"Read permission denied for file: {file_path}")
421
+ raise PermissionError(f"Read permission denied for file: {file_path}")
422
+ else:
423
+ logging.debug(f"Read access granted for file: {file_path}")
424
+
425
+ if mode in ('w', 'wb', 'a', 'ab'):
426
+ parent_dir = path.parent
427
+ if not os.access(parent_dir, os.W_OK):
428
+ logging.error(f"Write permission denied for directory: {parent_dir}")
429
+ raise PermissionError(f"Write permission denied for directory: {parent_dir}")
430
+ else:
431
+ logging.debug(f"Write access granted for directory: {parent_dir}")
432
+
433
+
434
+
435
+
436
+ # -------------------------- Functions below were imported from funkshuns.py --------------------------
437
+ # -------------------------- Converted to ras-commander style guide ----------------------------------
438
+
439
+
440
+
441
+
442
+ @staticmethod
443
+ def convert_to_dataframe(data_source: Union[pd.DataFrame, Path], **kwargs) -> pd.DataFrame:
444
+ """
445
+ Converts input to a pandas DataFrame. Supports existing DataFrames or file paths (CSV, Excel, TSV, Parquet).
446
+
447
+ Args:
448
+ data_source (Union[pd.DataFrame, Path]): The input to convert to a DataFrame. Can be a file path or an existing DataFrame.
449
+ **kwargs: Additional keyword arguments to pass to pandas read functions.
450
+
451
+ Returns:
452
+ pd.DataFrame: The resulting DataFrame.
453
+
454
+ Raises:
455
+ NotImplementedError: If the file type is unsupported or input type is invalid.
456
+
457
+ Example:
458
+ >>> df = RasUtils.convert_to_dataframe(Path("data.csv"))
459
+ >>> print(type(df))
460
+ <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
+ """
464
+ if isinstance(data_source, pd.DataFrame):
465
+ return data_source.copy()
466
+ elif isinstance(data_source, Path):
467
+ ext = data_source.suffix.replace('.', '', 1)
468
+ if ext == 'csv':
469
+ return pd.read_csv(data_source, **kwargs)
470
+ elif ext.startswith('x'):
471
+ return pd.read_excel(data_source, **kwargs)
472
+ elif ext == "tsv":
473
+ return pd.read_csv(data_source, sep="\t", **kwargs)
474
+ elif ext in ["parquet", "pq", "parq"]:
475
+ return pd.read_parquet(data_source, **kwargs)
476
+ else:
477
+ raise NotImplementedError(f"Unsupported file type {ext}. Should be one of csv, tsv, parquet, or xlsx.")
478
+ else:
479
+ raise NotImplementedError(f"Unsupported type {type(data_source)}. Only file path / existing DataFrame supported at this time")
480
+
481
+ @staticmethod
482
+ def save_to_excel(dataframe: pd.DataFrame, excel_path: Path, **kwargs) -> None:
483
+ """
484
+ Saves a pandas DataFrame to an Excel file with retry functionality.
485
+
486
+ Args:
487
+ dataframe (pd.DataFrame): The DataFrame to save.
488
+ excel_path (Path): The path to the Excel file where the DataFrame will be saved.
489
+ **kwargs: Additional keyword arguments passed to `DataFrame.to_excel()`.
490
+
491
+ Raises:
492
+ IOError: If the file cannot be saved after multiple attempts.
493
+
494
+ Example:
495
+ >>> df = pd.DataFrame({'A': [1, 2, 3], 'B': [4, 5, 6]})
496
+ >>> 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
+ """
500
+ saved = False
501
+ max_attempts = 3
502
+ attempt = 0
503
+
504
+ while not saved and attempt < max_attempts:
505
+ try:
506
+ dataframe.to_excel(excel_path, **kwargs)
507
+ print(f'DataFrame successfully saved to \n{excel_path}')
508
+ saved = True
509
+ except IOError as e:
510
+ attempt += 1
511
+ if attempt < max_attempts:
512
+ print(f"Error saving file. Attempt {attempt} of {max_attempts}. Please close the Excel document if it's open.")
513
+ else:
514
+ raise IOError(f"Failed to save {excel_path} after {max_attempts} attempts. Last error: {str(e)}")
515
+
516
+
517
+
518
+
519
+
520
+
521
+
522
+
523
+
524
+
525
+ ##### Statistical Metrics #####
526
+
527
+
528
+ @staticmethod
529
+ def calculate_rmse(observed_values: np.ndarray, predicted_values: np.ndarray, normalized: bool = True) -> float:
530
+ """
531
+ Calculate the Root Mean Squared Error (RMSE) between observed and predicted values.
532
+
533
+ Args:
534
+ observed_values (np.ndarray): Actual observations time series.
535
+ predicted_values (np.ndarray): Estimated/predicted time series.
536
+ normalized (bool, optional): Whether to normalize RMSE to a percentage of observed_values. Defaults to True.
537
+
538
+ Returns:
539
+ float: The calculated RMSE value.
540
+
541
+ Example:
542
+ >>> observed = np.array([1, 2, 3])
543
+ >>> predicted = np.array([1.1, 2.2, 2.9])
544
+ >>> RasUtils.calculate_rmse(observed, predicted)
545
+ 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
+ """
549
+ rmse = np.sqrt(np.mean((predicted_values - observed_values) ** 2))
550
+
551
+ if normalized:
552
+ rmse = rmse / np.abs(np.mean(observed_values))
553
+
554
+ return rmse
555
+
556
+ @staticmethod
557
+ def calculate_percent_bias(observed_values: np.ndarray, predicted_values: np.ndarray, as_percentage: bool = False) -> float:
558
+ """
559
+ Calculate the Percent Bias between observed and predicted values.
560
+
561
+ Args:
562
+ observed_values (np.ndarray): Actual observations time series.
563
+ predicted_values (np.ndarray): Estimated/predicted time series.
564
+ as_percentage (bool, optional): If True, return bias as a percentage. Defaults to False.
565
+
566
+ Returns:
567
+ float: The calculated Percent Bias.
568
+
569
+ Example:
570
+ >>> observed = np.array([1, 2, 3])
571
+ >>> predicted = np.array([1.1, 2.2, 2.9])
572
+ >>> RasUtils.calculate_percent_bias(observed, predicted, as_percentage=True)
573
+ 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
+ """
577
+ multiplier = 100 if as_percentage else 1
578
+
579
+ percent_bias = multiplier * (np.mean(predicted_values) - np.mean(observed_values)) / np.mean(observed_values)
580
+
581
+ return percent_bias
582
+
583
+ @staticmethod
584
+ def calculate_error_metrics(observed_values: np.ndarray, predicted_values: np.ndarray) -> Dict[str, float]:
585
+ """
586
+ Compute a trio of error metrics: correlation, RMSE, and Percent Bias.
587
+
588
+ Args:
589
+ observed_values (np.ndarray): Actual observations time series.
590
+ predicted_values (np.ndarray): Estimated/predicted time series.
591
+
592
+ Returns:
593
+ Dict[str, float]: A dictionary containing correlation ('cor'), RMSE ('rmse'), and Percent Bias ('pb').
594
+
595
+ Example:
596
+ >>> observed = np.array([1, 2, 3])
597
+ >>> predicted = np.array([1.1, 2.2, 2.9])
598
+ >>> RasUtils.calculate_error_metrics(observed, predicted)
599
+ {'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
+ """
603
+ correlation = np.corrcoef(observed_values, predicted_values)[0, 1]
604
+ rmse = RasUtils.calculate_rmse(observed_values, predicted_values)
605
+ percent_bias = RasUtils.calculate_percent_bias(observed_values, predicted_values)
606
+
607
+ return {'cor': correlation, 'rmse': rmse, 'pb': percent_bias}
608
+
609
+
610
+
310
611
 
ras_commander/__init__.py CHANGED
@@ -15,6 +15,7 @@ from .RasUnsteady import RasUnsteady
15
15
  from .RasCmdr import RasCmdr
16
16
  from .RasUtils import RasUtils
17
17
  from .RasExamples import RasExamples
18
+ from .RasHdf import RasHdf # Add this line
18
19
 
19
20
  # Import all attributes from these modules
20
21
  from .RasPrj import *
@@ -24,6 +25,7 @@ from .RasUnsteady import *
24
25
  from .RasCmdr import *
25
26
  from .RasUtils import *
26
27
  from .RasExamples import *
28
+ from .RasHdf import * # Add this line
27
29
 
28
30
  # Define __all__ to specify what should be imported when using "from ras_commander import *"
29
31
  __all__ = [
@@ -36,5 +38,6 @@ __all__ = [
36
38
  "RasUnsteady",
37
39
  "RasCmdr",
38
40
  "RasUtils",
39
- "RasExamples"
41
+ "RasExamples",
42
+ "RasHdf" # Add this line
40
43
  ]