Coverage for dak/queue_report.py: 41%

378 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2026-08-03 16:46 +0000

1"""Produces a report on NEW and BYHAND packages""" 

2 

3# Copyright (C) 2001, 2002, 2003, 2005, 2006 James Troup <james@nocrew.org> 

4 

5# This program is free software; you can redistribute it and/or modify 

6# it under the terms of the GNU General Public License as published by 

7# the Free Software Foundation; either version 2 of the License, or 

8# (at your option) any later version. 

9 

10# This program is distributed in the hope that it will be useful, 

11# but WITHOUT ANY WARRANTY; without even the implied warranty of 

12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

13# GNU General Public License for more details. 

14 

15# You should have received a copy of the GNU General Public License 

16# along with this program; if not, write to the Free Software 

17# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 

18 

19################################################################################ 

20 

21# <o-o> XP runs GCC, XFREE86, SSH etc etc,.,, I feel almost like linux.... 

22# <o-o> I am very confident that I can replicate any Linux application on XP 

23# <willy> o-o: *boggle* 

24# <o-o> building from source. 

25# <o-o> Viiru: I already run GIMP under XP 

26# <willy> o-o: why do you capitalise the names of all pieces of software? 

27# <o-o> willy: because I want the EMPHASIZE them.... 

28# <o-o> grr s/the/to/ 

29# <willy> o-o: it makes you look like ZIPPY the PINHEAD 

30# <o-o> willy: no idea what you are talking about. 

31# <willy> o-o: do some research 

32# <o-o> willy: for what reason? 

33 

34################################################################################ 

35 

36import datetime 

37import functools 

38import html 

39import os 

40import sys 

41import time 

42from typing import IO, Any, Literal, NoReturn, cast 

43 

44import apt_pkg 

45from sqlalchemy import select, sql 

46from sqlalchemy.engine import CursorResult 

47 

48from daklib import utils 

49from daklib.dak_exceptions import ParseMaintError 

50from daklib.dbconn import DBConn, PolicyQueue, get_uid_from_fingerprint, has_new_comment 

51from daklib.policy import PolicyQueueUploadHandler 

52from daklib.textutils import fix_maintainer 

53from daklib.utils import get_logins_from_ldap 

54 

55Cnf: apt_pkg.Configuration 

56direction: list[tuple[int, int, str | Literal[0]]] = [] 

57 

58################################################################################ 

59 

60 

61def usage(exit_code=0) -> NoReturn: 

62 print( 

63 """Usage: dak queue-report 

64Prints a report of packages in queues (usually new and byhand). 

65 

66 -h, --help show this help and exit. 

67 -8, --822 writes 822 formated output to the location set in dak.conf 

68 -n, --new produce html-output 

69 -s, --sort=key sort output according to key, see below. 

70 -a, --age=key if using sort by age, how should time be treated? 

71 If not given a default of hours will be used. 

72 -r, --rrd=key Directory where rrd files to be updated are stored 

73 -d, --directories=key A comma separated list of queues to be scanned 

74 

75 Sorting Keys: ao=age, oldest first. an=age, newest first. 

76 na=name, ascending nd=name, descending 

77 nf=notes, first nl=notes, last 

78 

79 Age Keys: m=minutes, h=hours, d=days, w=weeks, o=months, y=years 

80 

81""" 

82 ) 

83 sys.exit(exit_code) 

84 

85 

86################################################################################ 

87 

88 

89def plural(x: float | int) -> str: 

90 if x > 1: 

91 return "s" 

92 else: 

93 return "" 

94 

95 

96################################################################################ 

97 

98 

99def time_pp(x: float | int) -> str: 

100 if x < 60: 100 ↛ 102line 100 didn't jump to line 102 because the condition on line 100 was always true

101 unit = "second" 

102 elif x < 3600: 

103 x /= 60 

104 unit = "minute" 

105 elif x < 86400: 

106 x /= 3600 

107 unit = "hour" 

108 elif x < 604800: 

109 x /= 86400 

110 unit = "day" 

111 elif x < 2419200: 

112 x /= 604800 

113 unit = "week" 

