Coverage for procpath/sqliteviz.py: 100%

53 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2025-04-05 18:56 +0000

1import hashlib 

2import http.server 

3import io 

4import json 

5import logging 

6import textwrap 

7import zipfile 

8from functools import partial 

9from pathlib import Path 

10from urllib.request import urlopen 

11 

12from . import procret 

13 

14 

15__all__ = 'get_visualisation_bundle', 'install_sqliteviz', 'serve_dir', 'symlink_database' 

16 

17logger = logging.getLogger(__package__) 

18 

19 

20def install_sqliteviz(zip_url: str, target_dir: Path): 

21 response = urlopen(zip_url) (empty)procpath.test.cmd.TestExploreCommand.test_exploreprocpath.test.cmd.TestExploreCommand.test_explore_preload_databaseprocpath.test.cmd.TestExploreCommand.test_explore_preload_database_missing

22 with zipfile.ZipFile(io.BytesIO(response.read())) as z: (empty)procpath.test.cmd.TestExploreCommand.test_exploreprocpath.test.cmd.TestExploreCommand.test_explore_preload_databaseprocpath.test.cmd.TestExploreCommand.test_explore_preload_database_missing

23 z.extractall(target_dir) (empty)procpath.test.cmd.TestExploreCommand.test_exploreprocpath.test.cmd.TestExploreCommand.test_explore_preload_databaseprocpath.test.cmd.TestExploreCommand.test_explore_preload_database_missing

24 

25 bundle = json.dumps(get_visualisation_bundle(), sort_keys=True) (empty)procpath.test.cmd.TestExploreCommand.test_exploreprocpath.test.cmd.TestExploreCommand.test_explore_preload_databaseprocpath.test.cmd.TestExploreCommand.test_explore_preload_database_missing

26 (target_dir / 'inquiries.json').write_text(bundle) (empty)procpath.test.cmd.TestExploreCommand.test_exploreprocpath.test.cmd.TestExploreCommand.test_explore_preload_databaseprocpath.test.cmd.TestExploreCommand.test_explore_preload_database_missing

27 

28 

29def _get_line_chart_config(title: str) -> dict: 

30 return { (empty)procpath.test.cmd.TestExploreCommand.test_exploreprocpath.test.cmd.TestExploreCommand.test_explore_preload_databaseprocpath.test.cmd.TestExploreCommand.test_explore_preload_database_missingprocpath.test.unit.TestSqlitevizQuery.test_total_cpu_usageprocpath.test.unit.TestSqlitevizQuery.test_total_memory_consumption

31 'data': [{ 

32 'meta': {'columnNames': {'x': 'ts', 'y': 'value'}}, 

33 'mode': 'lines', 

34 'type': 'scatter', 

35 'x': None, 

36 'xsrc': 'ts', 

37 'y': None, 

38 'ysrc': 'value', 

39 'transforms': [{ 

40 'groups': None, 

41 'groupssrc': 'pid', 

42 'meta': {'columnNames': {'groups': 'pid'}}, 

43 'styles': [], 

44 'type': 'groupby', 

45 }], 

46 }], 

47 'frames': [], 

48 'layout': { 

49 'autosize': True, 

50 'title': {'text': title}, 

51 'xaxis': { 

52 'autorange': True, 

53 'range': [], 

54 'type': 'date' 

55 }, 

56 'yaxis': { 

57 'autorange': True, 

58 'range': [], 

59 'type': 'linear' 

60 }, 

61 }, 

62 } 

63 

64 

65def _get_sqliteviz_only_charts(): 

