lsst-ctrl-bps-panda 29.2025.4200__py3-none-any.whl → 29.2025.4400__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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 = idds_client.get_requests(request_id=wms_workflow_id, with_detail=True)
176
- _LOG.debug("PanDA get workflow status returned = %s", str(ret))
177
-
178
- request_status = ret[0]
179
- if request_status != 0:
180
- raise RuntimeError(f"Error to get workflow status: {ret} for id: {wms_workflow_id}")
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
- else:
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
- # The status of a task is taken from the first item of state_map.
203
- # The workflow is in status WmsStates.FAILED when:
204
- # All tasks have failed.
205
- # SubFinished tasks has jobs in
206
- # output_processed_files: Finished
207
- # output_failed_files: Failed
208
- # output_missing_files: Missing
209
- state_map = {
210
- "Finished": [WmsStates.SUCCEEDED],
211
- "SubFinished": [
212
- WmsStates.SUCCEEDED,
213
- WmsStates.FAILED,
214
- WmsStates.PRUNED,
215
- ],
216
- "Transforming": [
217
- WmsStates.RUNNING,
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
- request_status = new_ret[0]
278
- if request_status != 0:
279
- raise RuntimeError(
280
- f"Error to get workflow status: {new_ret} for id: {wms_workflow_id}"
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
- wmskey = list(task_info.keys())[0]
288
- wmsjobs = task_info[wmskey]
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
- err_msg = "Unexpected job return from PanDA: "
291
- err_msg += f"{task_info} for id: {transform_workload_id}"
292
- raise RuntimeError(err_msg)
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
- # To fill the EXPECTED column
316
- if wms_report.run_summary:
317
- wms_report.run_summary += ";"
318
- wms_report.run_summary += f"{tasklabel}:{totaljobs}"
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
- wms_report.exit_code_summary = exit_codes_all
321
- run_reports.append(wms_report)
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,2 +1,2 @@
1
1
  __all__ = ["__version__"]
2
- __version__ = "29.2025.4200"
2
+ __version__ = "29.2025.4400"
@@ -1,18 +1,18 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lsst-ctrl-bps-panda
3
- Version: 29.2025.4200
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 3-Clause License
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
@@ -4,9 +4,9 @@ lsst/ctrl/bps/panda/constants.py,sha256=hhV1CDHW9G-Z6z2wGaAc41EMlJ-yn2NN3A8psDyj
4
4
  lsst/ctrl/bps/panda/panda_auth_drivers.py,sha256=LoD-tP990ELmVks3Vxv76jm4a8j3h3cNRTNX2XtFGHk,3163
5
5
  lsst/ctrl/bps/panda/panda_auth_utils.py,sha256=kJnevhvjvUegbXfAyiVcoxinONsc_TJqfK4neTmcN5k,8544
6
6
  lsst/ctrl/bps/panda/panda_exceptions.py,sha256=HcOKWMuG79c16Y9j7IJbp990k4DBQ54e7haY1Fsl6XQ,629
7
- lsst/ctrl/bps/panda/panda_service.py,sha256=5briN6eFJ9RolV2PM5Y_OguiB-7844Pu-FSBO0QyhM8,18935
8
- lsst/ctrl/bps/panda/utils.py,sha256=H_dTQSHgvr3TTav97NsWM040T7DhG_57g_CIaGtIniA,41060
9
- lsst/ctrl/bps/panda/version.py,sha256=loN_SewEappJtRr1bfMkUAloXYiZtM4w0O03FF5yeZQ,55
7
+ lsst/ctrl/bps/panda/panda_service.py,sha256=tdqAs2ny1FvgULD876heR3rhaIHKCW707aFGlyaJYf8,19308
8
+ lsst/ctrl/bps/panda/utils.py,sha256=fwTMtFHldV8aN8LLqobf0ekkqEfCpUc_7xmKxe7YFaI,44999
9
+ lsst/ctrl/bps/panda/version.py,sha256=c_EQrzHIElqjF0MSUCohtezaryfBlqz_McPyAghglVk,55
10
10
  lsst/ctrl/bps/panda/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  lsst/ctrl/bps/panda/cli/panda_auth.py,sha256=i54ati_HoKSlyslQRBl7QpX1w5z8MjSfHHMpT43ZeXQ,2055
12
12
  lsst/ctrl/bps/panda/cli/cmd/__init__.py,sha256=QNhn-2QmXHKVUgwmtuX7wnDv3qOUuU_30juuM_2AGaE,1413
@@ -19,12 +19,12 @@ lsst/ctrl/bps/panda/conf_example/test_usdf.yaml,sha256=WIbXCJZDaG7zYUHt7U96MUjUs
19
19
  lsst/ctrl/bps/panda/edgenode/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
20
  lsst/ctrl/bps/panda/edgenode/build_cmd_line_decoder.py,sha256=CjB_ESDKLK67QPlcZHWoJzfaqgC733ih_iIQrwYkiUo,3067
21
21
  lsst/ctrl/bps/panda/edgenode/cmd_line_decoder.py,sha256=cqPeJLA7KfB7KnPTR-ykyEoHQ-_YE17h8_EfnqWA5eA,14616
22
- lsst_ctrl_bps_panda-29.2025.4200.dist-info/licenses/COPYRIGHT,sha256=5ATATZSyXxMNKoJuCJdATg4YNm56ubTwU_hDbShxIWw,116
23
- lsst_ctrl_bps_panda-29.2025.4200.dist-info/licenses/LICENSE,sha256=pRExkS03v0MQW-neNfIcaSL6aiAnoLxYgtZoFzQ6zkM,232
24
- lsst_ctrl_bps_panda-29.2025.4200.dist-info/licenses/bsd_license.txt,sha256=7MIcv8QRX9guUtqPSBDMPz2SnZ5swI-xZMqm_VDSfxY,1606
25
- lsst_ctrl_bps_panda-29.2025.4200.dist-info/licenses/gpl-v3.0.txt,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
26
- lsst_ctrl_bps_panda-29.2025.4200.dist-info/METADATA,sha256=gGr7iFwLXRyxH9nump0xAWij1YLm5ralbizqJDZ_keY,2375
27
- lsst_ctrl_bps_panda-29.2025.4200.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
28
- lsst_ctrl_bps_panda-29.2025.4200.dist-info/top_level.txt,sha256=eUWiOuVVm9wwTrnAgiJT6tp6HQHXxIhj2QSZ7NYZH80,5
29
- lsst_ctrl_bps_panda-29.2025.4200.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
30
- lsst_ctrl_bps_panda-29.2025.4200.dist-info/RECORD,,
22
+ lsst_ctrl_bps_panda-29.2025.4400.dist-info/licenses/COPYRIGHT,sha256=5ATATZSyXxMNKoJuCJdATg4YNm56ubTwU_hDbShxIWw,116
23
+ lsst_ctrl_bps_panda-29.2025.4400.dist-info/licenses/LICENSE,sha256=pRExkS03v0MQW-neNfIcaSL6aiAnoLxYgtZoFzQ6zkM,232
24
+ lsst_ctrl_bps_panda-29.2025.4400.dist-info/licenses/bsd_license.txt,sha256=7MIcv8QRX9guUtqPSBDMPz2SnZ5swI-xZMqm_VDSfxY,1606
25
+ lsst_ctrl_bps_panda-29.2025.4400.dist-info/licenses/gpl-v3.0.txt,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
26
+ lsst_ctrl_bps_panda-29.2025.4400.dist-info/METADATA,sha256=Du1WrzxsCN-B-8oDQN89Bcq7AEsIswImWfY4LXPZb8U,2398
27
+ lsst_ctrl_bps_panda-29.2025.4400.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
28
+ lsst_ctrl_bps_panda-29.2025.4400.dist-info/top_level.txt,sha256=eUWiOuVVm9wwTrnAgiJT6tp6HQHXxIhj2QSZ7NYZH80,5
29
+ lsst_ctrl_bps_panda-29.2025.4400.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
30
+ lsst_ctrl_bps_panda-29.2025.4400.dist-info/RECORD,,