114 elif x < 29030400: 

115 x /= 2419200 

116 unit = "month" 

117 else: 

118 x /= 29030400 

119 unit = "year" 

120 x = int(x) 

121 return "%s %s%s" % (x, unit, plural(x)) 

122 

123 

124################################################################################ 

125 

126 

127def sg_compare(a, b) -> int: 

128 a1 = a[1] 

129 b1 = b[1] 

130 # Sort by have pending action, have note, time of oldest upload. 

131 # Sort by have pending action 

132 a_note_state = a1["processed"] 

133 b_note_state = b1["processed"] 

134 if a_note_state < b_note_state: 134 ↛ 135line 134 didn't jump to line 135 because the condition on line 134 was never true

135 return -1 

136 elif a_note_state > b_note_state: 136 ↛ 137line 136 didn't jump to line 137 because the condition on line 136 was never true

137 return 1 

138 

139 # Sort by have note 

140 a_note_state = a1["note_state"] 

141 b_note_state = b1["note_state"] 

142 if a_note_state < b_note_state: 142 ↛ 143line 142 didn't jump to line 143 because the condition on line 142 was never true

143 return -1 

144 elif a_note_state > b_note_state: 144 ↛ 145line 144 didn't jump to line 145 because the condition on line 144 was never true

145 return 1 

146 

147 # Sort by time of oldest upload 

148 return a1["oldest"] - b1["oldest"] 

149 

150 

151############################################################ 

152 

153 

154def sortfunc(a, b) -> int: 

155 for sorting in direction: 155 ↛ 156line 155 didn't jump to line 156 because the loop on line 155 never started

156 (sortkey, way, time) = sorting 

157 ret = 0 

158 if time == "m": 

159 x = int(a[sortkey] / 60) 

160 y = int(b[sortkey] / 60) 

161 elif time == "h": 

162 x = int(a[sortkey] / 3600) 

163 y = int(b[sortkey] / 3600) 

164 elif time == "d": 

165 x = int(a[sortkey] / 86400) 

166 y = int(b[sortkey] / 86400) 

167 elif time == "w": 

168 x = int(a[sortkey] / 604800) 

169 y = int(b[sortkey] / 604800) 

170 elif time == "o": 

171 x = int(a[sortkey] / 2419200) 

172 y = int(b[sortkey] / 2419200) 

173 elif time == "y": 

174 x = int(a[sortkey] / 29030400) 

175 y = int(b[sortkey] / 29030400) 

176 else: 

177 x = a[sortkey] 

178 y = b[sortkey] 

179 if x < y: 

180 ret = -1 

181 elif x > y: 

182 ret = 1 

183 if ret != 0: 

184 if way < 0: 

185 ret = ret * -1 

186 return ret 

187 return 0 

188 

189 

190############################################################ 

191 

192 

193def header() -> None: 

194 print( 

195 """<!DOCTYPE html> 

196<html lang="en"> 

197 <head> 

198 <meta charset="utf-8"> 

199 <link rel="stylesheet" href="style.css"> 

200 <link rel="shortcut icon" href="https://www.debian.org/favicon.ico"> 

201 <title> 

202 Debian NEW and BYHAND Packages 

203 </title> 

204 <script> 

205 function togglePkg() { 

206 for (const el of document.getElementsByClassName('sourceNEW')) { 

207 el.style.display = el.style.display == '' ? 'none' : ''; 

208 } 

209 } 

210 </script> 

211 </head> 

212 <body id="NEW"> 

213 <div id="logo"> 

214 <a href="https://www.debian.org/"> 

215 <img src="https://www.debian.org/logos/openlogo-nd-50.png" 

216 alt=""></a> 

217 <a href="https://www.debian.org/"> 

218 <img src="https://www.debian.org/Pics/debian.png" 

219 alt="Debian Project"></a> 

220 </div> 

221 <div id="titleblock"> 

222 

223 <img src="https://www.debian.org/Pics/red-upperleft.png" 

224 id="red-upperleft" alt=""> 

225 <img src="https://www.debian.org/Pics/red-lowerleft.png" 

226 id="red-lowerleft" alt=""> 

227 <img src="https://www.debian.org/Pics/red-upperright.png" 

228 id="red-upperright" alt=""> 

229 <img src="https://www.debian.org/Pics/red-lowerright.png" 

230 id="red-lowerright" alt=""> 

231 <span class="title"> 

232 Debian NEW and BYHAND Packages 

233 </span> 

234 </div> 

235 """ 

236 ) 

