lsst-ctrl-bps-panda 29.2025.4200__tar.gz → 29.2025.4400__tar.gz
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.
- {lsst_ctrl_bps_panda-29.2025.4200/python/lsst_ctrl_bps_panda.egg-info → lsst_ctrl_bps_panda-29.2025.4400}/PKG-INFO +3 -3
- {lsst_ctrl_bps_panda-29.2025.4200 → lsst_ctrl_bps_panda-29.2025.4400}/pyproject.toml +3 -3
- {lsst_ctrl_bps_panda-29.2025.4200 → lsst_ctrl_bps_panda-29.2025.4400}/python/lsst/ctrl/bps/panda/panda_service.py +160 -134
- {lsst_ctrl_bps_panda-29.2025.4200 → lsst_ctrl_bps_panda-29.2025.4400}/python/lsst/ctrl/bps/panda/utils.py +131 -1
- lsst_ctrl_bps_panda-29.2025.4400/python/lsst/ctrl/bps/panda/version.py +2 -0
- {lsst_ctrl_bps_panda-29.2025.4200 → lsst_ctrl_bps_panda-29.2025.4400/python/lsst_ctrl_bps_panda.egg-info}/PKG-INFO +3 -3
- lsst_ctrl_bps_panda-29.2025.4200/python/lsst/ctrl/bps/panda/version.py +0 -2
- {lsst_ctrl_bps_panda-29.2025.4200 → lsst_ctrl_bps_panda-29.2025.4400}/COPYRIGHT +0 -0
- {lsst_ctrl_bps_panda-29.2025.4200 → lsst_ctrl_bps_panda-29.2025.4400}/LICENSE +0 -0
- {lsst_ctrl_bps_panda-29.2025.4200 → lsst_ctrl_bps_panda-29.2025.4400}/README.rst +0 -0
- {lsst_ctrl_bps_panda-29.2025.4200 → lsst_ctrl_bps_panda-29.2025.4400}/bsd_license.txt +0 -0
- {lsst_ctrl_bps_panda-29.2025.4200 → lsst_ctrl_bps_panda-29.2025.4400}/gpl-v3.0.txt +0 -0
- {lsst_ctrl_bps_panda-29.2025.4200 → lsst_ctrl_bps_panda-29.2025.4400}/python/lsst/ctrl/bps/panda/__init__.py +0 -0
- {lsst_ctrl_bps_panda-29.2025.4200 → lsst_ctrl_bps_panda-29.2025.4400}/python/lsst/ctrl/bps/panda/cli/__init__.py +0 -0
- {lsst_ctrl_bps_panda-29.2025.4200 → lsst_ctrl_bps_panda-29.2025.4400}/python/lsst/ctrl/bps/panda/cli/cmd/__init__.py +0 -0
- {lsst_ctrl_bps_panda-29.2025.4200 → lsst_ctrl_bps_panda-29.2025.4400}/python/lsst/ctrl/bps/panda/cli/cmd/panda_auth_commands.py +0 -0
- {lsst_ctrl_bps_panda-29.2025.4200 → lsst_ctrl_bps_panda-29.2025.4400}/python/lsst/ctrl/bps/panda/cli/panda_auth.py +0 -0
- {lsst_ctrl_bps_panda-29.2025.4200 → lsst_ctrl_bps_panda-29.2025.4400}/python/lsst/ctrl/bps/panda/cmd_line_embedder.py +0 -0
- {lsst_ctrl_bps_panda-29.2025.4200 → lsst_ctrl_bps_panda-29.2025.4400}/python/lsst/ctrl/bps/panda/conf_example/example_panda_SLAC.yaml +0 -0
- {lsst_ctrl_bps_panda-29.2025.4200 → lsst_ctrl_bps_panda-29.2025.4400}/python/lsst/ctrl/bps/panda/conf_example/pipelines_check_idf.yaml +0 -0
- {lsst_ctrl_bps_panda-29.2025.4200 → lsst_ctrl_bps_panda-29.2025.4400}/python/lsst/ctrl/bps/panda/conf_example/test_idf.yaml +0 -0
- {lsst_ctrl_bps_panda-29.2025.4200 → lsst_ctrl_bps_panda-29.2025.4400}/python/lsst/ctrl/bps/panda/conf_example/test_sdf.yaml +0 -0
- {lsst_ctrl_bps_panda-29.2025.4200 → lsst_ctrl_bps_panda-29.2025.4400}/python/lsst/ctrl/bps/panda/conf_example/test_usdf.yaml +0 -0
- {lsst_ctrl_bps_panda-29.2025.4200 → lsst_ctrl_bps_panda-29.2025.4400}/python/lsst/ctrl/bps/panda/constants.py +0 -0
- {lsst_ctrl_bps_panda-29.2025.4200 → lsst_ctrl_bps_panda-29.2025.4400}/python/lsst/ctrl/bps/panda/edgenode/__init__.py +0 -0
- {lsst_ctrl_bps_panda-29.2025.4200 → lsst_ctrl_bps_panda-29.2025.4400}/python/lsst/ctrl/bps/panda/edgenode/build_cmd_line_decoder.py +0 -0
- {lsst_ctrl_bps_panda-29.2025.4200 → lsst_ctrl_bps_panda-29.2025.4400}/python/lsst/ctrl/bps/panda/edgenode/cmd_line_decoder.py +0 -0
- {lsst_ctrl_bps_panda-29.2025.4200 → lsst_ctrl_bps_panda-29.2025.4400}/python/lsst/ctrl/bps/panda/panda_auth_drivers.py +0 -0
- {lsst_ctrl_bps_panda-29.2025.4200 → lsst_ctrl_bps_panda-29.2025.4400}/python/lsst/ctrl/bps/panda/panda_auth_utils.py +0 -0
- {lsst_ctrl_bps_panda-29.2025.4200 → lsst_ctrl_bps_panda-29.2025.4400}/python/lsst/ctrl/bps/panda/panda_exceptions.py +0 -0
- {lsst_ctrl_bps_panda-29.2025.4200 → lsst_ctrl_bps_panda-29.2025.4400}/python/lsst_ctrl_bps_panda.egg-info/SOURCES.txt +0 -0
- {lsst_ctrl_bps_panda-29.2025.4200 → lsst_ctrl_bps_panda-29.2025.4400}/python/lsst_ctrl_bps_panda.egg-info/dependency_links.txt +0 -0
- {lsst_ctrl_bps_panda-29.2025.4200 → lsst_ctrl_bps_panda-29.2025.4400}/python/lsst_ctrl_bps_panda.egg-info/requires.txt +0 -0
- {lsst_ctrl_bps_panda-29.2025.4200 → lsst_ctrl_bps_panda-29.2025.4400}/python/lsst_ctrl_bps_panda.egg-info/top_level.txt +0 -0
- {lsst_ctrl_bps_panda-29.2025.4200 → lsst_ctrl_bps_panda-29.2025.4400}/python/lsst_ctrl_bps_panda.egg-info/zip-safe +0 -0
- {lsst_ctrl_bps_panda-29.2025.4200 → lsst_ctrl_bps_panda-29.2025.4400}/setup.cfg +0 -0
- {lsst_ctrl_bps_panda-29.2025.4200 → lsst_ctrl_bps_panda-29.2025.4400}/tests/test_cmd_line_decoder.py +0 -0
- {lsst_ctrl_bps_panda-29.2025.4200 → lsst_ctrl_bps_panda-29.2025.4400}/tests/test_cmd_line_embedder.py +0 -0
- {lsst_ctrl_bps_panda-29.2025.4200 → lsst_ctrl_bps_panda-29.2025.4400}/tests/test_panda_auth_utils.py +0 -0
- {lsst_ctrl_bps_panda-29.2025.4200 → lsst_ctrl_bps_panda-29.2025.4400}/tests/test_panda_service.py +0 -0
- {lsst_ctrl_bps_panda-29.2025.4200 → lsst_ctrl_bps_panda-29.2025.4400}/tests/test_utils.py +0 -0
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lsst-ctrl-bps-panda
|
|
3
|
-
Version: 29.2025.
|
|
3
|
+
Version: 29.2025.4400
|
|
4
4
|
Summary: PanDA plugin for lsst-ctrl-bps.
|
|
5
5
|
Author-email: Rubin Observatory Data Management <dm-admin@lists.lsst.org>
|
|
6
|
-
License: BSD
|
|
6
|
+
License-Expression: BSD-3-Clause OR GPL-3.0-or-later
|
|
7
7
|
Project-URL: Homepage, https://github.com/lsst/ctrl_bps_panda
|
|
8
8
|
Keywords: lsst
|
|
9
9
|
Classifier: Intended Audience :: Science/Research
|
|
10
|
-
Classifier: License :: OSI Approved :: BSD License
|
|
11
10
|
Classifier: Operating System :: OS Independent
|
|
12
11
|
Classifier: Programming Language :: Python :: 3
|
|
13
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
16
|
Classifier: Topic :: Scientific/Engineering :: Astronomy
|
|
17
17
|
Requires-Python: >=3.11.0
|
|
18
18
|
Description-Content-Type: text/x-rst
|
|
@@ -6,19 +6,20 @@ build-backend = "setuptools.build_meta"
|
|
|
6
6
|
name = "lsst-ctrl-bps-panda"
|
|
7
7
|
requires-python = ">=3.11.0"
|
|
8
8
|
description = "PanDA plugin for lsst-ctrl-bps."
|
|
9
|
-
license =
|
|
9
|
+
license = "BSD-3-Clause OR GPL-3.0-or-later"
|
|
10
|
+
license-files = ["COPYRIGHT", "LICENSE", "bsd_license.txt", "gpl-v3.0.txt"]
|
|
10
11
|
readme = "README.rst"
|
|
11
12
|
authors = [
|
|
12
13
|
{name="Rubin Observatory Data Management", email="dm-admin@lists.lsst.org"},
|
|
13
14
|
]
|
|
14
15
|
classifiers = [
|
|
15
16
|
"Intended Audience :: Science/Research",
|
|
16
|
-
"License :: OSI Approved :: BSD License",
|
|
17
17
|
"Operating System :: OS Independent",
|
|
18
18
|
"Programming Language :: Python :: 3",
|
|
19
19
|
"Programming Language :: Python :: 3.11",
|
|
20
20
|
"Programming Language :: Python :: 3.12",
|
|
21
21
|
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Programming Language :: Python :: 3.14",
|
|
22
23
|
"Topic :: Scientific/Engineering :: Astronomy",
|
|
23
24
|
]
|
|
24
25
|
keywords = ["lsst"]
|
|
@@ -51,7 +52,6 @@ where = ["python"]
|
|
|
51
52
|
|
|
52
53
|
[tool.setuptools]
|
|
53
54
|
zip-safe = true
|
|
54
|
-
license-files = ["COPYRIGHT", "LICENSE", "bsd_license.txt", "gpl-v3.0.txt"]
|
|
55
55
|
|
|
56
56
|
[tool.setuptools.package-data]
|
|
57
57
|
"lsst.ctrl.bps.panda" = ["conf_example/*.yaml"]
|
|
@@ -49,10 +49,13 @@ from lsst.ctrl.bps.panda.constants import PANDA_DEFAULT_MAX_COPY_WORKERS
|
|
|
49
49
|
from lsst.ctrl.bps.panda.utils import (
|
|
50
50
|
add_final_idds_work,
|
|
51
51
|
add_idds_work,
|
|
52
|
+
aggregate_by_basename,
|
|
52
53
|
copy_files_for_distribution,
|
|
53
54
|
create_idds_build_workflow,
|
|
55
|
+
extract_taskname,
|
|
54
56
|
get_idds_client,
|
|
55
57
|
get_idds_result,
|
|
58
|
+
idds_call_with_check,
|
|
56
59
|
)
|
|
57
60
|
from lsst.resources import ResourcePath
|
|
58
61
|
from lsst.utils.timer import time_this
|
|
@@ -172,154 +175,177 @@ class PanDAService(BaseWmsService):
|
|
|
172
175
|
return run_reports, message
|
|
173
176
|
|
|
174
177
|
idds_client = get_idds_client(self.config)
|
|
175
|
-
ret =
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
178
|
+
ret = idds_call_with_check(
|
|
179
|
+
idds_client.get_requests,
|
|
180
|
+
func_name="get workflow status",
|
|
181
|
+
request_id=wms_workflow_id,
|
|
182
|
+
with_detail=True,
|
|
183
|
+
)
|
|
181
184
|
|
|
182
185
|
tasks = ret[1][1]
|
|
183
186
|
if not tasks:
|
|
184
187
|
message = f"No records found for workflow id '{wms_workflow_id}'. Hint: double check the id"
|
|
185
|
-
|
|
186
|
-
head = tasks[0]
|
|
187
|
-
wms_report = WmsRunReport(
|
|
188
|
-
wms_id=str(head["request_id"]),
|
|
189
|
-
operator=head["username"],
|
|
190
|
-
project="",
|
|
191
|
-
campaign="",
|
|
192
|
-
payload="",
|
|
193
|
-
run=head["name"],
|
|
194
|
-
state=WmsStates.UNKNOWN,
|
|
195
|
-
total_number_jobs=0,
|
|
196
|
-
job_state_counts=dict.fromkeys(WmsStates, 0),
|
|
197
|
-
job_summary={},
|
|
198
|
-
run_summary="",
|
|
199
|
-
exit_code_summary=[],
|
|
200
|
-
)
|
|
188
|
+
return run_reports, message
|
|
201
189
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
WmsStates.SUCCEEDED,
|
|
219
|
-
WmsStates.FAILED,
|
|
220
|
-
WmsStates.UNREADY,
|
|
221
|
-
WmsStates.PRUNED,
|
|
222
|
-
],
|
|
223
|
-
"Failed": [WmsStates.FAILED, WmsStates.PRUNED],
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
file_map = {
|
|
227
|
-
WmsStates.SUCCEEDED: "output_processed_files",
|
|
228
|
-
WmsStates.RUNNING: "output_processing_files",
|
|
229
|
-
WmsStates.FAILED: "output_failed_files",
|
|
230
|
-
WmsStates.UNREADY: "input_new_files",
|
|
231
|
-
WmsStates.PRUNED: "output_missing_files",
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
workflow_status = head["status"]["attributes"]["_name_"]
|
|
235
|
-
if workflow_status in ["Finished", "SubFinished"]:
|
|
236
|
-
wms_report.state = WmsStates.SUCCEEDED
|
|
237
|
-
elif workflow_status in ["Failed", "Expired"]:
|
|
238
|
-
wms_report.state = WmsStates.FAILED
|
|
239
|
-
elif workflow_status in ["Cancelled"]:
|
|
240
|
-
wms_report.state = WmsStates.DELETED
|
|
241
|
-
elif workflow_status in ["Suspended"]:
|
|
242
|
-
wms_report.state = WmsStates.HELD
|
|
243
|
-
else:
|
|
244
|
-
wms_report.state = WmsStates.RUNNING
|
|
245
|
-
|
|
246
|
-
try:
|
|
247
|
-
tasks.sort(key=lambda x: x["transform_workload_id"])
|
|
248
|
-
except Exception:
|
|
249
|
-
tasks.sort(key=lambda x: x["transform_id"])
|
|
250
|
-
|
|
251
|
-
exit_codes_all = {}
|
|
252
|
-
# Loop over all tasks data returned by idds_client
|
|
253
|
-
for task in tasks:
|
|
254
|
-
if task["transform_id"] is None:
|
|
255
|
-
# Not created task (It happens because of an outer join
|
|
256
|
-
# between requests table and transforms table).
|
|
257
|
-
continue
|
|
258
|
-
|
|
259
|
-
exit_codes = []
|
|
260
|
-
totaljobs = task["output_total_files"]
|
|
261
|
-
wms_report.total_number_jobs += totaljobs
|
|
262
|
-
tasklabel = task["transform_name"]
|
|
263
|
-
tasklabel = re.sub(wms_report.run + "_", "", tasklabel)
|
|
264
|
-
status = task["transform_status"]["attributes"]["_name_"]
|
|
265
|
-
taskstatus = {}
|
|
266
|
-
# if the state is failed, gather exit code information
|
|
267
|
-
if status in ["SubFinished", "Failed"]:
|
|
268
|
-
transform_workload_id = task["transform_workload_id"]
|
|
269
|
-
if not (task["transform_name"] and task["transform_name"].startswith("build_task")):
|
|
270
|
-
new_ret = idds_client.get_contents_output_ext(
|
|
271
|
-
request_id=wms_workflow_id, workload_id=transform_workload_id
|
|
272
|
-
)
|
|
273
|
-
_LOG.debug(
|
|
274
|
-
"PanDA get task %s detail returned = %s", transform_workload_id, str(new_ret)
|
|
275
|
-
)
|
|
190
|
+
# Create initial WmsRunReport
|
|
191
|
+
head = tasks[0]
|
|
192
|
+
wms_report = WmsRunReport(
|
|
193
|
+
wms_id=str(head["request_id"]),
|
|
194
|
+
operator=head["username"],
|
|
195
|
+
project="",
|
|
196
|
+
campaign="",
|
|
197
|
+
payload="",
|
|
198
|
+
run=head["name"],
|
|
199
|
+
state=WmsStates.UNKNOWN,
|
|
200
|
+
total_number_jobs=0,
|
|
201
|
+
job_state_counts=dict.fromkeys(WmsStates, 0),
|
|
202
|
+
job_summary={},
|
|
203
|
+
run_summary="",
|
|
204
|
+
exit_code_summary={},
|
|
205
|
+
)
|
|
276
206
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
207
|
+
# Define workflow status mapping
|
|
208
|
+
workflow_status = head["status"]["attributes"]["_name_"]
|
|
209
|
+
if workflow_status in ("Finished", "SubFinished"):
|
|
210
|
+
wms_report.state = WmsStates.SUCCEEDED
|
|
211
|
+
elif workflow_status in ("Failed", "Expired"):
|
|
212
|
+
wms_report.state = WmsStates.FAILED
|
|
213
|
+
elif workflow_status == "Cancelled":
|
|
214
|
+
wms_report.state = WmsStates.DELETED
|
|
215
|
+
elif workflow_status == "Suspended":
|
|
216
|
+
wms_report.state = WmsStates.HELD
|
|
217
|
+
else:
|
|
218
|
+
wms_report.state = WmsStates.RUNNING
|
|
219
|
+
|
|
220
|
+
# Define state mapping for job aggregation
|
|
221
|
+
# The status of a task is taken from the first item of state_map.
|
|
222
|
+
# The workflow is in status WmsStates.FAILED when:
|
|
223
|
+
# All tasks have failed.
|
|
224
|
+
# SubFinished tasks has jobs in
|
|
225
|
+
# output_processed_files: Finished
|
|
226
|
+
# output_failed_files: Failed
|
|
227
|
+
# output_missing_files: Missing
|
|
228
|
+
state_map = {
|
|
229
|
+
"Finished": [WmsStates.SUCCEEDED],
|
|
230
|
+
"SubFinished": [WmsStates.SUCCEEDED, WmsStates.FAILED, WmsStates.PRUNED],
|
|
231
|
+
"Transforming": [
|
|
232
|
+
WmsStates.RUNNING,
|
|
233
|
+
WmsStates.SUCCEEDED,
|
|
234
|
+
WmsStates.FAILED,
|
|
235
|
+
# WmsStates.READY,
|
|
236
|
+
WmsStates.UNREADY,
|
|
237
|
+
WmsStates.PRUNED,
|
|
238
|
+
],
|
|
239
|
+
"Failed": [WmsStates.FAILED, WmsStates.PRUNED],
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
file_map = {
|
|
243
|
+
WmsStates.SUCCEEDED: "output_processed_files",
|
|
244
|
+
WmsStates.RUNNING: "output_processing_files",
|
|
245
|
+
WmsStates.FAILED: "output_failed_files",
|
|
246
|
+
# WmsStates.READY: "output_activated_files",
|
|
247
|
+
WmsStates.UNREADY: "input_new_files",
|
|
248
|
+
WmsStates.PRUNED: "output_missing_files",
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
# Sort tasks by workload_id or fallback
|
|
252
|
+
try:
|
|
253
|
+
tasks.sort(key=lambda x: x["transform_workload_id"])
|
|
254
|
+
except (KeyError, TypeError):
|
|
255
|
+
tasks.sort(key=lambda x: x["transform_id"])
|
|
256
|
+
|
|
257
|
+
exit_codes_all = {}
|
|
258
|
+
|
|
259
|
+
# --- Process each task sequentially ---
|
|
260
|
+
for task in tasks:
|
|
261
|
+
if task.get("transform_id") is None:
|
|
262
|
+
# Not created task (It happens because of an outer join
|
|
263
|
+
# between requests table and transforms table).
|
|
264
|
+
continue
|
|
265
|
+
|
|
266
|
+
task_name = task.get("transform_name", "")
|
|
267
|
+
tasklabel = extract_taskname(task_name)
|
|
268
|
+
status = task["transform_status"]["attributes"]["_name_"]
|
|
269
|
+
totaljobs = task.get("output_total_files", 0)
|
|
270
|
+
wms_report.total_number_jobs += totaljobs
|
|
271
|
+
|
|
272
|
+
# --- If task failed/subfinished, fetch exit codes ---
|
|
273
|
+
if status in ("SubFinished", "Failed") and not task_name.startswith("build_task"):
|
|
274
|
+
transform_workload_id = task.get("transform_workload_id")
|
|
275
|
+
if transform_workload_id:
|
|
276
|
+
# When there are failed jobs, ctrl_bps check
|
|
277
|
+
# the number of exit codes
|
|
278
|
+
nfailed = task.get("output_failed_files", 0)
|
|
279
|
+
exit_codes_all[tasklabel] = [1] * nfailed
|
|
280
|
+
if return_exit_codes:
|
|
281
|
+
new_ret = idds_call_with_check(
|
|
282
|
+
idds_client.get_contents_output_ext,
|
|
283
|
+
func_name=f"get task {transform_workload_id} detail",
|
|
284
|
+
request_id=wms_workflow_id,
|
|
285
|
+
workload_id=transform_workload_id,
|
|
286
|
+
)
|
|
282
287
|
# task_info is a dictionary of len 1 that contains
|
|
283
288
|
# a list of dicts containing panda job info
|
|
284
289
|
task_info = new_ret[1][1]
|
|
285
|
-
|
|
286
290
|
if len(task_info) == 1:
|
|
287
|
-
|
|
288
|
-
|
|
291
|
+
_, wmsjobs = next(iter(task_info.items()))
|
|
292
|
+
exit_codes_all[tasklabel] = [
|
|
293
|
+
j["trans_exit_code"]
|
|
294
|
+
for j in wmsjobs
|
|
295
|
+
if j.get("trans_exit_code") not in (None, 0, "0")
|
|
296
|
+
]
|
|
297
|
+
if nfailed > 0 and len(exit_codes_all[tasklabel]) == 0:
|
|
298
|
+
_LOG.debug(
|
|
299
|
+
f"No exit codes in iDDS task info for workload {transform_workload_id}"
|
|
300
|
+
)
|
|
289
301
|
else:
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
exit_codes = [
|
|
294
|
-
wmsjob["trans_exit_code"]
|
|
295
|
-
for wmsjob in wmsjobs
|
|
296
|
-
if wmsjob["trans_exit_code"] is not None and int(wmsjob["trans_exit_code"]) != 0
|
|
297
|
-
]
|
|
298
|
-
exit_codes_all[tasklabel] = exit_codes
|
|
299
|
-
# Fill number of jobs in all WmsStates
|
|
300
|
-
for state in WmsStates:
|
|
301
|
-
njobs = 0
|
|
302
|
-
# Each WmsState have many iDDS status mapped to it.
|
|
303
|
-
if status in state_map:
|
|
304
|
-
for mappedstate in state_map[status]:
|
|
305
|
-
if state in file_map and mappedstate == state:
|
|
306
|
-
if task[file_map[mappedstate]] is not None:
|
|
307
|
-
njobs = task[file_map[mappedstate]]
|
|
308
|
-
if state == WmsStates.RUNNING:
|
|
309
|
-
njobs += task["output_new_files"] - task["input_new_files"]
|
|
310
|
-
break
|
|
311
|
-
wms_report.job_state_counts[state] += njobs
|
|
312
|
-
taskstatus[state] = njobs
|
|
313
|
-
wms_report.job_summary[tasklabel] = taskstatus
|
|
302
|
+
raise RuntimeError(
|
|
303
|
+
f"Unexpected iDDS task info for workload {transform_workload_id}: {task_info}"
|
|
304
|
+
)
|
|
314
305
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
306
|
+
# --- Aggregate job states ---
|
|
307
|
+
taskstatus = {}
|
|
308
|
+
mapped_states = state_map.get(status, [])
|
|
309
|
+
for state in WmsStates:
|
|
310
|
+
njobs = 0
|
|
311
|
+
if state in mapped_states and state in file_map:
|
|
312
|
+
val = task.get(file_map[state])
|
|
313
|
+
if val:
|
|
314
|
+
njobs = val
|
|
315
|
+
if state == WmsStates.RUNNING:
|
|
316
|
+
njobs += task.get("output_new_files", 0) - task.get("input_new_files", 0)
|
|
317
|
+
if state != WmsStates.UNREADY:
|
|
318
|
+
wms_report.job_state_counts[state] += njobs
|
|
319
|
+
taskstatus[state] = njobs
|
|
319
320
|
|
|
320
|
-
|
|
321
|
-
|
|
321
|
+
# Count UNREADY
|
|
322
|
+
unready = WmsStates.UNREADY
|
|
323
|
+
taskstatus[unready] = totaljobs - sum(
|
|
324
|
+
taskstatus[state] for state in WmsStates if state != unready
|
|
325
|
+
)
|
|
326
|
+
wms_report.job_state_counts[unready] += taskstatus[unready]
|
|
327
|
+
|
|
328
|
+
# Store task summary
|
|
329
|
+
wms_report.job_summary[tasklabel] = taskstatus
|
|
330
|
+
summary_part = f"{tasklabel}:{totaljobs}"
|
|
331
|
+
if wms_report.run_summary:
|
|
332
|
+
summary_part = f";{summary_part}"
|
|
333
|
+
wms_report.run_summary += summary_part
|
|
334
|
+
|
|
335
|
+
# Store all exit codes
|
|
336
|
+
wms_report.exit_code_summary = exit_codes_all
|
|
337
|
+
|
|
338
|
+
(
|
|
339
|
+
wms_report.job_summary,
|
|
340
|
+
wms_report.exit_code_summary,
|
|
341
|
+
wms_report.run_summary,
|
|
342
|
+
) = aggregate_by_basename(
|
|
343
|
+
wms_report.job_summary,
|
|
344
|
+
wms_report.exit_code_summary,
|
|
345
|
+
wms_report.run_summary,
|
|
346
|
+
)
|
|
322
347
|
|
|
348
|
+
run_reports.append(wms_report)
|
|
323
349
|
return run_reports, message
|
|
324
350
|
|
|
325
351
|
def list_submitted_jobs(self, wms_id=None, user=None, require_bps=True, pass_thru=None, is_global=False):
|
|
@@ -29,10 +29,13 @@
|
|
|
29
29
|
|
|
30
30
|
__all__ = [
|
|
31
31
|
"add_decoder_prefix",
|
|
32
|
+
"aggregate_by_basename",
|
|
32
33
|
"convert_exec_string_to_hex",
|
|
33
34
|
"copy_files_for_distribution",
|
|
35
|
+
"extract_taskname",
|
|
34
36
|
"get_idds_client",
|
|
35
37
|
"get_idds_result",
|
|
38
|
+
"idds_call_with_check",
|
|
36
39
|
]
|
|
37
40
|
|
|
38
41
|
import binascii
|
|
@@ -41,6 +44,7 @@ import json
|
|
|
41
44
|
import logging
|
|
42
45
|
import os
|
|
43
46
|
import random
|
|
47
|
+
import re
|
|
44
48
|
import tarfile
|
|
45
49
|
import time
|
|
46
50
|
import uuid
|
|
@@ -51,7 +55,7 @@ from idds.doma.workflowv2.domapandawork import DomaPanDAWork
|
|
|
51
55
|
from idds.workflowv2.workflow import AndCondition
|
|
52
56
|
from idds.workflowv2.workflow import Workflow as IDDS_client_workflow
|
|
53
57
|
|
|
54
|
-
from lsst.ctrl.bps import BpsConfig, GenericWorkflow, GenericWorkflowJob
|
|
58
|
+
from lsst.ctrl.bps import BpsConfig, GenericWorkflow, GenericWorkflowJob, WmsStates
|
|
55
59
|
from lsst.ctrl.bps.panda.cmd_line_embedder import CommandLineEmbedder
|
|
56
60
|
from lsst.ctrl.bps.panda.constants import (
|
|
57
61
|
PANDA_DEFAULT_CLOUD,
|
|
@@ -75,6 +79,98 @@ from lsst.resources import ResourcePath
|
|
|
75
79
|
_LOG = logging.getLogger(__name__)
|
|
76
80
|
|
|
77
81
|
|
|
82
|
+
def extract_taskname(s: str) -> str:
|
|
83
|
+
"""Extract the task name from a string that follows a pattern
|
|
84
|
+
CampaignName_timestamp_TaskNumber_TaskLabel_ChunkNumber.
|
|
85
|
+
|
|
86
|
+
Parameters
|
|
87
|
+
----------
|
|
88
|
+
s : `str`
|
|
89
|
+
The input string from which to extract the task name.
|
|
90
|
+
|
|
91
|
+
Returns
|
|
92
|
+
-------
|
|
93
|
+
taskname : `str`
|
|
94
|
+
The extracted task name as per the rules above.
|
|
95
|
+
"""
|
|
96
|
+
# remove surrounding quotes/spaces if present
|
|
97
|
+
s = s.strip().strip("'\"")
|
|
98
|
+
|
|
99
|
+
# find all occurrences of underscore + digits + underscore,
|
|
100
|
+
# take the last one
|
|
101
|
+
matches = re.findall(r"_(\d+)_", s)
|
|
102
|
+
if matches:
|
|
103
|
+
last_number = matches[-1]
|
|
104
|
+
last_pos = s.rfind(f"_{last_number}_") + len(f"_{last_number}_")
|
|
105
|
+
taskname = s[last_pos:]
|
|
106
|
+
return taskname
|
|
107
|
+
|
|
108
|
+
# fallback: if no such pattern, return everything
|
|
109
|
+
taskname = s
|
|
110
|
+
return taskname
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def aggregate_by_basename(job_summary, exit_code_summary, run_summary):
|
|
114
|
+
"""Aggregate job exit code and run summaries by
|
|
115
|
+
their base label (basename).
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
job_summary : `dict` [`str`, `dict` [`str`, `int`]]
|
|
120
|
+
A mapping of job labels to state-count mappings.
|
|
121
|
+
exit_code_summary : `dict` [`str`, `list` [`int`]]
|
|
122
|
+
A mapping of job labels to lists of exit codes.
|
|
123
|
+
run_summary : `str`
|
|
124
|
+
A semicolon-separated string of job summaries
|
|
125
|
+
where each entry has the format "<label>:<count>".
|
|
126
|
+
|
|
127
|
+
Returns
|
|
128
|
+
-------
|
|
129
|
+
aggregated_jobs : `dict` [`str`, `dict` [`str`, `int`]]
|
|
130
|
+
A dictionary mapping each base label to the summed job state counts
|
|
131
|
+
across all matching labels.
|
|
132
|
+
aggregated_exits : `dict` [`str`, `list` [`int`]]
|
|
133
|
+
A dictionary mapping each base label to a combined list of exit codes
|
|
134
|
+
from all matching labels.
|
|
135
|
+
aggregated_run : `str`
|
|
136
|
+
A semicolon-separated string with aggregated job counts by base label.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
def base_label(label):
|
|
140
|
+
return re.sub(r"_\d+$", "", label)
|
|
141
|
+
|
|
142
|
+
aggregated_jobs = {}
|
|
143
|
+
aggregated_exits = {}
|
|
144
|
+
|
|
145
|
+
for label, states in job_summary.items():
|
|
146
|
+
base = base_label(label)
|
|
147
|
+
if base not in aggregated_jobs:
|
|
148
|
+
aggregated_jobs[base] = dict.fromkeys(WmsStates, 0)
|
|
149
|
+
for state, count in states.items():
|
|
150
|
+
aggregated_jobs[base][state] += count
|
|
151
|
+
|
|
152
|
+
for label, codes in exit_code_summary.items():
|
|
153
|
+
base = base_label(label)
|
|
154
|
+
aggregated_exits.setdefault(base, []).extend(codes)
|
|
155
|
+
|
|
156
|
+
aggregated = {}
|
|
157
|
+
for entry in run_summary.split(";"):
|
|
158
|
+
entry = entry.strip()
|
|
159
|
+
if not entry:
|
|
160
|
+
continue
|
|
161
|
+
try:
|
|
162
|
+
label, num = entry.split(":")
|
|
163
|
+
num = int(num)
|
|
164
|
+
except ValueError:
|
|
165
|
+
continue
|
|
166
|
+
|
|
167
|
+
base = base_label(label)
|
|
168
|
+
aggregated[base] = aggregated.get(base, 0) + num
|
|
169
|
+
|
|
170
|
+
aggregated_run = ";".join(f"{base}:{count}" for base, count in aggregated.items())
|
|
171
|
+
return aggregated_jobs, aggregated_exits, aggregated_run
|
|
172
|
+
|
|
173
|
+
|
|
78
174
|
def copy_files_for_distribution(files_to_stage, file_distribution_uri, max_copy_workers):
|
|
79
175
|
"""Brings locally generated files into Cloud for further
|
|
80
176
|
utilization them on the edge nodes.
|
|
@@ -193,6 +289,40 @@ def get_idds_result(ret):
|
|
|
193
289
|
return status, result, error
|
|
194
290
|
|
|
195
291
|
|
|
292
|
+
def idds_call_with_check(func, *, func_name: str, request_id: int, **kwargs):
|
|
293
|
+
"""Call an iDDS client function, log, and check the return code.
|
|
294
|
+
|
|
295
|
+
Parameters
|
|
296
|
+
----------
|
|
297
|
+
func : callable
|
|
298
|
+
The iDDS client function to call.
|
|
299
|
+
func_name : `str`
|
|
300
|
+
Name used for logging.
|
|
301
|
+
request_id : `int`
|
|
302
|
+
The request or workflow ID.
|
|
303
|
+
**kwargs
|
|
304
|
+
Additional keyword arguments passed to the function.
|
|
305
|
+
|
|
306
|
+
Returns
|
|
307
|
+
-------
|
|
308
|
+
ret : `Any`
|
|
309
|
+
The return value from the iDDS client function.
|
|
310
|
+
"""
|
|
311
|
+
call_kwargs = dict(kwargs)
|
|
312
|
+
if request_id is not None:
|
|
313
|
+
call_kwargs["request_id"] = request_id
|
|
314
|
+
|
|
315
|
+
ret = func(**call_kwargs)
|
|
316
|
+
|
|
317
|
+
_LOG.debug("PanDA %s returned = %s", func_name, str(ret))
|
|
318
|
+
|
|
319
|
+
request_status = ret[0]
|
|
320
|
+
if request_status != 0:
|
|
321
|
+
raise RuntimeError(f"Error calling {func_name}: {ret} for id: {request_id}")
|
|
322
|
+
|
|
323
|
+
return ret
|
|
324
|
+
|
|
325
|
+
|
|
196
326
|
def _make_pseudo_filename(config, gwjob):
|
|
197
327
|
"""Make the job pseudo filename.
|
|
198
328
|
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lsst-ctrl-bps-panda
|
|
3
|
-
Version: 29.2025.
|
|
3
|
+
Version: 29.2025.4400
|
|
4
4
|
Summary: PanDA plugin for lsst-ctrl-bps.
|
|
5
5
|
Author-email: Rubin Observatory Data Management <dm-admin@lists.lsst.org>
|
|
6
|
-
License: BSD
|
|
6
|
+
License-Expression: BSD-3-Clause OR GPL-3.0-or-later
|
|
7
7
|
Project-URL: Homepage, https://github.com/lsst/ctrl_bps_panda
|
|
8
8
|
Keywords: lsst
|
|
9
9
|
Classifier: Intended Audience :: Science/Research
|
|
10
|
-
Classifier: License :: OSI Approved :: BSD License
|
|
11
10
|
Classifier: Operating System :: OS Independent
|
|
12
11
|
Classifier: Programming Language :: Python :: 3
|
|
13
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
16
|
Classifier: Topic :: Scientific/Engineering :: Astronomy
|
|
17
17
|
Requires-Python: >=3.11.0
|
|
18
18
|
Description-Content-Type: text/x-rst
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lsst_ctrl_bps_panda-29.2025.4200 → lsst_ctrl_bps_panda-29.2025.4400}/tests/test_cmd_line_decoder.py
RENAMED
|
File without changes
|
|
File without changes
|
{lsst_ctrl_bps_panda-29.2025.4200 → lsst_ctrl_bps_panda-29.2025.4400}/tests/test_panda_auth_utils.py
RENAMED
|
File without changes
|
{lsst_ctrl_bps_panda-29.2025.4200 → lsst_ctrl_bps_panda-29.2025.4400}/tests/test_panda_service.py
RENAMED
|
File without changes
|
|
File without changes
|