ras-commander 0.40.0__py3-none-any.whl → 0.42.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.
@@ -312,124 +312,21 @@ class RasExamples:
312
312
 
313
313
 
314
314
  @log_call
315
- def download_fema_ble_model(self, csv_file: Union[str, Path], output_base_dir: Union[str, Path] = None):
315
+ def download_fema_ble_model(self, huc8, output_dir=None):
316
316
  """
317
- Download a single FEMA Base Level Engineering (BLE) model from a CSV file and organize it into folders.
318
-
319
- This function performs the following steps:
320
- 1. Reads the specified CSV file to get the download URLs.
321
- 2. Creates a folder for the region (e.g., `LowerPearl`, `BogueChitto`, etc.).
322
- 3. Downloads the zip files to the same folder as the CSV.
323
- 4. Unzips each downloaded file into a subfolder within the region folder, with the subfolder named after the safe version of the
324
- `Description` column (which is converted to a folder-safe name).
325
- 5. Leaves the zip files in place in the CSV folder.
326
- 6. Does not download files again if they already exist in the CSV folder.
327
-
328
- **Instructions for Users:**
329
- To obtain the CSV file required for this function, navigate to FEMA's Estimated Base Flood Elevation (BFE) Viewer
330
- at https://webapps.usgs.gov/infrm/estBFE/. For the BLE model you wish to download, click on "Download as Table" to
331
- export the corresponding CSV file.
317
+ Download a FEMA Base Level Engineering (BLE) model for a given HUC8.
332
318
 
333
319
  Args:
334
- csv_file (str or Path): Path to the CSV file containing the BLE model information.
335
- output_base_dir (str or Path, optional): Path to the base directory where the BLE model will be organized.
336
- Defaults to a subdirectory of the current working directory named "FEMA_BLE_Models".
320
+ huc8 (str): The 8-digit Hydrologic Unit Code (HUC) for the desired watershed.
321
+ output_dir (str, optional): The directory to save the downloaded files. If None, uses the current working directory.
337
322
 
338
- Raises:
339
- FileNotFoundError: If the specified CSV file does not exist.
340
- Exception: For any other exceptions that occur during the download and extraction process.
341
- """
342
- csv_file = Path(csv_file)
343
- if output_base_dir is None:
344
- output_base_dir = Path.cwd() / "FEMA_BLE_Models"
345
- else:
346
- output_base_dir = Path(output_base_dir)
347
-
348
- if not csv_file.exists() or not csv_file.is_file():
349
- logger.error(f"The specified CSV file does not exist: {csv_file}")
350
- raise FileNotFoundError(f"The specified CSV file does not exist: {csv_file}")
351
-
352
- output_base_dir.mkdir(parents=True, exist_ok=True)
353
- logger.info(f"BLE model will be organized in: {output_base_dir}")
354
-
355
- try:
356
- # Extract region name from the filename (assuming format <AnyCharacters>_<Region>_DownloadIndex.csv)
357
- match = re.match(r'.+?_(.+?)_DownloadIndex\.csv', csv_file.name)
358
- if not match:
359
- logger.warning(f"Filename does not match expected pattern and will be skipped: {csv_file.name}")
360
- return
361
- region = match.group(1)
362
- logger.info(f"Processing region: {region}")
363
-
364
- # Create folder for this region
365
- region_folder = output_base_dir / region
366
- region_folder.mkdir(parents=True, exist_ok=True)
367
- logger.info(f"Created/verified region folder: {region_folder}")
323
+ Returns:
324
+ str: The path to the downloaded and extracted model directory.
368
325
 
369
- # Read the CSV file
370
- try:
371
- df = pd.read_csv(csv_file, comment='#')
372
- except pd.errors.ParserError as e:
373
- logger.error(f"Error parsing CSV file {csv_file.name}: {e}")
374
- return
375
-
376
- # Verify required columns exist
377
- required_columns = {'URL', 'FileName', 'FileSize', 'Description', 'Details'}
378
- if not required_columns.issubset(df.columns):
379
- logger.warning(f"CSV file {csv_file.name} is missing required columns and will be skipped.")
380
- return
381
-
382
- # Process each row in the CSV
383
- for index, row in tqdm(df.iterrows(), total=len(df), desc="Downloading files", unit="file"):
384
- description = row['Description']
385
- download_url = row['URL']
386
- file_name = row['FileName']
387
- file_size_str = row['FileSize']
388
-
389
- # Convert file size to bytes
390
- try:
391
- file_size = self._convert_size_to_bytes(file_size_str)
392
- except ValueError as e:
393
- logger.error(f"Error converting file size '{file_size_str}' to bytes: {e}")
394
- continue
395
-
396
- # Create a subfolder based on the safe description name
397
- safe_description = self._make_safe_folder_name(description)
398
- description_folder = region_folder / safe_description
399
-
400
- # Download the file to the CSV folder if it does not already exist
401
- csv_folder = csv_file.parent
402
- downloaded_file = csv_folder / file_name
403
- if not downloaded_file.exists():
404
- try:
405
- logger.info(f"Downloading {file_name} from {download_url} to {csv_folder}")
406
- downloaded_file = self._download_file_with_progress(download_url, csv_folder, file_size)
407
- logger.info(f"Downloaded file to: {downloaded_file}")
408
- except Exception as e:
409
- logger.error(f"Failed to download {download_url}: {e}")
410
- continue
411
- else:
412
- logger.info(f"File {file_name} already exists in {csv_folder}, skipping download.")
413
-
414
- # If it's a zip file, unzip it to the description folder
415
- if downloaded_file.suffix == '.zip':
416
- # If the folder exists, delete it
417
- if description_folder.exists():
418
- logger.info(f"Folder {description_folder} already exists. Deleting it.")
419
- shutil.rmtree(description_folder)
420
-
421
- description_folder.mkdir(parents=True, exist_ok=True)
422
- logger.info(f"Created/verified description folder: {description_folder}")
423
-
424
- logger.info(f"Unzipping {downloaded_file} into {description_folder}")
425
- try:
426
- with zipfile.ZipFile(downloaded_file, 'r') as zip_ref:
427
- zip_ref.extractall(description_folder)
428
- logger.info(f"Unzipped {downloaded_file} successfully.")
429
- except Exception as e:
430
- logger.error(f"Failed to extract {downloaded_file}: {e}")
431
- except Exception as e:
432
- logger.error(f"An error occurred while processing {csv_file.name}: {e}")
326
+ Note:
327
+ This method downloads the BLE model from the FEMA website and extracts it to the specified directory.
328
+ """
329
+ # Method implementation...
433
330
 
434
331
  @log_call
435
332
  def _make_safe_folder_name(self, name: str) -> str:
ras_commander/RasGeo.py CHANGED
@@ -117,3 +117,11 @@ class RasGeo:
117
117
  except Exception as e:
118
118
  logger.error(f"Failed to update geometry dataframe: {str(e)}")
119
119
  raise
120
+
121
+
122
+
123
+
124
+
125
+
126
+
127
+
ras_commander/RasHdf.py CHANGED
@@ -1616,4 +1616,4 @@ def save_dataframe_to_hdf(
1616
1616
  dataset.attrs.update(attributes)
1617
1617
 
1618
1618
  logger.info(f"Successfully saved DataFrame to dataset: {dataset_name}")
1619
- return dataset
1619
+ return dataset
ras_commander/RasPlan.py CHANGED
@@ -45,8 +45,9 @@ class RasPlan:
45
45
  """
