ras-commander 0.1.5__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.
- ras_commander/README.md +187 -0
- ras_commander/RasCommander.py +456 -0
- ras_commander/RasExamples.py +304 -0
- ras_commander/RasGeo.py +83 -0
- ras_commander/RasPlan.py +1216 -0
- ras_commander/RasPrj.py +400 -0
- ras_commander/RasUnsteady.py +53 -0
- ras_commander/RasUtils.py +283 -0
- ras_commander/__init__.py +33 -0
- ras_commander/_version.py +2 -2
- ras_commander-0.20.dev0.dist-info/METADATA +342 -0
- ras_commander-0.20.dev0.dist-info/RECORD +15 -0
- {ras_commander-0.1.5.dist-info → ras_commander-0.20.dev0.dist-info}/WHEEL +1 -1
- ras_commander/execution.py +0 -315
- ras_commander/file_operations.py +0 -173
- ras_commander/geometry_operations.py +0 -184
- ras_commander/plan_operations.py +0 -307
- ras_commander/project_config.py +0 -64
- ras_commander/project_init.py +0 -174
- ras_commander/project_management.py +0 -227
- ras_commander/project_setup.py +0 -15
- ras_commander/unsteady_operations.py +0 -172
- ras_commander/utilities.py +0 -195
- ras_commander-0.1.5.dist-info/METADATA +0 -133
- ras_commander-0.1.5.dist-info/RECORD +0 -17
- {ras_commander-0.1.5.dist-info → ras_commander-0.20.dev0.dist-info}/LICENSE +0 -0
- {ras_commander-0.1.5.dist-info → ras_commander-0.20.dev0.dist-info}/top_level.txt +0 -0
@@ -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}"
|
ras_commander/project_setup.py
DELETED
@@ -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}")
|
ras_commander/utilities.py
DELETED
@@ -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
|