237 

238 

239def footer() -> None: 

240 print( 

241 '<p class="timestamp">Timestamp: %s (UTC)</p>' 

242 % (time.strftime("%d.%m.%Y / %H:%M:%S", time.gmtime())) 

243 ) 

244 print( 

245 """ 

246 <p> 

247 There are <a href=\"/stat.html\">graphs about the queues</a> available. 

248 You can also look at the <a href="/new.822">RFC822 version</a>. 

249 </p> 

250 """ 

251 ) 

252 

253 print( 

254 """ 

255 <div class="footer"> 

256 <p>Hint: Age is the youngest upload of the package, if there is more than 

257 one version.<br> 

258 You may want to look at <a href="https://ftp-master.debian.org/REJECT-FAQ.html">the REJECT-FAQ</a> 

259 for possible reasons why one of the above packages may get rejected.</p> 

260 </div> </body> </html> 

261 """ 

262 ) 

263 

264 

265def table_header(type: str, source_count: int, total_count: int) -> None: 

266 print("<h1 class='sourceNEW'>Summary for: %s</h1>" % (type)) 

267 print( 

268 "<h1 class='sourceNEW' style='display: none'>Summary for: binary-%s only</h1>" 

269 % (type) 

270 ) 

271 print( 

272 """ 

273 <p class="togglepkg" onclick="togglePkg()">Click to toggle all/binary-NEW packages</p> 

274 <table class="NEW"> 

275 <caption class="sourceNEW"> 

276 """ 

277 ) 

278 print( 

279 "Package count in <strong>%s</strong>: <em>%s</em>&nbsp;|&nbsp; Total Package count: <em>%s</em>" 

280 % (type, source_count, total_count) 

281 ) 

282 print( 

283 """ 

284 </caption> 

285 <thead> 

286 <tr> 

287 <th>Package</th> 

288 <th>Version</th> 

289 <th>Arch</th> 

290 <th>Distribution</th> 

291 <th>Age</th> 

292 <th>Upload info</th> 

293 <th>Closes</th> 

294 </tr> 

295 </thead> 

296 <tbody> 

297 """ 

298 ) 

299 

300 

301def table_footer(type) -> None: 

302 print("</tbody></table>") 

303 

304 

305def table_row( 

306 source: str, 

307 version: str, 

308 arch: str, 

309 last_mod, 

310 maint, 

311 distribution, 

312 closes, 

313 fingerprint, 

314 sponsor, 

315 changedby, 

316) -> None: 

317 trclass = "sid" 

318 session = DBConn().session() 

319 for dist in distribution: 

320 if dist == "experimental": 

321 trclass = "exp" 

322 

323 query = """SELECT source 

324 FROM source_suite 

325 WHERE source = :source 

326 AND suite_name IN ('unstable', 'experimental')""" 

327 if not cast( 

328 CursorResult, session.execute(sql.text(query), {"source": source}) 

329 ).rowcount: 

330 trclass += " sourceNEW" 

331 session.commit() 

332 

333 print('<tr class="%s">' % (trclass)) 

334 

335 if "sourceNEW" in trclass: 

336 print( 

337 '<td class="package"><a href="https://dfsg-new-queue.debian.org/reviews/%(source)s">%(source)s</a></td>' 

338 % {"source": source} 

339 ) 

340 else: 

341 print( 

342 '<td class="package"><a href="https://tracker.debian.org/pkg/%(source)s">%(source)s</a></td>' 

343 % {"source": source} 

344 ) 

345 print('<td class="version">') 

346 for vers in version.split(): 

347 print( 

348 '<a href="https://dfsg-new-queue.debian.org/reviews/%s/%s">%s</a><br>' 

349 % (source, html.escape(vers), html.escape(vers, quote=False)) 

350 ) 