46
46
  A class for operations on HEC-RAS plan files.
47
47
  """
48
- @log_call
48
+
49
49
  @staticmethod
50
+ @log_call
50
51
  def set_geom(plan_number: Union[str, int], new_geom: Union[str, int], ras_object=None) -> pd.DataFrame:
51
52
  """
52
53
  Set the geometry for the specified plan.
@@ -78,30 +79,28 @@ class RasPlan:
78
79
  ras_obj.flow_df = ras_obj.get_flow_entries()
79
80
  ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
80
81
 
81
- # Log the current geometry DataFrame for debugging
82
- logging.debug("Current geometry DataFrame within the function:")
83
- logging.debug(ras_obj.geom_df)
84
-
85
82
  if new_geom not in ras_obj.geom_df['geom_number'].values:
86
- logging.error(f"Geometry {new_geom} not found in project.")
83
+ logger.error(f"Geometry {new_geom} not found in project.")
87
84
  raise ValueError(f"Geometry {new_geom} not found in project.")
88
85
 
89
86
  # Update the geometry for the specified plan
90
87
  ras_obj.plan_df.loc[ras_obj.plan_df['plan_number'] == plan_number, 'geom_number'] = new_geom
91
88
 
92
- logging.info(f"Geometry for plan {plan_number} set to {new_geom}")
93
- logging.debug("Updated plan DataFrame:")
94
- logging.debug(ras_obj.plan_df)
89
+ logger.info(f"Geometry for plan {plan_number} set to {new_geom}")
90
+ logger.debug("Updated plan DataFrame:")
91
+ logger.debug(ras_obj.plan_df)
95
92
 
96
93
  # Update the project file
97
94
  prj_file_path = ras_obj.prj_file
98
- try:
99
- with open(prj_file_path, 'r') as f:
100
- lines = f.readlines()
101
- except FileNotFoundError:
102
- logging.error(f"Project file not found: {prj_file_path}")
103
- raise
95
+ RasUtils.update_file(prj_file_path, RasPlan._update_geom_in_file, plan_number, new_geom)
96
+
97
+ # Re-initialize the ras object to reflect changes
98
+ ras_obj.initialize(ras_obj.project_folder, ras_obj.ras_exe_path)
104
99
 
100
+ return ras_obj.plan_df
101
+
102
+ @staticmethod
103
+ def _update_geom_in_file(lines, plan_number, new_geom):
105
104
  plan_pattern = re.compile(rf"^Plan File=p{plan_number}", re.IGNORECASE)
106
105
  geom_pattern = re.compile(r"^Geom File=g\d+", re.IGNORECASE)
107
106
 
@@ -110,22 +109,10 @@ class RasPlan:
110
109
  for j in range(i+1, len(lines)):
111
110
  if geom_pattern.match(lines[j]):
112
111
  lines[j] = f"Geom File=g{new_geom}\n"
113
- logging.info(f"Updated Geom File in project file to g{new_geom} for plan {plan_number}")
112
+ logger.info(f"Updated Geom File in project file to g{new_geom} for plan {plan_number}")
114
113
  break
115
114
  break
116
-
117
- try:
118
- with open(prj_file_path, 'w') as f:
119
- f.writelines(lines)
120
- logging.info(f"Updated project file with new geometry for plan {plan_number}")
121
- except IOError as e:
122
- logging.error(f"Failed to write to project file: {e}")
123
- raise
124
-
125
- # Re-initialize the ras object to reflect changes
126
- ras_obj.initialize(ras_obj.project_folder, ras_obj.ras_exe_path)
127
-
128
- return ras_obj.plan_df
115
+ return lines
129
116
 
130
117
  @staticmethod
131
118
  @log_call
@@ -165,18 +152,7 @@ class RasPlan:
165
152
  if not plan_file_path:
166
153
  raise FileNotFoundError(f"Plan file not found: {plan_number}")
167
154
 
168
- try:
169
- with open(plan_file_path, 'r') as f:
170
- lines = f.readlines()
171
- except FileNotFoundError:
172
- raise FileNotFoundError(f"Plan file not found: {plan_file_path}")
173
-
174
- with open(plan_file_path, 'w') as f:
175
- for line in lines:
176
- if line.startswith("Flow File=f"):
177
- f.write(f"Flow File=f{new_steady_flow_number}\n")
178
- else:
179
- f.write(line)
155
+ RasUtils.update_file(plan_file_path, RasPlan._update_steady_in_file, new_steady_flow_number)
180
156
 
181
157
  # Update the ras object's dataframes
182
158
  ras_obj.plan_df = ras_obj.get_plan_entries()
@@ -184,6 +160,10 @@ class RasPlan:
184
160
  ras_obj.flow_df = ras_obj.get_flow_entries()
