]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Chore: Cleanup command arguments and standardize process count handling (#4541)
authorTrenton H <797416+stumpylog@users.noreply.github.com>
Thu, 9 Nov 2023 19:46:37 +0000 (11:46 -0800)
committerGitHub <noreply@github.com>
Thu, 9 Nov 2023 19:46:37 +0000 (11:46 -0800)
Cleans up some command help text and adds more control over process count for command with a Pool

17 files changed:
docs/administration.md
src/documents/management/commands/decrypt_documents.py
src/documents/management/commands/document_archiver.py
src/documents/management/commands/document_create_classifier.py
src/documents/management/commands/document_exporter.py
src/documents/management/commands/document_fuzzy_match.py
src/documents/management/commands/document_importer.py
src/documents/management/commands/document_index.py
src/documents/management/commands/document_renamer.py
src/documents/management/commands/document_retagger.py
src/documents/management/commands/document_sanity_checker.py
src/documents/management/commands/document_thumbnails.py
src/documents/management/commands/manage_superuser.py
src/documents/management/commands/mixins.py [new file with mode: 0644]
src/documents/tests/test_management.py
src/documents/tests/test_management_thumbnails.py
src/paperless_mail/management/commands/mail_fetcher.py

index 99dddb2807d1485430f157d75111414a59bd3021..982c9e00774da20b03b8c73c450421892836f4b9 100644 (file)
@@ -414,6 +414,9 @@ This command takes no arguments.
 
 Use this command to re-create document thumbnails. Optionally include the ` --document {id}` option to generate thumbnails for a specific document only.
 
+You may also specify `--processes` to control the number of processes used to generate new thumbnails. The default is to utilize
+a quarter of the available processors.
+
 ```
 document_thumbnails
 ```
@@ -591,7 +594,7 @@ take into account by the detection.
 document_fuzzy_match [--ratio] [--processes N]
 ```
 
-| Option      | Required | Default | Description                                                                                                                    |
-| ----------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------ |
-| --ratio     | No       | 85.0    | a number between 0 and 100, setting how similar a document must be for it to be reported. Higher numbers mean more similarity. |
-| --processes | No       | 4       | Number of processes to use for matching. Setting 1 disables multiple processes                                                 |
+| Option      | Required | Default             | Description                                                                                                                    |
+| ----------- | -------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
+| --ratio     | No       | 85.0                | a number between 0 and 100, setting how similar a document must be for it to be reported. Higher numbers mean more similarity. |
+| --processes | No       | 1/4 of system cores | Number of processes to use for matching. Setting 1 disables multiple processes                                                 |
index 8b67ee7d0e6e5de633793e4bf73f3382a8e27743..c53147d07ac64dfb964da220e30f26abc3401ecf 100644 (file)
@@ -17,19 +17,27 @@ class Command(BaseCommand):
     def add_arguments(self, parser):
         parser.add_argument(
             "--passphrase",
-            help="If PAPERLESS_PASSPHRASE isn't set already, you need to "
-            "specify it here",
+            help=(
+                "If PAPERLESS_PASSPHRASE isn't set already, you need to "
+                "specify it here"
+            ),
         )
 
     def handle(self, *args, **options):
         try:
-            print(
-                "\n\nWARNING: This script is going to work directly on your "
-                "document originals, so\nWARNING: you probably shouldn't run "
-                "this unless you've got a recent backup\nWARNING: handy.  It "
-                "*should* work without a hitch, but be safe and backup your\n"
-                "WARNING: stuff first.\n\nHit Ctrl+C to exit now, or Enter to "
-                "continue.\n\n",
+            self.stdout.write(
+                self.style.WARNING(
+                    "\n\n"
+                    "WARNING: This script is going to work directly on your "
+                    "document originals, so\n"
+                    "WARNING: you probably shouldn't run "
+                    "this unless you've got a recent backup\n"
+                    "WARNING: handy.  It "
+                    "*should* work without a hitch, but be safe and backup your\n"
+                    "WARNING: stuff first.\n\n"
+                    "Hit Ctrl+C to exit now, or Enter to "
+                    "continue.\n\n",
+                ),
             )
             _ = input()
         except KeyboardInterrupt:
@@ -44,14 +52,13 @@ class Command(BaseCommand):
 
         self.__gpg_to_unencrypted(passphrase)
 
-    @staticmethod
-    def __gpg_to_unencrypted(passphrase):
+    def __gpg_to_unencrypted(self, passphrase: str):
         encrypted_files = Document.objects.filter(
             storage_type=Document.STORAGE_TYPE_GPG,
         )
 
         for document in encrypted_files:
-            print(f"Decrypting {document}".encode())
+            self.stdout.write(f"Decrypting {document}")
 
             old_paths = [document.source_path, document.thumbnail_path]
 
index 69d9a81832b04ebe983482cb97a7d7021831d10a..96b3b50ab75b8d5fdbe3096b909e1f30f08c4d80 100644 (file)
@@ -7,21 +7,20 @@ from django import db
 from django.conf import settings
 from django.core.management.base import BaseCommand
 
+from documents.management.commands.mixins import MultiProcessMixin
+from documents.management.commands.mixins import ProgressBarMixin
 from documents.models import Document
 from documents.tasks import update_document_archive_file
 
 logger = logging.getLogger("paperless.management.archiver")
 
 
-class Command(BaseCommand):
-    help = """
-        Using the current classification model, assigns correspondents, tags
-        and document types to all documents, effectively allowing you to
-        back-tag all previously indexed documents with metadata created (or
-        modified) after their initial import.
-    """.replace(
-        "    ",
-        "",
+class Command(MultiProcessMixin, ProgressBarMixin, BaseCommand):
+    help = (
+        "Using the current classification model, assigns correspondents, tags "
+        "and document types to all documents, effectively allowing you to "
+        "back-tag all previously indexed documents with metadata created (or "
+        "modified) after their initial import."
     )
 
     def add_arguments(self, parser):
@@ -30,8 +29,10 @@ class Command(BaseCommand):
             "--overwrite",
             default=False,
             action="store_true",
-            help="Recreates the archived document for documents that already "
-            "have an archived version.",
+            help=(
+                "Recreates the archived document for documents that already "
+                "have an archived version."
+            ),
         )
         parser.add_argument(
             "-d",
@@ -39,17 +40,18 @@ class Command(BaseCommand):
             default=None,
             type=int,
             required=False,
-            help="Specify the ID of a document, and this command will only "
-            "run on this specific document.",
-        )
-        parser.add_argument(
-            "--no-progress-bar",
-            default=False,
-            action="store_true",
-            help="If set, the progress bar will not be shown",
+            help=(
+                "Specify the ID of a document, and this command will only "
+                "run on this specific document."
+            ),
         )
+        self.add_argument_progress_bar_mixin(parser)
+        self.add_argument_processes_mixin(parser)
 
     def handle(self, *args, **options):
+        self.handle_processes_mixin(**options)
+        self.handle_progress_bar_mixin(**options)
+
         os.makedirs(settings.SCRATCH_DIR, exist_ok=True)
 
         overwrite = options["overwrite"]
@@ -67,19 +69,27 @@ class Command(BaseCommand):
         )
 
         # Note to future self: this prevents django from reusing database
-        # conncetions between processes, which is bad and does not work
+        # connections between processes, which is bad and does not work
         # with postgres.
         db.connections.close_all()
 
         try:
             logging.getLogger().handlers[0].level = logging.ERROR
-            with multiprocessing.Pool(processes=settings.TASK_WORKERS) as pool:
-                list(
-                    tqdm.tqdm(
-                        pool.imap_unordered(update_document_archive_file, document_ids),
-                        total=len(document_ids),
-                        disable=options["no_progress_bar"],
-                    ),
-                )
+
+            if self.process_count == 1:
+                for doc_id in document_ids:
+                    update_document_archive_file(doc_id)
+            else:  # pragma: no cover
+                with multiprocessing.Pool(self.process_count) as pool:
+                    list(
+                        tqdm.tqdm(
+                            pool.imap_unordered(
+                                update_document_archive_file,
+                                document_ids,
+                            ),
+                            total=len(document_ids),
+                            disable=self.no_progress_bar,
+                        ),
+                    )
         except KeyboardInterrupt:
             self.stdout.write(self.style.NOTICE("Aborting..."))
index 88d2092e303f25743c02662c024089b13aad3d1d..f5df51aacc4a549757c767645eb1184ecc0d0359 100644 (file)
@@ -4,16 +4,10 @@ from documents.tasks import train_classifier
 
 
 class Command(BaseCommand):
-    help = """
-        Trains the classifier on your data and saves the resulting models to a
-        file. The document consumer will then automatically use this new model.
-    """.replace(
-        "    ",
-        "",
+    help = (
+        "Trains the classifier on your data and saves the resulting models to a "
+        "file. The document consumer will then automatically use this new model."
     )
 
-    def __init__(self, *args, **kwargs):
-        BaseCommand.__init__(self, *args, **kwargs)
-
     def handle(self, *args, **options):
         train_classifier()
index 8af1c1f531d8a3f8a4da175cb3c349485385077e..ff200a8f595965e5b17e91b3011c0524caec32db 100644 (file)
@@ -43,13 +43,10 @@ from paperless_mail.models import MailRule
 
 
 class Command(BaseCommand):
-    help = """
-        Decrypt and rename all files in our collection into a given target
-        directory.  And include a manifest file containing document data for
-        easy import.
-    """.replace(
-        "    ",
-        "",
+    help = (
+        "Decrypt and rename all files in our collection into a given target "
+        "directory.  And include a manifest file containing document data for "
+        "easy import."
     )
 
     def add_arguments(self, parser):
@@ -60,9 +57,11 @@ class Command(BaseCommand):
             "--compare-checksums",
             default=False,
             action="store_true",
-            help="Compare file checksums when determining whether to export "
-            "a file or not. If not specified, file size and time "
-            "modified is used instead.",
+            help=(
+                "Compare file checksums when determining whether to export "
+                "a file or not. If not specified, file size and time "
+                "modified is used instead."
+            ),
         )
 
         parser.add_argument(
@@ -70,9 +69,11 @@ class Command(BaseCommand):
             "--delete",
             default=False,
             action="store_true",
-            help="After exporting, delete files in the export directory that "
-            "do not belong to the current export, such as files from "
-            "deleted documents.",
+            help=(
+                "After exporting, delete files in the export directory that "
+                "do not belong to the current export, such as files from "
+                "deleted documents."
+            ),
         )
 
         parser.add_argument(
@@ -80,8 +81,10 @@ class Command(BaseCommand):
             "--use-filename-format",
             default=False,
             action="store_true",
-            help="Use PAPERLESS_FILENAME_FORMAT for storing files in the "
-            "export directory, if configured.",
+            help=(
+                "Use PAPERLESS_FILENAME_FORMAT for storing files in the "
+                "export directory, if configured."
+            ),
         )
 
         parser.add_argument(
@@ -105,8 +108,10 @@ class Command(BaseCommand):
             "--use-folder-prefix",
             default=False,
             action="store_true",
-            help="Export files in dedicated folders according to their nature: "
-            "archive, originals or thumbnails",
+            help=(
+                "Export files in dedicated folders according to their nature: "
+                "archive, originals or thumbnails"
+            ),
         )
 
         parser.add_argument(
index 26ce55a39b771dff90adb4b54d04ea5b5da98c4f..597a9d2c131d3805f448af521e8bc563c2349d4a 100644 (file)
@@ -7,6 +7,8 @@ import tqdm
 from django.core.management import BaseCommand
 from django.core.management import CommandError
 
+from documents.management.commands.mixins import MultiProcessMixin
+from documents.management.commands.mixins import ProgressBarMixin
 from documents.models import Document
 
 
@@ -41,7 +43,7 @@ def _process_and_match(work: _WorkPackage) -> _WorkResult:
     return _WorkResult(work.first_doc.pk, work.second_doc.pk, match)
 
 
-class Command(BaseCommand):
+class Command(MultiProcessMixin, ProgressBarMixin, BaseCommand):
     help = "Searches for documents where the content almost matches"
 
     def add_arguments(self, parser):
@@ -51,23 +53,16 @@ class Command(BaseCommand):
             type=float,
             help="Ratio to consider documents a match",
         )
-        parser.add_argument(
-            "--processes",
-            default=4,
-            type=int,
-            help="Number of processes to distribute work amongst",
-        )
-        parser.add_argument(
-            "--no-progress-bar",
-            default=False,
-            action="store_true",
-            help="If set, the progress bar will not be shown",
-        )
+        self.add_argument_progress_bar_mixin(parser)
+        self.add_argument_processes_mixin(parser)
 
     def handle(self, *args, **options):
         RATIO_MIN: Final[float] = 0.0
         RATIO_MAX: Final[float] = 100.0
 
+        self.handle_processes_mixin(**options)
+        self.handle_progress_bar_mixin(**options)
+
         opt_ratio = options["ratio"]
         checked_pairs: set[tuple[int, int]] = set()
         work_pkgs: list[_WorkPackage] = []
@@ -76,9 +71,6 @@ class Command(BaseCommand):
         if opt_ratio < RATIO_MIN or opt_ratio > RATIO_MAX:
             raise CommandError("The ratio must be between 0 and 100")
 
-        if options["processes"] < 1:
-            raise CommandError("There must be at least 1 process")
-
         all_docs = Document.objects.all().order_by("id")
 
         # Build work packages for processing
@@ -101,17 +93,17 @@ class Command(BaseCommand):
                 work_pkgs.append(_WorkPackage(first_doc, second_doc))
 
         # Don't spin up a pool of 1 process
-        if options["processes"] == 1:
+        if self.process_count == 1:
             results = []
-            for work in tqdm.tqdm(work_pkgs, disable=options["no_progress_bar"]):
+            for work in tqdm.tqdm(work_pkgs, disable=self.no_progress_bar):
                 results.append(_process_and_match(work))
-        else:
-            with multiprocessing.Pool(processes=options["processes"]) as pool:
+        else:  # pragma: no cover
+            with multiprocessing.Pool(processes=self.process_count) as pool:
                 results = list(
                     tqdm.tqdm(
                         pool.imap_unordered(_process_and_match, work_pkgs),
                         total=len(work_pkgs),
-                        disable=options["no_progress_bar"],
+                        disable=self.no_progress_bar,
                     ),
                 )
 
index f76a8b7a2e8c079e19c5ef5242eab44665f030b7..7a3e40f28cfd35b2e01763b17083ef219f2d2c04 100644 (file)
@@ -40,12 +40,9 @@ def disable_signal(sig, receiver, sender):
 
 
 class Command(BaseCommand):
-    help = """
-        Using a manifest.json file, load the data from there, and import the
-        documents it refers to.
-    """.replace(
-        "    ",
-        "",
+    help = (
+        "Using a manifest.json file, load the data from there, and import the "
+        "documents it refers to."
     )
 
     def add_arguments(self, parser):
index 279408b367253f71f01526183fd67422fafd4ecf..1fa4f5a70e695cb42c336653569203e2a71f9404 100644 (file)
@@ -1,25 +1,22 @@
 from django.core.management import BaseCommand
 from django.db import transaction
 
+from documents.management.commands.mixins import ProgressBarMixin
 from documents.tasks import index_optimize
 from documents.tasks import index_reindex
 
 
-class Command(BaseCommand):
+class Command(ProgressBarMixin, BaseCommand):
     help = "Manages the document index."
 
     def add_arguments(self, parser):
         parser.add_argument("command", choices=["reindex", "optimize"])
-        parser.add_argument(
-            "--no-progress-bar",
-            default=False,
-            action="store_true",
-            help="If set, the progress bar will not be shown",
-        )
+        self.add_argument_progress_bar_mixin(parser)
 
     def handle(self, *args, **options):
+        self.handle_progress_bar_mixin(**options)
         with transaction.atomic():
             if options["command"] == "reindex":
-                index_reindex(progress_bar_disable=options["no_progress_bar"])
+                index_reindex(progress_bar_disable=self.no_progress_bar)
             elif options["command"] == "optimize":
                 index_optimize()
index be14469578399ef1bbe4d953ea076f2fc17a7f2a..25f8f2d2172c2afaba34bd3b44d90df9a91d1e69 100644 (file)
@@ -4,30 +4,22 @@ import tqdm
 from django.core.management.base import BaseCommand
 from django.db.models.signals import post_save
 
+from documents.management.commands.mixins import ProgressBarMixin
 from documents.models import Document
 
 
-class Command(BaseCommand):
-    help = """
-        This will rename all documents to match the latest filename format.
-    """.replace(
-        "    ",
-        "",
-    )
+class Command(ProgressBarMixin, BaseCommand):
+    help = "This will rename all documents to match the latest filename format."
 
     def add_arguments(self, parser):
-        parser.add_argument(
-            "--no-progress-bar",
-            default=False,
-            action="store_true",
-            help="If set, the progress bar will not be shown",
-        )
+        self.add_argument_progress_bar_mixin(parser)
 
     def handle(self, *args, **options):
+        self.handle_progress_bar_mixin(**options)
         logging.getLogger().handlers[0].level = logging.ERROR
 
         for document in tqdm.tqdm(
             Document.objects.all(),
-            disable=options["no_progress_bar"],
+            disable=self.no_progress_bar,
         ):
             post_save.send(Document, instance=document)
index 385cbf6081bf086f5b634e154757c5d1ace1993f..a7d2c7e123cf1a8fea0def21a280ffe81f75cee6 100644 (file)
@@ -4,6 +4,7 @@ import tqdm
 from django.core.management.base import BaseCommand
 
 from documents.classifier import load_classifier
+from documents.management.commands.mixins import ProgressBarMixin
 from documents.models import Document
 from documents.signals.handlers import set_correspondent
 from documents.signals.handlers import set_document_type
@@ -13,15 +14,12 @@ from documents.signals.handlers import set_tags
 logger = logging.getLogger("paperless.management.retagger")
 
 
-class Command(BaseCommand):
-    help = """
-        Using the current classification model, assigns correspondents, tags
-        and document types to all documents, effectively allowing you to
-        back-tag all previously indexed documents with metadata created (or
-        modified) after their initial import.
-    """.replace(
-        "    ",
-        "",
+class Command(ProgressBarMixin, BaseCommand):
+    help = (
+        "Using the current classification model, assigns correspondents, tags "
+        "and document types to all documents, effectively allowing you to "
+        "back-tag all previously indexed documents with metadata created (or "
+        "modified) after their initial import."
     )
 
     def add_arguments(self, parser):
@@ -34,25 +32,24 @@ class Command(BaseCommand):
             "--use-first",
             default=False,
             action="store_true",
-            help="By default this command won't try to assign a correspondent "
-            "if more than one matches the document.  Use this flag if "
-            "you'd rather it just pick the first one it finds.",
+            help=(
+                "By default this command won't try to assign a correspondent "
+                "if more than one matches the document.  Use this flag if "
+                "you'd rather it just pick the first one it finds."
+            ),
         )
         parser.add_argument(
             "-f",
             "--overwrite",
             default=False,
             action="store_true",
-            help="If set, the document retagger will overwrite any previously"
-            "set correspondent, document and remove correspondents, types"
-            "and tags that do not match anymore due to changed rules.",
-        )
-        parser.add_argument(
-            "--no-progress-bar",
-            default=False,
-            action="store_true",
-            help="If set, the progress bar will not be shown",
+            help=(
+                "If set, the document retagger will overwrite any previously"
+                "set correspondent, document and remove correspondents, types"
+                "and tags that do not match anymore due to changed rules."
+            ),
         )
+        self.add_argument_progress_bar_mixin(parser)
         parser.add_argument(
             "--suggest",
             default=False,
@@ -71,6 +68,7 @@ class Command(BaseCommand):
         )
 
     def handle(self, *args, **options):
+        self.handle_progress_bar_mixin(**options)
         # Detect if we support color
         color = self.style.ERROR("test") != "test"
 
@@ -88,7 +86,7 @@ class Command(BaseCommand):
 
         classifier = load_classifier()
 
-        for document in tqdm.tqdm(documents, disable=options["no_progress_bar"]):
+        for document in tqdm.tqdm(documents, disable=self.no_progress_bar):
             if options["correspondent"]:
                 set_correspondent(
                     sender=None,
index 4c06d2a84744278c5a83a597edf8649745ab3e8e..095781a9dbc0fb92ce9262e16e90d9731c394442 100644 (file)
@@ -1,25 +1,17 @@
 from django.core.management.base import BaseCommand
 
+from documents.management.commands.mixins import ProgressBarMixin
 from documents.sanity_checker import check_sanity
 
 
-class Command(BaseCommand):
-    help = """
-        This command checks your document archive for issues.
-    """.replace(
-        "    ",
-        "",
-    )
+class Command(ProgressBarMixin, BaseCommand):
+    help = "This command checks your document archive for issues."
 
     def add_arguments(self, parser):
-        parser.add_argument(
-            "--no-progress-bar",
-            default=False,
-            action="store_true",
-            help="If set, the progress bar will not be shown",
-        )
+        self.add_argument_progress_bar_mixin(parser)
 
     def handle(self, *args, **options):
-        messages = check_sanity(progress=not options["no_progress_bar"])
+        self.handle_progress_bar_mixin(**options)
+        messages = check_sanity(progress=self.use_progress_bar)
 
         messages.log_messages()
index 982634c5e6678d16a696ecbbc6fd56d523fdaec8..ecd26510267ddc95168739e42e5f89d5e77c0a57 100644 (file)
@@ -6,6 +6,8 @@ import tqdm
 from django import db
 from django.core.management.base import BaseCommand
 
+from documents.management.commands.mixins import MultiProcessMixin
+from documents.management.commands.mixins import ProgressBarMixin
 from documents.models import Document
 from documents.parsers import get_parser_class_for_mime_type
 
@@ -32,13 +34,8 @@ def _process_document(doc_id):
         parser.cleanup()
 
 
-class Command(BaseCommand):
-    help = """
-        This will regenerate the thumbnails for all documents.
-    """.replace(
-        "    ",
-        "",
-    )
+class Command(MultiProcessMixin, ProgressBarMixin, BaseCommand):
+    help = "This will regenerate the thumbnails for all documents."
 
     def add_arguments(self, parser):
         parser.add_argument(
@@ -47,19 +44,20 @@ class Command(BaseCommand):
             default=None,
             type=int,
             required=False,
-            help="Specify the ID of a document, and this command will only "
-            "run on this specific document.",
-        )
-        parser.add_argument(
-            "--no-progress-bar",
-            default=False,
-            action="store_true",
-            help="If set, the progress bar will not be shown",
+            help=(
+                "Specify the ID of a document, and this command will only "
+                "run on this specific document."
+            ),
         )
+        self.add_argument_progress_bar_mixin(parser)
+        self.add_argument_processes_mixin(parser)
 
     def handle(self, *args, **options):
         logging.getLogger().handlers[0].level = logging.ERROR
 
+        self.handle_processes_mixin(**options)
+        self.handle_progress_bar_mixin(**options)
+
         if options["document"]:
             documents = Document.objects.filter(pk=options["document"])
         else:
@@ -72,11 +70,15 @@ class Command(BaseCommand):
         # with postgres.
         db.connections.close_all()
 
-        with multiprocessing.Pool() as pool:
-            list(
-                tqdm.tqdm(
-                    pool.imap_unordered(_process_document, ids),
-                    total=len(ids),
-                    disable=options["no_progress_bar"],
-                ),
-            )
+        if self.process_count == 1:
+            for doc_id in ids:
+                _process_document(doc_id)
+        else:  # pragma: no cover
+            with multiprocessing.Pool(processes=self.process_count) as pool:
+                list(
+                    tqdm.tqdm(
+                        pool.imap_unordered(_process_document, ids),
+                        total=len(ids),
+                        disable=self.no_progress_bar,
+                    ),
+                )
index df0502f1746a511c12a49c0d7bede5f1f08cb89c..e0d23843853afa37fbd3dfa6cc6bfc4059a60ebc 100644 (file)
@@ -1,5 +1,6 @@
 import logging
 import os
+from argparse import RawTextHelpFormatter
 
 from django.contrib.auth.models import User
 from django.core.management.base import BaseCommand
@@ -8,20 +9,22 @@ logger = logging.getLogger("paperless.management.superuser")
 
 
 class Command(BaseCommand):
-    help = """
-        Creates a Django superuser:
-        User named: admin
-        Email: root@localhost
-        with password based on env variable.
-        No superuser will be created, when:
-        - The username is taken already exists
-        - A superuser already exists
-        - PAPERLESS_ADMIN_PASSWORD is not set
-    """.replace(
-        "    ",
-        "",
+    help = (
+        "Creates a Django superuser:\n"
+        "  User named: admin\n"
+        "  Email: root@localhost\n"
+        "  Password: based on env variable PAPERLESS_ADMIN_PASSWORD\n"
+        "No superuser will be created, when:\n"
+        "  - The username is taken already exists\n"
+        "  - A superuser already exists\n"
+        "  - PAPERLESS_ADMIN_PASSWORD is not set"
     )
 
+    def create_parser(self, *args, **kwargs):
+        parser = super().create_parser(*args, **kwargs)
+        parser.formatter_class = RawTextHelpFormatter
+        return parser
+
     def handle(self, *args, **options):
         username = os.getenv("PAPERLESS_ADMIN_USER", "admin")
         mail = os.getenv("PAPERLESS_ADMIN_MAIL", "root@localhost")
diff --git a/src/documents/management/commands/mixins.py b/src/documents/management/commands/mixins.py
new file mode 100644 (file)
index 0000000..6fed739
--- /dev/null
@@ -0,0 +1,43 @@
+import os
+from argparse import ArgumentParser
+
+from django.core.management import CommandError
+
+
+class MultiProcessMixin:
+    """
+    Small class to handle adding an argument and validating it
+    for the use of multiple processes
+    """
+
+    def add_argument_processes_mixin(self, parser: ArgumentParser):
+        parser.add_argument(
+            "--processes",
+            default=max(1, os.cpu_count() // 4),
+            type=int,
+            help="Number of processes to distribute work amongst",
+        )
+
+    def handle_processes_mixin(self, *args, **options):
+        self.process_count = options["processes"]
+        if self.process_count < 1:
+            raise CommandError("There must be at least 1 process")
+
+
+class ProgressBarMixin:
+    """
+    Many commands use a progress bar, which can be disabled
+    via this class
+    """
+
+    def add_argument_progress_bar_mixin(self, parser: ArgumentParser):
+        parser.add_argument(
+            "--no-progress-bar",
+            default=False,
+            action="store_true",
+            help="If set, the progress bar will not be shown",
+        )
+
+    def handle_progress_bar_mixin(self, *args, **options):
+        self.no_progress_bar = options["no_progress_bar"]
+        self.use_progress_bar = not self.no_progress_bar
index 1115325cab0a09d07143d0198fdc9b33afd614a8..d1efe27d476338a0c97fda843761921f8ad22247 100644 (file)
@@ -36,7 +36,7 @@ class TestArchiver(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
             os.path.join(self.dirs.originals_dir, f"{doc.id:07}.pdf"),
         )
 
-        call_command("document_archiver")
+        call_command("document_archiver", "--processes", "1")
 
     def test_handle_document(self):
         doc = self.make_models()
index 9f3ff63c593b0bcb230fb87ef22cf46b272d3335..4056b65fe7f6672be3a0bcecf60cb2f8930e0e94 100644 (file)
@@ -83,13 +83,13 @@ class TestMakeThumbnails(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
     def test_command(self):
         self.assertIsNotFile(self.d1.thumbnail_path)
         self.assertIsNotFile(self.d2.thumbnail_path)
-        call_command("document_thumbnails")
+        call_command("document_thumbnails", "--processes", "1")
         self.assertIsFile(self.d1.thumbnail_path)
         self.assertIsFile(self.d2.thumbnail_path)
 
     def test_command_documentid(self):
         self.assertIsNotFile(self.d1.thumbnail_path)
         self.assertIsNotFile(self.d2.thumbnail_path)
-        call_command("document_thumbnails", "-d", f"{self.d1.id}")
+        call_command("document_thumbnails", "--processes", "1", "-d", f"{self.d1.id}")
         self.assertIsFile(self.d1.thumbnail_path)
         self.assertIsNotFile(self.d2.thumbnail_path)
index ec87723ebcfedc87e513cf3a7300d22b70b02116..70df4630053d563c7fc82062393c1d3b5880b448 100644 (file)
@@ -4,11 +4,7 @@ from paperless_mail import tasks
 
 
 class Command(BaseCommand):
-    help = """
-    """.replace(
-        "    ",
-        "",
-    )
+    help = "Manually triggers a fetching and processing of all mail accounts"
 
     def handle(self, *args, **options):
         tasks.process_mail_accounts()