351 print("</td>") 

352 print('<td class="arch">%s</td>' % (arch)) 

353 print('<td class="distribution">') 

354 for dist in distribution: 

355 print("%s<br>" % (dist)) 

356 print("</td>") 

357 print( 

358 '<td class="age"><abbr title="%s">%s</abbr></td>' 

359 % ( 

360 datetime.datetime.utcfromtimestamp(int(time.time()) - last_mod).strftime( 

361 "%a, %d %b %Y %T UTC" 

362 ), 

363 time_pp(last_mod), 

364 ) 

365 ) 

366 (name, mail) = maint.split(":", 1) 

367 

368 print('<td class="upload-data">') 

369 print( 

370 '<span class="maintainer">Maintainer: <a href="https://qa.debian.org/developer.php?login=%s">%s</a></span><br>' 

371 % (html.escape(mail), html.escape(name, quote=False)) 

372 ) 

373 (name, mail) = changedby.split(":", 1) 

374 print( 

375 '<span class="changed-by">Changed-By: <a href="https://qa.debian.org/developer.php?login=%s">%s</a></span><br>' 

376 % (html.escape(mail), html.escape(name, quote=False)) 

377 ) 

378 

379 if sponsor: 

380 print( 

381 '<span class="sponsor">Sponsor: <a href="https://qa.debian.org/developer.php?login=%s">%s</a>@debian.org</span><br>' 

382 % (html.escape(sponsor), html.escape(sponsor, quote=False)) 

383 ) 

384 

385 print('<span class="signature">Fingerprint: %s</span>' % (fingerprint)) 

386 print("</td>") 

387 

388 print('<td class="closes">') 

389 for close in closes: 

390 print( 

391 '<a href="https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=%s">#%s</a><br>' 

392 % (html.escape(close), html.escape(close, quote=False)) 

393 ) 

394 print("</td></tr>") 

395 

396 

397############################################################ 

398 

399 

400def update_graph_database( 

401 rrd_dir: str | None, type: str, n_source: int, n_binary: int 

402) -> None: 

403 if not rrd_dir: 403 ↛ 406line 403 didn't jump to line 406 because the condition on line 403 was always true

404 return 

405 

406 import rrdtool 

407 

408 rrd_file = os.path.join(rrd_dir, type.lower() + ".rrd") 

409 update = [rrd_file, "N:%s:%s" % (n_source, n_binary)] 

410 

411 try: 

412 rrdtool.update(*update) 

413 except rrdtool.error: 

414 create = ( 

415 [rrd_file] 

416 + """ 

417--step 

418300 

419--start 

4200 

421DS:ds0:GAUGE:7200:0:1000 

422DS:ds1:GAUGE:7200:0:1000 

423RRA:AVERAGE:0.5:1:599 

424RRA:AVERAGE:0.5:6:700 

425RRA:AVERAGE:0.5:24:775 

426RRA:AVERAGE:0.5:288:795 

427RRA:MAX:0.5:1:600 

428RRA:MAX:0.5:6:700 

429RRA:MAX:0.5:24:775 

430RRA:MAX:0.5:288:795 

431""".strip().split( 

432 "\n" 

433 ) 

434 ) 

435 try: 

436 rrdtool.create(*create) 

437 rrdtool.update(*update) 

438 except rrdtool.error as e: 

439 print( 

440 "warning: queue_report: rrdtool error, skipping %s.rrd: %s" % (type, e) 

441 ) 

442 except NameError: 

443 pass 

444 

445 

446############################################################ 

447 

448 

449def process_queue(queue: PolicyQueue, log: IO[str] | None, rrd_dir: str | None) -> None: 

450 msg = "" 

451 type = queue.queue_name 

452 session = DBConn().session() 

453 

454 # Divide the .changes into per-source groups 

455 per_source: dict[str, dict[str, Any]] = {} 

456 total_pending = 0 

457 for upload in queue.uploads: 

458 source = upload.changes.source 

459 if source not in per_source: 459 ↛ 467line 459 didn't jump to line 467 because the condition on line 459 was always true