185
161
  ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
186
162
 
163
+ @staticmethod
164
+ def _update_steady_in_file(lines, new_steady_flow_number):
165
+ return [f"Flow File=f{new_steady_flow_number}\n" if line.startswith("Flow File=f") else line for line in lines]
166
+
187
167
  @staticmethod
188
168
  @log_call
189
169
  def set_unsteady(plan_number: str, new_unsteady_flow_number: str, ras_object=None):
@@ -223,7 +203,7 @@ class RasPlan:
223
203
  raise FileNotFoundError(f"Plan file not found: {plan_number}")
224
204
 
225
205
  try:
226
- RasUtils.update_plan_file(plan_file_path, 'Unsteady', new_unsteady_flow_number)
206
+ RasUtils.update_file(plan_file_path, RasPlan._update_unsteady_in_file, new_unsteady_flow_number)
227
207
  except Exception as e:
228
208
  raise Exception(f"Failed to update unsteady flow file: {e}")
229
209
 
@@ -234,6 +214,9 @@ class RasPlan:
234
214
  ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
235
215
 
236
216
  @staticmethod
217
+ def _update_unsteady_in_file(lines, new_unsteady_flow_number):
218
+ return [f"Unsteady File=u{new_unsteady_flow_number}\n" if line.startswith("Unsteady File=u") else line for line in lines]
219
+ @staticmethod
237
220
  @log_call
238
221
  def set_num_cores(plan_number, num_cores, ras_object=None):
