Coverage for python / lsst / daf / butler / cli / cmd / _remove_runs.py: 39%
63 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-24 08:17 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-24 08:17 +0000
1# This file is part of daf_butler.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (http://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This software is dual licensed under the GNU General Public License and also
10# under a 3-clause BSD license. Recipients may choose which of these licenses
11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt,
12# respectively. If you choose the GPL option then the following text applies
13# (but note that there is still no warranty even if you opt for BSD instead):
14#
15# This program is free software: you can redistribute it and/or modify
16# it under the terms of the GNU General Public License as published by
17# the Free Software Foundation, either version 3 of the License, or
18# (at your option) any later version.
19#
20# This program is distributed in the hope that it will be useful,
21# but WITHOUT ANY WARRANTY; without even the implied warranty of
22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23# GNU General Public License for more details.
24#
25# You should have received a copy of the GNU General Public License
26# along with this program. If not, see <http://www.gnu.org/licenses/>.
27from __future__ import annotations
29__all__ = ["remove_runs"]
31from collections.abc import Mapping, Sequence
32from typing import Any
34import click
36from ... import script
37from ..opt import collection_argument, confirm_option, options_file_option, repo_argument
38from ..utils import ButlerCommand
39from .commands import existingRepoHelp
41# messages emitted by remove-runs, defined separately for use in unit
42# tests.
43noRunCollectionsMsg = "No RUN collections were found."
44willRemoveRunsMsg = "The following RUN collections will be removed:"
45willRemoveDatasetsMsg = "The following datasets will be removed:"
46didRemoveRunsMsg = "The following RUN collections were removed:"
47didRemoveDatasetsMsg = "The following datasets were removed:"
48removedRunsMsg = "Removed collections"
49abortedMsg = "Aborted."
50requiresConfirmationMsg = (
51 "Removing runs that are in parent CHAINED collections requires confirmation. "
52 "\nTry again without --no-confirm to confirm removal of RUN collections from parents, "
53 "or add the --force flag to skip confirmation."
54)
55willUnlinkMsg = "{run}: will be unlinked from {parents}"
56didUnlinkMsg = "{run}: was removed and unlinked from {parents}"
57mustBeUnlinkedMsg = "{run}: must be unlinked from {parents}"
60def _quoted(items: Sequence[str]) -> list[str]:
61 return [f'"{i}"' for i in items]
64def _print_remove(will: bool, runs: Sequence[script.RemoveRun], datasets: Mapping[str, int]) -> None:
65 """Print the formatted remove statement.
67 Parameters
68 ----------
69 will : `bool`
70 True if remove "will" happen, False if the remove "did" happen.
71 runs : `~collections.abc.Sequence` [`str`]
72 The RUNs that will be or were removed.
73 datasets : `~collections.abc.Mapping` [`str`, `int`]
74 The dataset types & count that will be or were removed.
75 """
76 print(willRemoveRunsMsg if will else didRemoveRunsMsg)
77 unlinkMsg = willUnlinkMsg if will else didUnlinkMsg
78 for run in runs:
79 if run.parents:
80 print(unlinkMsg.format(run=run.name, parents=", ".join(_quoted(run.parents))))
81 else:
82 print(run.name)
83 print("\n" + willRemoveDatasetsMsg if will else didRemoveDatasetsMsg)
84 total = sum(datasets.values())
85 print(", ".join([f"{i[0]}({i[1]})" for i in datasets.items()]))
86 print("Total number of datasets to remove: ", total)
89def _print_requires_confirmation(runs: Sequence[script.RemoveRun], datasets: Mapping[str, int]) -> None:
90 print(requiresConfirmationMsg)
91 for run in runs:
92 if run.parents:
93 print(mustBeUnlinkedMsg.format(run=run.name, parents=", ".join(_quoted(run.parents))))
96@click.command(cls=ButlerCommand)
97@click.pass_context
98@repo_argument(
99 help=existingRepoHelp,
100 required=True,
101)
102@collection_argument(
103 help="COLLECTION is a glob-style expression that identifies the RUN collection(s) to remove."
104)
105@confirm_option()
106@click.option(
107 "--force",
108 is_flag=True,
109 help="Required to remove RUN collections from parent collections if using --no-confirm.",
110)
111@options_file_option()
112def remove_runs(context: click.Context, confirm: bool, force: bool, **kwargs: Any) -> None:
113 """Remove one or more RUN collections.
115 This command can be used to remove RUN collections and the datasets within
116 them.
117 """ # numpydoc ignore=PR01
118 result = script.removeRuns(**kwargs)
119 canRemoveRuns = len(result.runs)
120 if not canRemoveRuns:
121 print(noRunCollectionsMsg)
122 return
123 if confirm:
124 _print_remove(True, result.runs, result.datasets)
125 doContinue = click.confirm(text="Continue?", default=False)
126 if doContinue:
127 result.onConfirmation()
128 print(removedRunsMsg)
129 else:
130 print(abortedMsg)
131 else:
132 # if the user opted out of confirmation but there are runs with
133 # parent collections then they must confirm; print a message
134 # and exit.
135 if any(run.parents for run in result.runs) and not force:
136 _print_requires_confirmation(result.runs, result.datasets)
137 context.exit(1)
138 result.onConfirmation()
139 _print_remove(False, result.runs, result.datasets)