mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Chore: Cleanup command arguments and standardize process count handling (#4541)
Cleans up some command help text and adds more control over process count for command with a Pool
This commit is contained in:
		| @@ -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 | ||||
| ``` | ||||
| @@ -592,6 +595,6 @@ 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                                                 | | ||||
| | --processes | No       | 1/4 of system cores | Number of processes to use for matching. Setting 1 disables multiple processes                                                 | | ||||
|   | ||||
| @@ -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 " | ||||
|             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\nHit Ctrl+C to exit now, or Enter to " | ||||
|                     "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] | ||||
|  | ||||
|   | ||||
| @@ -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,18 +69,26 @@ 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: | ||||
|  | ||||
|             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), | ||||
|                             pool.imap_unordered( | ||||
|                                 update_document_archive_file, | ||||
|                                 document_ids, | ||||
|                             ), | ||||
|                             total=len(document_ids), | ||||
|                         disable=options["no_progress_bar"], | ||||
|                             disable=self.no_progress_bar, | ||||
|                         ), | ||||
|                     ) | ||||
|         except KeyboardInterrupt: | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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 " | ||||
|             help=( | ||||
|                 "Compare file checksums when determining whether to export " | ||||
|                 "a file or not. If not specified, file size and time " | ||||
|             "modified is used instead.", | ||||
|                 "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 " | ||||
|             help=( | ||||
|                 "After exporting, delete files in the export directory that " | ||||
|                 "do not belong to the current export, such as files from " | ||||
|             "deleted documents.", | ||||
|                 "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( | ||||
|   | ||||
| @@ -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, | ||||
|                     ), | ||||
|                 ) | ||||
|  | ||||
|   | ||||
| @@ -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): | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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 " | ||||
|             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.", | ||||
|                 "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" | ||||
|             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", | ||||
|                 "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, | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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: | ||||
|         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=options["no_progress_bar"], | ||||
|                         disable=self.no_progress_bar, | ||||
|                     ), | ||||
|                 ) | ||||
|   | ||||
| @@ -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") | ||||
|   | ||||
							
								
								
									
										43
									
								
								src/documents/management/commands/mixins.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/documents/management/commands/mixins.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Trenton H
					Trenton H