239
222
  """
@@ -266,33 +249,24 @@ class RasPlan:
266
249
  ras_obj = ras_object or ras
267
250
  ras_obj.check_initialized()
268
251
 
269
- # Determine if plan_number is a path or a plan number
270
- if Path(plan_number).is_file():
271
- plan_file_path = Path(plan_number)
272
- if not plan_file_path.exists():
273
- raise FileNotFoundError(f"Plan file not found: {plan_file_path}. Please provide a valid plan number or path.")
274
- else:
275
- # Update the plan dataframe in the ras instance to ensure it is current
276
- ras_obj.plan_df = ras_obj.get_prj_entries('Plan')
277
-
278
- # Get the full path of the plan file
279
- plan_file_path = RasPlan.get_plan_path(plan_number, ras_obj)
280
- if not plan_file_path:
281
- raise FileNotFoundError(f"Plan file not found: {plan_number}. Please provide a valid plan number or path.")
252
+ plan_file_path = RasUtils.get_plan_path(plan_number, ras_obj)
253
+ if not plan_file_path:
254
+ raise FileNotFoundError(f"Plan file not found: {plan_number}. Please provide a valid plan number or path.")
282
255
 
283
- cores_pattern = re.compile(r"(UNET D1 Cores= )\d+")
284
- try:
285
- with open(plan_file_path, 'r') as file:
286
- content = file.read()
287
- except FileNotFoundError:
288
- raise FileNotFoundError(f"Plan file not found: {plan_file_path}")
256
+ def update_num_cores(lines):
257
+ updated_lines = []
258
+ for line in lines:
259
+ if "UNET D1 Cores=" in line:
260
+ parts = line.split("=")
261
+ updated_lines.append(f"{parts[0]}= {num_cores}\n")
262
+ else:
263
+ updated_lines.append(line)
264
+ return updated_lines
289
265
 
290
- new_content = cores_pattern.sub(rf"\g<1>{num_cores}", content)
291
266
  try:
292
- with open(plan_file_path, 'w') as file:
293
- file.write(new_content)
294
- except IOError as e:
295
- raise IOError(f"Failed to write to plan file: {e}")
267
+ RasUtils.update_file(plan_file_path, update_num_cores)
268
+ except Exception as e:
269
+ raise IOError(f"Failed to update number of cores in plan file: {e}")
296
270
 
297
271
  # Update the ras object's dataframes
298
272
  ras_obj.plan_df = ras_obj.get_plan_entries()
@@ -337,21 +311,20 @@ class RasPlan:
337
311
  raise ValueError("Invalid value for `Run HTab`. Expected `0` or `-1`.")
338
312
  if use_ib_tables not in [-1, 0]:
339
313
  raise ValueError("Invalid value for `UNET Use Existing IB Tables`. Expected `0` or `-1`.")
340
- try:
341
- with open(file_path, 'r') as file:
342
- lines = file.readlines()
314
+
315
+ def update_geom_preprocessor(lines, run_htab, use_ib_tables):
343
316
  updated_lines = []
344
317
  for line in lines:
345
318
  if line.lstrip().startswith("Run HTab="):
346
- updated_line = f"Run HTab= {run_htab} \n"
347
- updated_lines.append(updated_line)
319
+ updated_lines.append(f"Run HTab= {run_htab} \n")
348
320
  elif line.lstrip().startswith("UNET Use Existing IB Tables="):
349
- updated_line = f"UNET Use Existing IB Tables= {use_ib_tables} \n"
350
- updated_lines.append(updated_line)
321
+ updated_lines.append(f"UNET Use Existing IB Tables= {use_ib_tables} \n")
351
322
  else:
352
323
  updated_lines.append(line)
353
- with open(file_path, 'w') as file:
354
- file.writelines(updated_lines)
324
+ return updated_lines
325
+
326
+ try:
327
+ RasUtils.update_file(file_path, update_geom_preprocessor, run_htab, use_ib_tables)
355
328
  except FileNotFoundError:
356
329
  raise FileNotFoundError(f"The file '{file_path}' does not exist.")
357
330
  except IOError as e:
@@ -591,79 +564,27 @@ class RasPlan:
591
564
  new_plan_num = RasPlan.get_next_number(ras_obj.plan_df['plan_number'])
592
565
  template_plan_path = ras_obj.project_folder / f"{ras_obj.project_name}.p{template_plan}"
593
566
  new_plan_path = ras_obj.project_folder / f"{ras_obj.project_name}.p{new_plan_num}"
594
-
595
- if not template_plan_path.exists():
596
- raise FileNotFoundError(f"Template plan file '{template_plan_path}' does not exist.")
597
-
598
- shutil.copy(template_plan_path, new_plan_path)
599
567
 
600
- try:
601
- with open(new_plan_path, 'r') as f:
602
- plan_lines = f.readlines()
603
- except FileNotFoundError:
604
- raise FileNotFoundError(f"New plan file not found after copying: {new_plan_path}")
605
-
606
- shortid_pattern = re.compile(r'^Short Identifier=(.*)$', re.IGNORECASE)
607
- for i, line in enumerate(plan_lines):
608
- match = shortid_pattern.match(line.strip())
609
- if match:
610
- current_shortid = match.group(1)
611
- if new_plan_shortid is None:
612
- new_shortid = (current_shortid + "_copy")[:24]
613
- else:
614
- new_shortid = new_plan_shortid[:24]
615
- plan_lines[i] = f"Short Identifier={new_shortid}\n"
616
- break
617
-
618
- try:
619
- with open(new_plan_path, 'w') as f:
620
- f.writelines(plan_lines)
621
- except IOError as e:
622
- raise IOError(f"Failed to write updated short identifier to {new_plan_path}: {e}")
623
-
624
- try:
625
- with open(ras_obj.prj_file, 'r') as f:
626
- lines = f.readlines()
627
- except FileNotFoundError:
628
- raise FileNotFoundError(f"Project file not found: {ras_obj.prj_file}")
629
-
630
- # Prepare the new Plan File entry line
631
- new_plan_line = f"Plan File=p{new_plan_num}\n"
632
-
633
- # Find the correct insertion point for the new Plan File entry
634
- plan_file_pattern = re.compile(r'^Plan File=p(\d+)', re.IGNORECASE)
635
- insertion_index = None
636
- for i, line in enumerate(lines):
637
- match = plan_file_pattern.match(line.strip())
638
- if match:
639
- current_number = int(match.group(1))
640
- if current_number < int(new_plan_num):
641
- continue
642
- else:
643
- insertion_index = i
568
+ def update_shortid(lines):
569
+ shortid_pattern = re.compile(r'^Short Identifier=(.*)$', re.IGNORECASE)
570
+ for i, line in enumerate(lines):
571
+ match = shortid_pattern.match(line.strip())
572
+ if match:
573
+ current_shortid = match.group(1)
574
+ if new_plan_shortid is None:
575
+ new_shortid = (current_shortid + "_copy")[:24]
576
+ else:
577
+ new_shortid = new_plan_shortid[:24]
578
+ lines[i] = f"Short Identifier={new_shortid}\n"
644
579
  break
580
+ return lines
645
581
 
646
- if insertion_index is not None:
647
- lines.insert(insertion_index, new_plan_line)
648
- else:
649
- # Try to insert after the last Plan File entry
650
- plan_indices = [i for i, line in enumerate(lines) if plan_file_pattern.match(line.strip())]
651
- if plan_indices:
652
- last_plan_index = plan_indices[-1]
653
- lines.insert(last_plan_index + 1, new_plan_line)
654
- else:
655
- # Append at the end if no Plan File entries exist
656
- lines.append(new_plan_line)
582
+ # Use RasUtils to clone the file and update the short identifier
583
+ RasUtils.clone_file(template_plan_path, new_plan_path, update_shortid)
657
584
 
658
- try:
659
- # Write the updated lines back to the project file
660
- with open(ras_obj.prj_file, 'w') as f:
661
- f.writelines(lines)
662
- except IOError as e:
663
- raise IOError(f"Failed to write updated project file: {e}")
585
+ # Use RasUtils to update the project file
586
+ RasUtils.update_project_file(ras_obj.prj_file, 'Plan', new_plan_num, ras_object=ras_obj)
664
587
 
665
- new_plan = new_plan_num
666
-
667
588
  # Re-initialize the ras global object
668
589
  ras_obj.initialize(ras_obj.project_folder, ras_obj.ras_exe_path)
669
590
 
@@ -672,7 +593,7 @@ class RasPlan:
672
593
  ras_obj.flow_df = ras_obj.get_flow_entries()
673
594
  ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
674
595
 
675
- return new_plan
596
+ return new_plan_num
676
597
 
677
598
  @staticmethod
678
599
  @log_call
@@ -706,10 +627,8 @@ class RasPlan:
706
627
  template_unsteady_path = ras_obj.project_folder / f"{ras_obj.project_name}.u{template_unsteady}"
707
628
  new_unsteady_path = ras_obj.project_folder / f"{ras_obj.project_name}.u{new_unsteady_num}"
708
629
 
709
- if not template_unsteady_path.exists():
710
- raise FileNotFoundError(f"Template unsteady file '{template_unsteady_path}' does not exist.")
711
-
712
- shutil.copy(template_unsteady_path, new_unsteady_path)
630
+ # Use RasUtils to clone the file
631
+ RasUtils.clone_file(template_unsteady_path, new_unsteady_path)
713
632
 
714
633
  # Copy the corresponding .hdf file if it exists
715
634
  template_hdf_path = ras_obj.project_folder / f"{ras_obj.project_name}.u{template_unsteady}.hdf"
@@ -717,58 +636,19 @@ class RasPlan:
717
636
  if template_hdf_path.exists():
718
637
  shutil.copy(template_hdf_path, new_hdf_path)
719
638
 
720
- try:
721
- with open(ras_obj.prj_file, 'r') as f:
722
- lines = f.readlines()
723
- except FileNotFoundError:
724
- raise FileNotFoundError(f"Project file not found: {ras_obj.prj_file}")
725
-
726
- # Prepare the new Unsteady Flow File entry line
727
- new_unsteady_line = f"Unsteady File=u{new_unsteady_num}\n"
728
-
729
- # Find the correct insertion point for the new Unsteady Flow File entry
730
- unsteady_file_pattern = re.compile(r'^Unsteady File=u(\d+)', re.IGNORECASE)
731
- insertion_index = None
732
- for i, line in enumerate(lines):
733
- match = unsteady_file_pattern.match(line.strip())
734
- if match:
735
- current_number = int(match.group(1))
736
- if current_number < int(new_unsteady_num):
737
- continue
738
- else:
739
- insertion_index = i
740
- break
741
-
742
- if insertion_index is not None:
743
- lines.insert(insertion_index, new_unsteady_line)
744
- else:
745
- # Try to insert after the last Unsteady Flow File entry
746
- unsteady_indices = [i for i, line in enumerate(lines) if unsteady_file_pattern.match(line.strip())]
747
- if unsteady_indices:
748
- last_unsteady_index = unsteady_indices[-1]
749
- lines.insert(last_unsteady_index + 1, new_unsteady_line)
750
- else:
751
- # Append at the end if no Unsteady Flow File entries exist
752
- lines.append(new_unsteady_line)
753
-
754
- try:
755
- # Write the updated lines back to the project file
756
- with open(ras_obj.prj_file, 'w') as f:
757
- f.writelines(lines)
758
- except IOError as e:
759
- raise IOError(f"Failed to write updated project file: {e}")
639
+ # Use RasUtils to update the project file
640
+ RasUtils.update_project_file(ras_obj.prj_file, 'Unsteady', new_unsteady_num, ras_object=ras_obj)
760
641
 
761
- new_unsteady = new_unsteady_num
762
-
763
642
  # Re-initialize the ras global object
764
643
  ras_obj.initialize(ras_obj.project_folder, ras_obj.ras_exe_path)
765
-
644
+
766
645
  ras_obj.plan_df = ras_obj.get_plan_entries()
767
646
  ras_obj.geom_df = ras_obj.get_geom_entries()
768
647
  ras_obj.flow_df = ras_obj.get_flow_entries()
769
648
  ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
770
-
771
- return new_unsteady
649
+
650
+ return new_unsteady_num
651
+
772
652
 
773
653
  @staticmethod
774
654
  @log_call
@@ -802,55 +682,12 @@ class RasPlan:
802
682
  template_flow_path = ras_obj.project_folder / f"{ras_obj.project_name}.f{template_flow}"
803
683
  new_flow_path = ras_obj.project_folder / f"{ras_obj.project_name}.f{new_flow_num}"
804
684
 
805
- if not template_flow_path.exists():
806
- raise FileNotFoundError(f"Template steady flow file '{template_flow_path}' does not exist.")
807
-
808
- shutil.copy(template_flow_path, new_flow_path)
809
-
810
- # Read the contents of the project file
811
- try:
812
- with open(ras_obj.prj_file, 'r') as f:
813
- lines = f.readlines()
814
- except FileNotFoundError:
815
- raise FileNotFoundError(f"Project file not found: {ras_obj.prj_file}")
816
-
817
- # Prepare the new Steady Flow File entry line
818
- new_flow_line = f"Flow File=f{new_flow_num}\n"
819
-
820
- # Find the correct insertion point for the new Steady Flow File entry
821
- flow_file_pattern = re.compile(r'^Flow File=f(\d+)', re.IGNORECASE)
822
- insertion_index = None
823
- for i, line in enumerate(lines):
824
- match = flow_file_pattern.match(line.strip())
825
- if match:
826
- current_number = int(match.group(1))
827
- if current_number < int(new_flow_num):
828
- continue
829
- else:
830
- insertion_index = i
831
- break
832
-
833
- if insertion_index is not None:
834
- lines.insert(insertion_index, new_flow_line)
835
- else:
836
- # Try to insert after the last Steady Flow File entry
837
- flow_indices = [i for i, line in enumerate(lines) if flow_file_pattern.match(line.strip())]
838
- if flow_indices:
839
- last_flow_index = flow_indices[-1]
840
- lines.insert(last_flow_index + 1, new_flow_line)
841
- else:
842
- # Append at the end if no Steady Flow File entries exist
843
- lines.append(new_flow_line)
685
+ # Use RasUtils to clone the file
686
+ RasUtils.clone_file(template_flow_path, new_flow_path)
844
687
 
845
- try:
846
- # Write the updated lines back to the project file
847
- with open(ras_obj.prj_file, 'w') as f:
848
- f.writelines(lines)
849
- except IOError as e:
850
- raise IOError(f"Failed to write updated project file: {e}")
688
+ # Use RasUtils to update the project file
689
+ RasUtils.update_project_file(ras_obj.prj_file, 'Flow', new_flow_num, ras_object=ras_obj)
851
690
 
852
- new_steady = new_flow_num
853
-
854
691
  # Re-initialize the ras global object
855
692
  ras_obj.initialize(ras_obj.project_folder, ras_obj.ras_exe_path)
856
693
 
@@ -859,7 +696,7 @@ class RasPlan:
859
696
  ras_obj.flow_df = ras_obj.get_flow_entries()
860
697
  ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
861
698
 
862
- return new_steady
699
+ return new_flow_num
863
700
 
864
701
  @staticmethod
865
702
  @log_call
@@ -884,75 +721,29 @@ class RasPlan:
884
721
  # Update geometry entries without reinitializing the entire project
885
722
  ras_obj.geom_df = ras_obj.get_prj_entries('Geom')
886
723
 
887
- template_geom_filename = f"{ras_obj.project_name}.g{template_geom}"
888
- template_geom_path = ras_obj.project_folder / template_geom_filename
724
+ new_geom_num = RasPlan.get_next_number(ras_obj.geom_df['geom_number'])
725
+ template_geom_path = ras_obj.project_folder / f"{ras_obj.project_name}.g{template_geom}"
726
+ new_geom_path = ras_obj.project_folder / f"{ras_obj.project_name}.g{new_geom_num}"
889
727
 
890
- if not template_geom_path.is_file():
891
- raise FileNotFoundError(f"Template geometry file '{template_geom_path}' does not exist.")
892
-
893
- next_geom_number = RasPlan.get_next_number(ras_obj.geom_df['geom_number'])
894
-
895
- new_geom_filename = f"{ras_obj.project_name}.g{next_geom_number}"
896
- new_geom_path = ras_obj.project_folder / new_geom_filename
897
-
898
- shutil.copyfile(template_geom_path, new_geom_path)
728
+ # Use RasUtils to clone the file
729
+ RasUtils.clone_file(template_geom_path, new_geom_path)
899
730
 
900
731
  # Handle HDF file copy
901
732
  template_hdf_path = ras_obj.project_folder / f"{ras_obj.project_name}.g{template_geom}.hdf"
902
- new_hdf_path = ras_obj.project_folder / f"{ras_obj.project_name}.g{next_geom_number}.hdf"
733
+ new_hdf_path = ras_obj.project_folder / f"{ras_obj.project_name}.g{new_geom_num}.hdf"
903
734
  if template_hdf_path.is_file():
904
- shutil.copyfile(template_hdf_path, new_hdf_path)
905
-
906
- try:
907
- with open(ras_obj.prj_file, 'r') as file:
908
- lines = file.readlines()
909
- except FileNotFoundError:
910
- raise FileNotFoundError(f"Project file not found: {ras_obj.prj_file}")
911
-
912
- # Prepare the new Geometry File entry line
913
- new_geom_line = f"Geom File=g{next_geom_number}\n"
914
-
915
- # Find the correct insertion point for the new Geometry File entry
916
- geom_file_pattern = re.compile(r'^Geom File=g(\d+)', re.IGNORECASE)
917
- insertion_index = None
918
- for i, line in enumerate(lines):
919
- match = geom_file_pattern.match(line.strip())
920
- if match:
921
- current_number = int(match.group(1))
922
- if current_number < int(next_geom_number):
923
- continue
924
- else:
925
- insertion_index = i
926
- break
927
-
928
- if insertion_index is not None:
929
- lines.insert(insertion_index, new_geom_line)
930
- else:
931
- # Try to insert after the last Geometry File entry
932
- geom_indices = [i for i, line in enumerate(lines) if geom_file_pattern.match(line.strip())]
933
- if geom_indices:
934
- last_geom_index = geom_indices[-1]
935
- lines.insert(last_geom_index + 1, new_geom_line)
936
- else:
937
- # Append at the end if no Geometry File entries exist
938
- lines.append(new_geom_line)
735
+ RasUtils.clone_file(template_hdf_path, new_hdf_path)
939
736
 
940
- try:
941
- # Write the updated lines back to the project file
942
- with open(ras_obj.prj_file, 'w') as file:
943
- file.writelines(lines)
944
- except IOError as e:
945
- raise IOError(f"Failed to write updated project file: {e}")
737
+ # Use RasUtils to update the project file
738
+ RasUtils.update_project_file(ras_obj.prj_file, 'Geom', new_geom_num, ras_object=ras_obj)
946
739
 
947
- new_geom = next_geom_number
948
-
949
740
  # Update all dataframes in the ras object
950
741
  ras_obj.plan_df = ras_obj.get_plan_entries()
951
742
  ras_obj.geom_df = ras_obj.get_geom_entries()
952
743
  ras_obj.flow_df = ras_obj.get_flow_entries()
953
744
  ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
954
745
 
955
- return new_geom
746
+ return new_geom_num
956
747
 
957
748
  @staticmethod
958
749
  @log_call
ras_commander/RasPrj.py CHANGED
@@ -398,8 +398,8 @@ class RasPrj:
398
398
  Get HDF entries for plans that have results.
399
399
 
400
400
  Returns:
401
- pd.DataFrame: A DataFrame containing plan entries with HDF results.
402
- Returns an empty DataFrame if no HDF entries are found.
401
+ pd.DataFrame: A DataFrame containing plan entries with HDF results.
402
+ Returns an empty DataFrame if no HDF entries are found.
403
403
  """