66 return [ (empty)procpath.test.cmd.TestExploreCommand.test_exploreprocpath.test.cmd.TestExploreCommand.test_explore_preload_databaseprocpath.test.cmd.TestExploreCommand.test_explore_preload_database_missingprocpath.test.unit.TestSqlitevizQuery.test_total_cpu_usageprocpath.test.unit.TestSqlitevizQuery.test_total_memory_consumption

67 # Process Timeline PID 

68 { 

69 'id': 'csfOTEpzlFfYz7OUc2aGI', 

70 'createdAt': '2023-09-03T12:00:00Z', 

71 'name': 'Process Timeline, PID', 

72 'query': textwrap.dedent(''' 

73 WITH RECURSIVE tree(pid, ppid, pid_comm) AS ( 

74 SELECT stat_pid, stat_ppid, stat_pid || ' ' || stat_comm 

75 FROM record 

76 GROUP BY 1 

77 UNION 

78 SELECT pid, stat_ppid, stat_pid || ' ' || stat_comm 

79 FROM record, tree 

80 WHERE record.stat_pid = tree.ppid 

81 ), lookup AS ( 

82 SELECT pid, group_concat(pid_comm, ' / ') path_to_root 

83 FROM tree 

84 GROUP BY 1 

85 ) 

86 SELECT 

87 ts * 1000 AS ts, 

88 stat_pid, 

89 stat_pid || ' ' || stat_comm AS pid_comm, 

90 iif( 

91 length(cmdline) > 0, 

92 substr(cmdline, 0, 75) || iif(length(cmdline) > 75, '...', ''), 

93 stat_comm 

94 ) || '<br>' || path_to_root AS cmd 

95 FROM record 

96 JOIN lookup ON stat_pid = pid 

97 ''').strip(), 

98 'viewType': 'chart', 

99 'viewOptions': { 

100 'data': [{ 

101 'type': 'scattergl', 

102 'mode': 'markers', 

103 'meta': {'columnNames': {'x': 'ts', 'y': 'stat_pid', 'text': 'cmd'}}, 

104 'transforms': [{ 

105 'type': 'groupby', 

106 'styles': [], 

107 'meta': {'columnNames': {'groups': 'pid_comm'}}, 

108 'groups': None, 

109 'groupssrc': 'pid_comm', 

110 }], 

111 'y': None, 

112 'ysrc': 'stat_pid', 

113 'x': None, 

114 'xsrc': 'ts', 

115 'text': None, 

116 'textsrc': 'cmd', 

117 'marker': {'size': 12, 'maxdisplayed': 0}, 

118 'line': {'width': 3}, 

119 'hoverinfo': 'x+text', 

120 }], 

121 'layout': { 

122 'xaxis': { 

123 'type': 'date', 

124 'range': [], 

125 'autorange': True, 

126 }, 

127 'yaxis': { 

128 'type': 'category', 

129 'range': [], 

130 'autorange': True, 

131 'showticklabels': False, 

132 }, 

133 'title': {'text': 'Process Timeline, PID'}, 

134 'hovermode': 'closest', 

135 }, 

136 'frames': [], 

137 }, 

138 }, 

139 # Process Timeline CPU 

140 { 

141 'id': '4PBtpi7inEAe-yjtRHCi0', 

142 'createdAt': '2023-09-03T12:00:00Z', 

143 'name': 'Process Timeline, CPU', 

144 'query': textwrap.dedent(''' 

145 WITH RECURSIVE tree(pid, ppid, pid_comm) AS ( 

146 SELECT stat_pid, stat_ppid, stat_pid || ' ' || stat_comm 

147 FROM record 

148 GROUP BY 1 

149 UNION 

150 SELECT pid, stat_ppid, stat_pid || ' ' || stat_comm 

151 FROM record, tree 

152 WHERE record.stat_pid = tree.ppid 

153 ), path_lookup AS ( 

154 SELECT pid, group_concat(pid_comm, ' / ') path_to_root 

155 FROM tree 

156 GROUP BY 1 

157 ), cpu_diff AS ( 

158 SELECT 

159 ts, 

160 stat_pid, 

161 stat_ppid, 

162 stat_priority, 

163 stat_comm, 

164 cmdline, 

165 stat_utime + stat_stime - LAG(stat_utime + stat_stime) OVER ( 

166 PARTITION BY stat_pid 

167 ORDER BY record_id 

168 ) tick_diff, 

169 ts - LAG(ts) OVER ( 

170 PARTITION BY stat_pid 

171 ORDER BY record_id 

172 ) ts_diff 

173 FROM record 

174 ), record_ext AS ( 

175 SELECT 

176 *, 

177 100.0 * tick_diff / ( 

178 SELECT value FROM meta WHERE key = 'clock_ticks' 

179 ) / ts_diff cpu_usage 

180 FROM cpu_diff 

181 WHERE tick_diff IS NOT NULL 

182 ) 

183 SELECT 

184 ts * 1000 AS ts, 

185 stat_pid, 

186 stat_pid || ' ' || stat_comm AS pid_comm, 

187 power(1.02, -r.stat_priority) priority_size, 

188 cpu_usage, 

189 iif( 

190 length(cmdline) > 0, 

191 substr(cmdline, 0, 75) || iif(length(cmdline) > 75, '...', ''), 

192 stat_comm 

193 ) 

194 || '<br>' || path_to_root 

195 || '<br>' || 'CPU, %: ' || printf('%.2f', cpu_usage) 

196 || '<br>' || 'priority: ' || stat_priority AS cmd 

197 FROM record_ext r 

198 JOIN path_lookup p ON r.stat_pid = p.pid 

199 -- Tune the following CPU usage inequality for a clearer figure 

200 WHERE cpu_usage > 0 

201 ''').strip(), 

202 'viewType': 'chart', 

203 'viewOptions': { 

204 'data': [{ 

205 'type': 'scattergl', 

206 'mode': 'markers', 

207 'meta': { 

208 'columnNames': { 

209 'text': 'cmd', 

210 'x': 'ts', 

211 'y': 'stat_pid', 

212 'marker': { 

213 'color': 'cpu_usage', 

214 'size': 'priority_size', 

215 }, 

216 }, 

217 }, 

218 'y': None, 

219 'ysrc': 'stat_pid', 

220 'x': None, 

221 'xsrc': 'ts', 

222 'text': None, 

223 'textsrc': 'cmd', 

224 'marker': { 

225 'maxdisplayed': 0, 

226 'color': None, 

227 'colorsrc': 'cpu_usage', 

228 'size': None, 

229 'sizesrc': 'priority_size', 

230 'sizeref': 0.00667, 

231 'sizemode': 'area', 

232 'showscale': True, 

233 'colorbar': {'title': {'text': 'CPU, %'}}, 

234 'line': {'width': 0}, 

235 }, 

236 'line': {'width': 3}, 

237 'hoverinfo': 'x+text', 

238 }], 

239 'layout': { 

240 'xaxis': { 

241 'type': 'date', 

242 'range': [], 

243 'autorange': True, 

244 }, 

245 'yaxis': { 

246 'type': 'category', 

247 'range': [], 

248 'autorange': True, 

249 'showticklabels': False, 

250 }, 

251 'title': {'text': 'Process Timeline, CPU'}, 

252 'hovermode': 'closest', 

253 }, 

254 'frames': [], 

255 }, 

256 }, 

257 # Process Tree 

258 { 

259 'id': '3XXe7a80GvD6Trk9FyXRz', 

260 'name': 'Process Tree', 

261 'createdAt': '2023-09-03T12:00:00Z', 

262 'query': textwrap.dedent(''' 

263 WITH lookup(pid, num) AS ( 

264 SELECT stat_pid, ROW_NUMBER() OVER(ORDER BY stat_pid) - 1 

265 FROM record 

266 GROUP BY 1 

267 ), nodes AS ( 

268 SELECT 

269 stat_pid, 

270 -- Opt-in for special bare column processing to prefer the 

271 -- first values (the minimum value is not used per se) 

272 MIN(ts), 

273 stat_ppid, 

274 stat_pid || ' ' || stat_comm AS pid_comm, 

275 iif( 

276 length(cmdline) > 0, 

277 substr(cmdline, 0, 75) || iif(length(cmdline) > 75, '...', ''), 

278 stat_comm 

279 ) cmd 

280 FROM record 

281 GROUP BY 1 

282 ) 

283 SELECT p.num p_num, pp.num pp_num, pid_comm, cmd, 1 value 

284 FROM nodes 

285 JOIN lookup p ON stat_pid = p.pid 

286 LEFT JOIN lookup pp ON stat_ppid = pp.pid 

287 ORDER BY p.num 

288 ''').strip(), 

289 'viewType': 'chart', 

290 'viewOptions': { 

291 'data': [ 

292 { 

293 'type': 'sankey', 

294 'mode': 'markers', 

295 'node': {'labelsrc': 'pid_comm'}, 

296 'link': { 

297 'valuesrc': 'value', 

298 'targetsrc': 'p_num', 

299 'sourcesrc': 'pp_num', 

300 'labelsrc': 'cmd' 

301 }, 

302 'meta': { 

303 'columnNames': { 

304 'node': {'label': 'pid_comm'}, 

305 'link': { 

306 'source': 'pp_num', 

307 'target': 'p_num', 

308 'value': 'value', 

309 'label': 'cmd' 

310 } 

311 } 

312 }, 

313 'orientation': 'h', 

314 'hoverinfo': 'name', 

315 'arrangement': 'freeform' 

316 } 

317 ], 

318 'layout': { 

319 'xaxis': {'range': [], 'autorange': True}, 

320 'yaxis': {'range': [], 'autorange': True}, 

321 'autosize': True, 

322 'title': {'text': 'Process Tree'} 

323 }, 

324 'frames': [] 

325 } 

326 }, 

327 # Total Memory Consumption 

328 { 

329 'id': 'boSs15w7Endl5V9bABjXv', 

330 'createdAt': '2023-09-03T12:00:00Z', 

331 'name': 'Total Resident Set Size, MiB', 

332 'query': textwrap.dedent(''' 

333 WITH downsampled_record AS ( 

334 SELECT 

335 stat_pid, 

336 -- Adjust downsampling factor 

337 CAST(ts / 10 as INT) * 10 ts, 

338 stat_comm, 

339 cmdline, 

340 MAX(stat_rss) stat_rss 

341 FROM record 

342 GROUP BY 1, 2 

343 ), proc_group AS ( 

344 SELECT 

345 -- Comment "stat_comm" group and uncomment this to have coarser grouping 

346 -- CASE 

347 -- WHEN cmdline LIKE '%firefox%' THEN '1. firefox' 

348 -- WHEN cmdline LIKE '%chromium%' THEN '2. chromium' 

349 -- ELSE '3. other' 

350 -- END pgroup, 

351 stat_comm pgroup, 

352 ts, 

353 SUM(stat_rss) 

354 / 1024.0 / 1024 * (SELECT value FROM meta WHERE key = 'page_size') value 

355 FROM downsampled_record 

356 GROUP BY ts, 1 

357 ORDER BY ts 

358 ), proc_group_avg AS ( 

359 SELECT 

360 ts, 

361 pgroup, 

362 AVG(value) OVER ( 

363 PARTITION BY pgroup 

364 ORDER BY ts 

365 -- Adjust centred moving average window 

366 RANGE BETWEEN 10 PRECEDING AND 10 FOLLOWING 

367 ) value 

368 FROM proc_group 

369 ), total_lookup(ts, total) AS ( 

370 SELECT ts, SUM(value) 

371 FROM proc_group_avg 

372 GROUP BY 1 

373 ) 

374 SELECT 

375 proc_group_avg.ts * 1000 ts, 

376 pgroup, 

377 value, 

378 'total: ' || round(total, 1) || ' MiB' total 

379 FROM proc_group_avg 

380 JOIN total_lookup ON proc_group_avg.ts = total_lookup.ts 

381 ORDER BY ts 

382 ''').strip(), 

383 'viewType': 'chart', 

384 'viewOptions': { 

385 'data': [{ 

386 'type': 'scatter', 

387 'mode': 'lines', 

388 'meta': {'columnNames': {'x': 'ts', 'y': 'value'}}, 

389 'transforms': [{ 

390 'type': 'groupby', 

391 'groupssrc': 'pgroup', 

392 'groups': None, 

393 'styles': [], 

394 'meta': {'columnNames': {'groups': 'pgroup'}}, 

395 }], 

396 'stackgroup': 1, 

397 'x': None, 

398 'xsrc': 'ts', 

399 'y': None, 

400 'ysrc': 'value', 

401 'text': None, 

402 'textsrc': 'total', 

403 'hoverinfo': 'x+text+name', 

404 }], 

405 'layout': { 

406 'xaxis': { 

407 'type': 'date', 

408 'range': [], 

409 'autorange': True, 

410 }, 

411 'yaxis': { 

412 'type': 'linear', 

413 'range': [], 

414 'autorange': True, 

415 'separatethousands': True, 

416 }, 

417 'title': {'text': 'Total Resident Set Size, MiB'}, 

418 'hovermode': 'closest', 

419 }, 

420 'frames': [] 

421 }, 

422 }, 

423 # Total CPU Usage 

424 { 

425 'id': 'kd17-XGI85L2Oogj74Uyb', 

426 'createdAt': '2023-09-03T12:00:00Z', 

427 'name': 'Total CPU Usage, %', 

428 'query': textwrap.dedent(''' 

429 WITH downsampled_record AS ( 

430 SELECT 

431 stat_pid, 

432 -- Adjust downsampling factor 

433 CAST(ts / 10 as INT) * 10 ts, 

434 stat_comm, 

435 cmdline, 

436 MAX(stat_utime) stat_utime, 

437 MAX(stat_stime) stat_stime 

438 FROM record 

439 GROUP BY 1, 2 

440 ), proc_cpu_diff AS ( 

441 SELECT 

442 stat_pid, 

443 ts, 

444 stat_comm, 

445 cmdline, 

446 stat_utime + stat_stime - LAG(stat_utime + stat_stime) OVER ( 

447 PARTITION BY stat_pid 

448 ORDER BY ts 

449 ) tick_diff, 

450 ts - LAG(ts) OVER ( 

451 PARTITION BY stat_pid 

452 ORDER BY ts 

453 ) ts_diff 

454 FROM downsampled_record 

455 ), proc_group AS ( 

456 SELECT 

457 -- Comment "stat_comm" group and uncomment this to have coarser grouping 

458 -- CASE 

459 -- WHEN cmdline LIKE '%firefox%' THEN '1. firefox' 

460 -- WHEN cmdline LIKE '%chromium%' THEN '2. chromium' 

461 -- ELSE '3. other' 

462 -- END pgroup, 

463 stat_comm pgroup, 

464 ts, 

465 SUM(tick_diff) tick_diff, 

466 AVG(ts_diff) ts_diff 

467 FROM proc_cpu_diff 

468 WHERE tick_diff IS NOT NULL 

469 GROUP BY ts, 1 

470 ORDER BY ts 

471 ), proc_group_avg AS ( 

472 SELECT 

473 ts, 

474 pgroup, 

475 AVG( 

476 100.0 

477 * tick_diff 

478 / ts_diff 

479 / (SELECT value FROM meta WHERE key = 'clock_ticks') 

480 ) OVER ( 

481 PARTITION BY pgroup 

482 ORDER BY ts 

483 -- Adjust centred moving average window 

484 RANGE BETWEEN 10 PRECEDING AND 10 FOLLOWING 

485 ) value 

486 FROM proc_group 

487 ), total_lookup(ts, total) AS ( 

488 SELECT ts, SUM(value) 

489 FROM proc_group_avg 

490 GROUP BY 1 

491 ) 

492 SELECT 

493 proc_group_avg.ts * 1000 ts, 

494 pgroup, 

495 value, 

496 'total: ' || round(total, 1) || ' %' total 

497 FROM proc_group_avg 

498 JOIN total_lookup ON proc_group_avg.ts = total_lookup.ts 

499 ORDER BY ts 

500 ''').strip(), 

501 'viewType': 'chart', 

502 'viewOptions': { 

503 'data': [{ 

504 'type': 'scatter', 

505 'mode': 'lines', 

506 'meta': {'columnNames': {'x': 'ts', 'y': 'value'}}, 

507 'transforms': [{ 

508 'type': 'groupby', 

509 'groupssrc': 'pgroup', 

510 'groups': None, 

511 'styles': [], 

512 'meta': {'columnNames': {'groups': 'pgroup'}}, 

513 }], 

514 'stackgroup': 1, 

515 'x': None, 

516 'xsrc': 'ts', 

517 'y': None, 

518 'ysrc': 'value', 

519 'text': None, 

520 'textsrc': 'total', 

521 'hoverinfo': 'x+text+name', 

522 }], 

523 'layout': { 

524 'xaxis': { 

525 'type': 'date', 

526 'range': [], 

527 'autorange': True, 

528 }, 

529 'yaxis': { 

530 'type': 'linear', 

531 'range': [], 

532 'autorange': True, 

533 'separatethousands': True, 

534 }, 

535 'title': {'text': 'Total CPU Usage, %'}, 

536 'hovermode': 'closest', 

537 }, 

538 'frames': [] 

539 }, 

540 }, 

541 ] 

