lsst-ctrl-bps-htcondor 29.2025.2100__py3-none-any.whl → 29.2025.3500__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.
@@ -38,3 +38,8 @@ provisioning:
38
38
 
39
39
  # By default, disable automatic provisioning of resources.
40
40
  provisionResources: false
41
+
42
+ # Whether automatic job retries overwrite stdout/stderr of previous attempt.
43
+ overwriteJobFiles: true
44
+ finalJob:
45
+ overwriteJobFiles: false
@@ -401,6 +401,54 @@ class HTCondorService(BaseWmsService):
401
401
  _LOG.debug("job_ids = %s", job_ids)
402
402
  return job_ids
403
403
 
404
+ def get_status(
405
+ self,
406
+ wms_workflow_id: str,
407
+ hist: float = 1,
408
+ is_global: bool = False,
409
+ ) -> tuple[WmsStates, str]:
410
+ """Return status of run based upon given constraints.
411
+
412
+ Parameters
413
+ ----------
414
+ wms_workflow_id : `str`
415
+ Limit to specific run based on id (queue id or path).
416
+ hist : `float`, optional
417
+ Limit history search to this many days. Defaults to 1.
418
+ is_global : `bool`, optional
419
+ If set, all job queues (and their histories) will be queried for
420
+ job information. Defaults to False which means that only the local
421
+ job queue will be queried.
422
+
423
+ Returns
424
+ -------
425
+ state : `lsst.ctrl.bps.WmsStates`
426
+ Status of single run from given information.
427
+ message : `str`
428
+ Extra message for status command to print. This could be pointers
429
+ to documentation or to WMS specific commands.
430
+ """
431
+ _LOG.debug("get_status: id=%s, hist=%s, is_global=%s", wms_workflow_id, hist, is_global)
432
+
433
+ id_type = _wms_id_type(wms_workflow_id)
434
+ _LOG.debug("id_type = %s", id_type.name)
435
+
436
+ if id_type == WmsIdType.LOCAL:
437
+ schedulers = _locate_schedds(locate_all=is_global)
438
+ _LOG.debug("schedulers = %s", schedulers)
439
+ state, message = _get_status_from_id(wms_workflow_id, hist, schedds=schedulers)
440
+ elif id_type == WmsIdType.GLOBAL:
441
+ schedulers = _locate_schedds(locate_all=True)
442
+ _LOG.debug("schedulers = %s", schedulers)
443
+ state, message = _get_status_from_id(wms_workflow_id, hist, schedds=schedulers)
444
+ elif id_type == WmsIdType.PATH:
445
+ state, message = _get_status_from_path(wms_workflow_id)
446
+ else:
447
+ state, message = WmsStates.UNKNOWN, "Invalid job id"
448
+ _LOG.debug("state: %s, %s", state, message)
449
+
450
+ return state, message
451
+
404
452
  def report(
405
453
  self,
406
454
  wms_workflow_id=None,
@@ -658,14 +706,25 @@ def _create_job(subdir_template, cached_values, generic_workflow, gwjob, out_pre
658
706
  htc_job_cmds.update(_translate_job_cmds(cached_values, generic_workflow, gwjob))
659
707
 
660
708
  # job stdout, stderr, htcondor user log.
661
- for key in ("output", "error", "log"):
662
- htc_job_cmds[key] = f"{gwjob.name}.$(Cluster).{key[:3]}"
709
+ for key in ("output", "error"):
710
+ if cached_values["overwriteJobFiles"]:
711
+ htc_job_cmds[key] = f"{gwjob.name}.$(Cluster).{key[:3]}"
712
+ else:
713
+ htc_job_cmds[key] = f"{gwjob.name}.$(Cluster).$$([NumJobStarts ?: 0]).{key[:3]}"
663
714
  _LOG.debug("HTCondor %s = %s", key, htc_job_cmds[key])
664
715
 
716
+ key = "log"
717
+ htc_job_cmds[key] = f"{gwjob.name}.$(Cluster).{key[:3]}"
718
+ _LOG.debug("HTCondor %s = %s", key, htc_job_cmds[key])
719
+
665
720
  htc_job_cmds.update(
666
721
  _handle_job_inputs(generic_workflow, gwjob.name, cached_values["bpsUseShared"], out_prefix)
667
722
  )
668
723
 
724
+ htc_job_cmds.update(
725
+ _handle_job_outputs(generic_workflow, gwjob.name, cached_values["bpsUseShared"], out_prefix)
726
+ )
727
+
669
728
  # Add the job cmds dict to the job object.
670
729
  htc_job.add_job_cmds(htc_job_cmds)
671
730
 
@@ -898,13 +957,7 @@ def _replace_file_vars(use_shared, arguments, workflow, gwjob):
898
957
  # Have shared filesystems and jobs can share file.
899
958
  uri = gwfile.src_uri
900
959
  else:
901
- # Taking advantage of inside knowledge. Not future-proof.
902
- # Temporary fix until have job wrapper that pulls files
903
- # within job.
904
- if gwfile.name == "butlerConfig" and Path(gwfile.src_uri).suffix != ".yaml":
905
- uri = "butler.yaml"
906
- else:
907
- uri = os.path.basename(gwfile.src_uri)
960
+ uri = os.path.basename(gwfile.src_uri)
908
961
  else: # Using push transfer
909
962
  uri = os.path.basename(gwfile.src_uri)
910
963
  arguments = arguments.replace(f"<FILE:{gwfile.name}>", uri)
@@ -953,7 +1006,9 @@ def _replace_cmd_vars(arguments, gwjob):
953
1006
  return arguments
954
1007
 
955
1008
 
956
- def _handle_job_inputs(generic_workflow: GenericWorkflow, job_name: str, use_shared: bool, out_prefix: str):
1009
+ def _handle_job_inputs(
1010
+ generic_workflow: GenericWorkflow, job_name: str, use_shared: bool, out_prefix: str
1011
+ ) -> dict[str, str]:
957
1012
  """Add job input files from generic workflow to job.
958
1013
 
959
1014
  Parameters
@@ -972,7 +1027,6 @@ def _handle_job_inputs(generic_workflow: GenericWorkflow, job_name: str, use_sha
972
1027
  htc_commands : `dict` [`str`, `str`]
973
1028
  HTCondor commands for the job submission script.
974
1029
  """
975
- htc_commands = {}
976
1030
  inputs = []
977
1031
  for gwf_file in generic_workflow.get_job_inputs(job_name, data=True, transfer_only=True):
978
1032
  _LOG.debug("src_uri=%s", gwf_file.src_uri)
@@ -982,38 +1036,139 @@ def _handle_job_inputs(generic_workflow: GenericWorkflow, job_name: str, use_sha
982
1036
  # Note if use_shared and job_shared, don't need to transfer file.
983
1037
 
984
1038
  if not use_shared: # Copy file using push to job
985
- inputs.append(str(uri.relative_to(out_prefix)))
1039
+ inputs.append(str(uri))
986
1040
  elif not gwf_file.job_shared: # Jobs require own copy
987
1041
  # if using shared filesystem, but still need copy in job. Use
988
1042
  # HTCondor's curl plugin for a local copy.
989
-
990
- # Execution butler is represented as a directory which the
991
- # curl plugin does not handle. Taking advantage of inside
992
- # knowledge for temporary fix until have job wrapper that pulls
993
- # files within job.
994
- if gwf_file.name == "butlerConfig":
995
- # The execution butler directory doesn't normally exist until
996
- # the submit phase so checking for suffix instead of using
997
- # is_dir(). If other non-yaml file exists they would have a
998
- # different gwf_file.name.
999
- if uri.suffix == ".yaml": # Single file, so just copy.
1000
- inputs.append(f"file://{uri}")
1001
- else:
1002
- inputs.append(f"file://{uri / 'butler.yaml'}")
1003
- inputs.append(f"file://{uri / 'gen3.sqlite3'}")
1004
- elif uri.is_dir():
1043
+ if uri.is_dir():
1005
1044
  raise RuntimeError(
1006
1045
  f"HTCondor plugin cannot transfer directories locally within job {gwf_file.src_uri}"
1007
1046
  )
1008
- else:
1009
- inputs.append(f"file://{uri}")
1047
+ inputs.append(f"file://{uri}")
1010
1048
 
1049
+ htc_commands = {}
1011
1050
  if inputs:
1012
1051
  htc_commands["transfer_input_files"] = ",".join(inputs)
1013
1052
  _LOG.debug("transfer_input_files=%s", htc_commands["transfer_input_files"])
1014
1053
  return htc_commands
1015
1054
 
1016
1055
 
1056
+ def _handle_job_outputs(
1057
+ generic_workflow: GenericWorkflow, job_name: str, use_shared: bool, out_prefix: str
1058
+ ) -> dict[str, str]:
1059
+ """Add job output files from generic workflow to the job if any.
1060
+
1061
+ Parameters
1062
+ ----------
1063
+ generic_workflow : `lsst.ctrl.bps.GenericWorkflow`
1064
+ The generic workflow (e.g., has executable name and arguments).
1065
+ job_name : `str`
1066
+ Unique name for the job.
1067
+ use_shared : `bool`
1068
+ Whether job has access to files via shared filesystem.
1069
+ out_prefix : `str`
1070
+ The root directory into which all WMS-specific files are written.
1071
+
1072
+ Returns
1073
+ -------
1074
+ htc_commands : `dict` [`str`, `str`]
1075
+ HTCondor commands for the job submission script.
1076
+ """
1077
+ outputs = []
1078
+ output_remaps = []
1079
+ for gwf_file in generic_workflow.get_job_outputs(job_name, data=True, transfer_only=True):
1080
+ _LOG.debug("src_uri=%s", gwf_file.src_uri)
1081
+
1082
+ uri = Path(gwf_file.src_uri)
1083
+ if not use_shared:
1084
+ outputs.append(uri.name)
1085
+ output_remaps.append(f"{uri.name}={str(uri)}")
1086
+
1087
+ # Set to an empty string to disable and only update if there are output
1088
+ # files to transfer. Otherwise, HTCondor will transfer back all files in
1089
+ # the job’s temporary working directory that have been modified or created
1090
+ # by the job.
1091
+ htc_commands = {"transfer_output_files": '""'}
1092
+ if outputs:
1093
+ htc_commands["transfer_output_files"] = ",".join(outputs)
1094
+ _LOG.debug("transfer_output_files=%s", htc_commands["transfer_output_files"])
1095
+
1096
+ htc_commands["transfer_output_remaps"] = f'"{";".join(output_remaps)}"'
1097
+ _LOG.debug("transfer_output_remaps=%s", htc_commands["transfer_output_remaps"])
1098
+ return htc_commands
1099
+
1100
+
1101
+ def _get_status_from_id(
1102
+ wms_workflow_id: str, hist: float, schedds: dict[str, htcondor.Schedd]
1103
+ ) -> tuple[WmsStates, str]:
1104
+ """Gather run information using workflow id.
1105
+
1106
+ Parameters
1107
+ ----------
1108
+ wms_workflow_id : `str`
1109
+ Limit to specific run based on id.
1110
+ hist : `float`
1111
+ Limit history search to this many days.
1112
+ schedds : `dict` [ `str`, `htcondor.Schedd` ]
1113
+ HTCondor schedulers which to query for job information. If empty
1114
+ dictionary, all queries will be run against the local scheduler only.
1115
+
1116
+ Returns
1117
+ -------
1118
+ state : `lsst.ctrl.bps.WmsStates`
1119
+ Status for the corresponding run.
1120
+ message : `str`
1121
+ Message with extra error information.
1122
+ """
1123
+ _LOG.debug("_get_status_from_id: id=%s, hist=%s, schedds=%s", wms_workflow_id, hist, schedds)
1124
+
1125
+ message = ""
1126
+
1127
+ # Collect information about the job by querying HTCondor schedd and
1128
+ # HTCondor history.
1129
+ schedd_dag_info = _get_info_from_schedd(wms_workflow_id, hist, schedds)
1130
+ if len(schedd_dag_info) == 1:
1131
+ schedd_name = next(iter(schedd_dag_info))
1132
+ dag_id = next(iter(schedd_dag_info[schedd_name]))
1133
+ dag_ad = schedd_dag_info[schedd_name][dag_id]
1134
+ state = _htc_status_to_wms_state(dag_ad)
1135
+ else:
1136
+ state = WmsStates.UNKNOWN
1137
+ message = f"DAGMan job {wms_workflow_id} not found in queue or history. Check id or try path."
1138
+ return state, message
1139
+
1140
+
1141
+ def _get_status_from_path(wms_path: str | os.PathLike) -> tuple[WmsStates, str]:
1142
+ """Gather run status from a given run directory.
1143
+
1144
+ Parameters
1145
+ ----------
1146
+ wms_path : `str` | `os.PathLike`
1147
+ The directory containing the submit side files (e.g., HTCondor files).
1148
+
1149
+ Returns
1150
+ -------
1151
+ state : `lsst.ctrl.bps.WmsStates`
1152
+ Status for the run.
1153
+ message : `str`
1154
+ Message to be printed.
1155
+ """
1156
+ wms_path = Path(wms_path).resolve()
1157
+ message = ""
1158
+ try:
1159
+ wms_workflow_id, dag_ad = read_dag_log(wms_path)
1160
+ except FileNotFoundError:
1161
+ wms_workflow_id = MISSING_ID
1162
+ message = f"DAGMan log not found in {wms_path}. Check path."
1163
+
1164
+ if wms_workflow_id == MISSING_ID:
1165
+ state = WmsStates.UNKNOWN
1166
+ else:
1167
+ state = _htc_status_to_wms_state(dag_ad[wms_workflow_id])
1168
+
1169
+ return state, message
1170
+
1171
+
1017
1172
  def _report_from_path(wms_path):
1018
1173
  """Gather run information from a given run directory.
1019
1174
 
@@ -1139,11 +1294,11 @@ def _get_info_from_schedd(
1139
1294
  ----------
1140
1295
  wms_workflow_id : `str`
1141
1296
  Limit to specific run based on id.
1142
- hist : `int`
1297
+ hist : `float`
1143
1298
  Limit history search to this many days.
1144
- schedds : `dict` [ `str`, `htcondor.Schedd` ], optional
1145
- HTCondor schedulers which to query for job information. If None
1146
- (default), all queries will be run against the local scheduler only.
1299
+ schedds : `dict` [ `str`, `htcondor.Schedd` ]
1300
+ HTCondor schedulers which to query for job information. If empty
1301
+ dictionary, all queries will be run against the local scheduler only.
1147
1302
 
1148
1303
  Returns
1149
1304
  -------
@@ -1152,6 +1307,8 @@ def _get_info_from_schedd(
1152
1307
  Scheduler, local HTCondor job ids are mapped to their respective
1153
1308
  classads.
1154
1309
  """
1310
+ _LOG.debug("_get_info_from_schedd: id=%s, hist=%s, schedds=%s", wms_workflow_id, hist, schedds)
1311
+
1155
1312
  dag_constraint = 'regexp("dagman$", Cmd)'
1156
1313
  try:
1157
1314
  cluster_id = int(float(wms_workflow_id))
@@ -2221,6 +2378,12 @@ def _gather_label_values(config: BpsConfig, label: str) -> dict[str, Any]:
2221
2378
  if found:
2222
2379
  values["releaseExpr"] = value
2223
2380
 
2381
+ found, value = config.search("overwriteJobFiles", opt=search_opts)
2382
+ if found:
2383
+ values["overwriteJobFiles"] = value
2384
+ else:
2385
+ values["overwriteJobFiles"] = True
2386
+
2224
2387
  if profile_key and profile_key in config:
2225
2388
  for subkey, val in config[profile_key].items():
2226
2389
  if subkey.startswith("+"):
@@ -205,6 +205,7 @@ HTC_VALID_JOB_KEYS = {
205
205
  "transfer_executable",
206
206
  "transfer_input_files",
207
207
  "transfer_output_files",
208
+ "transfer_output_remaps",
208
209
  "request_cpus",
209
210
  "request_memory",
210
211
  "request_disk",
@@ -1376,6 +1377,7 @@ def condor_search(constraint=None, hist=None, schedds=None):
1376
1377
 
1377
1378
  job_info = condor_q(constraint=constraint, schedds=schedds)
1378
1379
  if hist is not None:
1380
+ _LOG.debug("Searching history going back %s days", hist)
1379
1381
  epoch = (datetime.now() - timedelta(days=hist)).timestamp()
1380
1382
  constraint += f" && (CompletionDate >= {epoch} || JobFinishedHookDone >= {epoch})"
1381
1383
  hist_info = condor_history(constraint, schedds=schedds)
@@ -1,2 +1,2 @@
1
1
  __all__ = ["__version__"]
2
- __version__ = "29.2025.2100"
2
+ __version__ = "29.2025.3500"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lsst-ctrl-bps-htcondor
3
- Version: 29.2025.2100
3
+ Version: 29.2025.3500
4
4
  Summary: HTCondor plugin for lsst-ctrl-bps.
5
5
  Author-email: Rubin Observatory Data Management <dm-admin@lists.lsst.org>
6
6
  License: BSD 3-Clause License
@@ -0,0 +1,19 @@
1
+ lsst/ctrl/bps/htcondor/__init__.py,sha256=1gTmOVLJILvBqgqHVECo8uqoX8e4fiTeH_dHBUXgDvY,1417
2
+ lsst/ctrl/bps/htcondor/final_post.sh,sha256=chfaQV6Q7rGsK-8Hx58ch52m-PofvBanrl7VwCssHec,248
3
+ lsst/ctrl/bps/htcondor/handlers.py,sha256=2gM3Ac00in4ob9ckcP331W1LSEjs9UDKIqt4MULA4bg,11196
4
+ lsst/ctrl/bps/htcondor/htcondor_config.py,sha256=c4lCiYEwEXFdxgbMfEkbDm4LrvkRMF31SqLtQqzqIV4,1523
5
+ lsst/ctrl/bps/htcondor/htcondor_service.py,sha256=WKN6bC5cJu8X9UpN5DNMDxb8V0gBOWCthvFyEWUkU7s,96467
6
+ lsst/ctrl/bps/htcondor/lssthtc.py,sha256=MOxY30jCwbW1efgVcWtN4x13OO8monW95t6IidDf86Y,80515
7
+ lsst/ctrl/bps/htcondor/provisioner.py,sha256=hPN8YJUtwNHQylw68kfskF1S2vCeQvztF8W0d_QKqqM,7851
8
+ lsst/ctrl/bps/htcondor/version.py,sha256=UwJkYVJhfyZPaD0yyjLkfsS3KQL2uaxZwFIeRntHr30,55
9
+ lsst/ctrl/bps/htcondor/etc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ lsst/ctrl/bps/htcondor/etc/htcondor_defaults.yaml,sha256=C6DKJKmKFKczukpXVXev9u1-vmv2IcgcdtjTtgJWDQM,1561
11
+ lsst_ctrl_bps_htcondor-29.2025.3500.dist-info/licenses/COPYRIGHT,sha256=Lc6NoAEFQ65v_SmtS9NwfHTOuSUtC2Umbjv5zyowiQM,61
12
+ lsst_ctrl_bps_htcondor-29.2025.3500.dist-info/licenses/LICENSE,sha256=pRExkS03v0MQW-neNfIcaSL6aiAnoLxYgtZoFzQ6zkM,232
13
+ lsst_ctrl_bps_htcondor-29.2025.3500.dist-info/licenses/bsd_license.txt,sha256=7MIcv8QRX9guUtqPSBDMPz2SnZ5swI-xZMqm_VDSfxY,1606
14
+ lsst_ctrl_bps_htcondor-29.2025.3500.dist-info/licenses/gpl-v3.0.txt,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
15
+ lsst_ctrl_bps_htcondor-29.2025.3500.dist-info/METADATA,sha256=QoiKn5Fu2uQs7vEdQy8tYFqnVTW_bg8twcXFzGT8lkI,2139
16
+ lsst_ctrl_bps_htcondor-29.2025.3500.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
+ lsst_ctrl_bps_htcondor-29.2025.3500.dist-info/top_level.txt,sha256=eUWiOuVVm9wwTrnAgiJT6tp6HQHXxIhj2QSZ7NYZH80,5
18
+ lsst_ctrl_bps_htcondor-29.2025.3500.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
19
+ lsst_ctrl_bps_htcondor-29.2025.3500.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.8.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,19 +0,0 @@
1
- lsst/ctrl/bps/htcondor/__init__.py,sha256=1gTmOVLJILvBqgqHVECo8uqoX8e4fiTeH_dHBUXgDvY,1417
2
- lsst/ctrl/bps/htcondor/final_post.sh,sha256=chfaQV6Q7rGsK-8Hx58ch52m-PofvBanrl7VwCssHec,248
3
- lsst/ctrl/bps/htcondor/handlers.py,sha256=2gM3Ac00in4ob9ckcP331W1LSEjs9UDKIqt4MULA4bg,11196
4
- lsst/ctrl/bps/htcondor/htcondor_config.py,sha256=c4lCiYEwEXFdxgbMfEkbDm4LrvkRMF31SqLtQqzqIV4,1523
5
- lsst/ctrl/bps/htcondor/htcondor_service.py,sha256=Cn4_muurzMzOSJeP3GZWpi8o_DskNL9Wgz2ZEPrNhng,90979
6
- lsst/ctrl/bps/htcondor/lssthtc.py,sha256=Rsfr7ZehZHiRWmF-8FMDReZQrGMtKii7CO2O8Vu9hYg,80420
7
- lsst/ctrl/bps/htcondor/provisioner.py,sha256=hPN8YJUtwNHQylw68kfskF1S2vCeQvztF8W0d_QKqqM,7851
8
- lsst/ctrl/bps/htcondor/version.py,sha256=czd5myijXlfzN1l1OFP7x6hn5ASHoE02wXhI32IqLec,55
9
- lsst/ctrl/bps/htcondor/etc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- lsst/ctrl/bps/htcondor/etc/htcondor_defaults.yaml,sha256=xDRts4vHKov2PE_JRh-0nF3jfuNJXtKBXZqveASp_iA,1422
11
- lsst_ctrl_bps_htcondor-29.2025.2100.dist-info/licenses/COPYRIGHT,sha256=Lc6NoAEFQ65v_SmtS9NwfHTOuSUtC2Umbjv5zyowiQM,61
12
- lsst_ctrl_bps_htcondor-29.2025.2100.dist-info/licenses/LICENSE,sha256=pRExkS03v0MQW-neNfIcaSL6aiAnoLxYgtZoFzQ6zkM,232
13
- lsst_ctrl_bps_htcondor-29.2025.2100.dist-info/licenses/bsd_license.txt,sha256=7MIcv8QRX9guUtqPSBDMPz2SnZ5swI-xZMqm_VDSfxY,1606
14
- lsst_ctrl_bps_htcondor-29.2025.2100.dist-info/licenses/gpl-v3.0.txt,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
15
- lsst_ctrl_bps_htcondor-29.2025.2100.dist-info/METADATA,sha256=zJ6h3GvvoztRNyTngXqRLYeLjJUL9wSvkZg5GrHZPVA,2139
16
- lsst_ctrl_bps_htcondor-29.2025.2100.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
17
- lsst_ctrl_bps_htcondor-29.2025.2100.dist-info/top_level.txt,sha256=eUWiOuVVm9wwTrnAgiJT6tp6HQHXxIhj2QSZ7NYZH80,5
18
- lsst_ctrl_bps_htcondor-29.2025.2100.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
19
- lsst_ctrl_bps_htcondor-29.2025.2100.dist-info/RECORD,,