404
404
  self.check_initialized()
405
405
 
@@ -676,7 +676,7 @@ def init_ras_project(ras_project_folder, ras_version=None, ras_instance=None):
676
676
  ras_project_folder : str
677
677
  The path to the RAS project folder.
678
678
  ras_version : str, optional
679
- The version of RAS to use (e.g., "6.5").
679
+ The version of RAS to use (e.g., "6.6").
680
680
  The version can also be a full path to the Ras.exe file. (Useful when calling ras objects for folder copies.)
681
681
  If None, the function will attempt to use the version from the global 'ras' object or a default path.
682
682
  You MUST specify a version number via init at some point or ras will not run.
@@ -697,7 +697,7 @@ def init_ras_project(ras_project_folder, ras_version=None, ras_instance=None):
697
697
  # Use the global 'ras' object after initialization
698
698
 
699
699
  2. For managing multiple projects:
700
- project1 = init_ras_project("/path/to/project1", "6.5", ras_instance=RasPrj())
700
+ project1 = init_ras_project("/path/to/project1", "6.6", ras_instance=RasPrj())
701
701
  project2 = init_ras_project("/path/to/project2", ras_instance=RasPrj())
702
702
 
703
703
  Notes:
@@ -740,28 +740,23 @@ def get_ras_exe(ras_version=None):
740
740
  Determine the HEC-RAS executable path based on the input.