542 

543 

544def get_visualisation_bundle() -> dict: 

545 """Get Sqliteviz import-able visualisation bundle.""" 

546 

547 inquiries = [] (empty)procpath.test.cmd.TestExploreCommand.test_exploreprocpath.test.cmd.TestExploreCommand.test_explore_preload_databaseprocpath.test.cmd.TestExploreCommand.test_explore_preload_database_missingprocpath.test.unit.TestSqlitevizQuery.test_total_cpu_usageprocpath.test.unit.TestSqlitevizQuery.test_total_memory_consumption

548 result = {'version': 2, 'inquiries': inquiries} (empty)procpath.test.cmd.TestExploreCommand.test_exploreprocpath.test.cmd.TestExploreCommand.test_explore_preload_databaseprocpath.test.cmd.TestExploreCommand.test_explore_preload_database_missingprocpath.test.unit.TestSqlitevizQuery.test_total_cpu_usageprocpath.test.unit.TestSqlitevizQuery.test_total_memory_consumption

549 

550 for query in procret.registry.values(): (empty)procpath.test.cmd.TestExploreCommand.test_exploreprocpath.test.cmd.TestExploreCommand.test_explore_preload_databaseprocpath.test.cmd.TestExploreCommand.test_explore_preload_database_missingprocpath.test.unit.TestSqlitevizQuery.test_total_cpu_usageprocpath.test.unit.TestSqlitevizQuery.test_total_memory_consumption