460 per_source[source] = {} 

461 per_source[source]["list"] = [] 

462 per_source[source]["processed"] = "" 

463 handler = PolicyQueueUploadHandler(upload, session) 

464 if handler.get_action(): 464 ↛ 465line 464 didn't jump to line 465 because the condition on line 464 was never true

465 per_source[source]["processed"] = "PENDING %s" % handler.get_action() 

466 total_pending += 1 

467 per_source[source]["list"].append(upload) 

468 per_source[source]["list"].sort(key=lambda x: x.changes.created, reverse=True) 

469 # Determine oldest time and have note status for each source group 

470 for source in list(per_source.keys()): 

471 source_list = per_source[source]["list"] 

472 first = source_list[0] 

473 oldest = time.mktime(first.changes.created.timetuple()) 

474 have_note = 0 

475 for d in per_source[source]["list"]: 

476 mtime = time.mktime(d.changes.created.timetuple()) 

477 if "Queue-Report::Options::New" in Cnf: 477 ↛ 478line 477 didn't jump to line 478 because the condition on line 477 was never true

478 oldest = max(oldest, mtime) 

479 else: 

480 oldest = min(oldest, mtime) 

481 have_note += has_new_comment( 

482 d.policy_queue, d.changes.source, d.changes.version 

483 ) 

484 per_source[source]["oldest"] = oldest 

485 if not have_note: 485 ↛ 487line 485 didn't jump to line 487 because the condition on line 485 was always true

486 per_source[source]["note_state"] = 0 # none 

487 elif have_note < len(source_list): 

488 per_source[source]["note_state"] = 1 # some 

489 else: 

490 per_source[source]["note_state"] = 2 # all 

491 per_source_items = list(per_source.items()) 

492 per_source_items.sort(key=functools.cmp_to_key(sg_compare)) 

493 

494 update_graph_database(rrd_dir, type, len(per_source_items), len(queue.uploads)) 

495 

496 entries = [] 

497 max_source_len = 0 

498 max_version_len = 0 

499 max_arch_len = 0 

500 try: 

501 logins = get_logins_from_ldap() 

502 except: 

503 logins = {} 

504 for i in per_source_items: 

505 maintainer = {} 

506 maint = "" 

507 distribution = "" 

508 closes = "" 

509 fingerprint = "" 

510 changeby = {} 

511 changedby = "" 

512 sponsor = "" 

513 filename = i[1]["list"][0].changes.changesname 

514 last_modified = time.time() - i[1]["oldest"] 

515 source = i[1]["list"][0].changes.source 

516 max_source_len = max(max_source_len, len(source)) 

517 binary_list = i[1]["list"][0].binaries 

518 binary = ", ".join([b.package for b in binary_list]) 

519 arches = set() 

520 versions = set() 

521 for j in i[1]["list"]: 

522 dbc = j.changes 

523 

524 if ( 524 ↛ 528line 524 didn't jump to line 528

525 "Queue-Report::Options::New" in Cnf 

526 or "Queue-Report::Options::822" in Cnf 

527 ): 

528 try: 

529 ( 

530 maintainer["maintainer822"], 

531 maintainer["maintainer2047"], 

532 maintainer["maintainername"], 

533 maintainer["maintaineremail"], 

534 ) = fix_maintainer(dbc.maintainer) 

535 except ParseMaintError: 

536 print("Problems while parsing maintainer address\n") 

537 maintainer["maintainername"] = "Unknown" 

538 maintainer["maintaineremail"] = "Unknown" 

539 maint = "%s:%s" % ( 

540 maintainer["maintainername"], 

541 maintainer["maintaineremail"], 

542 ) 

543 # ...likewise for the Changed-By: field if it exists. 

544 try: 

545 ( 

546 changeby["changedby822"], 

547 changeby["changedby2047"], 

548 changeby["changedbyname"], 

549 changeby["changedbyemail"], 

550 ) = fix_maintainer(dbc.changedby) 

551 except ParseMaintError: 

552 ( 

553 changeby["changedby822"], 

554 changeby["changedby2047"], 

555 changeby["changedbyname"], 

556 changeby["changedbyemail"], 

557 ) = ("", "", "", "") 