741
741
 
742
742
  Args:
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.
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.
746
746
 
747
747
  Returns:
748
- str: The full path to the HEC-RAS executable.
748
+ str: The full path to the HEC-RAS executable.
749
749
 
750
750
  Raises:
751
- ValueError: If the input is neither a valid version number nor a valid file path.
751
+ ValueError: If the input is neither a valid version number nor a valid file path.
752
752
 
753
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.
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.
759
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
760
  if ras_version is None:
766
761
  if hasattr(ras, 'ras_exe_path') and ras.ras_exe_path:
767
762
  logger.info(f"Using HEC-RAS executable from global 'ras' object: {ras.ras_exe_path}")
@@ -772,7 +767,7 @@ def get_ras_exe(ras_version=None):
772
767
  return str(default_path)
773
768
 
774
769
  ras_version_numbers = [
775
- "6.5", "6.4.1", "6.3.1", "6.3", "6.2", "6.1", "6.0",
770
+ "6.6", "6.5", "6.4.1", "6.3.1", "6.3", "6.2", "6.1", "6.0",
776
771
  "5.0.7", "5.0.6", "5.0.5", "5.0.4", "5.0.3", "5.0.1", "5.0",
777
772
  "4.1", "4.0", "3.1.3", "3.1.2", "3.1.1", "3.0", "2.2"
778
773
  ]