551 query_text = query.get_short_query(ts_as_milliseconds=True) (empty)procpath.test.cmd.TestExploreCommand.test_exploreprocpath.test.cmd.TestExploreCommand.test_explore_preload_databaseprocpath.test.cmd.TestExploreCommand.test_explore_preload_database_missingprocpath.test.unit.TestSqlitevizQuery.test_total_cpu_usageprocpath.test.unit.TestSqlitevizQuery.test_total_memory_consumption

552 inquiries.append({ (empty)procpath.test.cmd.TestExploreCommand.test_exploreprocpath.test.cmd.TestExploreCommand.test_explore_preload_databaseprocpath.test.cmd.TestExploreCommand.test_explore_preload_database_missingprocpath.test.unit.TestSqlitevizQuery.test_total_cpu_usageprocpath.test.unit.TestSqlitevizQuery.test_total_memory_consumption

553 'id': hashlib.md5(query_text.encode()).hexdigest()[:21], 

554 'createdAt': '2023-09-03T12:00:00Z', 

555 'name': query.title, 

556 'query': textwrap.dedent(query_text).strip(), 

557 'viewType': 'chart', 

558 'viewOptions': _get_line_chart_config(query.title), 

559 }) 

560 

561 inquiries.extend(_get_sqliteviz_only_charts()) (empty)procpath.test.cmd.TestExploreCommand.test_exploreprocpath.test.cmd.TestExploreCommand.test_explore_preload_databaseprocpath.test.cmd.TestExploreCommand.test_explore_preload_database_missingprocpath.test.unit.TestSqlitevizQuery.test_total_cpu_usageprocpath.test.unit.TestSqlitevizQuery.test_total_memory_consumption