558 changedby = "%s:%s" % ( 

559 changeby["changedbyname"], 

560 changeby["changedbyemail"], 

561 ) 

562 

563 distribution = dbc.distribution.split() 

564 closes = dbc.closes 

565 

566 fingerprint = dbc.fingerprint 

567 sponsor_uid = get_uid_from_fingerprint(fingerprint, session) 

568 sponsor_name = sponsor_uid.name if sponsor_uid else "(Unknown)" 

569 sponsor_login = sponsor_uid.uid if sponsor_uid else "(Unknown)" 

570 if "@" in sponsor_login and fingerprint in logins: 

571 sponsor_login = logins[fingerprint] 

572 if ( 

573 sponsor_name != maintainer["maintainername"] 

574 and sponsor_name != changeby["changedbyname"] 

575 and sponsor_login + "@debian.org" != maintainer["maintaineremail"] 

576 and sponsor_name != changeby["changedbyemail"] 

577 ): 

578 sponsor = sponsor_login 

579 

580 for arch in dbc.architecture.split(): 

581 arches.add(arch) 

582 versions.add(dbc.version) 

583 arches_list = sorted(arches, key=utils.ArchKey) 

584 arch_list = " ".join(arches_list) 

585 version_list = " ".join(sorted(versions, reverse=True)) 

586 max_version_len = max(max_version_len, len(version_list)) 

587 max_arch_len = max(max_arch_len, len(arch_list)) 

588 if i[1]["note_state"]: 588 ↛ 589line 588 didn't jump to line 589 because the condition on line 588 was never true

589 note = " | [N]" 

590 else: 

591 note = "" 

592 entries.append( 

593 [ 

594 source, 

595 binary, 

596 version_list, 

597 arch_list, 

598 per_source[source]["processed"], 

599 note, 

600 last_modified, 

601 maint, 

602 distribution, 

603 closes, 

604 fingerprint, 

605 sponsor, 

606 changedby, 

607 filename, 

608 ] 

609 ) 

610 

611 # direction entry consists of "Which field, which direction, time-consider" where 

612 # time-consider says how we should treat last_modified. Thats all. 

613 

614 # Look for the options for sort and then do the sort. 

615 age = "h" 

616 if "Queue-Report::Options::Age" in Cnf: 616 ↛ 617line 616 didn't jump to line 617 because the condition on line 616 was never true

617 age = Cnf["Queue-Report::Options::Age"] 

618 if "Queue-Report::Options::New" in Cnf: 618 ↛ 620line 618 didn't jump to line 620 because the condition on line 618 was never true

619 # If we produce html we always have oldest first. 

620 direction.append((6, -1, "ao")) 

621 else: 

622 if "Queue-Report::Options::Sort" in Cnf: 622 ↛ 623line 622 didn't jump to line 623 because the condition on line 622 was never true

623 for j in Cnf["Queue-Report::Options::Sort"].split(","): 

624 if j == "ao": 

625 # Age, oldest first. 

626 direction.append((6, -1, age)) 

627 elif j == "an": 

628 # Age, newest first. 

629 direction.append((6, 1, age)) 

630 elif j == "na": 

631 # Name, Ascending. 

632 direction.append((0, 1, 0)) 

633 elif j == "nd": 

634 # Name, Descending. 

635 direction.append((0, -1, 0)) 

636 elif j == "nl": 

637 # Notes last. 

638 direction.append((5, 1, 0)) 

639 elif j == "nf": 

640 # Notes first. 

641 direction.append((5, -1, 0)) 

642 entries.sort(key=functools.cmp_to_key(sortfunc)) 

643 # Yes, in theory you can add several sort options at the commandline with. But my mind is to small 

644 # at the moment to come up with a real good sorting function that considers all the sidesteps you 

645 # have with it. (If you combine options it will simply take the last one at the moment). 

646 # Will be enhanced in the future. 

647 

648 if log is not None: 648 ↛ 650line 648 didn't jump to line 650 because the condition on line 648 was never true

