ras-commander 0.81.0__py3-none-any.whl → 0.82.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/RasControl.py +774 -0
- ras_commander/RasPlan.py +98 -31
- ras_commander/RasPrj.py +70 -33
- ras_commander/__init__.py +3 -2
- {ras_commander-0.81.0.dist-info → ras_commander-0.82.0.dist-info}/METADATA +15 -2
- {ras_commander-0.81.0.dist-info → ras_commander-0.82.0.dist-info}/RECORD +9 -8
- {ras_commander-0.81.0.dist-info → ras_commander-0.82.0.dist-info}/WHEEL +0 -0
- {ras_commander-0.81.0.dist-info → ras_commander-0.82.0.dist-info}/licenses/LICENSE +0 -0
- {ras_commander-0.81.0.dist-info → ras_commander-0.82.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
"""
|
|
2
|
+
RasControl - HECRASController API Wrapper (ras-commander style)
|
|
3
|
+
|
|
4
|
+
Provides ras-commander style API for legacy HEC-RAS versions (3.x-4.x)
|
|
5
|
+
that use HECRASController COM interface instead of HDF files.
|
|
6
|
+
|
|
7
|
+
Public functions:
|
|
8
|
+
- RasControl.run_plan(plan: Union[str, Path], ras_object: Optional[Any] = None) -> Tuple[bool, List[str]]
|
|
9
|
+
- RasControl.get_steady_results(plan: Union[str, Path], ras_object: Optional[Any] = None) -> pandas.DataFrame
|
|
10
|
+
- RasControl.get_unsteady_results(plan: Union[str, Path], max_times: Optional[int] = None, ras_object: Optional[Any] = None) -> pandas.DataFrame
|
|
11
|
+
- RasControl.get_output_times(plan: Union[str, Path], ras_object: Optional[Any] = None) -> List[str]
|
|
12
|
+
- RasControl.get_plans(plan: Union[str, Path], ras_object: Optional[Any] = None) -> List[dict]
|
|
13
|
+
- RasControl.set_current_plan(plan: Union[str, Path], ras_object: Optional[Any] = None) -> bool
|
|
14
|
+
|
|
15
|
+
Private functions:
|
|
16
|
+
- _terminate_ras_process() -> None
|
|
17
|
+
- _is_ras_running() -> bool
|
|
18
|
+
- RasControl._normalize_version(version: str) -> str
|
|
19
|
+
- RasControl._get_project_info(plan: Union[str, Path], ras_object: Optional[Any] = None) -> Tuple[Path, str, Optional[str], Optional[str]]
|
|
20
|
+
- RasControl._com_open_close(project_path: Path, version: str, operation_func: Callable[[Any], Any]) -> Any
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import win32com.client
|
|
25
|
+
import psutil
|
|
26
|
+
import pandas as pd
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Optional, List, Tuple, Callable, Any, Union
|
|
29
|
+
import logging
|
|
30
|
+
import time
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
# Import ras-commander components
|
|
35
|
+
from .RasPrj import ras
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _terminate_ras_process() -> None:
|
|
39
|
+
"""Force terminate any running ras.exe processes."""
|
|
40
|
+
for proc in psutil.process_iter(['name']):
|
|
41
|
+
try:
|
|
42
|
+
if proc.info['name'] and proc.info['name'].lower() == 'ras.exe':
|
|
43
|
+
proc.terminate()
|
|
44
|
+
proc.wait(timeout=3)
|
|
45
|
+
logger.info("Terminated ras.exe process")
|
|
46
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.TimeoutExpired):
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _is_ras_running() -> bool:
|
|
51
|
+
"""Check if HEC-RAS is currently running"""
|
|
52
|
+
for proc in psutil.process_iter(['name']):
|
|
53
|
+
try:
|
|
54
|
+
if proc.info['name'] and proc.info['name'].lower() == 'ras.exe':
|
|
55
|
+
return True
|
|
56
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
57
|
+
pass
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class RasControl:
|
|
62
|
+
"""
|
|
63
|
+
HECRASController API wrapper with ras-commander style interface.
|
|
64
|
+
|
|
65
|
+
Works with legacy HEC-RAS versions (3.x-4.x) that use COM interface
|
|
66
|
+
instead of HDF files. Integrates with ras-commander project management.
|
|
67
|
+
|
|
68
|
+
Usage (ras-commander style):
|
|
69
|
+
>>> from ras_commander import init_ras_project, RasControl
|
|
70
|
+
>>>
|
|
71
|
+
>>> # Initialize with version (with or without periods)
|
|
72
|
+
>>> init_ras_project(path, "4.1") # or "41"
|
|
73
|
+
>>>
|
|
74
|
+
>>> # Use plan numbers like HDF methods
|
|
75
|
+
>>> RasControl.run_plan("02")
|
|
76
|
+
>>> df = RasControl.get_steady_results("02")
|
|
77
|
+
|
|
78
|
+
Supported Versions:
|
|
79
|
+
All installed versions: 3.x, 4.x, 5.0.x, 6.0-6.7+
|
|
80
|
+
Accepts formats: "4.1", "41", "5.0.6", "506", "6.6", "66", etc.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
# Version mapping based on ACTUAL COM interfaces registered on system
|
|
84
|
+
# Only these COM interfaces exist: RAS41, RAS503, RAS505, RAS506, RAS507,
|
|
85
|
+
# RAS60, RAS631, RAS641, RAS65, RAS66, RAS67
|
|
86
|
+
# Other versions use nearest available fallback
|
|
87
|
+
VERSION_MAP = {
|
|
88
|
+
# HEC-RAS 3.x → Use 4.1 (3.x COM not registered)
|
|
89
|
+
'3.0': 'RAS41.HECRASController',
|
|
90
|
+
'30': 'RAS41.HECRASController',
|
|
91
|
+
'3.1': 'RAS41.HECRASController',
|
|
92
|
+
'31': 'RAS41.HECRASController',
|
|
93
|
+
'3.1.1': 'RAS41.HECRASController',
|
|
94
|
+
'311': 'RAS41.HECRASController',
|
|
95
|
+
'3.1.2': 'RAS41.HECRASController',
|
|
96
|
+
'312': 'RAS41.HECRASController',
|
|
97
|
+
'3.1.3': 'RAS41.HECRASController',
|
|
98
|
+
'313': 'RAS41.HECRASController',
|
|
99
|
+
|
|
100
|
+
# HEC-RAS 4.x
|
|
101
|
+
'4.0': 'RAS41.HECRASController', # Use 4.1 (4.0 COM not registered)
|
|
102
|
+
'40': 'RAS41.HECRASController',
|
|
103
|
+
'4.1': 'RAS41.HECRASController', # ✓ EXISTS
|
|
104
|
+
'41': 'RAS41.HECRASController',
|
|
105
|
+
'4.1.0': 'RAS41.HECRASController',
|
|
106
|
+
'410': 'RAS41.HECRASController',
|
|
107
|
+
|
|
108
|
+
# HEC-RAS 5.0.x
|
|
109
|
+
'5.0': 'RAS503.HECRASController', # Use 5.0.3 (RAS50 COM not registered)
|
|
110
|
+
'50': 'RAS503.HECRASController',
|
|
111
|
+
'5.0.1': 'RAS501.HECRASController', # ✓ EXISTS
|
|
112
|
+
'501': 'RAS501.HECRASController',
|
|
113
|
+
'5.0.3': 'RAS503.HECRASController', # ✓ EXISTS
|
|
114
|
+
'503': 'RAS503.HECRASController',
|
|
115
|
+
'5.0.4': 'RAS504.HECRASController', # ✓ EXISTS (newly installed)
|
|
116
|
+
'504': 'RAS504.HECRASController',
|
|
117
|
+
'5.0.5': 'RAS505.HECRASController', # ✓ EXISTS
|
|
118
|
+
'505': 'RAS505.HECRASController',
|
|
119
|
+
'5.0.6': 'RAS506.HECRASController', # ✓ EXISTS
|
|
120
|
+
'506': 'RAS506.HECRASController',
|
|
121
|
+
'5.0.7': 'RAS507.HECRASController', # ✓ EXISTS
|
|
122
|
+
'507': 'RAS507.HECRASController',
|
|
123
|
+
|
|
124
|
+
# HEC-RAS 6.x
|
|
125
|
+
'6.0': 'RAS60.HECRASController', # ✓ EXISTS
|
|
126
|
+
'60': 'RAS60.HECRASController',
|
|
127
|
+
'6.1': 'RAS60.HECRASController', # Use 6.0 (6.1 COM not registered)
|
|
128
|
+
'61': 'RAS60.HECRASController',
|
|
129
|
+
'6.2': 'RAS60.HECRASController', # Use 6.0 (6.2 COM not registered)
|
|
130
|
+
'62': 'RAS60.HECRASController',
|
|
131
|
+
'6.3': 'RAS631.HECRASController', # Use 6.3.1 (6.3 COM not registered)
|
|
132
|
+
'63': 'RAS631.HECRASController',
|
|
133
|
+
'6.3.1': 'RAS631.HECRASController', # ✓ EXISTS
|
|
134
|
+
'631': 'RAS631.HECRASController',
|
|
135
|
+
'6.4': 'RAS641.HECRASController', # Use 6.4.1 (6.4 COM not registered)
|
|
136
|
+
'64': 'RAS641.HECRASController',
|
|
137
|
+
'6.4.1': 'RAS641.HECRASController', # ✓ EXISTS
|
|
138
|
+
'641': 'RAS641.HECRASController',
|
|
139
|
+
'6.5': 'RAS65.HECRASController', # ✓ EXISTS
|
|
140
|
+
'65': 'RAS65.HECRASController',
|
|
141
|
+
'6.6': 'RAS66.HECRASController', # ✓ EXISTS
|
|
142
|
+
'66': 'RAS66.HECRASController',
|
|
143
|
+
'6.7': 'RAS67.HECRASController', # ✓ EXISTS
|
|
144
|
+
'67': 'RAS67.HECRASController',
|
|
145
|
+
'6.7 Beta 4': 'RAS67.HECRASController',
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
# Legacy reference (kept for backwards compatibility)
|
|
149
|
+
SUPPORTED_VERSIONS = VERSION_MAP
|
|
150
|
+
|
|
151
|
+
# Output variable codes
|
|
152
|
+
WSEL = 2
|
|
153
|
+
ENERGY = 3
|
|
154
|
+
MAX_CHL_DPTH = 4
|
|
155
|
+
MIN_CH_EL = 5
|
|
156
|
+
ENERGY_SLOPE = 6
|
|
157
|
+
FLOW_TOTAL = 24
|
|
158
|
+
VEL_TOTAL = 25
|
|
159
|
+
STA_WS_LFT = 36
|
|
160
|
+
STA_WS_RGT = 37
|
|
161
|
+
FROUDE_CHL = 48
|
|
162
|
+
FROUDE_XS = 49
|
|
163
|
+
Q_WEIR = 94
|
|
164
|
+
Q_CULVERT_TOT = 242
|
|
165
|
+
|
|
166
|
+
# ========== PRIVATE METHODS (HECRASController COM API) ==========
|
|
167
|
+
|
|
168
|
+
@staticmethod
|
|
169
|
+
def _normalize_version(version: str) -> str:
|
|
170
|
+
"""
|
|
171
|
+
Normalize version string to match VERSION_MAP keys.
|
|
172
|
+
|
|
173
|
+
Handles formats like:
|
|
174
|
+
"6.6", "66" → "6.6"
|
|
175
|
+
"4.1", "41" → "4.1"
|
|
176
|
+
"5.0.6", "506" → "5.0.6"
|
|
177
|
+
"6.7 Beta 4" → "6.7 Beta 4"
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Normalized version string that exists in VERSION_MAP
|
|
181
|
+
|
|
182
|
+
Raises:
|
|
183
|
+
ValueError: If version cannot be normalized or is not supported
|
|
184
|
+
"""
|
|
185
|
+
version_str = str(version).strip()
|
|
186
|
+
|
|
187
|
+
# Direct match
|
|
188
|
+
if version_str in RasControl.VERSION_MAP:
|
|
189
|
+
return version_str
|
|
190
|
+
|
|
191
|
+
# Try common normalizations
|
|
192
|
+
normalized_candidates = [
|
|
193
|
+
version_str,
|
|
194
|
+
version_str.replace('.', ''), # "6.6" → "66"
|
|
195
|
+
]
|
|
196
|
+
|
|
197
|
+
# Try adding periods for compact formats
|
|
198
|
+
if len(version_str) == 2: # "66" → "6.6"
|
|
199
|
+
normalized_candidates.append(f"{version_str[0]}.{version_str[1]}")
|
|
200
|
+
elif len(version_str) == 3 and version_str.startswith('5'): # "506" → "5.0.6"
|
|
201
|
+
normalized_candidates.append(f"5.0.{version_str[2]}")
|
|
202
|
+
elif len(version_str) == 3: # "631" → "6.3.1"
|
|
203
|
+
normalized_candidates.append(f"{version_str[0]}.{version_str[1]}.{version_str[2]}")
|
|
204
|
+
|
|
205
|
+
# Check all candidates
|
|
206
|
+
for candidate in normalized_candidates:
|
|
207
|
+
if candidate in RasControl.VERSION_MAP:
|
|
208
|
+
logger.debug(f"Normalized version '{version}' → '{candidate}'")
|
|
209
|
+
return candidate
|
|
210
|
+
|
|
211
|
+
# Not found
|
|
212
|
+
raise ValueError(
|
|
213
|
+
f"Version '{version}' not supported. Supported versions:\n"
|
|
214
|
+
f" 3.x: 3.0, 3.1 (3.1.1, 3.1.2, 3.1.3)\n"
|
|
215
|
+
f" 4.x: 4.0, 4.1\n"
|
|
216
|
+
f" 5.0.x: 5.0, 5.0.1, 5.0.3, 5.0.4, 5.0.5, 5.0.6, 5.0.7\n"
|
|
217
|
+
f" 6.x: 6.0, 6.1, 6.2, 6.3, 6.3.1, 6.4, 6.4.1, 6.5, 6.6, 6.7\n"
|
|
218
|
+
f" Formats: Can use '6.6' or '66', '5.0.6' or '506', etc."
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
@staticmethod
|
|
222
|
+
def _get_project_info(plan: Union[str, Path], ras_object=None):
|
|
223
|
+
"""
|
|
224
|
+
Resolve plan number/path to project path, version, and plan details.
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
Tuple[Path, str, str, str]: (project_path, version, plan_number, plan_name)
|
|
228
|
+
plan_number and plan_name are None if using direct .prj path
|
|
229
|
+
"""
|
|
230
|
+
if ras_object is None:
|
|
231
|
+
ras_object = ras
|
|
232
|
+
|
|
233
|
+
# If it's a path to .prj file
|
|
234
|
+
plan_path = Path(plan) if isinstance(plan, str) else plan
|
|
235
|
+
if plan_path.exists() and plan_path.suffix == '.prj':
|
|
236
|
+
# Direct path - need version from ras_object
|
|
237
|
+
if not hasattr(ras_object, 'ras_version') or not ras_object.ras_version:
|
|
238
|
+
raise ValueError(
|
|
239
|
+
"When using direct .prj paths, project must be initialized with version.\n"
|
|
240
|
+
"Use: init_ras_project(path, '4.1') or similar"
|
|
241
|
+
)
|
|
242
|
+
return plan_path, ras_object.ras_version, None, None
|
|
243
|
+
|
|
244
|
+
# Otherwise treat as plan number
|
|
245
|
+
plan_num = str(plan).zfill(2)
|
|
246
|
+
|
|
247
|
+
# Get project path from ras_object
|
|
248
|
+
if not hasattr(ras_object, 'prj_file') or not ras_object.prj_file:
|
|
249
|
+
raise ValueError(
|
|
250
|
+
"No project initialized. Use init_ras_project() first.\n"
|
|
251
|
+
"Example: init_ras_project(path, '4.1')"
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
project_path = Path(ras_object.prj_file)
|
|
255
|
+
|
|
256
|
+
# Get version
|
|
257
|
+
if not hasattr(ras_object, 'ras_version') or not ras_object.ras_version:
|
|
258
|
+
raise ValueError(
|
|
259
|
+
"Project initialized without version. Re-initialize with:\n"
|
|
260
|
+
"init_ras_project(path, '4.1') # or '41', '501', etc."
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
version = ras_object.ras_version
|
|
264
|
+
|
|
265
|
+
# Get plan name from plan_df
|
|
266
|
+
plan_row = ras_object.plan_df[ras_object.plan_df['plan_number'] == plan_num]
|
|
267
|
+
if plan_row.empty:
|
|
268
|
+
raise ValueError(f"Plan '{plan_num}' not found in project")
|
|
269
|
+
|
|
270
|
+
plan_name = plan_row['Plan Title'].iloc[0]
|
|
271
|
+
|
|
272
|
+
return project_path, version, plan_num, plan_name
|
|
273
|
+
|
|
274
|
+
@staticmethod
|
|
275
|
+
def _com_open_close(project_path: Path, version: str, operation_func: Callable[[Any], Any]) -> Any:
|
|
276
|
+
"""
|
|
277
|
+
PRIVATE: Open HEC-RAS via COM, run operation, close HEC-RAS.
|
|
278
|
+
|
|
279
|
+
This is the core COM interface handler. All public methods use this.
|
|
280
|
+
"""
|
|
281
|
+
# Normalize version (handles "6.6" → "6.6", "66" → "6.6", etc.)
|
|
282
|
+
normalized_version = RasControl._normalize_version(version)
|
|
283
|
+
|
|
284
|
+
if not project_path.exists():
|
|
285
|
+
raise FileNotFoundError(f"Project file not found: {project_path}")
|
|
286
|
+
|
|
287
|
+
com_rc = None
|
|
288
|
+
result = None
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
# Open HEC-RAS COM interface
|
|
292
|
+
com_string = RasControl.VERSION_MAP[normalized_version]
|
|
293
|
+
logger.info(f"Opening HEC-RAS: {com_string} (version: {version})")
|
|
294
|
+
com_rc = win32com.client.Dispatch(com_string)
|
|
295
|
+
|
|
296
|
+
# Open project
|
|
297
|
+
logger.info(f"Opening project: {project_path}")
|
|
298
|
+
com_rc.Project_Open(str(project_path))
|
|
299
|
+
|
|
300
|
+
# Perform operation
|
|
301
|
+
logger.info("Executing operation...")
|
|
302
|
+
result = operation_func(com_rc)
|
|
303
|
+
logger.info("Operation completed successfully")
|
|
304
|
+
|
|
305
|
+
return result
|
|
306
|
+
|
|
307
|
+
except Exception as e:
|
|
308
|
+
logger.error(f"Operation failed: {e}")
|
|
309
|
+
raise
|
|
310
|
+
|
|
311
|
+
finally:
|
|
312
|
+
# ALWAYS close
|
|
313
|
+
logger.info("Closing HEC-RAS...")
|
|
314
|
+
|
|
315
|
+
if com_rc is not None:
|
|
316
|
+
try:
|
|
317
|
+
com_rc.QuitRas()
|
|
318
|
+
logger.info("HEC-RAS closed via QuitRas()")
|
|
319
|
+
except Exception as e:
|
|
320
|
+
logger.warning(f"QuitRas() failed: {e}")
|
|
321
|
+
|
|
322
|
+
_terminate_ras_process()
|
|
323
|
+
|
|
324
|
+
if _is_ras_running():
|
|
325
|
+
logger.warning("HEC-RAS may still be running!")
|
|
326
|
+
else:
|
|
327
|
+
logger.info("HEC-RAS fully closed")
|
|
328
|
+
|
|
329
|
+
# ========== PUBLIC API (ras-commander style) ==========
|
|
330
|
+
|
|
331
|
+
@staticmethod
|
|
332
|
+
def run_plan(plan: Union[str, Path], ras_object=None) -> Tuple[bool, List[str]]:
|
|
333
|
+
"""
|
|
334
|
+
Run a plan (steady or unsteady) and wait for completion.
|
|
335
|
+
|
|
336
|
+
This method starts the computation and polls Compute_Complete()
|
|
337
|
+
until the run finishes. It will block until completion.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
plan: Plan number ("01", "02") or path to .prj file
|
|
341
|
+
ras_object: Optional RasPrj instance (uses global ras if None)
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
Tuple of (success: bool, messages: List[str])
|
|
345
|
+
|
|
346
|
+
Example:
|
|
347
|
+
>>> from ras_commander import init_ras_project, RasControl
|
|
348
|
+
>>> init_ras_project(path, "4.1")
|
|
349
|
+
>>> success, msgs = RasControl.run_plan("02")
|
|
350
|
+
>>> # Blocks until plan finishes running
|
|
351
|
+
|
|
352
|
+
Note:
|
|
353
|
+
Can take several minutes for large models or unsteady runs.
|
|
354
|
+
Progress is logged every 30 seconds.
|
|
355
|
+
"""
|
|
356
|
+
project_path, version, plan_num, plan_name = RasControl._get_project_info(plan, ras_object)
|
|
357
|
+
|
|
358
|
+
def _run_operation(com_rc):
|
|
359
|
+
# Set current plan if we have plan_name (using plan number)
|
|
360
|
+
if plan_name:
|
|
361
|
+
logger.info(f"Setting current plan to: {plan_name}")
|
|
362
|
+
com_rc.Plan_SetCurrent(plan_name)
|
|
363
|
+
|
|
364
|
+
# Version-specific behavior (normalize for checking)
|
|
365
|
+
norm_version = RasControl._normalize_version(version)
|
|
366
|
+
|
|
367
|
+
# Start computation (returns immediately - ASYNCHRONOUS!)
|
|
368
|
+
logger.info("Starting computation...")
|
|
369
|
+
if norm_version.startswith('4') or norm_version.startswith('3'):
|
|
370
|
+
status, _, messages = com_rc.Compute_CurrentPlan(None, None)
|
|
371
|
+
else:
|
|
372
|
+
status, _, messages, _ = com_rc.Compute_CurrentPlan(None, None)
|
|
373
|
+
|
|
374
|
+
# CRITICAL: Wait for computation to complete
|
|
375
|
+
# Compute_CurrentPlan is ASYNCHRONOUS - it returns before computation finishes
|
|
376
|
+
logger.info("Waiting for computation to complete...")
|
|
377
|
+
poll_count = 0
|
|
378
|
+
while True:
|
|
379
|
+
try:
|
|
380
|
+
# Check if computation is complete
|
|
381
|
+
is_complete = com_rc.Compute_Complete()
|
|
382
|
+
|
|
383
|
+
if is_complete:
|
|
384
|
+
logger.info(f"Computation completed (polled {poll_count} times)")
|
|
385
|
+
break
|
|
386
|
+
|
|
387
|
+
# Still computing - wait and poll again
|
|
388
|
+
time.sleep(1) # Poll every second
|
|
389
|
+
poll_count += 1
|
|
390
|
+
|
|
391
|
+
# Log progress every 30 seconds
|
|
392
|
+
if poll_count % 30 == 0:
|
|
393
|
+
logger.info(f"Still computing... ({poll_count} seconds elapsed)")
|
|
394
|
+
|
|
395
|
+
except Exception as e:
|
|
396
|
+
logger.error(f"Error checking completion status: {e}")
|
|
397
|
+
# If we can't check status, break and hope for the best
|
|
398
|
+
break
|
|
399
|
+
|
|
400
|
+
return status, list(messages) if messages else []
|
|
401
|
+
|
|
402
|
+
return RasControl._com_open_close(project_path, version, _run_operation)
|
|
403
|
+
|
|
404
|
+
@staticmethod
|
|
405
|
+
def get_steady_results(plan: Union[str, Path], ras_object=None) -> pd.DataFrame:
|
|
406
|
+
"""
|
|
407
|
+
Extract steady state profile results.
|
|
408
|
+
|
|
409
|
+
Args:
|
|
410
|
+
plan: Plan number ("01", "02") or path to .prj file
|
|
411
|
+
ras_object: Optional RasPrj instance (uses global ras if None)
|
|
412
|
+
|
|
413
|
+
Returns:
|
|
414
|
+
DataFrame with columns: river, reach, node_id, profile, wsel,
|
|
415
|
+
velocity, flow, froude, energy, max_depth, min_ch_el
|
|
416
|
+
|
|
417
|
+
Example:
|
|
418
|
+
>>> from ras_commander import init_ras_project, RasControl
|
|
419
|
+
>>> init_ras_project(path, "4.1")
|
|
420
|
+
>>> df = RasControl.get_steady_results("02")
|
|
421
|
+
>>> df.to_csv('results.csv', index=False)
|
|
422
|
+
"""
|
|
423
|
+
project_path, version, plan_num, plan_name = RasControl._get_project_info(plan, ras_object)
|
|
424
|
+
|
|
425
|
+
def _extract_operation(com_rc):
|
|
426
|
+
# Set current plan if we have plan_name (using plan number)
|
|
427
|
+
if plan_name:
|
|
428
|
+
logger.info(f"Setting current plan to: {plan_name}")
|
|
429
|
+
com_rc.Plan_SetCurrent(plan_name)
|
|
430
|
+
|
|
431
|
+
results = []
|
|
432
|
+
|
|
433
|
+
# Get profiles
|
|
434
|
+
_, profile_names = com_rc.Output_GetProfiles(2, None)
|
|
435
|
+
|
|
436
|
+
if profile_names is None:
|
|
437
|
+
raise RuntimeError(
|
|
438
|
+
"No steady state results found. Please ensure:\n"
|
|
439
|
+
" 1. The model has been run (use RasControl.run_plan() first)\n"
|
|
440
|
+
" 2. The current plan is a steady state plan\n"
|
|
441
|
+
" 3. Results were successfully computed"
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
profiles = [{'name': name, 'code': i+1} for i, name in enumerate(profile_names)]
|
|
445
|
+
logger.info(f"Found {len(profiles)} profiles")
|
|
446
|
+
|
|
447
|
+
# Get rivers
|
|
448
|
+
_, river_names = com_rc.Output_GetRivers(0, None)
|
|
449
|
+
|
|
450
|
+
if river_names is None:
|
|
451
|
+
raise RuntimeError("No river geometry found in model.")
|
|
452
|
+
|
|
453
|
+
logger.info(f"Found {len(river_names)} rivers")
|
|
454
|
+
|
|
455
|
+
# Extract data
|
|
456
|
+
for riv_code, riv_name in enumerate(river_names, start=1):
|
|
457
|
+
_, _, reach_names = com_rc.Geometry_GetReaches(riv_code, None, None)
|
|
458
|
+
|
|
459
|
+
for rch_code, rch_name in enumerate(reach_names, start=1):
|
|
460
|
+
_, _, _, node_ids, node_types = com_rc.Geometry_GetNodes(
|
|
461
|
+
riv_code, rch_code, None, None, None
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
for node_code, (node_id, node_type) in enumerate(
|
|
465
|
+
zip(node_ids, node_types), start=1
|
|
466
|
+
):
|
|
467
|
+
if node_type == '': # Cross sections only
|
|
468
|
+
for profile in profiles:
|
|
469
|
+
try:
|
|
470
|
+
row = {
|
|
471
|
+
'river': riv_name.strip(),
|
|
472
|
+
'reach': rch_name.strip(),
|
|
473
|
+
'node_id': node_id.strip(),
|
|
474
|
+
'profile': profile['name'].strip(),
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
# Extract output variables
|
|
478
|
+
row['wsel'] = com_rc.Output_NodeOutput(
|
|
479
|
+
riv_code, rch_code, node_code, 0,
|
|
480
|
+
profile['code'], RasControl.WSEL
|
|
481
|
+
)[0]
|
|
482
|
+
|
|
483
|
+
row['min_ch_el'] = com_rc.Output_NodeOutput(
|
|
484
|
+
riv_code, rch_code, node_code, 0,
|
|
485
|
+
profile['code'], RasControl.MIN_CH_EL
|
|
486
|
+
)[0]
|
|
487
|
+
|
|
488
|
+
row['velocity'] = com_rc.Output_NodeOutput(
|
|
489
|
+
riv_code, rch_code, node_code, 0,
|
|
490
|
+
profile['code'], RasControl.VEL_TOTAL
|
|
491
|
+
)[0]
|
|
492
|
+
|
|
493
|
+
row['flow'] = com_rc.Output_NodeOutput(
|
|
494
|
+
riv_code, rch_code, node_code, 0,
|
|
495
|
+
profile['code'], RasControl.FLOW_TOTAL
|
|
496
|
+
)[0]
|
|
497
|
+
|
|
498
|
+
row['froude'] = com_rc.Output_NodeOutput(
|
|
499
|
+
riv_code, rch_code, node_code, 0,
|
|
500
|
+
profile['code'], RasControl.FROUDE_CHL
|
|
501
|
+
)[0]
|
|
502
|
+
|
|
503
|
+
row['energy'] = com_rc.Output_NodeOutput(
|
|
504
|
+
riv_code, rch_code, node_code, 0,
|
|
505
|
+
profile['code'], RasControl.ENERGY
|
|
506
|
+
)[0]
|
|
507
|
+
|
|
508
|
+
row['max_depth'] = com_rc.Output_NodeOutput(
|
|
509
|
+
riv_code, rch_code, node_code, 0,
|
|
510
|
+
profile['code'], RasControl.MAX_CHL_DPTH
|
|
511
|
+
)[0]
|
|
512
|
+
|
|
513
|
+
results.append(row)
|
|
514
|
+
|
|
515
|
+
except Exception as e:
|
|
516
|
+
logger.warning(
|
|
517
|
+
f"Failed to extract {riv_name}/{rch_name}/"
|
|
518
|
+
f"{node_id} profile {profile['name']}: {e}"
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
logger.info(f"Extracted {len(results)} result rows")
|
|
522
|
+
return pd.DataFrame(results)
|
|
523
|
+
|
|
524
|
+
return RasControl._com_open_close(project_path, version, _extract_operation)
|
|
525
|
+
|
|
526
|
+
@staticmethod
|
|
527
|
+
def get_unsteady_results(plan: Union[str, Path], max_times: int = None,
|
|
528
|
+
ras_object=None) -> pd.DataFrame:
|
|
529
|
+
"""
|
|
530
|
+
Extract unsteady flow time series results.
|
|
531
|
+
|
|
532
|
+
Args:
|
|
533
|
+
plan: Plan number ("01", "02") or path to .prj file
|
|
534
|
+
max_times: Optional limit on timesteps (for testing/large datasets)
|
|
535
|
+
ras_object: Optional RasPrj instance (uses global ras if None)
|
|
536
|
+
|
|
537
|
+
Returns:
|
|
538
|
+
DataFrame with columns: river, reach, node_id, time_index, time_string,
|
|
539
|
+
wsel, velocity, flow, froude, energy, max_depth, min_ch_el
|
|
540
|
+
|
|
541
|
+
Important - Understanding "Max WS":
|
|
542
|
+
The first row (time_index=1, time_string="Max WS") contains the MAXIMUM
|
|
543
|
+
values that occurred at ANY computational timestep during the simulation,
|
|
544
|
+
not just at output intervals. This is critical data for identifying peaks.
|
|
545
|
+
|
|
546
|
+
For time series plotting, filter to actual timesteps:
|
|
547
|
+
df_timeseries = df[df['time_string'] != 'Max WS']
|
|
548
|
+
|
|
549
|
+
For showing maximum as reference line:
|
|
550
|
+
max_wse = df[df['time_string'] == 'Max WS']['wsel'].iloc[0]
|
|
551
|
+
plt.axhline(max_wse, color='r', linestyle='--', label='Max WS')
|
|
552
|
+
|
|
553
|
+
Example:
|
|
554
|
+
>>> from ras_commander import init_ras_project, RasControl
|
|
555
|
+
>>> init_ras_project(path, "4.1")
|
|
556
|
+
>>> df = RasControl.get_unsteady_results("01", max_times=10)
|
|
557
|
+
>>> # Includes "Max WS" as first row
|
|
558
|
+
"""
|
|
559
|
+
project_path, version, plan_num, plan_name = RasControl._get_project_info(plan, ras_object)
|
|
560
|
+
|
|
561
|
+
def _extract_operation(com_rc):
|
|
562
|
+
# Set current plan if we have plan_name (using plan number)
|
|
563
|
+
if plan_name:
|
|
564
|
+
logger.info(f"Setting current plan to: {plan_name}")
|
|
565
|
+
com_rc.Plan_SetCurrent(plan_name)
|
|
566
|
+
|
|
567
|
+
results = []
|
|
568
|
+
|
|
569
|
+
# Get output times
|
|
570
|
+
_, time_strings = com_rc.Output_GetProfiles(0, None)
|
|
571
|
+
|
|
572
|
+
if time_strings is None:
|
|
573
|
+
raise RuntimeError(
|
|
574
|
+
"No unsteady results found. Please ensure:\n"
|
|
575
|
+
" 1. The model has been run (use RasControl.run_plan() first)\n"
|
|
576
|
+
" 2. The current plan is an unsteady flow plan\n"
|
|
577
|
+
" 3. Results were successfully computed"
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
times = list(time_strings)
|
|
581
|
+
if max_times:
|
|
582
|
+
times = times[:max_times]
|
|
583
|
+
|
|
584
|
+
logger.info(f"Extracting {len(times)} time steps")
|
|
585
|
+
|
|
586
|
+
# Get rivers
|
|
587
|
+
_, river_names = com_rc.Output_GetRivers(0, None)
|
|
588
|
+
|
|
589
|
+
if river_names is None:
|
|
590
|
+
raise RuntimeError("No river geometry found in model.")
|
|
591
|
+
|
|
592
|
+
logger.info(f"Found {len(river_names)} rivers")
|
|
593
|
+
|
|
594
|
+
# Extract data
|
|
595
|
+
for riv_code, riv_name in enumerate(river_names, start=1):
|
|
596
|
+
_, _, reach_names = com_rc.Geometry_GetReaches(riv_code, None, None)
|
|
597
|
+
|
|
598
|
+
for rch_code, rch_name in enumerate(reach_names, start=1):
|
|
599
|
+
_, _, _, node_ids, node_types = com_rc.Geometry_GetNodes(
|
|
600
|
+
riv_code, rch_code, None, None, None
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
for node_code, (node_id, node_type) in enumerate(
|
|
604
|
+
zip(node_ids, node_types), start=1
|
|
605
|
+
):
|
|
606
|
+
if node_type == '': # Cross sections only
|
|
607
|
+
for time_idx, time_str in enumerate(times, start=1):
|
|
608
|
+
try:
|
|
609
|
+
row = {
|
|
610
|
+
'river': riv_name.strip(),
|
|
611
|
+
'reach': rch_name.strip(),
|
|
612
|
+
'node_id': node_id.strip(),
|
|
613
|
+
'time_index': time_idx,
|
|
614
|
+
'time_string': time_str.strip(),
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
# Extract output variables (time_idx is profile code for unsteady)
|
|
618
|
+
row['wsel'] = com_rc.Output_NodeOutput(
|
|
619
|
+
riv_code, rch_code, node_code, 0,
|
|
620
|
+
time_idx, RasControl.WSEL
|
|
621
|
+
)[0]
|
|
622
|
+
|
|
623
|
+
row['min_ch_el'] = com_rc.Output_NodeOutput(
|
|
624
|
+
riv_code, rch_code, node_code, 0,
|
|
625
|
+
time_idx, RasControl.MIN_CH_EL
|
|
626
|
+
)[0]
|
|
627
|
+
|
|
628
|
+
row['velocity'] = com_rc.Output_NodeOutput(
|
|
629
|
+
riv_code, rch_code, node_code, 0,
|
|
630
|
+
time_idx, RasControl.VEL_TOTAL
|
|
631
|
+
)[0]
|
|
632
|
+
|
|
633
|
+
row['flow'] = com_rc.Output_NodeOutput(
|
|
634
|
+
riv_code, rch_code, node_code, 0,
|
|
635
|
+
time_idx, RasControl.FLOW_TOTAL
|
|
636
|
+
)[0]
|
|
637
|
+
|
|
638
|
+
row['froude'] = com_rc.Output_NodeOutput(
|
|
639
|
+
riv_code, rch_code, node_code, 0,
|
|
640
|
+
time_idx, RasControl.FROUDE_CHL
|
|
641
|
+
)[0]
|
|
642
|
+
|
|
643
|
+
row['energy'] = com_rc.Output_NodeOutput(
|
|
644
|
+
riv_code, rch_code, node_code, 0,
|
|
645
|
+
time_idx, RasControl.ENERGY
|
|
646
|
+
)[0]
|
|
647
|
+
|
|
648
|
+
row['max_depth'] = com_rc.Output_NodeOutput(
|
|
649
|
+
riv_code, rch_code, node_code, 0,
|
|
650
|
+
time_idx, RasControl.MAX_CHL_DPTH
|
|
651
|
+
)[0]
|
|
652
|
+
|
|
653
|
+
results.append(row)
|
|
654
|
+
|
|
655
|
+
except Exception as e:
|
|
656
|
+
logger.warning(
|
|
657
|
+
f"Failed to extract {riv_name}/{rch_name}/"
|
|
658
|
+
f"{node_id} time {time_str}: {e}"
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
logger.info(f"Extracted {len(results)} result rows")
|
|
662
|
+
return pd.DataFrame(results)
|
|
663
|
+
|
|
664
|
+
return RasControl._com_open_close(project_path, version, _extract_operation)
|
|
665
|
+
|
|
666
|
+
@staticmethod
|
|
667
|
+
def get_output_times(plan: Union[str, Path], ras_object=None) -> List[str]:
|
|
668
|
+
"""
|
|
669
|
+
Get list of output times for unsteady run.
|
|
670
|
+
|
|
671
|
+
Args:
|
|
672
|
+
plan: Plan number ("01", "02") or path to .prj file
|
|
673
|
+
ras_object: Optional RasPrj instance (uses global ras if None)
|
|
674
|
+
|
|
675
|
+
Returns:
|
|
676
|
+
List of time strings (e.g., ["01JAN2000 0000", ...])
|
|
677
|
+
|
|
678
|
+
Example:
|
|
679
|
+
>>> times = RasControl.get_output_times("01")
|
|
680
|
+
>>> print(f"Found {len(times)} output times")
|
|
681
|
+
"""
|
|
682
|
+
project_path, version, plan_num, plan_name = RasControl._get_project_info(plan, ras_object)
|
|
683
|
+
|
|
684
|
+
def _get_times(com_rc):
|
|
685
|
+
# Set current plan if we have plan_name (using plan number)
|
|
686
|
+
if plan_name:
|
|
687
|
+
logger.info(f"Setting current plan to: {plan_name}")
|
|
688
|
+
com_rc.Plan_SetCurrent(plan_name)
|
|
689
|
+
|
|
690
|
+
_, time_strings = com_rc.Output_GetProfiles(0, None)
|
|
691
|
+
|
|
692
|
+
if time_strings is None:
|
|
693
|
+
raise RuntimeError(
|
|
694
|
+
"No unsteady output times found. Ensure plan has been run."
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
times = list(time_strings)
|
|
698
|
+
logger.info(f"Found {len(times)} output times")
|
|
699
|
+
return times
|
|
700
|
+
|
|
701
|
+
return RasControl._com_open_close(project_path, version, _get_times)
|
|
702
|
+
|
|
703
|
+
@staticmethod
|
|
704
|
+
def get_plans(plan: Union[str, Path], ras_object=None) -> List[dict]:
|
|
705
|
+
"""
|
|
706
|
+
Get list of plans in project.
|
|
707
|
+
|
|
708
|
+
Args:
|
|
709
|
+
plan: Plan number or path to .prj file
|
|
710
|
+
ras_object: Optional RasPrj instance
|
|
711
|
+
|
|
712
|
+
Returns:
|
|
713
|
+
List of dicts with 'name' and 'filename' keys
|
|
714
|
+
"""
|
|
715
|
+
project_path, version, _, _ = RasControl._get_project_info(plan, ras_object)
|
|
716
|
+
|
|
717
|
+
def _get_plans(com_rc):
|
|
718
|
+
# Don't set current plan - just getting list
|
|
719
|
+
_, plan_names, _ = com_rc.Plan_Names(None, None, None)
|
|
720
|
+
|
|
721
|
+
plans = []
|
|
722
|
+
for name in plan_names:
|
|
723
|
+
filename, _ = com_rc.Plan_GetFilename(name)
|
|
724
|
+
plans.append({'name': name, 'filename': filename})
|
|
725
|
+
|
|
726
|
+
logger.info(f"Found {len(plans)} plans")
|
|
727
|
+
return plans
|
|
728
|
+
|
|
729
|
+
return RasControl._com_open_close(project_path, version, _get_plans)
|
|
730
|
+
|
|
731
|
+
@staticmethod
|
|
732
|
+
def set_current_plan(plan: Union[str, Path], ras_object=None) -> bool:
|
|
733
|
+
"""
|
|
734
|
+
Set the current/active plan by plan number.
|
|
735
|
+
|
|
736
|
+
Note: This is rarely needed - run_plan() and get_*_results()
|
|
737
|
+
automatically set the correct plan. This is provided for
|
|
738
|
+
advanced use cases.
|
|
739
|
+
|
|
740
|
+
Args:
|
|
741
|
+
plan: Plan number ("01", "02") or path to .prj file
|
|
742
|
+
ras_object: Optional RasPrj instance
|
|
743
|
+
|
|
744
|
+
Returns:
|
|
745
|
+
True if successful
|
|
746
|
+
|
|
747
|
+
Example:
|
|
748
|
+
>>> RasControl.set_current_plan("02") # Set to Plan 02
|
|
749
|
+
"""
|
|
750
|
+
project_path, version, plan_num, plan_name = RasControl._get_project_info(plan, ras_object)
|
|
751
|
+
|
|
752
|
+
if not plan_name:
|
|
753
|
+
raise ValueError("Cannot set current plan - plan name could not be determined")
|
|
754
|
+
|
|
755
|
+
def _set_plan(com_rc):
|
|
756
|
+
com_rc.Plan_SetCurrent(plan_name)
|
|
757
|
+
logger.info(f"Set current plan to Plan {plan_num}: {plan_name}")
|
|
758
|
+
return True
|
|
759
|
+
|
|
760
|
+
return RasControl._com_open_close(project_path, version, _set_plan)
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
if __name__ == '__main__':
|
|
764
|
+
logging.basicConfig(
|
|
765
|
+
level=logging.INFO,
|
|
766
|
+
format='%(asctime)s - %(levelname)s - %(message)s'
|
|
767
|
+
)
|
|
768
|
+
|
|
769
|
+
print("RasControl (ras-commander API) loaded successfully")
|
|
770
|
+
print(f"Supported versions: {list(RasControl.SUPPORTED_VERSIONS.keys())}")
|
|
771
|
+
print("\nUsage example:")
|
|
772
|
+
print(" from ras_commander import init_ras_project, RasControl")
|
|
773
|
+
print(" init_ras_project(path, '4.1')")
|
|
774
|
+
print(" df = RasControl.get_steady_results('02')")
|
ras_commander/RasPlan.py
CHANGED
|
@@ -652,29 +652,44 @@ class RasPlan:
|
|
|
652
652
|
|
|
653
653
|
@staticmethod
|
|
654
654
|
@log_call
|
|
655
|
-
def clone_plan(template_plan,
|
|
655
|
+
def clone_plan(template_plan, new_shortid=None, new_title=None, ras_object=None):
|
|
656
656
|
"""
|
|
657
657
|
Create a new plan file based on a template and update the project file.
|
|
658
|
-
|
|
658
|
+
|
|
659
659
|
Parameters:
|
|
660
660
|
template_plan (str): Plan number to use as template (e.g., '01')
|
|
661
|
-
|
|
661
|
+
new_shortid (str, optional): New short identifier for the plan file (max 24 chars).
|
|
662
|
+
If not provided, appends '_copy' to original.
|
|
663
|
+
new_title (str, optional): New plan title (max 32 chars, updates "Plan Title=" line).
|
|
664
|
+
If not provided, keeps original title.
|
|
662
665
|
ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
|
|
663
|
-
|
|
666
|
+
|
|
664
667
|
Returns:
|
|
665
668
|
str: New plan number
|
|
666
|
-
|
|
669
|
+
|
|
667
670
|
Example:
|
|
668
|
-
>>>
|
|
669
|
-
>>>
|
|
670
|
-
>>>
|
|
671
|
+
>>> # Clone with default shortid and title
|
|
672
|
+
>>> new_plan = RasPlan.clone_plan('01')
|
|
673
|
+
>>>
|
|
674
|
+
>>> # Clone with custom shortid and title
|
|
675
|
+
>>> new_plan = RasPlan.clone_plan('01',
|
|
676
|
+
... new_shortid='Steady_v41',
|
|
677
|
+
... new_title='Steady Flow - HEC-RAS 4.1')
|
|
671
678
|
|
|
672
679
|
Note:
|
|
680
|
+
Both new_shortid and new_title are optional.
|
|
673
681
|
This function updates the ras object's dataframes after modifying the project structure.
|
|
674
682
|
"""
|
|
675
683
|
ras_obj = ras_object or ras
|
|
676
684
|
ras_obj.check_initialized()
|
|
677
685
|
|
|
686
|
+
# Validate new_title length if provided
|
|
687
|
+
if new_title is not None and len(new_title) > 32:
|
|
688
|
+
raise ValueError(
|
|
689
|
+
f"Plan title must be 32 characters or less. "
|
|
690
|
+
f"Got {len(new_title)} characters: '{new_title}'"
|
|
691
|
+
)
|
|
692
|
+
|
|
678
693
|
# Update plan entries without reinitializing the entire project
|
|
679
694
|
ras_obj.plan_df = ras_obj.get_prj_entries('Plan')
|
|
680
695
|
|
|
@@ -682,22 +697,32 @@ class RasPlan:
|
|
|
682
697
|
template_plan_path = ras_obj.project_folder / f"{ras_obj.project_name}.p{template_plan}"
|
|
683
698
|
new_plan_path = ras_obj.project_folder / f"{ras_obj.project_name}.p{new_plan_num}"
|
|
684
699
|
|
|
685
|
-
def
|
|
700
|
+
def update_plan_metadata(lines):
|
|
701
|
+
"""Update both Plan Title and Short Identifier"""
|
|
702
|
+
title_pattern = re.compile(r'^Plan Title=(.*)$', re.IGNORECASE)
|
|
686
703
|
shortid_pattern = re.compile(r'^Short Identifier=(.*)$', re.IGNORECASE)
|
|
704
|
+
|
|
687
705
|
for i, line in enumerate(lines):
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
706
|
+
# Update Plan Title if new_title provided
|
|
707
|
+
title_match = title_pattern.match(line.strip())
|
|
708
|
+
if title_match and new_title is not None:
|
|
709
|
+
lines[i] = f"Plan Title={new_title[:32]}\n"
|
|
710
|
+
continue
|
|
711
|
+
|
|
712
|
+
# Update Short Identifier
|
|
713
|
+
shortid_match = shortid_pattern.match(line.strip())
|
|
714
|
+
if shortid_match:
|
|
715
|
+
current_shortid = shortid_match.group(1)
|
|
716
|
+
if new_shortid is None:
|
|
717
|
+
new_shortid_value = (current_shortid + "_copy")[:24]
|
|
693
718
|
else:
|
|
694
|
-
|
|
695
|
-
lines[i] = f"Short Identifier={
|
|
696
|
-
|
|
719
|
+
new_shortid_value = new_shortid[:24]
|
|
720
|
+
lines[i] = f"Short Identifier={new_shortid_value}\n"
|
|
721
|
+
|
|
697
722
|
return lines
|
|
698
723
|
|
|
699
|
-
# Use RasUtils to clone the file and update
|
|
700
|
-
RasUtils.clone_file(template_plan_path, new_plan_path,
|
|
724
|
+
# Use RasUtils to clone the file and update metadata
|
|
725
|
+
RasUtils.clone_file(template_plan_path, new_plan_path, update_plan_metadata)
|
|
701
726
|
|
|
702
727
|
# Use RasUtils to update the project file
|
|
703
728
|
RasUtils.update_project_file(ras_obj.prj_file, 'Plan', new_plan_num, ras_object=ras_obj)
|
|
@@ -714,21 +739,22 @@ class RasPlan:
|
|
|
714
739
|
|
|
715
740
|
@staticmethod
|
|
716
741
|
@log_call
|
|
717
|
-
def clone_unsteady(template_unsteady, ras_object=None):
|
|
742
|
+
def clone_unsteady(template_unsteady, new_title=None, ras_object=None):
|
|
718
743
|
"""
|
|
719
744
|
Copy unsteady flow files from a template, find the next unsteady number,
|
|
720
745
|
and update the project file accordingly.
|
|
721
746
|
|
|
722
747
|
Parameters:
|
|
723
748
|
template_unsteady (str): Unsteady flow number to be used as a template (e.g., '01')
|
|
749
|
+
new_title (str, optional): New flow title (max 32 chars, updates "Flow Title=" line)
|
|
724
750
|
ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
|
|
725
751
|
|
|
726
752
|
Returns:
|
|
727
753
|
str: New unsteady flow number (e.g., '03')
|
|
728
754
|
|
|
729
755
|
Example:
|
|
730
|
-
>>>
|
|
731
|
-
|
|
756
|
+
>>> new_unsteady_num = RasPlan.clone_unsteady('01',
|
|
757
|
+
... new_title='Unsteady - HEC-RAS 4.1')
|
|
732
758
|
>>> print(f"New unsteady flow file created: u{new_unsteady_num}")
|
|
733
759
|
|
|
734
760
|
Note:
|
|
@@ -737,6 +763,13 @@ class RasPlan:
|
|
|
737
763
|
ras_obj = ras_object or ras
|
|
738
764
|
ras_obj.check_initialized()
|
|
739
765
|
|
|
766
|
+
# Validate new_title length if provided
|
|
767
|
+
if new_title is not None and len(new_title) > 32:
|
|
768
|
+
raise ValueError(
|
|
769
|
+
f"Flow title must be 32 characters or less. "
|
|
770
|
+
f"Got {len(new_title)} characters: '{new_title}'"
|
|
771
|
+
)
|
|
772
|
+
|
|
740
773
|
# Update unsteady entries without reinitializing the entire project
|
|
741
774
|
ras_obj.unsteady_df = ras_obj.get_prj_entries('Unsteady')
|
|
742
775
|
|
|
@@ -744,8 +777,21 @@ class RasPlan:
|
|
|
744
777
|
template_unsteady_path = ras_obj.project_folder / f"{ras_obj.project_name}.u{template_unsteady}"
|
|
745
778
|
new_unsteady_path = ras_obj.project_folder / f"{ras_obj.project_name}.u{new_unsteady_num}"
|
|
746
779
|
|
|
747
|
-
|
|
748
|
-
|
|
780
|
+
def update_flow_title(lines):
|
|
781
|
+
"""Update Flow Title if new_title provided"""
|
|
782
|
+
if new_title is None:
|
|
783
|
+
return lines
|
|
784
|
+
|
|
785
|
+
title_pattern = re.compile(r'^Flow Title=(.*)$', re.IGNORECASE)
|
|
786
|
+
for i, line in enumerate(lines):
|
|
787
|
+
title_match = title_pattern.match(line.strip())
|
|
788
|
+
if title_match:
|
|
789
|
+
lines[i] = f"Flow Title={new_title[:32]}\n"
|
|
790
|
+
break
|
|
791
|
+
return lines
|
|
792
|
+
|
|
793
|
+
# Use RasUtils to clone the file and update flow title
|
|
794
|
+
RasUtils.clone_file(template_unsteady_path, new_unsteady_path, update_flow_title)
|
|
749
795
|
|
|
750
796
|
# Copy the corresponding .hdf file if it exists
|
|
751
797
|
template_hdf_path = ras_obj.project_folder / f"{ras_obj.project_name}.u{template_unsteady}.hdf"
|
|
@@ -769,21 +815,22 @@ class RasPlan:
|
|
|
769
815
|
|
|
770
816
|
@staticmethod
|
|
771
817
|
@log_call
|
|
772
|
-
def clone_steady(template_flow, ras_object=None):
|
|
818
|
+
def clone_steady(template_flow, new_title=None, ras_object=None):
|
|
773
819
|
"""
|
|
774
820
|
Copy steady flow files from a template, find the next flow number,
|
|
775
821
|
and update the project file accordingly.
|
|
776
|
-
|
|
822
|
+
|
|
777
823
|
Parameters:
|
|
778
824
|
template_flow (str): Flow number to be used as a template (e.g., '01')
|
|
825
|
+
new_title (str, optional): New flow title (max 32 chars, updates "Flow Title=" line)
|
|
779
826
|
ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
|
|
780
|
-
|
|
827
|
+
|
|
781
828
|
Returns:
|
|
782
829
|
str: New flow number (e.g., '03')
|
|
783
830
|
|
|
784
831
|
Example:
|
|
785
|
-
>>>
|
|
786
|
-
|
|
832
|
+
>>> new_flow_num = RasPlan.clone_steady('01',
|
|
833
|
+
... new_title='Steady Flow - HEC-RAS 4.1')
|
|
787
834
|
>>> print(f"New steady flow file created: f{new_flow_num}")
|
|
788
835
|
|
|
789
836
|
Note:
|
|
@@ -792,6 +839,13 @@ class RasPlan:
|
|
|
792
839
|
ras_obj = ras_object or ras
|
|
793
840
|
ras_obj.check_initialized()
|
|
794
841
|
|
|
842
|
+
# Validate new_title length if provided
|
|
843
|
+
if new_title is not None and len(new_title) > 32:
|
|
844
|
+
raise ValueError(
|
|
845
|
+
f"Flow title must be 32 characters or less. "
|
|
846
|
+
f"Got {len(new_title)} characters: '{new_title}'"
|
|
847
|
+
)
|
|
848
|
+
|
|
795
849
|
# Update flow entries without reinitializing the entire project
|
|
796
850
|
ras_obj.flow_df = ras_obj.get_prj_entries('Flow')
|
|
797
851
|
|
|
@@ -799,8 +853,21 @@ class RasPlan:
|
|
|
799
853
|
template_flow_path = ras_obj.project_folder / f"{ras_obj.project_name}.f{template_flow}"
|
|
800
854
|
new_flow_path = ras_obj.project_folder / f"{ras_obj.project_name}.f{new_flow_num}"
|
|
801
855
|
|
|
802
|
-
|
|
803
|
-
|
|
856
|
+
def update_flow_title(lines):
|
|
857
|
+
"""Update Flow Title if new_title provided"""
|
|
858
|
+
if new_title is None:
|
|
859
|
+
return lines
|
|
860
|
+
|
|
861
|
+
title_pattern = re.compile(r'^Flow Title=(.*)$', re.IGNORECASE)
|
|
862
|
+
for i, line in enumerate(lines):
|
|
863
|
+
title_match = title_pattern.match(line.strip())
|
|
864
|
+
if title_match:
|
|
865
|
+
lines[i] = f"Flow Title={new_title[:32]}\n"
|
|
866
|
+
break
|
|
867
|
+
return lines
|
|
868
|
+
|
|
869
|
+
# Use RasUtils to clone the file and update flow title
|
|
870
|
+
RasUtils.clone_file(template_flow_path, new_flow_path, update_flow_title)
|
|
804
871
|
|
|
805
872
|
# Use RasUtils to update the project file
|
|
806
873
|
RasUtils.update_project_file(ras_obj.prj_file, 'Flow', new_flow_num, ras_object=ras_obj)
|
ras_commander/RasPrj.py
CHANGED
|
@@ -1452,12 +1452,17 @@ def init_ras_project(ras_project_folder, ras_version=None, ras_object=None):
|
|
|
1452
1452
|
else:
|
|
1453
1453
|
ras_object.initialize(project_folder, ras_exe_path)
|
|
1454
1454
|
|
|
1455
|
+
# Store version for RasControl (legacy COM interface support)
|
|
1456
|
+
ras_object.ras_version = ras_version if ras_version else detected_version
|
|
1457
|
+
|
|
1455
1458
|
# Always update the global ras object as well
|
|
1456
1459
|
if ras_object is not ras:
|
|
1457
1460
|
if specified_prj_file is not None:
|
|
1458
1461
|
ras.initialize(project_folder, ras_exe_path, prj_file=specified_prj_file)
|
|
1459
1462
|
else:
|
|
1460
1463
|
ras.initialize(project_folder, ras_exe_path)
|
|
1464
|
+
# Also store version in global ras object
|
|
1465
|
+
ras.ras_version = ras_version if ras_version else detected_version
|
|
1461
1466
|
logger.debug("Global 'ras' object also updated to match the new project.")
|
|
1462
1467
|
|
|
1463
1468
|
logger.debug(f"Project initialized. Project folder: {ras_object.project_folder}")
|
|
@@ -1501,60 +1506,92 @@ def get_ras_exe(ras_version=None):
|
|
|
1501
1506
|
logger.warning(f"HEC-RAS is not installed or version not specified. Running HEC-RAS will fail unless a valid installed version is specified.")
|
|
1502
1507
|
return default_path
|
|
1503
1508
|
|
|
1504
|
-
|
|
1505
|
-
|
|
1509
|
+
# ACTUAL folder names in C:/Program Files (x86)/HEC/HEC-RAS/
|
|
1510
|
+
# This list matches the exact folder names on disk (verified 2025-10-30)
|
|
1511
|
+
ras_version_folders = [
|
|
1512
|
+
"6.7 Beta 4", "6.6", "6.5", "6.4.1", "6.3.1", "6.3", "6.2", "6.1", "6.0",
|
|
1506
1513
|
"5.0.7", "5.0.6", "5.0.5", "5.0.4", "5.0.3", "5.0.1", "5.0",
|
|
1507
|
-
"4.1
|
|
1514
|
+
"4.1.0", "4.0"
|
|
1508
1515
|
]
|
|
1509
|
-
|
|
1516
|
+
|
|
1517
|
+
# User-friendly aliases (user_input → actual_folder_name)
|
|
1518
|
+
# Allows users to pass "4.1" and find "4.1.0" folder, or "66" to find "6.6"
|
|
1519
|
+
version_aliases = {
|
|
1520
|
+
# 4.x aliases
|
|
1521
|
+
"4.1": "4.1.0", # User passes "4.1" → finds "4.1.0" folder
|
|
1522
|
+
"41": "4.1.0", # Compact format
|
|
1523
|
+
"410": "4.1.0", # Full compact
|
|
1524
|
+
"40": "4.0", # Compact format for 4.0
|
|
1525
|
+
|
|
1526
|
+
# 5.0.x aliases
|
|
1527
|
+
"50": "5.0", # Compact format
|
|
1528
|
+
"501": "5.0.1", # Compact format for 5.0.1
|
|
1529
|
+
"503": "5.0.3", # Compact format
|
|
1530
|
+
"504": "5.0.4", # Compact format for 5.0.4
|
|
1531
|
+
"505": "5.0.5",
|
|
1532
|
+
"506": "5.0.6",
|
|
1533
|
+
"507": "5.0.7",
|
|
1534
|
+
|
|
1535
|
+
# 6.x aliases
|
|
1536
|
+
"60": "6.0",
|
|
1537
|
+
"61": "6.1",
|
|
1538
|
+
"62": "6.2",
|
|
1539
|
+
"63": "6.3",
|
|
1540
|
+
"631": "6.3.1",
|
|
1541
|
+
"6.4": "6.4.1", # No 6.4 folder, use 6.4.1
|
|
1542
|
+
"64": "6.4.1",
|
|
1543
|
+
"641": "6.4.1",
|
|
1544
|
+
"65": "6.5",
|
|
1545
|
+
"66": "6.6",
|
|
1546
|
+
"6.7": "6.7 Beta 4", # User passes "6.7" → finds "6.7 Beta 4"
|
|
1547
|
+
"67": "6.7 Beta 4",
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1510
1550
|
# Check if input is a direct path to an executable
|
|
1511
1551
|
hecras_path = Path(ras_version)
|
|
1512
1552
|
if hecras_path.is_file() and hecras_path.suffix.lower() == '.exe':
|
|
1513
1553
|
logger.debug(f"HEC-RAS executable found at specified path: {hecras_path}")
|
|
1514
1554
|
return str(hecras_path)
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1555
|
+
|
|
1556
|
+
version_str = str(ras_version)
|
|
1557
|
+
|
|
1558
|
+
# Check if there's an alias for this version
|
|
1559
|
+
if version_str in version_aliases:
|
|
1560
|
+
actual_folder = version_aliases[version_str]
|
|
1561
|
+
logger.debug(f"Mapped version '{version_str}' to folder '{actual_folder}'")
|
|
1562
|
+
version_str = actual_folder
|
|
1563
|
+
|
|
1564
|
+
# Check if this is a known folder name
|
|
1565
|
+
if version_str in ras_version_folders:
|
|
1566
|
+
default_path = Path(f"C:/Program Files (x86)/HEC/HEC-RAS/{version_str}/Ras.exe")
|
|
1519
1567
|
if default_path.is_file():
|
|
1520
1568
|
logger.debug(f"HEC-RAS executable found at default path: {default_path}")
|
|
1521
1569
|
return str(default_path)
|
|
1522
1570
|
else:
|
|
1523
|
-
error_msg = f"HEC-RAS Version {
|
|
1571
|
+
error_msg = f"HEC-RAS Version {version_str} folder exists but Ras.exe not found at expected path. Running HEC-RAS will fail."
|
|
1524
1572
|
logger.error(error_msg)
|
|
1525
1573
|
return "Ras.exe"
|
|
1526
1574
|
|
|
1527
|
-
# Try to
|
|
1575
|
+
# Final fallback: Try to find a matching version from folder list
|
|
1528
1576
|
try:
|
|
1529
|
-
# First check if it's a direct version number
|
|
1530
|
-
version_str = str(ras_version)
|
|
1531
|
-
|
|
1532
|
-
# Check for paths like "C:/Path/To/Ras.exe"
|
|
1533
|
-
if os.path.sep in version_str and version_str.lower().endswith('.exe'):
|
|
1534
|
-
exe_path = Path(version_str)
|
|
1535
|
-
if exe_path.is_file():
|
|
1536
|
-
logger.debug(f"HEC-RAS executable found at specified path: {exe_path}")
|
|
1537
|
-
return str(exe_path)
|
|
1538
|
-
|
|
1539
1577
|
# Try to find a matching version from our list
|
|
1540
|
-
for
|
|
1541
|
-
|
|
1542
|
-
|
|
1578
|
+
for known_folder in ras_version_folders:
|
|
1579
|
+
# Check for partial matches or compact formats
|
|
1580
|
+
if version_str in known_folder or known_folder.replace('.', '') == version_str:
|
|
1581
|
+
default_path = Path(f"C:/Program Files (x86)/HEC/HEC-RAS/{known_folder}/Ras.exe")
|
|
1543
1582
|
if default_path.is_file():
|
|
1544
|
-
logger.debug(f"HEC-RAS executable found
|
|
1583
|
+
logger.debug(f"HEC-RAS executable found via fuzzy match: {default_path}")
|
|
1545
1584
|
return str(default_path)
|
|
1546
|
-
|
|
1547
|
-
#
|
|
1585
|
+
|
|
1586
|
+
# Try direct path construction for newer versions
|
|
1548
1587
|
if '.' in version_str:
|
|
1549
|
-
|
|
1550
|
-
if
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
logger.debug(f"HEC-RAS executable found at path for newer version: {default_path}")
|
|
1554
|
-
return str(default_path)
|
|
1588
|
+
default_path = Path(f"C:/Program Files (x86)/HEC/HEC-RAS/{version_str}/Ras.exe")
|
|
1589
|
+
if default_path.is_file():
|
|
1590
|
+
logger.debug(f"HEC-RAS executable found at path: {default_path}")
|
|
1591
|
+
return str(default_path)
|
|
1555
1592
|
except Exception as e:
|
|
1556
1593
|
logger.error(f"Error parsing version or finding path: {e}")
|
|
1557
|
-
|
|
1594
|
+
|
|
1558
1595
|
error_msg = f"HEC-RAS Version {ras_version} is not recognized or installed. Running HEC-RAS will fail unless a valid installed version is specified."
|
|
1559
1596
|
logger.error(error_msg)
|
|
1560
1597
|
return "Ras.exe"
|
ras_commander/__init__.py
CHANGED
|
@@ -10,7 +10,7 @@ try:
|
|
|
10
10
|
__version__ = version("ras-commander")
|
|
11
11
|
except PackageNotFoundError:
|
|
12
12
|
# package is not installed
|
|
13
|
-
__version__ = "0.
|
|
13
|
+
__version__ = "0.82.0"
|
|
14
14
|
|
|
15
15
|
# Set up logging
|
|
16
16
|
setup_logging()
|
|
@@ -23,6 +23,7 @@ from .RasUnsteady import RasUnsteady
|
|
|
23
23
|
from .RasUtils import RasUtils
|
|
24
24
|
from .RasExamples import RasExamples
|
|
25
25
|
from .RasCmdr import RasCmdr
|
|
26
|
+
from .RasControl import RasControl
|
|
26
27
|
from .RasMap import RasMap
|
|
27
28
|
from .HdfFluvialPluvial import HdfFluvialPluvial
|
|
28
29
|
|
|
@@ -50,7 +51,7 @@ __all__ = [
|
|
|
50
51
|
# Core functionality
|
|
51
52
|
'RasPrj', 'init_ras_project', 'get_ras_exe', 'ras',
|
|
52
53
|
'RasPlan', 'RasGeo', 'RasUnsteady', 'RasUtils',
|
|
53
|
-
'RasExamples', 'RasCmdr', 'RasMap', 'HdfFluvialPluvial',
|
|
54
|
+
'RasExamples', 'RasCmdr', 'RasControl', 'RasMap', 'HdfFluvialPluvial',
|
|
54
55
|
|
|
55
56
|
# HDF handling
|
|
56
57
|
'HdfBase', 'HdfBndry', 'HdfMesh', 'HdfPlan',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ras-commander
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.82.0
|
|
4
4
|
Summary: A Python library for automating HEC-RAS 6.x operations
|
|
5
5
|
Home-page: https://github.com/gpt-cmdr/ras-commander
|
|
6
6
|
Author: William M. Katzenmeyer, P.E., C.F.M.
|
|
@@ -21,6 +21,8 @@ Requires-Dist: shapely
|
|
|
21
21
|
Requires-Dist: pathlib
|
|
22
22
|
Requires-Dist: rasterstats
|
|
23
23
|
Requires-Dist: rtree
|
|
24
|
+
Requires-Dist: pywin32>=227
|
|
25
|
+
Requires-Dist: psutil>=5.6.6
|
|
24
26
|
Dynamic: author
|
|
25
27
|
Dynamic: author-email
|
|
26
28
|
Dynamic: description
|
|
@@ -81,6 +83,15 @@ HEC-RAS Project Management & Execution
|
|
|
81
83
|
- Progress tracking and logging
|
|
82
84
|
- Execution error handling and recovery
|
|
83
85
|
|
|
86
|
+
Legacy Version Support (NEW in v0.81.0)
|
|
87
|
+
- RasControl class for HEC-RAS 3.x-4.x via COM interface
|
|
88
|
+
- ras-commander style API - use plan numbers, not file paths
|
|
89
|
+
- Extract steady state profiles AND unsteady time series
|
|
90
|
+
- Supports versions: 3.1, 4.1, 5.0.x, 6.0, 6.3, 6.6
|
|
91
|
+
- Version migration validation and comparison
|
|
92
|
+
- Open-operate-close pattern prevents conflicts with modern workflows
|
|
93
|
+
- Seamless integration with ras-commander project management
|
|
94
|
+
|
|
84
95
|
HDF Data Access & Analysis
|
|
85
96
|
- 2D mesh results processing (depths, velocities, WSE)
|
|
86
97
|
- Cross-section data extraction
|
|
@@ -115,7 +126,9 @@ RAS ASCII File Operations
|
|
|
115
126
|
- Unsteady flow file management
|
|
116
127
|
- Project file updates and validation
|
|
117
128
|
|
|
118
|
-
Note about support for Pipe Networks: As a relatively new feature, only read access to Pipe Network geometry and results data has been included. Users will need to code their own methods to modify/add pipe network data, and pull requests are always welcome to incorporate this capability.
|
|
129
|
+
Note about support for Pipe Networks: As a relatively new feature, only read access to Pipe Network geometry and results data has been included. Users will need to code their own methods to modify/add pipe network data, and pull requests are always welcome to incorporate this capability.
|
|
130
|
+
|
|
131
|
+
Note about version support: The modern HDF-based features target HEC-RAS 6.2+ for optimal compatibility. For legacy versions (3.1, 4.1, 5.0.x), use the RasControl class which provides COM-based access to steady state profile extraction and plan execution (see example notebook 17).
|
|
119
132
|
|
|
120
133
|
## Installation
|
|
121
134
|
|
|
@@ -17,16 +17,17 @@ ras_commander/HdfUtils.py,sha256=VkIKAXBrLwTlk2VtXSO-W3RU-NHpfHbE1QcZUZgl-t8,152
|
|
|
17
17
|
ras_commander/HdfXsec.py,sha256=4DuJvzTTtn4zGcf1lv_TyWyRnYRnR_SE-iWFKix5QzM,27776
|
|
18
18
|
ras_commander/LoggingConfig.py,sha256=gWe5K5XTmMQpSczsTysAqpC9my24i_IyM8dvD85fxYg,2704
|
|
19
19
|
ras_commander/RasCmdr.py,sha256=37GnchoQ0fIAkPnssnCr1mRUXY8gm-hIMTmuHZlnYP8,34591
|
|
20
|
+
ras_commander/RasControl.py,sha256=OGEgY_Zw9J3qjXjIm9P9CkSaGK2Z_N1LrXHvEJ2O1fo,32396
|
|
20
21
|
ras_commander/RasExamples.py,sha256=QFWnWnxACpQzewzA3QFMp4z4iEkg5PWf9cPDdMay7MA,24556
|
|
21
22
|
ras_commander/RasGeo.py,sha256=Wy5N1yP7_St3cA3ENJliojQ2sb2w2dL8Fy8L_sZsykc,22208
|
|
22
23
|
ras_commander/RasMap.py,sha256=20db61KkUz2CgjfCCYY8F-IYy5doHOtdnTKChiK0ENs,20257
|
|
23
|
-
ras_commander/RasPlan.py,sha256=
|
|
24
|
-
ras_commander/RasPrj.py,sha256=
|
|
24
|
+
ras_commander/RasPlan.py,sha256=_aDVD3WmncGKmMGDahTQ_KBIMV0OIpfEUABFt5DcbMs,68630
|
|
25
|
+
ras_commander/RasPrj.py,sha256=OkFClPPwZdFU9nTCP9ytZ1pk0H18pZXT3jEiML9R9Hk,69298
|
|
25
26
|
ras_commander/RasUnsteady.py,sha256=PdQQMiY7Mz1EsOQk6ygFQtlC2sFEa96Ntg-pznWVpLQ,37187
|
|
26
27
|
ras_commander/RasUtils.py,sha256=0fm4IIs0LH1dgDj3pGd66mR82DhWLEkRKUvIo2M_5X0,35886
|
|
27
|
-
ras_commander/__init__.py,sha256=
|
|
28
|
-
ras_commander-0.
|
|
29
|
-
ras_commander-0.
|
|
30
|
-
ras_commander-0.
|
|
31
|
-
ras_commander-0.
|
|
32
|
-
ras_commander-0.
|
|
28
|
+
ras_commander/__init__.py,sha256=cs-SaZXXgyDR0c30577Ay-IlDzi1qAgQmcIWjFnlL6c,2089
|
|
29
|
+
ras_commander-0.82.0.dist-info/licenses/LICENSE,sha256=_pbd6qHnlsz1iQ-ozDW_49r86BZT6CRwO2iBtw0iN6M,457
|
|
30
|
+
ras_commander-0.82.0.dist-info/METADATA,sha256=TuEymRIjnn9mOV80U8thii1GYucRwLt1AbsMkiQjB4c,28653
|
|
31
|
+
ras_commander-0.82.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
32
|
+
ras_commander-0.82.0.dist-info/top_level.txt,sha256=i76S7eKLFC8doKcXDl3aiOr9RwT06G8adI6YuKbQDaA,14
|
|
33
|
+
ras_commander-0.82.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|