psr-factory 5.0.0b50__py3-none-win_amd64.whl → 5.0.0b52__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/factory.dll +0 -0
- psr/factory/factory.pmd +1 -1
- psr/factory/libcurl-x64.dll +0 -0
- psr/runner/runner.py +14 -0
- {psr_factory-5.0.0b50.dist-info → psr_factory-5.0.0b52.dist-info}/METADATA +1 -1
- {psr_factory-5.0.0b50.dist-info → psr_factory-5.0.0b52.dist-info}/RECORD +14 -14
- {psr_factory-5.0.0b50.dist-info → psr_factory-5.0.0b52.dist-info}/WHEEL +0 -0
- {psr_factory-5.0.0b50.dist-info → psr_factory-5.0.0b52.dist-info}/licenses/LICENSE.txt +0 -0
- {psr_factory-5.0.0b50.dist-info → psr_factory-5.0.0b52.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/factory.dll
CHANGED
|
Binary file
|
psr/factory/factory.pmd
CHANGED
|
@@ -5065,7 +5065,6 @@ DEFINE_MODEL MODL:SDDP_ConsumoCombustivel
|
|
|
5065
5065
|
DIMENSION block
|
|
5066
5066
|
DIMENSION segment
|
|
5067
5067
|
VETOR DATE Data
|
|
5068
|
-
VETOR REAL GerMax INDEX Data
|
|
5069
5068
|
VETOR REAL O&MCost INDEX Data
|
|
5070
5069
|
VETOR REAL G DIM(segment) INDEX Data
|
|
5071
5070
|
VETOR REAL CTransp INDEX Data
|
|
@@ -5681,6 +5680,7 @@ DEFINE_MODEL MODL:SDDP_Execution_Options
|
|
|
5681
5680
|
PARM INTEGER GAME
|
|
5682
5681
|
PARM INTEGER TRUP
|
|
5683
5682
|
PARM INTEGER HINS
|
|
5683
|
+
PARM INTEGER HOTM
|
|
5684
5684
|
|
|
5685
5685
|
PARM REAL NCPL_MIPR DEFAULT 0.005
|
|
5686
5686
|
PARM REAL NCPL_LTOL
|
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
|
|
19
|
+
psr/factory/__init__.py,sha256=akpsaiZNbTA_pC27H6hXlTPZ_E7kYLlYlWbF5pbVPpQ,219
|
|
20
20
|
psr/factory/api.py,sha256=tezioW1Dqv6m7Ozvc0AOTpZcb1q7rLsICqB58twEG5c,106100
|
|
21
|
-
psr/factory/factory.dll,sha256=
|
|
22
|
-
psr/factory/factory.pmd,sha256=
|
|
21
|
+
psr/factory/factory.dll,sha256=wOq4KWgelawH1G3pd64Y3WQBOfH0mhEknfm-d0zEshg,18719744
|
|
22
|
+
psr/factory/factory.pmd,sha256=frnwgnN0daxVlKdJTiILYTVH1m-2VXVhDGxGZWo41C0,252201
|
|
23
23
|
psr/factory/factory.pmk,sha256=gM05-6muZmJJhrceHhlDIIaMMM6FJe6y1ERaHaWJQJc,601130
|
|
24
24
|
psr/factory/factorylib.py,sha256=HCjHezBrzf2hTrHpszhXOREsQ022I0CZvPjigYFxWeE,28799
|
|
25
|
-
psr/factory/libcurl-x64.dll,sha256=
|
|
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.0b52.dist-info/licenses/LICENSE.txt,sha256=N6mqZK2Ft3iXGHj-by_MHC_dJo9qwn0URjakEPys3H4,1089
|
|
40
|
+
psr_factory-5.0.0b52.dist-info/METADATA,sha256=rdCDq-KIHR1eTccNUCRllq5xSDlqzLWqEyyqV6CjVZU,3486
|
|
41
|
+
psr_factory-5.0.0b52.dist-info/WHEEL,sha256=ZjXRCNaQ9YSypEK2TE0LRB0sy2OVXSszb4Sx1XjM99k,97
|
|
42
|
+
psr_factory-5.0.0b52.dist-info/top_level.txt,sha256=Jb393O96WQk3b5D1gMcrZBLKJJgZpzNjTPoldUi00ck,4
|
|
43
|
+
psr_factory-5.0.0b52.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|