649 # print stuff out in 822 format 

650 for entry in entries: 

651 ( 

652 source, 

653 binary, 

654 version_list, 

655 arch_list, 

656 processed, 

657 note, 

658 last_modified, 

659 maint, 

660 distribution, 

661 closes, 

662 fingerprint, 

663 sponsor, 

664 changedby, 

665 changes_file, 

666 ) = entry 

667 

668 # We'll always have Source, Version, Arch, Mantainer, and Dist 

669 # For the rest, check to see if we have them, then print them out 

670 log.write("Source: " + source + "\n") 

671 log.write("Binary: " + binary + "\n") 

672 log.write("Version: " + version_list + "\n") 

673 log.write("Architectures: ") 

674 log.write((", ".join(arch_list.split(" "))) + "\n") 

675 log.write("Age: " + time_pp(last_modified) + "\n") 

676 log.write( 

677 "Last-Modified: " + str(int(time.time()) - int(last_modified)) + "\n" 

678 ) 

679 log.write("Queue: " + type + "\n") 

680 

681 (name, mail) = maint.split(":", 1) 

682 log.write("Maintainer: " + name + " <" + mail + ">" + "\n") 

683 if changedby: 

684 (name, mail) = changedby.split(":", 1) 

685 log.write("Changed-By: " + name + " <" + mail + ">" + "\n") 

686 if sponsor: 

687 log.write("Sponsored-By: %s@debian.org\n" % sponsor) 

688 log.write("Distribution:") 

689 for dist in distribution: 

690 log.write(" " + dist) 

691 log.write("\n") 

692 log.write("Fingerprint: " + fingerprint + "\n") 

693 if closes: 

694 bug_string = "" 

695 for bugs in closes: 

696 bug_string += "#" + bugs + ", " 

697 log.write("Closes: " + bug_string[:-2] + "\n") 

698 log.write("Changes-File: " + os.path.basename(changes_file) + "\n") 

699 log.write("\n") 

700 

701 total_count = len(queue.uploads) 

702 source_count = len(per_source_items) 

703 

704 if "Queue-Report::Options::New" in Cnf: 704 ↛ 705line 704 didn't jump to line 705 because the condition on line 704 was never true

705 direction.append((6, 1, "ao")) 

706 entries.sort(key=functools.cmp_to_key(sortfunc)) 

707 # Output for a html file. First table header. then table_footer. 

708 # Any line between them is then a <tr> printed from subroutine table_row. 

709 if len(entries) > 0: 

710 table_header(type.upper(), source_count, total_count) 

711 for entry in entries: 

712 ( 

713 source, 

714 binary, 

715 version_list, 

716 arch_list, 

717 processed, 

718 note, 

719 last_modified, 

720 maint, 

721 distribution, 

722 closes, 

723 fingerprint, 

724 sponsor, 

725 changedby, 

726 _, 

727 ) = entry 

728 table_row( 

729 source, 

730 version_list, 

731 arch_list, 

732 last_modified, 

733 maint, 

734 distribution, 

735 closes, 

736 fingerprint, 

737 sponsor, 

738 changedby, 

739 ) 

740 table_footer(type.upper()) 

741 elif "Queue-Report::Options::822" not in Cnf: 741 ↛ exitline 741 didn't return from function 'process_queue' because the condition on line 741 was always true

742 # The "normal" output without any formatting. 

743 msg = "" 

744 for entry in entries: 

745 ( 

746 source, 

747 binary, 

748 version_list, 

749 arch_list, 

750 processed, 

751 note, 

752 last_modified, 

753 _, 

754 _, 

755 _, 

756 _, 

757 _, 

758 _, 

759 _, 

760 ) = entry 

761 if processed: 761 ↛ 762line 761 didn't jump to line 762 because the condition on line 761 was never true

762 format = "%%-%ds | %%-%ds | %%-%ds | %%s\n" % ( 

763 max_source_len, 

764 max_version_len, 

765 max_arch_len, 

766 ) 

767 msg += format % (source, version_list, arch_list, processed) 

768 else: 

