Coverage for documents/management/commands/document_sanity_checker.py: 0%
54 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 09:07 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 09:07 +0000
1"""Management command to check the document archive for issues."""
3from __future__ import annotations
5import logging
6from typing import Any
8from rich.panel import Panel
9from rich.table import Table
10from rich.text import Text
12from documents.management.commands.base import PaperlessCommand
13from documents.models import Document
14from documents.sanity_checker import SanityCheckMessages
15from documents.sanity_checker import check_sanity
17_LEVEL_STYLE: dict[int, tuple[str, str]] = {
18 logging.ERROR: ("bold red", "ERROR"),
19 logging.WARNING: ("yellow", "WARN"),
20 logging.INFO: ("dim", "INFO"),
21}
24class Command(PaperlessCommand):
25 help = "This command checks your document archive for issues."
27 supports_progress_bar = True
28 supports_multiprocessing = False
30 def _render_results(self, messages: SanityCheckMessages) -> None:
31 """Render sanity check results as a Rich table."""
33 if (
34 not messages.has_error
35 and not messages.has_warning
36 and not messages.has_info
37 ):
38 self.console.print(
39 Panel(
40 "[green]No issues detected.[/green]",
41 title="Sanity Check",
42 border_style="green",
43 ),
44 )
45 return
47 # Build a lookup for document titles
48 doc_pks = [pk for pk in messages.document_pks() if pk is not None]
49 titles: dict[int, str] = {}
50 if doc_pks:
51 titles = dict(
52 Document.global_objects.filter(pk__in=doc_pks)
53 .only("pk", "title")
54 .values_list("pk", "title"),
55 )
57 table = Table(
58 title="Sanity Check Results",
59 show_lines=True,
60 title_style="bold",
61 )
62 table.add_column("Level", width=7, no_wrap=True)
63 table.add_column("Document", min_width=20)
64 table.add_column("Issue", ratio=1, overflow="fold")
66 for doc_pk, doc_messages in messages.iter_messages():
67 if doc_pk is not None:
68 title = titles.get(doc_pk, "Unknown")
69 doc_label = f"#{doc_pk} {title}"
70 else:
71 doc_label = "(global)"
73 for msg in doc_messages:
74 style, label = _LEVEL_STYLE.get(
75 msg["level"],
76 ("dim", "INFO"),
77 )
78 table.add_row(
79 Text(label, style=style),
80 Text(doc_label),
81 Text(str(msg["message"])),
82 )
84 self.console.print(table)
86 parts: list[str] = []
88 if messages.document_error_count:
89 parts.append(
90 f"{messages.document_error_count} document(s) with [bold red]errors[/bold red]",
91 )
92 if messages.document_warning_count:
93 parts.append(
94 f"{messages.document_warning_count} document(s) with [yellow]warnings[/yellow]",
95 )
96 if messages.document_info_count:
97 parts.append(f"{messages.document_info_count} document(s) with infos")
98 if messages.global_warning_count:
99 parts.append(
100 f"{messages.global_warning_count} global [yellow]warning(s)[/yellow]",
101 )
103 if parts:
104 if len(parts) > 1:
105 summary = ", ".join(parts[:-1]) + " and " + parts[-1]
106 else:
107 summary = parts[0]
108 self.console.print(f"\nFound {summary}.")
109 else:
110 self.console.print("\nNo issues found.")
112 def handle(self, *args: Any, **options: Any) -> None:
113 messages = check_sanity(
114 iter_wrapper=lambda docs: self.track(
115 docs,
116 description="Checking documents...",
117 ),
118 )
119 self._render_results(messages)