ras_commander/RasUtils.py CHANGED
@@ -24,12 +24,13 @@ Example:
24
24
  import os
25
25
  from pathlib import Path
26
26
  from .RasPrj import ras
27
- from typing import Union, Optional, Dict
27
+ from typing import Union, Optional, Dict, Callable
28
28
  import pandas as pd
29
29
  import numpy as np
30
30
  import shutil
31
31
  from ras_commander import get_logger
32
32
  from ras_commander.logging_config import get_logger, log_call
33
+ import re
33
34
 
34
35
  logger = get_logger(__name__)
35
36
  # Module code starts here
@@ -622,6 +623,118 @@ class RasUtils:
622
623
  logger.info(f"Calculated error metrics: {metrics}")
623
624
  return metrics
624
625
 
626
+
627
+ @staticmethod
628
+ @log_call
629
+ def update_file(file_path: Path, update_function: Callable, *args) -> None:
630
+ """
631
+ Generic method to update a file.
625
632
 
633
+ Parameters:
634
+ file_path (Path): Path to the file to be updated
635
+ update_function (Callable): Function to update the file contents
636
+ *args: Additional arguments to pass to the update_function
626
637
 
638
+ Raises:
639
+ Exception: If there's an error updating the file
627
640
 
641
+ Example:
642
+ >>> def update_content(lines, new_value):
643
+ ... lines[0] = f"New value: {new_value}\\n"
644
+ ... return lines
645
+ >>> RasUtils.update_file(Path("example.txt"), update_content, "Hello")
646
+ """
647
+ try:
648
+ with open(file_path, 'r') as f:
649
+ lines = f.readlines()
650
+
651
+ updated_lines = update_function(lines, *args) if args else update_function(lines)
652
+
653
+ with open(file_path, 'w') as f:
654
+ f.writelines(updated_lines)
655
+ logger.info(f"Successfully updated file: {file_path}")
656
+ except Exception as e:
657
+ logger.exception(f"Failed to update file {file_path}")
658
+ raise
659
+
660
+ @staticmethod
661
+ @log_call
662
+ def get_next_number(existing_numbers: list) -> str:
663
+ """
664
+ Determine the next available number from a list of existing numbers.
665
+
666
+ Parameters:
667
+ existing_numbers (list): List of existing numbers as strings
668
+
669
+ Returns:
670
+ str: Next available number as a zero-padded string
671
+
672
+ Example:
673
+ >>> RasUtils.get_next_number(["01", "02", "04"])
674
+ "05"
675
+ """
676
+ existing_numbers = sorted(int(num) for num in existing_numbers)
677
+ next_number = max(existing_numbers, default=0) + 1
678
+ return f"{next_number:02d}"
679
+
680
+ @staticmethod
681
+ @log_call
682
+ def clone_file(template_path: Path, new_path: Path, update_function: Optional[Callable] = None, *args) -> None:
683
+ """
684
+ Generic method to clone a file and optionally update it.
685
+
686
+ Parameters:
687
+ template_path (Path): Path to the template file
688
+ new_path (Path): Path where the new file will be created
689
+ update_function (Optional[Callable]): Function to update the cloned file
690
+ *args: Additional arguments to pass to the update_function
691
+
692
+ Raises:
693
+ FileNotFoundError: If the template file doesn't exist
694
+
695
+ Example:
696
+ >>> def update_content(lines, new_value):
697
+ ... lines[0] = f"New value: {new_value}\\n"
698
+ ... return lines
699
+ >>> RasUtils.clone_file(Path("template.txt"), Path("new.txt"), update_content, "Hello")
700
+ """
701
+ if not template_path.exists():
702
+ logger.error(f"Template file '{template_path}' does not exist.")
703
+ raise FileNotFoundError(f"Template file '{template_path}' does not exist.")
704
+
705
+ shutil.copy(template_path, new_path)
706
+ logger.info(f"File cloned from {template_path} to {new_path}")
707
+
708
+ if update_function:
709
+ RasUtils.update_file(new_path, update_function, *args)
710
+ @staticmethod
711
+ @log_call
712
+ def update_project_file(prj_file: Path, file_type: str, new_num: str, ras_object=None) -> None:
713
+ """
714
+ Update the project file with a new entry.
715
+
716
+ Parameters:
717
+ prj_file (Path): Path to the project file
718
+ file_type (str): Type of file being added (e.g., 'Plan', 'Geom')
719
+ new_num (str): Number of the new file entry
720
+ ras_object (RasPrj, optional): RAS object to use. If None, uses the default ras object.
721
+
722
+ Example:
723
+ >>> RasUtils.update_project_file(Path("project.prj"), "Plan", "02")
724
+ """
725
+ ras_obj = ras_object or ras
726
+ ras_obj.check_initialized()
727
+
728
+ try:
729
+ with open(prj_file, 'r') as f:
730
+ lines = f.readlines()
731
+
732
+ new_line = f"{file_type} File={file_type[0].lower()}{new_num}\n"
733
+ lines.append(new_line)
734
+
735
+ with open(prj_file, 'w') as f:
736
+ f.writelines(lines)
737
+ logger.info(f"Project file updated with new {file_type} entry: {new_num}")
738
+ except Exception as e:
739
+ logger.exception(f"Failed to update project file {prj_file}")
740
+ raise
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ras-commander
3
- Version: 0.40.0
3
+ Version: 0.42.0
4
4
  Summary: A Python library for automating HEC-RAS operations
5
5
  Home-page: https://github.com/billk-FM/ras-commander
6
6
  Author: William M. Katzenmeyer
@@ -61,10 +61,11 @@ Create a virtual environment with conda or venv (ask ChatGPT if you need help)
61
61
 
62
62
  In your virtual environment, install ras-commander using pip:
63
63
  ```
64
- pip install pandas requests pathlib
64
+ pip install h5py numpy pandas requests tqdm scipy
65
65
  pip install ras-commander
66
66
  ```
67
67
 
68
+ If you have dependency issues with pip (especially if you have errors with numpy), try clearing your local pip packages 'C:\Users\your_username\AppData\Roaming\Python\' and then creating a new virtual environment.
68
69
 