769 format = "%%-%ds | %%-%ds | %%-%ds%%s | %%s old\n" % ( 

770 max_source_len, 

771 max_version_len, 

772 max_arch_len, 

773 ) 

774 msg += format % ( 

775 source, 

776 version_list, 

777 arch_list, 

778 note, 

779 time_pp(last_modified), 

780 ) 

781 

782 if msg: 

783 print(type.upper()) 

784 print("-" * len(type)) 

785 print() 

786 print(msg) 

787 print( 

788 "%s %s source package%s / %s %s package%s in total / %s %s package%s to be processed." 

789 % ( 

790 source_count, 

791 type, 

792 plural(source_count), 

793 total_count, 

794 type, 

795 plural(total_count), 

796 total_pending, 

797 type, 

798 plural(total_pending), 

799 ) 

800 ) 

801 print() 

802 

803 

804################################################################################ 

805 

806 

807def main() -> None: 

808 global Cnf 

809 

810 Cnf = utils.get_conf() 

811 Arguments = [ 

812 ("h", "help", "Queue-Report::Options::Help"), 

813 ("n", "new", "Queue-Report::Options::New"), 

814 ("8", "822", "Queue-Report::Options::822"), 

815 ("s", "sort", "Queue-Report::Options::Sort", "HasArg"), 

816 ("a", "age", "Queue-Report::Options::Age", "HasArg"), 

817 ("r", "rrd", "Queue-Report::Options::Rrd", "HasArg"), 

818 ("d", "directories", "Queue-Report::Options::Directories", "HasArg"), 

819 ] 

820 for i in ["help"]: 

821 key = "Queue-Report::Options::%s" % i 

822 if key not in Cnf: 822 ↛ 820line 822 didn't jump to line 820 because the condition on line 822 was always true

823 Cnf[key] = "" # type: ignore[index] 

824 

825 apt_pkg.parse_commandline(Cnf, Arguments, sys.argv) # type: ignore[attr-defined] 

826 

827 Options = Cnf.subtree("Queue-Report::Options") # type: ignore[attr-defined] 

828 if Options["Help"]: 

829 usage() 

830 

831 if "Queue-Report::Options::New" in Cnf: 831 ↛ 832line 831 didn't jump to line 832 because the condition on line 831 was never true

832 header() 

833 

834 queue_names = [] 

835 

836 if "Queue-Report::Options::Directories" in Cnf: 836 ↛ 837line 836 didn't jump to line 837 because the condition on line 836 was never true

837 for i in Cnf["Queue-Report::Options::Directories"].split(","): 

838 queue_names.append(i) 

839 elif "Queue-Report::Directories" in Cnf: 839 ↛ 840line 839 didn't jump to line 840 because the condition on line 839 was never true

840 queue_names = Cnf.value_list("Queue-Report::Directories") 

841 else: 

842 queue_names = ["byhand", "new"] 

843 

844 rrd_dir = Cnf.get("Queue-Report::Options::Rrd") or Cnf.get("Dir::Rrd") or None 

845 

846 f = None 

847 if "Queue-Report::Options::822" in Cnf: 847 ↛ 849line 847 didn't jump to line 849 because the condition on line 847 was never true

848 # Open the report file 

849 f = sys.stdout 

850 filename822 = Cnf.get("Queue-Report::ReportLocations::822Location") 

851 if filename822: 

852 f = open(filename822, "w") 

853 

854 session = DBConn().session() 

855 

856 for queue_name in queue_names: 

857 queue = session.scalars( 

858 select(PolicyQueue).filter_by(queue_name=queue_name).limit(1) 

859 ).first() 

860 if queue is not None: 860 ↛ 863line 860 didn't jump to line 863 because the condition on line 860 was always true

861 process_queue(queue, f, rrd_dir) 

862 else: 

863 utils.warn("Cannot find queue %s" % queue_name) 

864 

865 if f is not None: 865 ↛ 866line 865 didn't jump to line 866 because the condition on line 865 was never true

866 f.close() 

867 

868 if "Queue-Report::Options::New" in Cnf: 868 ↛ 869line 868 didn't jump to line 869 because the condition on line 868 was never true

869 footer()