Compare commits

..
10 changed files with 170 additions and 224 deletions
+8
View File
@@ -1209,6 +1209,14 @@ left unassigned, preventing low-confidence guesses from being applied.
Defaults to 0.6.
#### [`PAPERLESS_MATCH_REGEX_TIMEOUT_SECONDS=<float>`](#PAPERLESS_MATCH_REGEX_TIMEOUT_SECONDS) {#PAPERLESS_MATCH_REGEX_TIMEOUT_SECONDS}
: Sets the timeout, in seconds, for regular expression matching. Increase this
value if date parsing or user-defined matching rules time out when processing
long documents, especially on slower hardware.
Defaults to 0.1 seconds.
#### [`PAPERLESS_DATE_PARSER_LANGUAGES=<lang>`](#PAPERLESS_DATE_PARSER_LANGUAGES) {#PAPERLESS_DATE_PARSER_LANGUAGES}
: Specifies which language Paperless should use when parsing dates from documents.
+5 -1
View File
@@ -21,6 +21,7 @@ from typing import ClassVar
from typing import Generic
from typing import TypeVar
import django
from django import db
from django.core.management import CommandError
from django.db.models import QuerySet
@@ -534,7 +535,10 @@ class PaperlessCommand(RichCommand):
with self._create_progress(description) as progress:
task_id = progress.add_task(description, total=total)
with ProcessPoolExecutor(max_workers=self.process_count) as executor:
with ProcessPoolExecutor(
max_workers=self.process_count,
initializer=django.setup,
) as executor:
# Submit all tasks and map futures back to items
future_to_item = {executor.submit(fn, item): item for item in items}
+41 -29
View File
@@ -1749,18 +1749,8 @@ class MergeDocumentsAsVersionsSerializer(DocumentListSerializer):
return attrs
class PdfEditOperationSerializer(serializers.Serializer[dict[str, int]]):
page = serializers.IntegerField(min_value=1)
rotate = serializers.IntegerField(required=False)
doc = serializers.IntegerField(required=False, min_value=0)
class EditPdfDocumentsSerializer(DocumentListSerializer, SourceModeValidationMixin):
operations = serializers.ListField(
child=PdfEditOperationSerializer(),
required=True,
allow_empty=False,
)
operations = serializers.ListField(required=True, allow_empty=False)
delete_original = serializers.BooleanField(required=False, default=False)
update_document = serializers.BooleanField(required=False, default=False)
include_metadata = serializers.BooleanField(required=False, default=True)
@@ -1778,9 +1768,18 @@ class EditPdfDocumentsSerializer(DocumentListSerializer, SourceModeValidationMix
)
operations = attrs["operations"]
if not isinstance(operations, list):
raise serializers.ValidationError("operations must be a list")
if any(op.get("doc", 0) >= len(operations) for op in operations):
raise serializers.ValidationError("doc index is out of bounds")
for op in operations:
if not isinstance(op, dict):
raise serializers.ValidationError("invalid operation entry")
if "page" not in op or not isinstance(op["page"], int):
raise serializers.ValidationError("page must be an integer")
if "rotate" in op and not isinstance(op["rotate"], int):
raise serializers.ValidationError("rotate must be an integer")
if "doc" in op and not isinstance(op["doc"], int):
raise serializers.ValidationError("doc must be an integer")
if attrs["update_document"]:
max_idx = max(op.get("doc", 0) for op in operations)
@@ -1789,10 +1788,16 @@ class EditPdfDocumentsSerializer(DocumentListSerializer, SourceModeValidationMix
"update_document only allowed with a single output document",
)
if any(
op.get("doc", 0) < 0 or op.get("doc", 0) >= len(operations)
for op in operations
):
raise serializers.ValidationError("doc index is out of bounds")
doc = Document.objects.get(id=documents[0])
if doc.page_count:
for op in operations:
if op["page"] > doc.page_count:
if op["page"] < 1 or op["page"] > doc.page_count:
raise serializers.ValidationError(
f"Page {op['page']} is out of bounds for document with {doc.page_count} pages.",
)
@@ -2123,15 +2128,19 @@ class BulkEditSerializer(
def _validate_parameters_edit_pdf(self, parameters, document_id) -> None:
if "operations" not in parameters:
raise serializers.ValidationError("operations not specified")
operations_field = serializers.ListField(
child=PdfEditOperationSerializer(),
allow_empty=False,
)
parameters["operations"] = operations_field.run_validation(
parameters["operations"],
)
operations = parameters["operations"]
if not isinstance(parameters["operations"], list):
raise serializers.ValidationError("operations must be a list")
if not parameters["operations"]:
raise serializers.ValidationError("operations must not be empty")
for op in parameters["operations"]:
if not isinstance(op, dict):
raise serializers.ValidationError("invalid operation entry")
if "page" not in op or not isinstance(op["page"], int):
raise serializers.ValidationError("page must be an integer")
if "rotate" in op and not isinstance(op["rotate"], int):
raise serializers.ValidationError("rotate must be an integer")
if "doc" in op and not isinstance(op["doc"], int):
raise serializers.ValidationError("doc must be an integer")
if "update_document" in parameters:
if not isinstance(parameters["update_document"], bool):
raise serializers.ValidationError("update_document must be a boolean")
@@ -2143,21 +2152,24 @@ class BulkEditSerializer(
else:
parameters["include_metadata"] = True
if any(op.get("doc", 0) >= len(operations) for op in operations):
raise serializers.ValidationError("doc index is out of bounds")
if parameters["update_document"]:
max_idx = max(op.get("doc", 0) for op in operations)
max_idx = max(op.get("doc", 0) for op in parameters["operations"])
if max_idx > 0:
raise serializers.ValidationError(
"update_document only allowed with a single output document",
)
if any(
op.get("doc", 0) < 0 or op.get("doc", 0) >= len(parameters["operations"])
for op in parameters["operations"]
):
raise serializers.ValidationError("doc index is out of bounds")
doc = Document.objects.get(id=document_id)
# doc existence is already validated
if doc.page_count:
for op in operations:
if op["page"] > doc.page_count:
for op in parameters["operations"]:
if op["page"] < 1 or op["page"] > doc.page_count:
raise serializers.ValidationError(
f"Page {op['page']} is out of bounds for document with {doc.page_count} pages.",
)
+11 -6
View File
@@ -1189,13 +1189,18 @@ def before_task_publish_handler(
trigger_source = _determine_trigger_source(headers)
owner_id = _extract_owner_id(task_type, task_kwargs)
PaperlessTask.objects.create(
# A retried task is republished with the same task_id, so this fires
# again for it; get_or_create keeps the original PENDING record
# instead of raising a duplicate-key IntegrityError on the retry.
PaperlessTask.objects.get_or_create(
task_id=task_id,
task_type=task_type,
trigger_source=trigger_source,
status=PaperlessTask.Status.PENDING,
input_data=input_data,
owner_id=owner_id,
defaults={
"task_type": task_type,
"trigger_source": trigger_source,
"status": PaperlessTask.Status.PENDING,
"input_data": input_data,
"owner_id": owner_id,
},
)
except Exception: # pragma: no cover
logger.exception("Creating PaperlessTask failed")
+30 -134
View File
@@ -1667,6 +1667,22 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn(b"doc index is out of bounds", response.content)
def test_legacy_bulk_edit_rejects_empty_pdf_operations(self) -> None:
response = self.client.post(
"/api/documents/bulk_edit/",
json.dumps(
{
"documents": [self.doc2.id],
"method": "edit_pdf",
"parameters": {"operations": []},
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn(b"operations must not be empty", response.content)
@mock.patch("documents.views.bulk_edit.edit_pdf")
def test_edit_pdf(self, m) -> None:
self.setup_mock(m, "edit_pdf")
@@ -1717,6 +1733,13 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn(b"Expected a list of items", response.content)
response = self.client.post(
"/api/documents/edit_pdf/",
{"documents": [self.doc2.id], "operations": []},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
response = self.client.post(
"/api/documents/edit_pdf/",
json.dumps(
@@ -1728,7 +1751,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn(b"Expected a dictionary", response.content)
self.assertIn(b"invalid operation entry", response.content)
response = self.client.post(
"/api/documents/edit_pdf/",
@@ -1741,7 +1764,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn(b"valid integer is required", response.content)
self.assertIn(b"page must be an integer", response.content)
response = self.client.post(
"/api/documents/edit_pdf/",
@@ -1754,7 +1777,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn(b"valid integer is required", response.content)
self.assertIn(b"rotate must be an integer", response.content)
response = self.client.post(
"/api/documents/edit_pdf/",
@@ -1767,16 +1790,9 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn(b"valid integer is required", response.content)
self.assertIn(b"doc must be an integer", response.content)
# A negative doc index is rejected by PdfEditOperationSerializer's
# own min_value=0 field constraint, before the "doc index is out
# of bounds" object-level check (against len(operations)) ever
# runs -- hence the different expected message per case.
for doc_index, expected_message in (
(-1, b"greater than or equal to 0"),
(2**32, b"doc index is out of bounds"),
):
for doc_index in (-1, 2**32):
with self.subTest(doc_index=doc_index):
response = self.client.post(
"/api/documents/edit_pdf/",
@@ -1789,7 +1805,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn(expected_message, response.content)
self.assertIn(b"doc index is out of bounds", response.content)
response = self.client.post(
"/api/documents/edit_pdf/",
@@ -1797,7 +1813,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
{
"documents": [self.doc2.id],
"update_document": True,
"operations": [{"page": 1, "doc": 0}, {"page": 2, "doc": 1}],
"operations": [{"page": 1, "doc": 1}, {"page": 2, "doc": 2}],
},
),
content_type="application/json",
@@ -1822,86 +1838,6 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn(b"Invalid source_mode", response.content)
@mock.patch("documents.views.bulk_edit.edit_pdf")
def test_edit_pdf_rejects_empty_operations(self, m) -> None:
"""
An empty operations list previously reached bulk_edit.edit_pdf()
and crashed with `ValueError: max() iterable argument is empty`
(via `max(op.get("doc", 0) for op in operations)`) whenever
update_document was true. Must now be rejected up front.
"""
self.setup_mock(m, "edit_pdf")
response = self.client.post(
"/api/documents/edit_pdf/",
json.dumps(
{
"documents": [self.doc2.id],
"operations": [],
"update_document": True,
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
m.assert_not_called()
@mock.patch("documents.views.bulk_edit.edit_pdf")
def test_edit_pdf_rejects_negative_doc_index(self, m) -> None:
"""
A negative `doc` index was previously silently accepted and used
as a wrapping Python list index instead of being rejected.
"""
self.setup_mock(m, "edit_pdf")
response = self.client.post(
"/api/documents/edit_pdf/",
json.dumps(
{
"documents": [self.doc2.id],
"operations": [{"page": 1, "doc": -1}],
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
m.assert_not_called()
@mock.patch("documents.views.bulk_edit.edit_pdf")
def test_edit_pdf_rejects_out_of_bounds_doc_index(self, m) -> None:
"""
A `doc` index far larger than the number of operations previously
drove `pdf_docs = [pikepdf.new() for _ in range(max_idx + 1)]` to
attempt allocating an enormous number of real objects.
"""
self.setup_mock(m, "edit_pdf")
response = self.client.post(
"/api/documents/edit_pdf/",
json.dumps(
{
"documents": [self.doc2.id],
"operations": [{"page": 1, "doc": 2**33}],
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
m.assert_not_called()
@mock.patch("documents.views.bulk_edit.edit_pdf")
def test_edit_pdf_rejects_non_positive_page(self, m) -> None:
self.setup_mock(m, "edit_pdf")
response = self.client.post(
"/api/documents/edit_pdf/",
json.dumps(
{
"documents": [self.doc2.id],
"operations": [{"page": 0}],
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
m.assert_not_called()
@mock.patch("documents.views.bulk_edit.edit_pdf")
def test_edit_pdf_page_out_of_bounds(self, m) -> None:
self.setup_mock(m, "edit_pdf")
@@ -1919,46 +1855,6 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
self.assertIn(b"out of bounds", response.content)
m.assert_not_called()
@mock.patch("documents.serialisers.bulk_edit.edit_pdf")
def test_bulk_edit_edit_pdf_rejects_empty_operations(self, m) -> None:
"""
Same validation gap as test_edit_pdf_rejects_empty_operations, but
via the legacy generic /api/documents/bulk_edit/ method="edit_pdf"
path, which hand-parses `parameters["operations"]` independently
in BulkEditSerializer._validate_parameters_edit_pdf.
"""
self.setup_mock(m, "edit_pdf")
response = self.client.post(
"/api/documents/bulk_edit/",
json.dumps(
{
"documents": [self.doc2.id],
"method": "edit_pdf",
"parameters": {"operations": [], "update_document": True},
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
m.assert_not_called()
@mock.patch("documents.serialisers.bulk_edit.edit_pdf")
def test_bulk_edit_edit_pdf_rejects_out_of_bounds_doc_index(self, m) -> None:
self.setup_mock(m, "edit_pdf")
response = self.client.post(
"/api/documents/bulk_edit/",
json.dumps(
{
"documents": [self.doc2.id],
"method": "edit_pdf",
"parameters": {"operations": [{"page": 1, "doc": 2**33}]},
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
m.assert_not_called()
@mock.patch("documents.views.bulk_edit.edit_pdf")
def test_edit_pdf_insufficient_permissions(self, m) -> None:
self.doc1.owner = User.objects.get(username="temp_admin")
+6 -7
View File
@@ -1643,13 +1643,12 @@ class TestPDFActions(DirectoriesMixin, TestCase):
mock_consume_file.assert_not_called()
@mock.patch("pikepdf.open")
def test_edit_pdf_rejects_out_of_bounds_output_index(self, mock_open) -> None:
with self.assertLogs("paperless.bulk_edit", level="ERROR"):
with self.assertRaisesRegex(ValueError, "index is out of bounds"):
bulk_edit.edit_pdf(
[self.doc2.id],
[{"page": 1, "doc": 2**32}],
)
def test_edit_pdf_rejects_invalid_operations(self, mock_open) -> None:
for operations in ([], [{"page": 1, "doc": 2**32}]):
with self.subTest(operations=operations):
with self.assertLogs("paperless.bulk_edit", level="ERROR"):
with self.assertRaisesRegex(ValueError, "index is out of bounds"):
bulk_edit.edit_pdf([self.doc2.id], operations)
mock_open.assert_not_called()
+7
View File
@@ -1,5 +1,6 @@
import pytest
import regex
from django.conf import settings
from pytest_mock import MockerFixture
from documents.regex import safe_regex_finditer
@@ -9,6 +10,12 @@ from documents.regex import safe_regex_sub
from documents.regex import validate_regex_pattern
def test_regex_timeout_uses_configured_setting() -> None:
from documents.regex import REGEX_TIMEOUT_SECONDS
assert REGEX_TIMEOUT_SECONDS == settings.MATCH_REGEX_TIMEOUT_SECONDS
class TestValidateRegexPattern:
def test_valid_pattern(self) -> None:
validate_regex_pattern(r"\d+")
+11
View File
@@ -106,6 +106,17 @@ class TestBeforeTaskPublishHandler:
assert task.task_type == PaperlessTask.TaskType.TRAIN_CLASSIFIER
assert task.trigger_source == PaperlessTask.TriggerSource.MANUAL
# A Celery retry republishes with the same task_id; this must not
# raise a duplicate-key IntegrityError, and must leave the original
# PENDING record alone.
send_publish(
"documents.tasks.train_classifier",
(),
{},
headers={"id": task_id},
)
assert PaperlessTask.objects.filter(task_id=task_id).count() == 1
def test_creates_task_for_sanity_check(self) -> None:
task_id = send_publish("documents.tasks.sanity_check", (), {})
task = PaperlessTask.objects.get(task_id=task_id)
+47 -47
View File
@@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-09-12 23:18+0000\n"
"POT-Creation-Date: 2026-09-13 22:13+0000\n"
"PO-Revision-Date: 2022-02-17 04:17\n"
"Last-Translator: \n"
"Language-Team: English\n"
@@ -1632,7 +1632,7 @@ msgid "workflow runs"
msgstr ""
#: documents/serialisers.py:525 documents/serialisers.py:882
#: documents/serialisers.py:2854 documents/views.py:319 documents/views.py:2694
#: documents/serialisers.py:2868 documents/views.py:319 documents/views.py:2694
#: paperless_mail/serialisers.py:156
msgid "Insufficient permissions."
msgstr ""
@@ -1641,39 +1641,39 @@ msgstr ""
msgid "Invalid color."
msgstr ""
#: documents/serialisers.py:2327
#: documents/serialisers.py:2341
#, python-format
msgid "File type %(type)s not supported"
msgstr ""
#: documents/serialisers.py:2371
#: documents/serialisers.py:2385
#, python-format
msgid "Custom field id must be an integer: %(id)s"
msgstr ""
#: documents/serialisers.py:2378
#: documents/serialisers.py:2392
#, python-format
msgid "Custom field with id %(id)s does not exist"
msgstr ""
#: documents/serialisers.py:2395 documents/serialisers.py:2405
#: documents/serialisers.py:2409 documents/serialisers.py:2419
msgid ""
"Custom fields must be a list of integers or an object mapping ids to values."
msgstr ""
#: documents/serialisers.py:2400
#: documents/serialisers.py:2414
msgid "Some custom fields don't exist or were specified twice."
msgstr ""
#: documents/serialisers.py:2547
#: documents/serialisers.py:2561
msgid "Invalid variable detected."
msgstr ""
#: documents/serialisers.py:2910
#: documents/serialisers.py:2924
msgid "Duplicate document identifiers are not allowed."
msgstr ""
#: documents/serialisers.py:2940 documents/views.py:4707
#: documents/serialisers.py:2954 documents/views.py:4707
#, python-format
msgid "Documents not found: %(ids)s"
msgstr ""
@@ -2258,151 +2258,151 @@ msgstr ""
msgid "paperless application settings"
msgstr ""
#: paperless/settings/__init__.py:556
#: paperless/settings/__init__.py:560
msgid "English (US)"
msgstr ""
#: paperless/settings/__init__.py:557
#: paperless/settings/__init__.py:561
msgid "Arabic"
msgstr ""
#: paperless/settings/__init__.py:558
#: paperless/settings/__init__.py:562
msgid "Afrikaans"
msgstr ""
#: paperless/settings/__init__.py:559
#: paperless/settings/__init__.py:563
msgid "Belarusian"
msgstr ""
#: paperless/settings/__init__.py:560
#: paperless/settings/__init__.py:564
msgid "Bulgarian"
msgstr ""
#: paperless/settings/__init__.py:561
#: paperless/settings/__init__.py:565
msgid "Catalan"
msgstr ""
#: paperless/settings/__init__.py:562
#: paperless/settings/__init__.py:566
msgid "Czech"
msgstr ""
#: paperless/settings/__init__.py:563
#: paperless/settings/__init__.py:567
msgid "Danish"
msgstr ""
#: paperless/settings/__init__.py:564
#: paperless/settings/__init__.py:568
msgid "German"
msgstr ""
#: paperless/settings/__init__.py:565
#: paperless/settings/__init__.py:569
msgid "Greek"
msgstr ""
#: paperless/settings/__init__.py:566
#: paperless/settings/__init__.py:570
msgid "English (GB)"
msgstr ""
#: paperless/settings/__init__.py:567
#: paperless/settings/__init__.py:571
msgid "Spanish"
msgstr ""
#: paperless/settings/__init__.py:568
#: paperless/settings/__init__.py:572
msgid "Persian"
msgstr ""
#: paperless/settings/__init__.py:569
#: paperless/settings/__init__.py:573
msgid "Finnish"
msgstr ""
#: paperless/settings/__init__.py:570
#: paperless/settings/__init__.py:574
msgid "French"
msgstr ""
#: paperless/settings/__init__.py:571
#: paperless/settings/__init__.py:575
msgid "Hungarian"
msgstr ""
#: paperless/settings/__init__.py:572
#: paperless/settings/__init__.py:576
msgid "Indonesian"
msgstr ""
#: paperless/settings/__init__.py:573
#: paperless/settings/__init__.py:577
msgid "Italian"
msgstr ""
#: paperless/settings/__init__.py:574
#: paperless/settings/__init__.py:578
msgid "Japanese"
msgstr ""
#: paperless/settings/__init__.py:575
#: paperless/settings/__init__.py:579
msgid "Korean"
msgstr ""
#: paperless/settings/__init__.py:576
#: paperless/settings/__init__.py:580
msgid "Luxembourgish"
msgstr ""
#: paperless/settings/__init__.py:577
#: paperless/settings/__init__.py:581
msgid "Norwegian"
msgstr ""
#: paperless/settings/__init__.py:578
#: paperless/settings/__init__.py:582
msgid "Dutch"
msgstr ""
#: paperless/settings/__init__.py:579
#: paperless/settings/__init__.py:583
msgid "Polish"
msgstr ""
#: paperless/settings/__init__.py:580
#: paperless/settings/__init__.py:584
msgid "Portuguese (Brazil)"
msgstr ""
#: paperless/settings/__init__.py:581
#: paperless/settings/__init__.py:585
msgid "Portuguese"
msgstr ""
#: paperless/settings/__init__.py:582
#: paperless/settings/__init__.py:586
msgid "Romanian"
msgstr ""
#: paperless/settings/__init__.py:583
#: paperless/settings/__init__.py:587
msgid "Russian"
msgstr ""
#: paperless/settings/__init__.py:584
#: paperless/settings/__init__.py:588
msgid "Slovak"
msgstr ""
#: paperless/settings/__init__.py:585
#: paperless/settings/__init__.py:589
msgid "Slovenian"
msgstr ""
#: paperless/settings/__init__.py:586
#: paperless/settings/__init__.py:590
msgid "Serbian"
msgstr ""
#: paperless/settings/__init__.py:587
#: paperless/settings/__init__.py:591
msgid "Swedish"
msgstr ""
#: paperless/settings/__init__.py:588
#: paperless/settings/__init__.py:592
msgid "Turkish"
msgstr ""
#: paperless/settings/__init__.py:589
#: paperless/settings/__init__.py:593
msgid "Ukrainian"
msgstr ""
#: paperless/settings/__init__.py:590
#: paperless/settings/__init__.py:594
msgid "Vietnamese"
msgstr ""
#: paperless/settings/__init__.py:591
#: paperless/settings/__init__.py:595
msgid "Chinese Simplified"
msgstr ""
#: paperless/settings/__init__.py:592
#: paperless/settings/__init__.py:596
msgid "Chinese Traditional"
msgstr ""
+4
View File
@@ -102,6 +102,10 @@ CLASSIFIER_MATCH_THRESHOLD: Final[float] = get_float_from_env(
"PAPERLESS_CLASSIFIER_MATCH_THRESHOLD",
0.6,
)
MATCH_REGEX_TIMEOUT_SECONDS: Final[float] = get_float_from_env(
"PAPERLESS_MATCH_REGEX_TIMEOUT_SECONDS",
0.1,
)
LLM_INDEX_DIR = DATA_DIR / "llm_index"
LLM_INDEX_LOCK = LLM_INDEX_DIR / "index.lock"
# Cross-process read/write lock guarding the LLM index compaction/migration