69
70
 
70
71
  ## Requirements
@@ -0,0 +1,16 @@
1
+ ras_commander/RasCmdr.py,sha256=_opzPdFuja2wXmFu2iayP6igJqGeILAavPC1XsCC6ks,25010
2
+ ras_commander/RasExamples.py,sha256=XKnbqQTc5t43iFkjJLF3lgqZW4YaGh-79sGYDAmtqiU,19245
3
+ ras_commander/RasGeo.py,sha256=GiRtgRg0JsuCDkpYS6SkJoaLksPUskHGDRpJLYVUMz8,5411
4
+ ras_commander/RasGpt.py,sha256=-524sU_PBPxCmjDKJbDXg6Q3k1-Uhk2tYj6HeW8QFJ8,4201
5
+ ras_commander/RasHdf.py,sha256=K5apgOaHRqiOrrzZvAEMUbLmw7LWmW18bDk3JCy2z6c,77160
6
+ ras_commander/RasPlan.py,sha256=GT8-2X_Or6ufrfpQPv6G3WMLuTNsOO0OCYh1jrbsqZ0,40303
7
+ ras_commander/RasPrj.py,sha256=-iEltmz7B-wXrs4R3iuj6tXX9i-6u_yEl7ZcLmasoWs,33980
8
+ ras_commander/RasUnsteady.py,sha256=37GKaYNJZ39y-khhy01LbHwZnf7HT0V2XKQ-UUaJHlY,4639
9
+ ras_commander/RasUtils.py,sha256=vA3DzvNMeKH3A4cMQLK2a3dihftXBGypYvoi-ckMlSs,29480
10
+ ras_commander/__init__.py,sha256=h4xld8gpvjTTpOOJcPKXwsRMUVGtg8tRqf64AHwZB3k,1051
11
+ ras_commander/logging_config.py,sha256=5bYd_5KMlf81bXsiu2mABBlw0USMhcu5uRv8DIYJSFE,2317
12
+ ras_commander-0.42.0.dist-info/LICENSE,sha256=_pbd6qHnlsz1iQ-ozDW_49r86BZT6CRwO2iBtw0iN6M,457
13
+ ras_commander-0.42.0.dist-info/METADATA,sha256=9opk4wxJ_Qmaeiky2Ui1AP8zyCtfiy-l8Hzav_WnGPQ,15671
14
+ ras_commander-0.42.0.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
15
+ ras_commander-0.42.0.dist-info/top_level.txt,sha256=i76S7eKLFC8doKcXDl3aiOr9RwT06G8adI6YuKbQDaA,14
16
+ ras_commander-0.42.0.dist-info/RECORD,,
ras_commander/_version.py DELETED
@@ -1,16 +0,0 @@
1
- # file generated by setuptools_scm
2
- # don't change, don't track in version control
3
- TYPE_CHECKING = False
4
- if TYPE_CHECKING:
5
- from typing import Tuple, Union
6
- VERSION_TUPLE = Tuple[Union[int, str], ...]
7
- else:
8
- VERSION_TUPLE = object
9
-
10
- version: str
11
- __version__: str
12
- __version_tuple__: VERSION_TUPLE
13
- version_tuple: VERSION_TUPLE
14
-
15
- __version__ = version = '0.29.dev1+g22e75d4.d20240919'
16
- __version_tuple__ = version_tuple = (0, 29, 'dev1', 'g22e75d4.d20240919')
@@ -1,17 +0,0 @@
1
- ras_commander/RasCmdr.py,sha256=_opzPdFuja2wXmFu2iayP6igJqGeILAavPC1XsCC6ks,25010
2
- ras_commander/RasExamples.py,sha256=g2HIbppxIVeCKriy9Th1RDtrCPzFkdoP-BWEzHPsedY,25112
3
- ras_commander/RasGeo.py,sha256=cqKpN-1r_uXw24acqA8ubGGw2-Od51p-I_X0kGSkWBE,5395
4
- ras_commander/RasGpt.py,sha256=-524sU_PBPxCmjDKJbDXg6Q3k1-Uhk2tYj6HeW8QFJ8,4201
5
- ras_commander/RasHdf.py,sha256=_aJ2PAqFPAnTWN2btmASjWT8dHAFZveAorAu6QXLwgA,77162
6
- ras_commander/RasPlan.py,sha256=p9IVQ5vKzv01jcrY0-Lbx8N7YwkPf4WKovBNx_FgXpE,48835
7
- ras_commander/RasPrj.py,sha256=qDJJiWnAaf-Uzc31DVI5aTKjOulIEoVeH05-kjb4tZQ,34444
8
- ras_commander/RasUnsteady.py,sha256=37GKaYNJZ39y-khhy01LbHwZnf7HT0V2XKQ-UUaJHlY,4639
9
- ras_commander/RasUtils.py,sha256=P9SopqMd6awZyj0U2xNxc-pl1-HEktmyY1lz3LiduPs,25079
10
- ras_commander/__init__.py,sha256=h4xld8gpvjTTpOOJcPKXwsRMUVGtg8tRqf64AHwZB3k,1051
11
- ras_commander/_version.py,sha256=BReLomJ164W3bJhfQJi0gbNKc3DXCzwusmCheUzClB8,478
12
- ras_commander/logging_config.py,sha256=5bYd_5KMlf81bXsiu2mABBlw0USMhcu5uRv8DIYJSFE,2317
13
- ras_commander-0.40.0.dist-info/LICENSE,sha256=_pbd6qHnlsz1iQ-ozDW_49r86BZT6CRwO2iBtw0iN6M,457
14
- ras_commander-0.40.0.dist-info/METADATA,sha256=78DRlI1Qcckp4bsA3mxbdxMAU5WnANe_tPc1D8J24BE,15440
15
- ras_commander-0.40.0.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
16
- ras_commander-0.40.0.dist-info/top_level.txt,sha256=i76S7eKLFC8doKcXDl3aiOr9RwT06G8adI6YuKbQDaA,14
17
- ras_commander-0.40.0.dist-info/RECORD,,