562 

563 return result (empty)procpath.test.cmd.TestExploreCommand.test_exploreprocpath.test.cmd.TestExploreCommand.test_explore_preload_databaseprocpath.test.cmd.TestExploreCommand.test_explore_preload_database_missingprocpath.test.unit.TestSqlitevizQuery.test_total_cpu_usageprocpath.test.unit.TestSqlitevizQuery.test_total_memory_consumption

564 

565 

566class HttpRequestHandler(http.server.SimpleHTTPRequestHandler): 

567 def send_head(self): 

568 # Disable cache validation based on modified timestamp of the 

569 # file because it's a symlink pointing to different files, and 

570 # next one can easily be older than current one 

571 if self.path == '/db.sqlite': 

572 del self.headers['If-Modified-Since'] 

573 

574 return super().send_head() 

575 

576 def end_headers(self): 

577 if self.path == '/db.sqlite': 

578 # The "no-store" response directive indicates that caches 

579 # should not store this response. No point to try to cache 

580 # big database files 

581 self.send_header('Cache-Control', 'no-store') 

582 else: 

583 # The "no-cache" response directive indicates that the 

584 # response can be stored in caches, but the response must 

585 # be validated with the origin server before each reuse 

586 self.send_header('Cache-Control', 'no-cache') 

587 

