psr-factory 5.0.0b49__py3-none-win_amd64.whl → 5.0.0b51__py3-none-win_amd64.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.
Potentially problematic release.
This version of psr-factory might be problematic. Click here for more details.
- psr/execqueue/client.py +26 -1
- psr/execqueue/config.py +7 -0
- psr/execqueue/db.py +61 -6
- psr/execqueue/server.py +251 -20
- psr/factory/__init__.py +1 -1
- psr/factory/api.py +28 -0
- psr/factory/factory.dll +0 -0
- psr/factory/factory.pmd +6 -2
- psr/factory/factory.pmk +4 -3
- psr/factory/factorylib.py +4 -0
- psr/factory/libcurl-x64.dll +0 -0
- psr/runner/runner.py +14 -0
- {psr_factory-5.0.0b49.dist-info → psr_factory-5.0.0b51.dist-info}/METADATA +1 -1
- {psr_factory-5.0.0b49.dist-info → psr_factory-5.0.0b51.dist-info}/RECORD +17 -17
- {psr_factory-5.0.0b49.dist-info → psr_factory-5.0.0b51.dist-info}/WHEEL +0 -0
- {psr_factory-5.0.0b49.dist-info → psr_factory-5.0.0b51.dist-info}/licenses/LICENSE.txt +0 -0
- {psr_factory-5.0.0b49.dist-info → psr_factory-5.0.0b51.dist-info}/top_level.txt +0 -0
psr/execqueue/client.py
CHANGED
|
@@ -28,6 +28,18 @@ def upload_case_file(zip_path, server_url):
|
|
|
28
28
|
print("Upload failed:", response.text)
|
|
29
29
|
return None
|
|
30
30
|
|
|
31
|
+
def run_module(case_id: str, module_name: str, server_url: str) -> Optional[str]:
|
|
32
|
+
"""Add a module to the execution queue. Returns the execution id."""
|
|
33
|
+
data = {"case_id": case_id, "module_name": module_name}
|
|
34
|
+
response = requests.post(f"{server_url}/run_module",data=data)
|
|
35
|
+
|
|
36
|
+
if response.status_code == 200:
|
|
37
|
+
print("Added to execution queue successfully!")
|
|
38
|
+
print("Execution ID:", response.json().get('execution_id'))
|
|
39
|
+
return response.json().get('execution_id')
|
|
40
|
+
else:
|
|
41
|
+
print("Module enqueue failed:", response.status_code, response.text)
|
|
42
|
+
return None
|
|
31
43
|
|
|
32
44
|
def run_case(case_id: str, server_url: str, cloud_execution: bool = False) -> Optional[str]:
|
|
33
45
|
"""Add a case to the execution queue. For server-local run,
|
|
@@ -45,7 +57,20 @@ def run_case(case_id: str, server_url: str, cloud_execution: bool = False) -> Op
|
|
|
45
57
|
print("Cloud upload ID:", response.json().get('cloud_upload_id'))
|
|
46
58
|
return response.json().get('cloud_upload_id')
|
|
47
59
|
else:
|
|
48
|
-
print("
|
|
60
|
+
print("Run case failed:", response.status_code, response.text)
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def get_module_log(case_id: str, server_url: str, module_name: Optional[str] = None) -> Optional[str]:
|
|
65
|
+
"""Fetch the content of a module's fixed log file. If module_name is None, returns last module run log."""
|
|
66
|
+
params = {}
|
|
67
|
+
if module_name:
|
|
68
|
+
params['module'] = module_name
|
|
69
|
+
response = requests.get(f"{server_url}/module_log/{case_id}", params=params)
|
|
70
|
+
if response.status_code == 200:
|
|
71
|
+
return response.text
|
|
72
|
+
else:
|
|
73
|
+
print("Fetch module log failed:", response.text)
|
|
49
74
|
return None
|
|
50
75
|
|
|
51
76
|
|
psr/execqueue/config.py
CHANGED
|
@@ -43,3 +43,10 @@ CLOUD_RESULTS_FOLDER = os.path.join(STORAGE_PATH, 'cloud_results')
|
|
|
43
43
|
# Where temporary extracted case files will be stored
|
|
44
44
|
TEMPORARY_UPLOAD_FOLDER = os.path.join(STORAGE_PATH, 'tmp')
|
|
45
45
|
|
|
46
|
+
# Optional: modules configuration
|
|
47
|
+
# Expected format in server_settings.toml:
|
|
48
|
+
# [modules.<name>]
|
|
49
|
+
# command = "python some_script.py --case \"{case_path}\""
|
|
50
|
+
# log_file = "<optional fixed log file name>"
|
|
51
|
+
MODULES = settings.get("modules", {})
|
|
52
|
+
|
psr/execqueue/db.py
CHANGED
|
@@ -62,7 +62,10 @@ class LocalExecution(Base):
|
|
|
62
62
|
case_id = Column(String(26), ForeignKey('cases.case_id'))
|
|
63
63
|
start_time = Column(TIMESTAMP, default=datetime.datetime.utcnow)
|
|
64
64
|
finish_time = Column(TIMESTAMP)
|
|
65
|
-
status = Column(Integer, default=
|
|
65
|
+
status = Column(Integer, default=LOCAL_EXECUTION_RUNNING)
|
|
66
|
+
# Module-related fields
|
|
67
|
+
is_module = Column(Integer, default=0) # 0 = no, 1 = yes
|
|
68
|
+
module = Column(String(128)) # optional module name
|
|
66
69
|
|
|
67
70
|
case = relationship("Case", back_populates="local_executions")
|
|
68
71
|
|
|
@@ -103,6 +106,23 @@ def initialize():
|
|
|
103
106
|
if _create_db:
|
|
104
107
|
Base.metadata.create_all(engine)
|
|
105
108
|
_first_time_setup(session)
|
|
109
|
+
else:
|
|
110
|
+
# Basic migration: add columns if missing
|
|
111
|
+
# Note: SQLite supports limited ALTERs; use simple try/except for idempotency
|
|
112
|
+
from sqlalchemy import inspect
|
|
113
|
+
inspector = inspect(engine)
|
|
114
|
+
cols = {c['name'] for c in inspector.get_columns('local_executions')}
|
|
115
|
+
with engine.connect() as conn:
|
|
116
|
+
if 'is_module' not in cols:
|
|
117
|
+
try:
|
|
118
|
+
conn.execute("ALTER TABLE local_executions ADD COLUMN is_module INTEGER DEFAULT 0")
|
|
119
|
+
except Exception:
|
|
120
|
+
pass
|
|
121
|
+
if 'module' not in cols:
|
|
122
|
+
try:
|
|
123
|
+
conn.execute("ALTER TABLE local_executions ADD COLUMN module VARCHAR(128)")
|
|
124
|
+
except Exception:
|
|
125
|
+
pass
|
|
106
126
|
|
|
107
127
|
return session, engine
|
|
108
128
|
|
|
@@ -127,12 +147,15 @@ def register_case(session, case_id, checksum):
|
|
|
127
147
|
return case
|
|
128
148
|
|
|
129
149
|
|
|
130
|
-
def register_local_execution(session, case_id: str, execution_id: str):
|
|
150
|
+
def register_local_execution(session, case_id: str, execution_id: str, *, is_module: int = 0, module: Optional[str] = None):
|
|
131
151
|
case = session.query(Case).filter(Case.case_id == case_id).first()
|
|
132
|
-
local_execution = LocalExecution(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
152
|
+
local_execution = LocalExecution(
|
|
153
|
+
execution_id=execution_id,
|
|
154
|
+
case_id=case_id,
|
|
155
|
+
start_time=datetime.datetime.utcnow(),
|
|
156
|
+
is_module=is_module,
|
|
157
|
+
module=module,
|
|
158
|
+
)
|
|
136
159
|
case.local_executions.append(local_execution)
|
|
137
160
|
session.commit()
|
|
138
161
|
return local_execution
|
|
@@ -168,6 +191,38 @@ def get_local_execution_status(session, execution_id: str) -> Optional[int]:
|
|
|
168
191
|
local_execution = session.query(LocalExecution).filter(LocalExecution.execution_id == execution_id).first()
|
|
169
192
|
return local_execution.status if local_execution else None
|
|
170
193
|
|
|
194
|
+
def any_running_modules_for_case(session, case_id: str) -> bool:
|
|
195
|
+
return session.query(LocalExecution).filter(
|
|
196
|
+
LocalExecution.case_id == case_id,
|
|
197
|
+
LocalExecution.is_module == 1,
|
|
198
|
+
LocalExecution.status == LOCAL_EXECUTION_RUNNING
|
|
199
|
+
).count() > 0
|
|
200
|
+
|
|
201
|
+
def any_failed_modules_for_case(session, case_id: str) -> bool:
|
|
202
|
+
return session.query(LocalExecution).filter(
|
|
203
|
+
LocalExecution.case_id == case_id,
|
|
204
|
+
LocalExecution.is_module == 1,
|
|
205
|
+
LocalExecution.status == LOCAL_EXECUTION_ERROR
|
|
206
|
+
).count() > 0
|
|
207
|
+
|
|
208
|
+
def last_module_execution_for_case(session, case_id: str, module: Optional[str] = None) -> Optional[LocalExecution]:
|
|
209
|
+
q = session.query(LocalExecution).filter(
|
|
210
|
+
LocalExecution.case_id == case_id,
|
|
211
|
+
LocalExecution.is_module == 1
|
|
212
|
+
)
|
|
213
|
+
if module:
|
|
214
|
+
q = q.filter(LocalExecution.module == module)
|
|
215
|
+
return q.order_by(LocalExecution.start_time.desc()).first()
|
|
216
|
+
|
|
217
|
+
def get_distinct_module_names_for_case(session, case_id: str) -> List[str]:
|
|
218
|
+
rows = session.query(LocalExecution.module).filter(
|
|
219
|
+
LocalExecution.case_id == case_id,
|
|
220
|
+
LocalExecution.is_module == 1,
|
|
221
|
+
LocalExecution.module.isnot(None)
|
|
222
|
+
).distinct().all()
|
|
223
|
+
# rows is a list of tuples [(module,), ...]
|
|
224
|
+
return [r[0] for r in rows if r and r[0]]
|
|
225
|
+
|
|
171
226
|
|
|
172
227
|
def register_cloud_upload(session, case_id: str, cloud_upload_id: str):
|
|
173
228
|
case = session.query(Case).filter(Case.case_id == case_id).first()
|
psr/execqueue/server.py
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import hashlib
|
|
2
|
+
import os
|
|
2
3
|
import queue
|
|
3
4
|
import shutil
|
|
4
5
|
import sys
|
|
5
6
|
import threading
|
|
6
7
|
import time
|
|
7
8
|
import zipfile
|
|
9
|
+
import subprocess
|
|
10
|
+
import shlex
|
|
8
11
|
from dotenv import load_dotenv
|
|
9
12
|
from flask import (
|
|
10
13
|
Flask,
|
|
@@ -64,6 +67,104 @@ def run_local_case(execution_id: str, case_path: str):
|
|
|
64
67
|
db.update_local_execution_status(session, execution_id, status)
|
|
65
68
|
|
|
66
69
|
|
|
70
|
+
def _ensure_case_workdir(case_id: str) -> str:
|
|
71
|
+
"""Ensure a working directory exists at uploads/<case_id> with extracted contents.
|
|
72
|
+
If it does not exist or is empty, extract the uploaded zip there.
|
|
73
|
+
Returns the absolute path to the working directory.
|
|
74
|
+
"""
|
|
75
|
+
workdir = os.path.join(UPLOADS_FOLDER, case_id)
|
|
76
|
+
zip_upload_path = os.path.join(UPLOADS_FOLDER, f"{case_id}.zip")
|
|
77
|
+
os.makedirs(workdir, exist_ok=True)
|
|
78
|
+
|
|
79
|
+
# If directory is empty or looks incomplete, (re)extract
|
|
80
|
+
try:
|
|
81
|
+
if not os.listdir(workdir):
|
|
82
|
+
with zipfile.ZipFile(zip_upload_path, 'r') as zip_ref:
|
|
83
|
+
zip_ref.extractall(workdir)
|
|
84
|
+
except FileNotFoundError:
|
|
85
|
+
# If there's no zip, still return folder (may be pre-populated)
|
|
86
|
+
pass
|
|
87
|
+
return workdir
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def run_local_module(execution_id: str, case_id: str, module_name: str) -> int:
|
|
91
|
+
"""Run a configured module locally inside the case's upload workdir.
|
|
92
|
+
Returns process return code (0=success, non-zero=failure).
|
|
93
|
+
Updates LocalExecution status accordingly.
|
|
94
|
+
"""
|
|
95
|
+
global session
|
|
96
|
+
# Fetch module configuration
|
|
97
|
+
module_cfg = MODULES.get(module_name) if isinstance(MODULES, dict) else None
|
|
98
|
+
if not module_cfg or 'command' not in module_cfg:
|
|
99
|
+
print(f"Module '{module_name}' not configured.")
|
|
100
|
+
db.update_local_execution_status(session, execution_id, db.LOCAL_EXECUTION_ERROR)
|
|
101
|
+
return 1
|
|
102
|
+
|
|
103
|
+
workdir = _ensure_case_workdir(case_id)
|
|
104
|
+
|
|
105
|
+
# Build command and log file path
|
|
106
|
+
cmd_tmpl = module_cfg.get('command')
|
|
107
|
+
# Allow placeholders
|
|
108
|
+
cmd = cmd_tmpl.format(case_path=workdir, case_id=case_id, module=module_name)
|
|
109
|
+
log_name = module_cfg.get('log_file', f"module_{module_name}.log")
|
|
110
|
+
log_path = os.path.join(workdir, log_name)
|
|
111
|
+
|
|
112
|
+
print(f"Running module '{module_name}' for case {case_id} in {workdir}")
|
|
113
|
+
print(f"Command: {cmd}")
|
|
114
|
+
|
|
115
|
+
rc = 1
|
|
116
|
+
try:
|
|
117
|
+
# Prefer to run without shell to avoid platform-specific exit code mappings
|
|
118
|
+
# If the command starts with 'python' or references .py, build argv accordingly
|
|
119
|
+
argv = None
|
|
120
|
+
# Heuristic: if command contains .py, run with current Python executable
|
|
121
|
+
if '.py' in cmd:
|
|
122
|
+
parts = shlex.split(cmd)
|
|
123
|
+
# If the command already starts with python, use as-is; else prepend sys.executable
|
|
124
|
+
if parts[0].endswith('python') or parts[0].endswith('python.exe'):
|
|
125
|
+
argv = parts
|
|
126
|
+
else:
|
|
127
|
+
argv = [sys.executable] + parts
|
|
128
|
+
else:
|
|
129
|
+
argv = shlex.split(cmd)
|
|
130
|
+
|
|
131
|
+
with open(log_path, 'a', encoding='utf-8', errors='ignore') as logf:
|
|
132
|
+
proc = subprocess.Popen(
|
|
133
|
+
argv,
|
|
134
|
+
cwd=workdir,
|
|
135
|
+
stdout=logf,
|
|
136
|
+
stderr=logf,
|
|
137
|
+
)
|
|
138
|
+
rc = proc.wait()
|
|
139
|
+
|
|
140
|
+
# Now rc follows the subprocess return code semantics: 0 success, non-zero failure
|
|
141
|
+
status = db.LOCAL_EXECUTION_FINISHED if rc == 0 else db.LOCAL_EXECUTION_ERROR
|
|
142
|
+
db.update_local_execution_status(session, execution_id, status)
|
|
143
|
+
except Exception as e:
|
|
144
|
+
print(f"Error running module {module_name} for case {case_id}: {e}")
|
|
145
|
+
db.update_local_execution_status(session, execution_id, db.LOCAL_EXECUTION_ERROR)
|
|
146
|
+
rc = 1
|
|
147
|
+
return rc
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _copy_tree(src: str, dst: str):
|
|
151
|
+
os.makedirs(dst, exist_ok=True)
|
|
152
|
+
for root, dirs, files in os.walk(src):
|
|
153
|
+
rel = os.path.relpath(root, src)
|
|
154
|
+
target_root = os.path.join(dst, rel) if rel != '.' else dst
|
|
155
|
+
os.makedirs(target_root, exist_ok=True)
|
|
156
|
+
for d in dirs:
|
|
157
|
+
os.makedirs(os.path.join(target_root, d), exist_ok=True)
|
|
158
|
+
for f in files:
|
|
159
|
+
s = os.path.join(root, f)
|
|
160
|
+
t = os.path.join(target_root, f)
|
|
161
|
+
try:
|
|
162
|
+
shutil.copy2(s, t)
|
|
163
|
+
except Exception:
|
|
164
|
+
# Best-effort copy; skip problematic files
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
|
|
67
168
|
def initialize_db():
|
|
68
169
|
session, engine = db.initialize()
|
|
69
170
|
return session
|
|
@@ -91,22 +192,59 @@ def run_cloud_case(execution_id: str, case_path: str):
|
|
|
91
192
|
def process_local_execution_queue():
|
|
92
193
|
global session
|
|
93
194
|
while True:
|
|
94
|
-
|
|
195
|
+
item = _execution_queue.get()
|
|
95
196
|
try:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
197
|
+
# Detect item type (backward compatibility for tuple)
|
|
198
|
+
if isinstance(item, dict) and item.get('type') == 'module':
|
|
199
|
+
execution_id = item['execution_id']
|
|
200
|
+
case_id = item['case_id']
|
|
201
|
+
module_name = item['module']
|
|
202
|
+
print(f"Processing module {module_name} for case {case_id} (exec {execution_id})...")
|
|
203
|
+
run_local_module(execution_id, case_id, module_name)
|
|
204
|
+
else:
|
|
205
|
+
if isinstance(item, (list, tuple)):
|
|
206
|
+
execution_id, case_id = item
|
|
207
|
+
else:
|
|
208
|
+
execution_id = item.get('execution_id')
|
|
209
|
+
case_id = item.get('case_id')
|
|
210
|
+
|
|
211
|
+
print(f"Processing case execution {execution_id} for case {case_id}...")
|
|
212
|
+
|
|
213
|
+
# Wait for running modules to finish; abort if any failed
|
|
214
|
+
wait_loops = 0
|
|
215
|
+
while db.any_running_modules_for_case(session, case_id):
|
|
216
|
+
print(f"Case {case_id} has running modules; waiting...")
|
|
217
|
+
time.sleep(5)
|
|
218
|
+
wait_loops += 1
|
|
219
|
+
# Safety: avoid infinite wait in worker
|
|
220
|
+
if wait_loops > 240: # ~20 minutes
|
|
221
|
+
break
|
|
222
|
+
|
|
223
|
+
# Check last execution per distinct module: if any last module execution failed, mark error
|
|
224
|
+
failing_modules = []
|
|
225
|
+
for mname in db.get_distinct_module_names_for_case(session, case_id):
|
|
226
|
+
last = db.last_module_execution_for_case(session, case_id, mname)
|
|
227
|
+
if last and last.status == db.LOCAL_EXECUTION_ERROR:
|
|
228
|
+
failing_modules.append(mname)
|
|
229
|
+
|
|
230
|
+
if failing_modules:
|
|
231
|
+
print(f"Case {case_id} has failed modules {failing_modules}; marking local execution {execution_id} as error and skipping run")
|
|
232
|
+
db.update_local_execution_status(session, execution_id, db.LOCAL_EXECUTION_ERROR)
|
|
233
|
+
else:
|
|
234
|
+
# Prepare a dedicated results folder copying the current working directory (with module changes)
|
|
235
|
+
execution_extraction_path = os.path.join(LOCAL_RESULTS_FOLDER, execution_id)
|
|
236
|
+
os.makedirs(execution_extraction_path, exist_ok=True)
|
|
237
|
+
workdir = _ensure_case_workdir(case_id)
|
|
238
|
+
_copy_tree(workdir, execution_extraction_path)
|
|
239
|
+
# Run SDDP
|
|
240
|
+
run_local_case(execution_id, execution_extraction_path)
|
|
107
241
|
|
|
108
242
|
except Exception as e:
|
|
109
|
-
|
|
243
|
+
# Use safe prints in case execution_id isn't available
|
|
244
|
+
try:
|
|
245
|
+
print(f"Error processing {execution_id}: {e}")
|
|
246
|
+
except Exception:
|
|
247
|
+
print(f"Error processing item: {e}")
|
|
110
248
|
finally:
|
|
111
249
|
_execution_queue.task_done()
|
|
112
250
|
|
|
@@ -121,13 +259,31 @@ def process_cloud_execution_queue():
|
|
|
121
259
|
cloud_upload_id, case_id = _cloud_upload_queue.get()
|
|
122
260
|
try:
|
|
123
261
|
print(f"Processing {cloud_upload_id}...")
|
|
124
|
-
#
|
|
125
|
-
|
|
262
|
+
# Wait for running modules to finish; abort if any failed
|
|
263
|
+
wait_loops = 0
|
|
264
|
+
while db.any_running_modules_for_case(session, case_id):
|
|
265
|
+
print(f"Case {case_id} has running modules; waiting before cloud run...")
|
|
266
|
+
time.sleep(5)
|
|
267
|
+
wait_loops += 1
|
|
268
|
+
if wait_loops > 240: # ~20 minutes
|
|
269
|
+
break
|
|
270
|
+
# Block if the last execution of any distinct module failed
|
|
271
|
+
failing_modules = []
|
|
272
|
+
for mname in db.get_distinct_module_names_for_case(session, case_id):
|
|
273
|
+
last = db.last_module_execution_for_case(session, case_id, mname)
|
|
274
|
+
if last and last.status == db.LOCAL_EXECUTION_ERROR:
|
|
275
|
+
failing_modules.append(mname)
|
|
276
|
+
if failing_modules:
|
|
277
|
+
print(f"Case {case_id} has failing modules in last execution {failing_modules}; skipping cloud run for upload {cloud_upload_id}")
|
|
278
|
+
# Nothing else to do; do not run in the cloud
|
|
279
|
+
continue
|
|
280
|
+
# Prepare temp folder by copying current working directory (with module changes)
|
|
126
281
|
tmp_extraction_path = os.path.join(TEMPORARY_UPLOAD_FOLDER, cloud_upload_id)
|
|
127
|
-
|
|
282
|
+
workdir = _ensure_case_workdir(case_id)
|
|
283
|
+
if os.path.isdir(tmp_extraction_path):
|
|
284
|
+
shutil.rmtree(tmp_extraction_path, ignore_errors=True)
|
|
128
285
|
os.makedirs(tmp_extraction_path, exist_ok=True)
|
|
129
|
-
|
|
130
|
-
zip_ref.extractall(tmp_extraction_path)
|
|
286
|
+
_copy_tree(workdir, tmp_extraction_path)
|
|
131
287
|
|
|
132
288
|
# Run SDDP
|
|
133
289
|
repository_id = run_cloud_case(cloud_upload_id, tmp_extraction_path)
|
|
@@ -136,9 +292,10 @@ def process_cloud_execution_queue():
|
|
|
136
292
|
shutil.rmtree(tmp_extraction_path)
|
|
137
293
|
|
|
138
294
|
execution_extraction_path = os.path.join(CLOUD_RESULTS_FOLDER, repository_id)
|
|
295
|
+
if os.path.isdir(execution_extraction_path):
|
|
296
|
+
shutil.rmtree(execution_extraction_path, ignore_errors=True)
|
|
139
297
|
os.makedirs(execution_extraction_path, exist_ok=True)
|
|
140
|
-
|
|
141
|
-
zip_ref.extractall(execution_extraction_path)
|
|
298
|
+
_copy_tree(workdir, execution_extraction_path)
|
|
142
299
|
|
|
143
300
|
db.register_cloud_execution(session, repository_id, cloud_upload_id, case_id)
|
|
144
301
|
|
|
@@ -234,6 +391,15 @@ def run_endpoint():
|
|
|
234
391
|
if not os.path.exists(zip_case_path):
|
|
235
392
|
return jsonify({'error': 'Upload file for this case ID not found'}), 404
|
|
236
393
|
|
|
394
|
+
# Pre-check: for each distinct module, if the last execution failed, block the run
|
|
395
|
+
failing_modules = []
|
|
396
|
+
for mname in db.get_distinct_module_names_for_case(session, case_id):
|
|
397
|
+
last = db.last_module_execution_for_case(session, case_id, mname)
|
|
398
|
+
if last and last.status == db.LOCAL_EXECUTION_ERROR:
|
|
399
|
+
failing_modules.append(mname)
|
|
400
|
+
if failing_modules:
|
|
401
|
+
return jsonify({'error': 'Case has failed modules in last execution', 'modules': failing_modules}), 409
|
|
402
|
+
|
|
237
403
|
if cloud_execution:
|
|
238
404
|
cloud_upload_id = str(ulid.ULID())
|
|
239
405
|
_cloud_upload_queue.put((cloud_upload_id, case_id))
|
|
@@ -246,10 +412,40 @@ def run_endpoint():
|
|
|
246
412
|
_execution_queue.put((execution_id, case_id))
|
|
247
413
|
|
|
248
414
|
db.register_local_execution(session, case_id, execution_id)
|
|
415
|
+
# Mark as running explicitly
|
|
416
|
+
db.update_local_execution_status(session, execution_id, db.LOCAL_EXECUTION_RUNNING)
|
|
249
417
|
|
|
250
418
|
return jsonify({'case_id': case_id, 'execution_id': execution_id}), 200
|
|
251
419
|
|
|
252
420
|
|
|
421
|
+
@app.route('/run_module', methods=['POST'])
|
|
422
|
+
def run_module_endpoint():
|
|
423
|
+
global session
|
|
424
|
+
case_id = request.form.get('case_id')
|
|
425
|
+
module_name = request.form.get('module') or request.form.get('module_name')
|
|
426
|
+
|
|
427
|
+
if not case_id or not module_name:
|
|
428
|
+
return jsonify({'error': 'case_id and module are required'}), 400
|
|
429
|
+
|
|
430
|
+
# Validate case zip exists
|
|
431
|
+
zip_case_path = os.path.join(UPLOADS_FOLDER, f"{case_id}.zip")
|
|
432
|
+
workdir = os.path.join(UPLOADS_FOLDER, case_id)
|
|
433
|
+
if not os.path.exists(zip_case_path) and not os.path.isdir(workdir):
|
|
434
|
+
return jsonify({'error': 'Upload file or working directory for this case ID not found'}), 404
|
|
435
|
+
|
|
436
|
+
# Validate module exists in config
|
|
437
|
+
module_cfg = MODULES.get(module_name) if isinstance(MODULES, dict) else None
|
|
438
|
+
if not module_cfg or 'command' not in module_cfg:
|
|
439
|
+
return jsonify({'error': f"Module '{module_name}' not configured"}), 400
|
|
440
|
+
|
|
441
|
+
execution_id = str(ulid.ULID())
|
|
442
|
+
_execution_queue.put({'type': 'module', 'execution_id': execution_id, 'case_id': case_id, 'module': module_name})
|
|
443
|
+
db.register_local_execution(session, case_id, execution_id, is_module=1, module=module_name)
|
|
444
|
+
db.update_local_execution_status(session, execution_id, db.LOCAL_EXECUTION_RUNNING)
|
|
445
|
+
|
|
446
|
+
return jsonify({'case_id': case_id, 'module': module_name, 'execution_id': execution_id}), 200
|
|
447
|
+
|
|
448
|
+
|
|
253
449
|
@app.route('/upload_and_run', methods=['POST'])
|
|
254
450
|
def upload_and_run_file():
|
|
255
451
|
global session
|
|
@@ -371,6 +567,41 @@ def get_results(execution_id: str):
|
|
|
371
567
|
return jsonify({'execution_id': execution_id, 'files': result_files}), 200
|
|
372
568
|
|
|
373
569
|
|
|
570
|
+
@app.route('/module_log/<case_id>', methods=['GET'])
|
|
571
|
+
def get_module_log(case_id: str):
|
|
572
|
+
"""Return the content of the module's fixed log file for the last module run of the case,
|
|
573
|
+
or for a specific module if provided as query parameter ?module=<name>.
|
|
574
|
+
"""
|
|
575
|
+
global session
|
|
576
|
+
module_name = request.args.get('module') or request.args.get('module_name')
|
|
577
|
+
|
|
578
|
+
# Determine module and log file name
|
|
579
|
+
if not module_name:
|
|
580
|
+
last = db.last_module_execution_for_case(session, case_id)
|
|
581
|
+
if not last or not last.module:
|
|
582
|
+
return jsonify({'error': 'No module execution found for this case'}), 404
|
|
583
|
+
module_name = last.module
|
|
584
|
+
module_cfg = MODULES.get(module_name) if isinstance(MODULES, dict) else None
|
|
585
|
+
if not module_cfg:
|
|
586
|
+
return jsonify({'error': f"Module '{module_name}' not configured"}), 400
|
|
587
|
+
log_name = module_cfg.get('log_file', f"module_{module_name}.log")
|
|
588
|
+
|
|
589
|
+
workdir = os.path.join(UPLOADS_FOLDER, case_id)
|
|
590
|
+
if not os.path.isdir(workdir):
|
|
591
|
+
# Ensure workdir is created (may extract zip if needed)
|
|
592
|
+
workdir = _ensure_case_workdir(case_id)
|
|
593
|
+
log_path = os.path.join(workdir, log_name)
|
|
594
|
+
if not os.path.exists(log_path):
|
|
595
|
+
return jsonify({'error': 'Log file not found', 'module': module_name, 'log': log_name}), 404
|
|
596
|
+
|
|
597
|
+
try:
|
|
598
|
+
with open(log_path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
599
|
+
content = f.read()
|
|
600
|
+
return content, 200, {'Content-Type': 'text/plain; charset=utf-8'}
|
|
601
|
+
except Exception as e:
|
|
602
|
+
return jsonify({'error': str(e)}), 500
|
|
603
|
+
|
|
604
|
+
|
|
374
605
|
@app.route('/results/<execution_id>/<file>', methods=['GET'])
|
|
375
606
|
def download_file(execution_id: str, file):
|
|
376
607
|
global session
|
psr/factory/__init__.py
CHANGED
psr/factory/api.py
CHANGED
|
@@ -1236,6 +1236,34 @@ class DataObject(_BaseObject):
|
|
|
1236
1236
|
def type(self):
|
|
1237
1237
|
raise AttributeError("do not delete type")
|
|
1238
1238
|
|
|
1239
|
+
@property
|
|
1240
|
+
def key(self) -> str:
|
|
1241
|
+
err = Error()
|
|
1242
|
+
size = factorylib.lib.psrd_object_get_key(self._hdr, None,
|
|
1243
|
+
0, err.handler())
|
|
1244
|
+
if err.code != 0:
|
|
1245
|
+
raise FactoryException(err.what)
|
|
1246
|
+
buffer = ctypes.create_string_buffer(size)
|
|
1247
|
+
factorylib.lib.psrd_object_get_key(self._hdr, buffer,
|
|
1248
|
+
size, err.handler())
|
|
1249
|
+
if err.code == 0:
|
|
1250
|
+
return _from_c_str(buffer.value)
|
|
1251
|
+
raise FactoryException(err.what)
|
|
1252
|
+
|
|
1253
|
+
@key.setter
|
|
1254
|
+
def key(self, value: str):
|
|
1255
|
+
err = Error()
|
|
1256
|
+
factorylib.lib.psrd_object_set_key(self._hdr,
|
|
1257
|
+
_c_str(value),
|
|
1258
|
+
_bytes(value),
|
|
1259
|
+
err.handler())
|
|
1260
|
+
if err.code != 0:
|
|
1261
|
+
raise FactoryException(err.what)
|
|
1262
|
+
|
|
1263
|
+
@key.deleter
|
|
1264
|
+
def key(self):
|
|
1265
|
+
raise AttributeError("do not delete key")
|
|
1266
|
+
|
|
1239
1267
|
@property
|
|
1240
1268
|
def name(self) -> str:
|
|
1241
1269
|
err = Error()
|
psr/factory/factory.dll
CHANGED
|
Binary file
|
psr/factory/factory.pmd
CHANGED
|
@@ -5103,6 +5103,7 @@ DEFINE_MODEL MODL:SDDP_ContratoCombustivel
|
|
|
5103
5103
|
PARM REAL MinOfftakeRatePen DEFAULT 0
|
|
5104
5104
|
PARM REAL MinOfftakeRateDay DEFAULT -1
|
|
5105
5105
|
PARM REAL MaxOfftakeRateDay DEFAULT -1
|
|
5106
|
+
PARM REAL MinOfftakeRatePerStage DEFAULT -1
|
|
5106
5107
|
|
|
5107
5108
|
VETOR DATE DataCost @chronological @addyear_chronological
|
|
5108
5109
|
VETOR REAL Cost INDEX DataCost
|
|
@@ -5111,10 +5112,13 @@ DEFINE_MODEL MODL:SDDP_ContratoCombustivel
|
|
|
5111
5112
|
VETOR REAL MaxOfftake DIM(block) INDEX DataMaxOfftake
|
|
5112
5113
|
|
|
5113
5114
|
VETOR DATE DateMinOfftakeRateDayChro @chronological @addyear_chronological
|
|
5114
|
-
VETOR REAL MinOfftakeRateDayChro INDEX
|
|
5115
|
+
VETOR REAL MinOfftakeRateDayChro INDEX DateMinOfftakeRateDayChro
|
|
5115
5116
|
|
|
5116
5117
|
VETOR DATE DateMaxOfftakeRateDayChro @chronological @addyear_chronological
|
|
5117
|
-
VETOR REAL MaxOfftakeRateDayChro INDEX
|
|
5118
|
+
VETOR REAL MaxOfftakeRateDayChro INDEX DateMaxOfftakeRateDayChro
|
|
5119
|
+
|
|
5120
|
+
VETOR DATE DateMinOfftakeRatePerStageChro @chronological @addyear_chronological
|
|
5121
|
+
VETOR REAL MinOfftakeRatePerStageChro INDEX DateMinOfftakeRatePerStageChro
|
|
5118
5122
|
|
|
5119
5123
|
VETOR DATE DataAvailability @chronological @addyear_chronological
|
|
5120
5124
|
VETOR REAL Availability INDEX DataAvailability DEFAULT -1
|
psr/factory/factory.pmk
CHANGED
|
@@ -12468,7 +12468,7 @@ END_MASK
|
|
|
12468
12468
|
DEFINE_MASK ROWDATA SDDP_fuecnt_v2
|
|
12469
12469
|
DEFINE_HEADER
|
|
12470
12470
|
$version=2
|
|
12471
|
-
!Num Nome........ Comb Custo... Tipo d1/m1/yy01 d2/m2/yy02 nrep cnt.min. cnt.max. cnt.ini. disp.max traf.max CustoToP Availab. Tcin SpillPen MinOfft. MinOfPen MinOfftD MaxOfftD
|
|
12471
|
+
!Num Nome........ Comb Custo... Tipo d1/m1/yy01 d2/m2/yy02 nrep cnt.min. cnt.max. cnt.ini. disp.max traf.max CustoToP Availab. Tcin SpillPen MinOfft. MinOfPen MinOfftD MaxOfftD MinOfftS
|
|
12472
12472
|
END_HEADER
|
|
12473
12473
|
DEFINE_DATA
|
|
12474
12474
|
Num INTEGER 1,4
|
|
@@ -12490,8 +12490,9 @@ UnitConsumedAmount INTEGER 128,131 AUTOSET(model.parm("UnitConsumedAmount"))
|
|
|
12490
12490
|
SpillagePen REAL 133,140 AUTOSET(model.parm("SpillagePen"))
|
|
12491
12491
|
MinOfftakeRate REAL 142,149 AUTOSET(model.parm("MinOfftakeRate"))
|
|
12492
12492
|
MinOfftakeRatePen REAL 151,158 AUTOSET(model.parm("MinOfftakeRatePen"))
|
|
12493
|
-
MinOfftakeRateDay REAL 160,167
|
|
12494
|
-
MaxOfftakeRateDay REAL 169,176
|
|
12493
|
+
MinOfftakeRateDay REAL 160,167 AUTOSET(model.parm("MinOfftakeRateDay"))
|
|
12494
|
+
MaxOfftakeRateDay REAL 169,176 AUTOSET(model.parm("MaxOfftakeRateDay"))
|
|
12495
|
+
MinOfftakeRatePerStage REAL 178,185 AUTOSET(model.parm("MinOfftakeRatePerStage"))
|
|
12495
12496
|
END_DATA
|
|
12496
12497
|
|
|
12497
12498
|
END_MASK
|
psr/factory/factorylib.py
CHANGED
|
@@ -143,6 +143,10 @@ def initialize():
|
|
|
143
143
|
lib.psrd_object_get_parent.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
|
|
144
144
|
lib.psrd_object_get_type.restype = ctypes.c_int
|
|
145
145
|
lib.psrd_object_get_type.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_long, ctypes.c_void_p]
|
|
146
|
+
lib.psrd_object_get_key.restype = ctypes.c_int
|
|
147
|
+
lib.psrd_object_get_key.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_long, ctypes.c_void_p]
|
|
148
|
+
lib.psrd_object_set_key.restype = ctypes.c_int
|
|
149
|
+
lib.psrd_object_set_key.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_long, ctypes.c_void_p]
|
|
146
150
|
lib.psrd_object_get_code.restype = ctypes.c_int
|
|
147
151
|
lib.psrd_object_get_code.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_int), ctypes.c_void_p]
|
|
148
152
|
lib.psrd_object_set_code.restype = ctypes.c_int
|
psr/factory/libcurl-x64.dll
CHANGED
|
Binary file
|
psr/runner/runner.py
CHANGED
|
@@ -595,6 +595,20 @@ def run_tslconsole(tsl_path: Union[str, pathlib.Path], script_path: Union[str, p
|
|
|
595
595
|
cmd = f'TimeSeriesConsole.exe "{str(script_path)}"'
|
|
596
596
|
exec_cmd(cmd, **kwargs)
|
|
597
597
|
|
|
598
|
+
def run_tsl_generate_inflow_from_external_natural(case_path: Union[str, pathlib.Path], tsl_path: Union[str, pathlib.Path], **kwargs):
|
|
599
|
+
commands = ["generate_inflow_from_external_natural"]
|
|
600
|
+
case_path = os.path.abspath(str(case_path))
|
|
601
|
+
tsl_path = str(tsl_path)
|
|
602
|
+
_run_tslconsole_command(tsl_path, case_path, commands)
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def run_tsl_generate_inflow_from_external_incremental(case_path: Union[str, pathlib.Path], tsl_path: Union[str, pathlib.Path], **kwargs):
|
|
606
|
+
commands = ["generate_inflow_from_external_incremental"]
|
|
607
|
+
case_path = os.path.abspath(str(case_path))
|
|
608
|
+
tsl_path = str(tsl_path)
|
|
609
|
+
_run_tslconsole_command(tsl_path, case_path, commands)
|
|
610
|
+
|
|
611
|
+
|
|
598
612
|
def run_tsl(case_path: Union[str, pathlib.Path], tsl_path: Union[str, pathlib.Path], base_type: str, **kwargs):
|
|
599
613
|
if os.name != 'nt':
|
|
600
614
|
raise NotImplementedError("Running TimeSeriesLab is only available on Windows")
|
|
@@ -11,18 +11,18 @@ psr/cloud/status.py,sha256=vcI4B9S6wCt9maT5NNrVwYaEgGIvy6kkC1UVpJjYbtw,3607
|
|
|
11
11
|
psr/cloud/tempfile.py,sha256=1IOeye0eKWnmBynK5K5FMWiTaEVhn4GbQ8_y0THEva0,3893
|
|
12
12
|
psr/cloud/version.py,sha256=-oC5DNaS_iYTfpsusfmeG-WGktZ5b-x8xT-4zszWvz8,193
|
|
13
13
|
psr/cloud/xml.py,sha256=ac2lyflOQm8khPvJn0zmI26I4sfUDY6A_OTsxzbMQEs,1896
|
|
14
|
-
psr/execqueue/client.py,sha256=
|
|
15
|
-
psr/execqueue/config.py,sha256=
|
|
16
|
-
psr/execqueue/db.py,sha256=
|
|
17
|
-
psr/execqueue/server.py,sha256=
|
|
14
|
+
psr/execqueue/client.py,sha256=xw08nwLEsfEXe5TvTRonWDWnhfvn1PW1CwIpVZzro2o,5617
|
|
15
|
+
psr/execqueue/config.py,sha256=F8sp-JGeoRspQRR63SjSKV5wDz0OVGnA-cNm1UDYHBY,1693
|
|
16
|
+
psr/execqueue/db.py,sha256=UvGjex6DpnVrfTtq1z78ZKS4tHkzp08cn8wdrImYRaM,10796
|
|
17
|
+
psr/execqueue/server.py,sha256=aeNzdZabXR5MSE0KDPxTWzr78PHklUVpvzDQRFNus0I,26872
|
|
18
18
|
psr/execqueue/watcher.py,sha256=R1dyXJ-OYn_QjqdItBwbLJZQ2LcbtdHqnRaYkyphi4w,5637
|
|
19
|
-
psr/factory/__init__.py,sha256=
|
|
20
|
-
psr/factory/api.py,sha256=
|
|
21
|
-
psr/factory/factory.dll,sha256=
|
|
22
|
-
psr/factory/factory.pmd,sha256=
|
|
23
|
-
psr/factory/factory.pmk,sha256=
|
|
24
|
-
psr/factory/factorylib.py,sha256=
|
|
25
|
-
psr/factory/libcurl-x64.dll,sha256=
|
|
19
|
+
psr/factory/__init__.py,sha256=PoytVAyZ423_WkKgh9LigDyE8BXRyI13t1OAc73bGeg,219
|
|
20
|
+
psr/factory/api.py,sha256=tezioW1Dqv6m7Ozvc0AOTpZcb1q7rLsICqB58twEG5c,106100
|
|
21
|
+
psr/factory/factory.dll,sha256=HoRzUAwsVbRNbGQSn3WR4iJGLMzRvyCroNCGVd3M6UI,18719232
|
|
22
|
+
psr/factory/factory.pmd,sha256=b9yPyDmkfwMC1BVnLwcM8UqV3jJTuMkfhWdMFwbFlEc,252219
|
|
23
|
+
psr/factory/factory.pmk,sha256=gM05-6muZmJJhrceHhlDIIaMMM6FJe6y1ERaHaWJQJc,601130
|
|
24
|
+
psr/factory/factorylib.py,sha256=HCjHezBrzf2hTrHpszhXOREsQ022I0CZvPjigYFxWeE,28799
|
|
25
|
+
psr/factory/libcurl-x64.dll,sha256=6WGBmqX4q_eD8Vc0E2VpCvVrFV3W7TQoaKqSdbhXBu0,5313096
|
|
26
26
|
psr/factory/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
27
27
|
psr/factory/samples/__init__.py,sha256=xxOch5Fokzjy571a6OHD87FWM17qKgvfcbr8xn-n36I,80
|
|
28
28
|
psr/factory/samples/sddp_case01.py,sha256=h8MDJ6ajXMH_ngiUFSCVFG3XCKDRys6y78vFYyfCMmE,5038
|
|
@@ -34,10 +34,10 @@ psr/psrfcommon/__init__.py,sha256=WXR560XQllIjtFpWd0jiJEbUAQIyh5-6lwj-42_J95c,20
|
|
|
34
34
|
psr/psrfcommon/psrfcommon.py,sha256=NABM5ahvyfSizDC9c0Vu9dVK1pD_vOzIGFHL1oz2E1o,1464
|
|
35
35
|
psr/psrfcommon/tempfile.py,sha256=5S13wa2DCLYTUdwbLm_KMBRnDRJ0WDlu8GO2BmZoNdg,3939
|
|
36
36
|
psr/runner/__init__.py,sha256=kI9HDX-B_LMQJUHHylFHas2rNpWfNNa0pZXoIvX_Alw,230
|
|
37
|
-
psr/runner/runner.py,sha256=
|
|
37
|
+
psr/runner/runner.py,sha256=qs7Gabo531-GWxgU1MneJYCiio8A2QXUYmct29b7e18,28222
|
|
38
38
|
psr/runner/version.py,sha256=mch2Y8anSXGMn9w72Z78PhSRhOyn55EwaoLAYhY4McE,194
|
|
39
|
-
psr_factory-5.0.
|
|
40
|
-
psr_factory-5.0.
|
|
41
|
-
psr_factory-5.0.
|
|
42
|
-
psr_factory-5.0.
|
|
43
|
-
psr_factory-5.0.
|
|
39
|
+
psr_factory-5.0.0b51.dist-info/licenses/LICENSE.txt,sha256=N6mqZK2Ft3iXGHj-by_MHC_dJo9qwn0URjakEPys3H4,1089
|
|
40
|
+
psr_factory-5.0.0b51.dist-info/METADATA,sha256=w-k8QAlQLA7Ocq3zuEqRTihujIlFX3K8UKHiNFdnp4g,3486
|
|
41
|
+
psr_factory-5.0.0b51.dist-info/WHEEL,sha256=ZjXRCNaQ9YSypEK2TE0LRB0sy2OVXSszb4Sx1XjM99k,97
|
|
42
|
+
psr_factory-5.0.0b51.dist-info/top_level.txt,sha256=Jb393O96WQk3b5D1gMcrZBLKJJgZpzNjTPoldUi00ck,4
|
|
43
|
+
psr_factory-5.0.0b51.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|