ras-commander 0.1.6__py2.py3-none-any.whl → 0.20.dev0__py2.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.
@@ -1,227 +0,0 @@
1
- """
2
- Project management operations for HEC-RAS projects.
3
- """
4
- import os
5
- import re
6
- import shutil
7
- from pathlib import Path
8
- from .file_operations import FileOperations
9
- from .project_config import ProjectConfig
10
-
11
- class ProjectManager:
12
- """
13
- A class for functions that interface with the HEC-RAS project's project file.
14
- """
15
- @staticmethod
16
- def get_next_available_number(existing_numbers):
17
- """
18
- Determine the first available number for plan, unsteady, steady, or geometry files from 01 to 99.
19
- Parameters:
20
- existing_numbers (pandas.Series): Series of existing numbers as strings (e.g., ['01', '02', '03'])
21
- Returns:
22
- str: First available number as a two-digit string
23
- """
24
- existing_set = set(existing_numbers.astype(int))
25
- for num in range(1, 100):
26
- if num not in existing_set:
27
- return f"{num:02d}"
28
- return None
29
-
30
- @staticmethod
31
- def copy_plan_from_template(template_plan, new_plan_shortid=None):
32
- """
33
- Create a new plan file based on a template and update the project file.
34
- Parameters:
35
- template_plan (str): Plan file to use as template (e.g., 'p01')
36
- new_plan_shortid (str, optional): New short identifier for the plan file
37
- Returns:
38
- str: New plan number
39
- """
40
- config = ProjectConfig()
41
- config.check_initialized()
42
-
43
- new_plan_num = ProjectManager.get_next_available_number(config.ras_plan_entries['plan_number'])
44
- template_plan_path = config.project_folder / f"{config.project_name}.{template_plan}"
45
- new_plan_path = config.project_folder / f"{config.project_name}.p{new_plan_num}"
46
- shutil.copy(template_plan_path, new_plan_path)
47
- print(f"Copied {template_plan_path} to {new_plan_path}")
48
-
49
- with open(new_plan_path, 'r') as f:
50
- plan_lines = f.readlines()
51
-
52
- shortid_pattern = re.compile(r'^Short Identifier=(.*)$', re.IGNORECASE)
53
- for i, line in enumerate(plan_lines):
54
- match = shortid_pattern.match(line.strip())
55
- if match:
56
- current_shortid = match.group(1)
57
- if new_plan_shortid is None:
58
- new_shortid = (current_shortid + "_copy")[:24]
59
- else:
60
- new_shortid = new_plan_shortid[:24]
61
- plan_lines[i] = f"Short Identifier={new_shortid}\n"
62
- break
63
-
64
- with open(new_plan_path, 'w') as f:
65
- f.writelines(plan_lines)
66
-
67
- print(f"Updated short identifier in {new_plan_path}")
68
-
69
- with open(config.project_file, 'r') as f:
70
- lines = f.readlines()
71
-
72
- new_plan_line = f"Plan File=p{new_plan_num}\n"
73
- plan_file_pattern = re.compile(r'^Plan File=p(\d+)', re.IGNORECASE)
74
- insertion_index = None
75
- for i, line in enumerate(lines):
76
- match = plan_file_pattern.match(line.strip())
77
- if match:
78
- current_number = int(match.group(1))
79
- if current_number > int(new_plan_num):
80
- insertion_index = i
81
- break
82
-
83
- if insertion_index is not None:
84
- lines.insert(insertion_index, new_plan_line)
85
- else:
86
- last_plan_index = max([i for i, line in enumerate(lines) if plan_file_pattern.match(line.strip())], default=-1)
87
- if last_plan_index != -1:
88
- lines.insert(last_plan_index + 1, new_plan_line)
89
- else:
90
- lines.append(new_plan_line)
91
-
92
- with open(config.project_file, 'w') as f:
93
- f.writelines(lines)
94
-
95
- print(f"Updated {config.project_file} with new plan p{new_plan_num}")
96
- return f"p{new_plan_num}"
97
-
98
- @staticmethod
99
- def copy_geometry_from_template(template_geom):
100
- """
101
- Copy geometry files from a template, find the next geometry number,
102
- and update the project file accordingly.
103
- Parameters:
104
- template_geom (str): Geometry number to be used as a template (e.g., 'g01')
105
- Returns:
106
- str: New geometry number (e.g., 'g03')
107
- """
108
- config = ProjectConfig()
109
- config.check_initialized()
110
-
111
- template_geom_filename = f"{config.project_name}.{template_geom}"
112
- template_geom_path = config.project_folder / template_geom_filename
113
-
114
- if not template_geom_path.is_file():
115
- raise FileNotFoundError(f"Template geometry file '{template_geom_path}' does not exist.")
116
-
117
- template_hdf_path = template_geom_path.with_suffix('.hdf')
118
- if not template_hdf_path.is_file():
119
- raise FileNotFoundError(f"Template geometry .hdf file '{template_hdf_path}' does not exist.")
120
-
121
- next_geom_number = ProjectManager.get_next_available_number(config.ras_geom_entries['geom_number'])
122
-
123
- new_geom_filename = f"{config.project_name}.g{next_geom_number}"
124
- new_geom_path = config.project_folder / new_geom_filename
125
-
126
- shutil.copyfile(template_geom_path, new_geom_path)
127
- print(f"Copied '{template_geom_path}' to '{new_geom_path}'.")
128
-
129
- new_hdf_path = new_geom_path.with_suffix('.hdf')
130
- shutil.copyfile(template_hdf_path, new_hdf_path)
131
- print(f"Copied '{template_hdf_path}' to '{new_hdf_path}'.")
132
-
133
- with open(config.project_file, 'r') as file:
134
- lines = file.readlines()
135
-
136
- new_geom_line = f"Geom File=g{next_geom_number}\n"
137
- geom_file_pattern = re.compile(r'^Geom File=g(\d+)', re.IGNORECASE)
138
- insertion_index = None
139
- for i, line in enumerate(lines):
140
- match = geom_file_pattern.match(line.strip())
141
- if match:
142
- current_number = int(match.group(1))
143
- if current_number > int(next_geom_number):
144
- insertion_index = i
145
- break
146
-
147
- if insertion_index is not None:
148
- lines.insert(insertion_index, new_geom_line)
149
- else:
150
- header_pattern = re.compile(r'^(Proj Title|Current Plan|Default Exp/Contr|English Units)', re.IGNORECASE)
151
- header_indices = [i for i, line in enumerate(lines) if header_pattern.match(line.strip())]
152
- if header_indices:
153
- last_header_index = header_indices[-1]
154
- lines.insert(last_header_index + 2, new_geom_line)
155
- else:
156
- lines.insert(0, new_geom_line)
157
-
158
- with open(config.project_file, 'w') as file:
159
- file.writelines(lines)
160
-
161
- print(f"Inserted 'Geom File=g{next_geom_number}' into project file '{config.project_file}'.")
162
- return f"g{next_geom_number}"
163
-
164
- @staticmethod
165
- def copy_unsteady_from_template(template_unsteady):
166
- """
167
- Copy unsteady flow files from a template, find the next unsteady number,
168
- and update the project file accordingly.
169
- Parameters:
170
- template_unsteady (str): Unsteady flow number to be used as a template (e.g., 'u01')
171
- Returns:
172
- str: New unsteady flow number (e.g., 'u03')
173
- """
174
- config = ProjectConfig()
175
- config.check_initialized()
176
-
177
- template_unsteady_filename = f"{config.project_name}.{template_unsteady}"
178
- template_unsteady_path = config.project_folder / template_unsteady_filename
179
-
180
- if not template_unsteady_path.is_file():
181
- raise FileNotFoundError(f"Template unsteady flow file '{template_unsteady_path}' does not exist.")
182
-
183
- next_unsteady_number = ProjectManager.get_next_available_number(config.ras_unsteady_entries['unsteady_number'])
184
-
185
- new_unsteady_filename = f"{config.project_name}.u{next_unsteady_number}"
186
- new_unsteady_path = config.project_folder / new_unsteady_filename
187
-
188
- shutil.copyfile(template_unsteady_path, new_unsteady_path)
189
- print(f"Copied '{template_unsteady_path}' to '{new_unsteady_path}'.")
190
-
191
- template_hdf_path = template_unsteady_path.with_suffix('.hdf')
192
- new_hdf_path = new_unsteady_path.with_suffix('.hdf')
193
-
194
- if template_hdf_path.is_file():
195
- shutil.copyfile(template_hdf_path, new_hdf_path)
196
- print(f"Copied '{template_hdf_path}' to '{new_hdf_path}'.")
197
- else:
198
- print(f"No corresponding '.hdf' file found for '{template_unsteady_filename}'. Skipping '.hdf' copy.")
199
-
200
- with open(config.project_file, 'r') as file:
201
- lines = file.readlines()
202
-
203
- new_unsteady_line = f"Unsteady File=u{next_unsteady_number}\n"
204
- unsteady_file_pattern = re.compile(r'^Unsteady File=u(\d+)', re.IGNORECASE)
205
- insertion_index = None
206
- for i, line in enumerate(lines):
207
- match = unsteady_file_pattern.match(line.strip())
208
- if match:
209
- current_number = int(match.group(1))
210
- if current_number > int(next_unsteady_number):
211
- insertion_index = i
212
- break
213
-
214
- if insertion_index is not None:
215
- lines.insert(insertion_index, new_unsteady_line)
216
- else:
217
- last_unsteady_index = max([i for i, line in enumerate(lines) if unsteady_file_pattern.match(line.strip())], default=-1)
218
- if last_unsteady_index != -1:
219
- lines.insert(last_unsteady_index + 1, new_unsteady_line)
220
- else:
221
- lines.append(new_unsteady_line)
222
-
223
- with open(config.project_file, 'w') as file:
224
- file.writelines(lines)
225
-
226
- print(f"Inserted 'Unsteady File=u{next_unsteady_number}' into project file '{config.project_file}'.")
227
- return f"u{next_unsteady_number}"
@@ -1,15 +0,0 @@
1
- # REVISION NOTE: We should be able to move these functions into the project_management.py file and delete this file.
2
-
3
- from pathlib import Path
4
- from .file_operations import FileOperations
5
-
6
- def find_hecras_project_file(folder_path):
7
- return FileOperations.find_hecras_project_file(folder_path)
8
-
9
- def load_project_data(project_file):
10
- return {
11
- 'ras_plan_entries': FileOperations.get_plan_entries(project_file),
12
- 'ras_flow_entries': FileOperations.get_flow_entries(project_file),
13
- 'ras_unsteady_entries': FileOperations.get_unsteady_entries(project_file),
14
- 'ras_geom_entries': FileOperations.get_geom_entries(project_file)
15
- }
@@ -1,172 +0,0 @@
1
- """
2
- Operations for handling unsteady flow files in HEC-RAS projects.
3
- """
4
- from pathlib import Path
5
- from .project_config import ProjectConfig
6
- import re
7
-
8
- class UnsteadyOperations:
9
- """
10
- Class for all operations related to HEC-RAS unsteady flow files.
11
- """
12
- @staticmethod
13
- def copy_unsteady_files(dst_folder, template_unsteady):
14
- """
15
- Copy unsteady flow files from a template to the next available unsteady number
16
- and update the destination folder accordingly.
17
-
18
- Parameters:
19
- dst_folder (str): Destination folder path
20
- template_unsteady (str): Template unsteady flow number (e.g., 'u01')
21
-
22
- Returns:
23
- str: New unsteady flow number (e.g., 'u03')
24
- """
25
- config = ProjectConfig()
26
- config.check_initialized()
27
-
28
- src_folder = config.project_folder
29
- dst_folder = Path(dst_folder)
30
- dst_folder.mkdir(parents=True, exist_ok=True)
31
-
32
- # Determine the next available unsteady number
33
- existing_numbers = []
34
- unsteady_file_pattern = re.compile(r'^Flow File=u(\d+)', re.IGNORECASE)
35
-
36
- for plan_file in src_folder.glob("*.p[0-9][0-9]"):
37
- with open(plan_file, 'r') as file:
38
- lines = file.readlines()
39
- for line in lines:
40
- match = unsteady_file_pattern.match(line.strip())
41
- if match:
42
- existing_numbers.append(int(match.group(1)))
43
-
44
- if existing_numbers:
45
- existing_numbers.sort()
46
- next_number = 1
47
- for num in existing_numbers:
48
- if num == next_number:
49
- next_number += 1
50
- else:
51
- break
52
- else:
53
- next_number = 1
54
-
55
- next_unsteady_number = f"{next_number:02d}"
56
-
57
- # Copy the template unsteady file to the new unsteady file
58
- template_unsteady_filename = f"{config.project_name}.u{template_unsteady}"
59
- src_u_file = src_folder / template_unsteady_filename
60
- if src_u_file.exists():
61
- new_unsteady_filename = f"{config.project_name}.u{next_unsteady_number}"
62
- dst_u_file = dst_folder / new_unsteady_filename
63
- dst_u_file.write_bytes(src_u_file.read_bytes())
64
- print(f"Copied {src_u_file} to {dst_u_file}")
65
- else:
66
- raise FileNotFoundError(f"Template unsteady file '{src_u_file}' does not exist.")
67
-
68
- # Copy the corresponding .hdf file
69
- src_hdf_file = src_folder / f"{template_unsteady_filename}.hdf"
70
- if src_hdf_file.exists():
71
- new_hdf_filename = f"{new_unsteady_filename}.hdf"
72
- dst_hdf_file = dst_folder / new_hdf_filename
73
- dst_hdf_file.write_bytes(src_hdf_file.read_bytes())
74
- print(f"Copied {src_hdf_file} to {dst_hdf_file}")
75
- else:
76
- print(f"No corresponding .hdf file found for '{template_unsteady_filename}'. Skipping '.hdf' copy.")
77
- config = ProjectConfig()
78
- return f"u{next_unsteady_number}"
79
-
80
- @staticmethod
81
- def rename_unsteady_files(old_number, new_number):
82
- """
83
- Rename unsteady flow files in the project folder.
84
- Parameters:
85
- old_number (str): Old unsteady flow number (e.g., 'u01')
86
- new_number (str): New unsteady flow number (e.g., 'u02')
87
- Returns:
88
- None
89
- """
90
- config = ProjectConfig()
91
- config.check_initialized()
92
-
93
- folder = config.project_folder
94
- old_u_file = next(folder.glob(f"*.{old_number}"), None)
95
- if old_u_file:
96
- new_u_file = folder / old_u_file.name.replace(old_number, new_number)
97
- old_u_file.rename(new_u_file)
98
- print(f"Renamed {old_u_file} to {new_u_file}")
99
- else:
100
- print(f"No .{old_number} file found in {folder}")
101
-
102
- old_hdf_file = next(folder.glob(f"*.{old_number}.hdf"), None)
103
- if old_hdf_file:
104
- new_hdf_file = folder / old_hdf_file.name.replace(old_number, new_number)
105
- old_hdf_file.rename(new_hdf_file)
106
- print(f"Renamed {old_hdf_file} to {new_hdf_file}")
107
- else:
108
- print(f"No .{old_number}.hdf file found in {folder}")
109
-
110
- @staticmethod
111
- def update_unsteady_reference_in_plan(plan_file, new_unsteady_number):
112
- """
113
- Update the unsteady flow reference in a plan file.
114
-
115
- Parameters:
116
- plan_file (str): Full path to the plan file
117
- new_unsteady_number (str): New unsteady flow number (e.g., 'u02')
118
-
119
- Returns:
120
- None
121
- """
122
- config = ProjectConfig()
123
- config.check_initialized()
124
-
125
- plan_path = Path(plan_file)
126
- if not plan_path.is_file():
127
- raise FileNotFoundError(f"Plan file '{plan_path}' not found. Check that the file exists.")
128
-
129
- with open(plan_path, 'r') as f:
130
- lines = f.readlines()
131
- updated = False
132
- for i, line in enumerate(lines):
133
- if line.startswith("Unsteady File=") or line.startswith("Flow File=u"):
134
- lines[i] = f"Unsteady File={new_unsteady_number}\n"
135
- updated = True
136
- break
137
- if updated:
138
- with open(plan_path, 'w') as f:
139
- f.writelines(lines)
140
- print(f"Updated unsteady flow reference in {plan_file} to {new_unsteady_number}")
141
- else:
142
- print(f"No unsteady flow reference found in {plan_file}")
143
-
144
- @staticmethod
145
- def modify_unsteady_flow_parameters(unsteady_file, modifications):
146
- """
147
- Modify parameters in an unsteady flow file.
148
- Parameters:
149
- unsteady_file (str): Full path to the unsteady flow file
150
- modifications (dict): Dictionary of modifications to apply, where keys are parameter names and values are new values
151
- Returns:
152
- None
153
- """
154
- config = ProjectConfig()
155
- config.check_initialized()
156
-
157
- unsteady_path = Path(unsteady_file)
158
- with open(unsteady_path, 'r') as f:
159
- lines = f.readlines()
160
- updated = False
161
- for i, line in enumerate(lines):
162
- for param, new_value in modifications.items():
163
- if line.startswith(f"{param}="):
164
- lines[i] = f"{param}={new_value}\n"
165
- updated = True
166
- print(f"Updated {param} to {new_value}")
167
- if updated:
168
- with open(unsteady_path, 'w') as f:
169
- f.writelines(lines)
170
- print(f"Applied modifications to {unsteady_file}")
171
- else:
172
- print(f"No matching parameters found in {unsteady_file}")
@@ -1,195 +0,0 @@
1
- """
2
- Utility functions for the ras_commander library.
3
- """
4
- import shutil
5
- import logging
6
- import time
7
- from pathlib import Path
8
- from .project_config import ProjectConfig
9
-
10
- class Utilities:
11
- """
12
- A class containing the utility functions for the ras_commander library.
13
- When integrating new functions that do not clearly fit into other classes, add them here.
14
- """
15
- @staticmethod
16
- def create_backup(file_path, backup_suffix="_backup"):
17
- """
18
- Create a backup of the specified file.
19
- Parameters:
20
- file_path (str): Path to the file to be backed up
21
- backup_suffix (str): Suffix to append to the backup file name
22
- Returns:
23
- str: Path to the created backup file
24
- """
25
- config = ProjectConfig()
26
- config.check_initialized()
27
-
28
- original_path = Path(file_path)
29
- backup_path = original_path.with_name(f"{original_path.stem}{backup_suffix}{original_path.suffix}")
30
- shutil.copy2(original_path, backup_path)
31
- print(f"Backup created: {backup_path}")
32
- return str(backup_path)
33
-
34
- @staticmethod
35
- def restore_from_backup(backup_path, remove_backup=True):
36
- """
37
- Restore a file from its backup.
38
- Parameters:
39
- backup_path (str): Path to the backup file
40
- remove_backup (bool): Whether to remove the backup file after restoration
41
- Returns:
42
- str: Path to the restored file
43
- """
44
- config = ProjectConfig()
45
- config.check_initialized()
46
-
47
- backup_path = Path(backup_path)
48
- original_path = backup_path.with_name(backup_path.stem.rsplit('_backup', 1)[0] + backup_path.suffix)
49
- shutil.copy2(backup_path, original_path)
50
- print(f"File restored: {original_path}")
51
- if remove_backup:
52
- backup_path.unlink()
53
- print(f"Backup removed: {backup_path}")
54
- return str(original_path)
55
-
56
- @staticmethod
57
- def safe_remove(file_path):
58
- """
59
- Safely remove a file if it exists.
60
- Parameters:
61
- file_path (str): Path to the file to be removed
62
- Returns:
63
- bool: True if the file was removed, False if it didn't exist
64
- """
65
- config = ProjectConfig()
66
- config.check_initialized()
67
-
68
- path = Path(file_path)
69
- if path.exists():
70
- path.unlink()
71
- print(f"File removed: {path}")
72
- return True
73
- else:
74
- print(f"File not found: {path}")
75
- return False
76
-
77
- @staticmethod
78
- def ensure_directory(directory_path):
79
- """
80
- Ensure that a directory exists, creating it if necessary.
81
- Parameters:
82
- directory_path (str): Path to the directory
83
- Returns:
84
- str: Path to the ensured directory
85
- """
86
- config = ProjectConfig()
87
- config.check_initialized()
88
-
89
- path = Path(directory_path)
90
- path.mkdir(parents=True, exist_ok=True)
91
- print(f"Directory ensured: {path}")
92
- return str(path)
93
-
94
- @staticmethod
95
- def list_files_with_extension(extension):
96
- """
97
- List all files in the project directory with a specific extension.
98
- Parameters:
99
- extension (str): File extension to filter (e.g., '.prj')
100
- Returns:
101
- list: List of file paths matching the extension
102
- """
103
- config = ProjectConfig()
104
- config.check_initialized()
105
-
106
- files = list(config.project_folder.glob(f"*{extension}"))
107
- return [str(file) for file in files]
108
-
109
- @staticmethod
110
- def get_file_size(file_path):
111
- """
112
- Get the size of a file in bytes.
113
- Parameters:
114
- file_path (str): Path to the file
115
- Returns:
116
- int: Size of the file in bytes
117
- """
118
- config = ProjectConfig()
119
- config.check_initialized()
120
-
121
- path = Path(file_path)
122
- if path.exists():
123
- size = path.stat().st_size
124
- print(f"File size: {size} bytes")
125
- return size
126
- else:
127
- print(f"File not found: {path}")
128
- return None
129
-
130
- @staticmethod
131
- def get_modification_time(file_path):
132
- """
133
- Get the last modification time of a file.
134
- Parameters:
135
- file_path (str): Path to the file
136
- Returns:
137
- float: Last modification time as a timestamp
138
- """
139
- config = ProjectConfig()
140
- config.check_initialized()
141
-
142
- path = Path(file_path)
143
- if path.exists():
144
- mtime = path.stat().st_mtime
145
- print(f"Last modified: {mtime}")
146
- return mtime
147
- else:
148
- print(f"File not found: {path}")
149
- return None
150
-
151
- @staticmethod
152
- def get_plan_path(current_plan_number):
153
- """
154
- Get the path for a plan file with a given plan number.
155
- Parameters:
156
- current_plan_number (str or int): The plan number (01 to 99)
157
- Returns:
158
- str: Full path to the plan file with the given plan number
159
- """
160
- config = ProjectConfig()
161
- config.check_initialized()
162
-
163
- current_plan_number = f"{int(current_plan_number):02d}" # Ensure two-digit format
164
- plan_name = f"{config.project_name}.p{current_plan_number}"
165
- return str(config.project_folder / plan_name)
166
-
167
- @staticmethod
168
- def retry_remove_folder(folder_path: str, max_attempts: int = 5, initial_delay: float = 1.0) -> bool:
169
- """
170
- Attempts to remove a folder with retry logic and exponential backoff.
171
- Args:
172
- folder_path (str): Path to the folder to be removed.
173
- max_attempts (int): Maximum number of removal attempts.
174
- initial_delay (float): Initial delay between attempts in seconds.
175
- Returns:
176
- bool: True if the folder was successfully removed, False otherwise.
177
- """
178
- config = ProjectConfig()
179
- config.check_initialized()
180
-
181
- folder = Path(folder_path)
182
- for attempt in range(max_attempts):
183
- try:
184
- if folder.exists():
185
- shutil.rmtree(folder)
186
- return True
187
- except PermissionError:
188
- if attempt < max_attempts - 1:
189
- delay = initial_delay * (2 ** attempt) # Exponential backoff
190
- logging.warning(f"Failed to remove folder {folder}. Retrying in {delay} seconds...")
191
- time.sleep(delay)
192
- else:
193
- logging.error(f"Failed to remove folder {folder} after {max_attempts} attempts. Skipping.")
194
- return False
195
- return False