588 super().end_headers() 

589 

590 

591def serve_dir( 

592 bind: str, port: int, directory: str, *, server_cls=http.server.ThreadingHTTPServer 

593): 

594 handler_cls = partial(HttpRequestHandler, directory=directory) 

595 with server_cls((bind, port), handler_cls) as httpd: 

596 httpd.serve_forever() 

597 

598 

599def symlink_database(database_file: str, sqliteviz_dir: Path) -> Path: 

600 db_path = Path(database_file).absolute() procpath.test.cmd.TestExploreCommand.test_explore_preload_databaseprocpath.test.cmd.TestExploreCommand.test_explore_preload_database_missingprocpath.test.cmd.TestExploreCommand.test_explore_serveprocpath.test.cmd.TestExploreCommand.test_symlink_database

601 if not db_path.exists(): procpath.test.cmd.TestExploreCommand.test_explore_preload_databaseprocpath.test.cmd.TestExploreCommand.test_explore_preload_database_missingprocpath.test.cmd.TestExploreCommand.test_explore_serveprocpath.test.cmd.TestExploreCommand.test_symlink_database

602 raise FileNotFoundError procpath.test.cmd.TestExploreCommand.test_explore_preload_database_missing

603 

604 sym_path = sqliteviz_dir / 'db.sqlite' procpath.test.cmd.TestExploreCommand.test_explore_preload_databaseprocpath.test.cmd.TestExploreCommand.test_explore_serveprocpath.test.cmd.TestExploreCommand.test_symlink_database

605 sym_path.unlink(missing_ok=True) procpath.test.cmd.TestExploreCommand.test_explore_preload_databaseprocpath.test.cmd.TestExploreCommand.test_explore_serveprocpath.test.cmd.TestExploreCommand.test_symlink_database

606 sym_path.symlink_to(db_path) procpath.test.cmd.TestExploreCommand.test_explore_preload_databaseprocpath.test.cmd.TestExploreCommand.test_explore_serveprocpath.test.cmd.TestExploreCommand.test_symlink_database

607 return sym_path procpath.test.cmd.TestExploreCommand.test_explore_preload_databaseprocpath.test.cmd.TestExploreCommand.test_explore_serveprocpath.test.cmd.TestExploreCommand.test_symlink_database