Coverage for rdak/commands/policy_queue.py: 35%
145 statements
« prev ^ index » next coverage.py v7.6.0, created at 2026-08-03 16:46 +0000
« prev ^ index » next coverage.py v7.6.0, created at 2026-08-03 16:46 +0000
1# SPDX-License-Identifier: GPL-2.0-or-later
2# © 2026, Ansgar 🙀 <ansgar@debian.org>
4"""
5`rdak policy-queue ...` - manage uploads in policy queues.
7Mirrors the RPCs of `dak.policyqueue.v1.PolicyQueueService`.
8"""
10import argparse
11import sys
12from typing import IO
14from pydantic import BaseModel, ConfigDict, TypeAdapter, ValidationError
15from tabulate import tabulate
17from dak.policyqueue.v1 import policyqueue_pb2, policyqueue_pb2_grpc
18from rdak import config, connection, output
19from rdak.errors import CliError, rpc_errors
22class TextOutput:
23 """Render PolicyQueueService responses as human-readable text."""
25 def __init__(self, stream: IO[str] | None = None) -> None:
26 self._stream = stream if stream is not None else sys.stdout
28 def print_uploads(self, response: policyqueue_pb2.ListUploadsResponse) -> None:
29 rows = sorted((u.name, u.target_suite) for u in response.uploads)
30 if not rows:
31 print("no uploads", file=self._stream)
32 return
33 print(tabulate(rows, headers=["upload", "target suite"]), file=self._stream)
35 def print_upload(self, upload: policyqueue_pb2.Upload) -> None:
36 print(f"upload: {upload.name}", file=self._stream)
37 print(f"policy queue: {upload.policy_queue}", file=self._stream)
38 print(f"target suite: {upload.target_suite}", file=self._stream)
39 if upload.HasField("create_time"): 39 ↛ 40line 39 didn't jump to line 40 because the condition on line 39 was never true
40 created = upload.create_time.ToDatetime().isoformat()
41 print(f"created: {created}", file=self._stream)
42 print(file=self._stream)
43 if not upload.missing_overrides:
44 print("no missing overrides", file=self._stream)
45 return
46 print("missing overrides:", file=self._stream)
47 rows = sorted(
48 (o.package, o.type, o.component, o.priority, o.section)
49 for o in upload.missing_overrides
50 )
51 print(
52 tabulate(
53 rows,
54 headers=["package", "type", "component", "priority", "section"],
55 ),
56 file=self._stream,
57 )
60class JsonOutput:
61 """Render PolicyQueueService responses as JSON."""
63 def __init__(self, stream: IO[str] | None = None) -> None:
64 self._stream = stream if stream is not None else sys.stdout
66 def print_uploads(self, response: policyqueue_pb2.ListUploadsResponse) -> None:
67 output.dump_json(output.to_dict(response), self._stream)
69 def print_upload(self, upload: policyqueue_pb2.Upload) -> None:
70 output.dump_json(output.to_dict(upload), self._stream)
73type Output = TextOutput | JsonOutput
76def _output(args: argparse.Namespace) -> Output:
77 return JsonOutput() if args.json else TextOutput()
80def register(subparsers: "argparse._SubParsersAction") -> None:
81 parser = subparsers.add_parser(
82 "policy-queue", help="manage uploads in policy queues"
83 )
84 verbs = parser.add_subparsers(dest="command", required=True)
86 p = verbs.add_parser("list", help="list uploads in a queue")
87 p.add_argument("queue")
88 p.set_defaults(func=cmd_list)
90 p = verbs.add_parser("show", help="show details for one upload")
91 p.add_argument("queue")
92 p.add_argument("name")
93 p.set_defaults(func=cmd_show)
95 p = verbs.add_parser("add-overrides", help="add overrides for an upload")
96 p.add_argument("queue")
97 p.add_argument("name")
98 p.add_argument(
99 "--all-missing",
100 action="store_true",
101 help="add every override suggested by the server",
102 )
103 p.add_argument(
104 "--from-json",
105 metavar="FILE",
106 help="read overrides from a JSON file ('-' for stdin)",
107 )
108 p.add_argument(
109 "--override",
110 action="append",
111 default=[],
112 metavar="PACKAGE:TYPE:COMPONENT:PRIORITY:SECTION",
113 help="a single override (repeatable)",
114 )
115 p.set_defaults(func=cmd_add_overrides)
117 p = verbs.add_parser("accept", help="mark an upload for acceptance")
118 p.add_argument("queue")
119 p.add_argument("name")
120 p.set_defaults(func=cmd_accept)
122 p = verbs.add_parser("reject", help="reject an upload")
123 p.add_argument("queue")
124 p.add_argument("name")
125 p.add_argument("--reason", required=True, help="reason for the rejection")
126 p.add_argument(
127 "--rejected-by",
128 default="",
129 metavar='"Name <mail@example.com>"',
130 help="entity that rejected the upload (used as the mail From address)",
131 )
132 p.set_defaults(func=cmd_reject)
135def _stub(args: argparse.Namespace) -> policyqueue_pb2_grpc.PolicyQueueServiceStub:
136 cfg = config.load_config(args.config)
137 _name, instance = cfg.select(args.instance)
138 channel = connection.connect(instance)
139 return policyqueue_pb2_grpc.PolicyQueueServiceStub(channel)
142def cmd_list(args: argparse.Namespace) -> int:
143 stub = _stub(args)
144 with rpc_errors():
145 response = stub.ListUploads(
146 policyqueue_pb2.ListUploadsRequest(policy_queue=args.queue)
147 )
148 _output(args).print_uploads(response)
149 return 0
152def cmd_show(args: argparse.Namespace) -> int:
153 stub = _stub(args)
154 with rpc_errors():
155 upload = stub.GetUpload(
156 policyqueue_pb2.GetUploadRequest(policy_queue=args.queue, name=args.name)
157 )
158 _output(args).print_upload(upload)
159 return 0
162def _parse_override(spec: str) -> "policyqueue_pb2.Override":
163 parts = spec.split(":")
164 if len(parts) != 5:
165 raise CliError(
166 f"invalid --override {spec!r}; expected "
167 "PACKAGE:TYPE:COMPONENT:PRIORITY:SECTION",
168 exit_code=2,
169 )
170 package, type_, component, priority, section = parts
171 return policyqueue_pb2.Override(
172 component=component,
173 type=type_,
174 package=package,
175 priority=priority,
176 section=section,
177 )
180class _OverrideInput(BaseModel):
181 """One override entry read from `--from-json`"""
183 model_config = ConfigDict(extra="forbid")
185 component: str
186 type: str
187 package: str
188 priority: str
189 section: str
192_OVERRIDES_ADAPTER = TypeAdapter(list[_OverrideInput])
195def _overrides_from_json(source: str) -> list["policyqueue_pb2.Override"]:
196 if source == "-":
197 text = sys.stdin.read()
198 else:
199 try:
200 with open(source, encoding="utf-8") as f:
201 text = f.read()
202 except OSError as e:
203 raise CliError(f"cannot read {source}: {e}", exit_code=2) from e
204 try:
205 entries = _OVERRIDES_ADAPTER.validate_json(text)
206 except ValidationError as e:
207 raise CliError(f"invalid overrides in {source}: {e}", exit_code=2) from e
208 return [
209 policyqueue_pb2.Override(
210 component=o.component,
211 type=o.type,
212 package=o.package,
213 priority=o.priority,
214 section=o.section,
215 )
216 for o in entries
217 ]
220def cmd_add_overrides(args: argparse.Namespace) -> int:
221 stub = _stub(args)
222 if args.all_missing:
223 if args.from_json or args.override:
224 raise CliError(
225 "--all-missing cannot be combined with --from-json/--override",
226 exit_code=2,
227 )
228 with rpc_errors():
229 upload = stub.GetUpload(
230 policyqueue_pb2.GetUploadRequest(
231 policy_queue=args.queue, name=args.name
232 )
233 )
234 overrides = list(upload.missing_overrides)
235 if not overrides:
236 print("no missing overrides to add")
237 return 0
238 else:
239 overrides = []
240 if args.from_json:
241 overrides.extend(_overrides_from_json(args.from_json))
242 overrides.extend(_parse_override(spec) for spec in args.override)
243 if not overrides:
244 raise CliError(
245 "specify --all-missing, --from-json or --override", exit_code=2
246 )
247 with rpc_errors():
248 stub.AddOverrides(
249 policyqueue_pb2.AddOverridesRequest(
250 policy_queue=args.queue, name=args.name, overrides=overrides
251 )
252 )
253 print(f"added {len(overrides)} override(s)")
254 return 0
257def cmd_accept(args: argparse.Namespace) -> int:
258 stub = _stub(args)
259 with rpc_errors():
260 stub.AcceptUpload(
261 policyqueue_pb2.AcceptUploadRequest(policy_queue=args.queue, name=args.name)
262 )
263 print(f"accepted {args.name}")
264 return 0
267def cmd_reject(args: argparse.Namespace) -> int:
268 stub = _stub(args)
269 with rpc_errors():
270 stub.RejectUpload(
271 policyqueue_pb2.RejectUploadRequest(
272 policy_queue=args.queue,
273 name=args.name,
274 rejected_by=args.rejected_by,
275 reason=args.reason,
276 )
277 )
278 print(f"rejected {args.name}")
279 return 0