Coverage for paperless/config.py: 73%
153 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
1import dataclasses
2import json
3from typing import Any
5from django.conf import settings
7from paperless.models import ApplicationConfiguration
8from paperless.models import ArchiveFileGenerationChoices
9from paperless.models import CleanChoices
10from paperless.models import ColorConvertChoices
11from paperless.models import ModeChoices
12from paperless.models import OutputTypeChoices
13from paperless.models import RemoteOCRMode
16@dataclasses.dataclass
17class BaseConfig:
18 """
19 Almost all parsers care about the chosen PDF output format
20 """
22 @staticmethod
23 def _get_config_instance() -> ApplicationConfiguration:
24 app_config = ApplicationConfiguration.objects.all().first()
25 # Workaround for a test where the migration hasn't run to create the single model
26 if app_config is None:
27 ApplicationConfiguration.objects.create()
28 app_config = ApplicationConfiguration.objects.all().first()
29 return app_config
32@dataclasses.dataclass
33class OutputTypeConfig(BaseConfig):
34 """
35 Almost all parsers care about the chosen PDF output format
36 """
38 output_type: OutputTypeChoices = dataclasses.field(init=False)
40 def __post_init__(self) -> None:
41 app_config = self._get_config_instance()
43 self.output_type = app_config.output_type or OutputTypeChoices(
44 settings.OCR_OUTPUT_TYPE,
45 )
48@dataclasses.dataclass
49class OcrConfig(OutputTypeConfig):
50 """
51 Specific settings for the Tesseract based parser. Options generally
52 correspond almost directly to the OCRMyPDF options
53 """
55 pages: int | None = dataclasses.field(init=False)
56 language: str = dataclasses.field(init=False)
57 mode: ModeChoices = dataclasses.field(init=False)
58 archive_file_generation: ArchiveFileGenerationChoices = dataclasses.field(
59 init=False,
60 )
61 image_dpi: int | None = dataclasses.field(init=False)
62 clean: CleanChoices = dataclasses.field(init=False)
63 deskew: bool = dataclasses.field(init=False)
64 rotate: bool = dataclasses.field(init=False)
65 rotate_threshold: float = dataclasses.field(init=False)
66 max_image_pixel: float | None = dataclasses.field(init=False)
67 color_conversion_strategy: ColorConvertChoices = dataclasses.field(init=False)
68 user_args: dict[str, str] | None = dataclasses.field(init=False)
70 def __post_init__(self) -> None:
71 super().__post_init__()
73 app_config = self._get_config_instance()
75 self.pages = app_config.pages or settings.OCR_PAGES
76 self.language = app_config.language or settings.OCR_LANGUAGE
77 self.mode = app_config.mode or ModeChoices(settings.OCR_MODE)
78 self.archive_file_generation = (
79 app_config.archive_file_generation
80 or ArchiveFileGenerationChoices(settings.ARCHIVE_FILE_GENERATION)
81 )
82 self.image_dpi = app_config.image_dpi or settings.OCR_IMAGE_DPI
83 self.clean = app_config.unpaper_clean or CleanChoices(settings.OCR_CLEAN)
84 self.deskew = (
85 app_config.deskew if app_config.deskew is not None else settings.OCR_DESKEW
86 )
87 self.rotate = (
88 app_config.rotate_pages
89 if app_config.rotate_pages is not None
90 else settings.OCR_ROTATE_PAGES
91 )
92 self.rotate_threshold = (
93 app_config.rotate_pages_threshold or settings.OCR_ROTATE_PAGES_THRESHOLD
94 )
95 self.max_image_pixel = (
96 app_config.max_image_pixels or settings.OCR_MAX_IMAGE_PIXELS
97 )
98 self.color_conversion_strategy = (
99 app_config.color_conversion_strategy
100 or ColorConvertChoices(settings.OCR_COLOR_CONVERSION_STRATEGY)
101 )
103 user_args = None
104 if app_config.user_args:
105 user_args = app_config.user_args
106 elif settings.OCR_USER_ARGS is not None: # pragma: no cover
107 try:
108 user_args = json.loads(settings.OCR_USER_ARGS)
109 except json.JSONDecodeError:
110 user_args = {}
111 self.user_args = user_args
114@dataclasses.dataclass
115class BarcodeConfig(BaseConfig):
116 """
117 Barcodes settings
118 """
120 barcodes_enabled: bool = dataclasses.field(init=False)
121 barcode_enable_tiff_support: bool = dataclasses.field(init=False)
122 barcode_string: str = dataclasses.field(init=False)
123 barcode_retain_split_pages: bool = dataclasses.field(init=False)
124 barcode_enable_asn: bool = dataclasses.field(init=False)
125 barcode_asn_prefix: str = dataclasses.field(init=False)
126 barcode_upscale: float = dataclasses.field(init=False)
127 barcode_dpi: int = dataclasses.field(init=False)
128 barcode_max_pages: int = dataclasses.field(init=False)
129 barcode_enable_tag: bool = dataclasses.field(init=False)
130 barcode_tag_mapping: dict[str, str] = dataclasses.field(init=False)
131 barcode_tag_split: bool = dataclasses.field(init=False)
132 barcode_store_values: bool = dataclasses.field(init=False)
134 def __post_init__(self) -> None:
135 app_config = self._get_config_instance()
137 self.barcodes_enabled = (
138 app_config.barcodes_enabled
139 if app_config.barcodes_enabled is not None
140 else settings.CONSUMER_ENABLE_BARCODES
141 )
142 self.barcode_enable_tiff_support = (
143 app_config.barcode_enable_tiff_support
144 if app_config.barcode_enable_tiff_support is not None
145 else settings.CONSUMER_BARCODE_TIFF_SUPPORT
146 )
147 self.barcode_string = (
148 app_config.barcode_string or settings.CONSUMER_BARCODE_STRING
149 )
150 self.barcode_retain_split_pages = (
151 app_config.barcode_retain_split_pages
152 if app_config.barcode_retain_split_pages is not None
153 else settings.CONSUMER_BARCODE_RETAIN_SPLIT_PAGES
154 )
155 self.barcode_enable_asn = (
156 app_config.barcode_enable_asn
157 if app_config.barcode_enable_asn is not None
158 else settings.CONSUMER_ENABLE_ASN_BARCODE
159 )
160 self.barcode_asn_prefix = (
161 app_config.barcode_asn_prefix or settings.CONSUMER_ASN_BARCODE_PREFIX
162 )
163 self.barcode_upscale = (
164 app_config.barcode_upscale or settings.CONSUMER_BARCODE_UPSCALE
165 )
166 self.barcode_dpi = app_config.barcode_dpi or settings.CONSUMER_BARCODE_DPI
167 self.barcode_max_pages = (
168 app_config.barcode_max_pages or settings.CONSUMER_BARCODE_MAX_PAGES
169 )
170 self.barcode_enable_tag = (
171 app_config.barcode_enable_tag
172 if app_config.barcode_enable_tag is not None
173 else settings.CONSUMER_ENABLE_TAG_BARCODE
174 )
175 self.barcode_tag_mapping = (
176 app_config.barcode_tag_mapping or settings.CONSUMER_TAG_BARCODE_MAPPING
177 )
178 self.barcode_tag_split = (
179 app_config.barcode_tag_split
180 if app_config.barcode_tag_split is not None
181 else settings.CONSUMER_TAG_BARCODE_SPLIT
182 )
183 self.barcode_store_values = (
184 app_config.barcode_store_values
185 if app_config.barcode_store_values is not None
186 else settings.CONSUMER_STORE_BARCODE_VALUES
187 )
190@dataclasses.dataclass
191class GeneralConfig(BaseConfig):
192 """
193 General application settings that require global scope
194 """
196 app_title: str = dataclasses.field(init=False)
197 app_logo: str = dataclasses.field(init=False)
199 def __post_init__(self) -> None:
200 app_config = self._get_config_instance()
202 self.app_title = app_config.app_title or None
203 self.app_logo = app_config.app_logo.url if app_config.app_logo else None
206@dataclasses.dataclass
207class RemoteOCRConfig(BaseConfig):
208 """
209 Settings for the remote (cloud) OCR parser
210 """
212 remote_ocr_engine: str | None = dataclasses.field(init=False)
213 remote_ocr_api_key: str | None = dataclasses.field(init=False)
214 remote_ocr_endpoint: str | None = dataclasses.field(init=False)
215 remote_ocr_mode: RemoteOCRMode = dataclasses.field(init=False)
217 def __post_init__(self) -> None:
218 app_config = self._get_config_instance()
220 self.remote_ocr_engine = (
221 app_config.remote_ocr_engine or settings.REMOTE_OCR_ENGINE
222 )
223 self.remote_ocr_api_key = (
224 app_config.remote_ocr_api_key or settings.REMOTE_OCR_API_KEY
225 )
226 self.remote_ocr_endpoint = (
227 app_config.remote_ocr_endpoint or settings.REMOTE_OCR_ENDPOINT
228 )
229 self.remote_ocr_mode = app_config.remote_ocr_mode or RemoteOCRMode(
230 settings.REMOTE_OCR_MODE,
231 )
233 @property
234 def remote_ocr_by_default(self) -> bool:
235 """
236 Whether every supported document goes to the remote engine.
238 When False the remote engine is used only for documents that
239 explicitly asked for it, i.e. a workflow matched during consumption or
240 the user ticked the box when reprocessing.
241 """
242 return self.remote_ocr_mode == RemoteOCRMode.ALWAYS
245@dataclasses.dataclass
246class AIConfig(BaseConfig):
247 """
248 AI related settings that require global scope
249 """
251 ai_enabled: bool = dataclasses.field(init=False)
252 llm_embedding_backend: str = dataclasses.field(init=False)
253 llm_embedding_model: str = dataclasses.field(init=False)
254 llm_embedding_api_key: str = dataclasses.field(init=False)
255 llm_embedding_endpoint: str = dataclasses.field(init=False)
256 llm_embedding_chunk_size: int = dataclasses.field(init=False)
257 llm_context_size: int = dataclasses.field(init=False)
258 llm_request_timeout: int = dataclasses.field(init=False)
259 llm_backend: str = dataclasses.field(init=False)
260 llm_model: str = dataclasses.field(init=False)
261 llm_api_key: str = dataclasses.field(init=False)
262 llm_endpoint: str = dataclasses.field(init=False)
263 llm_output_language: str = dataclasses.field(init=False)
264 llm_allow_internal_endpoints: bool = dataclasses.field(init=False)
265 llm_extra_params: dict[str, Any] = dataclasses.field(init=False)
267 def __post_init__(self) -> None:
268 app_config = self._get_config_instance()
270 self.ai_enabled = (
271 app_config.ai_enabled
272 if app_config.ai_enabled is not None
273 else settings.AI_ENABLED
274 )
275 self.llm_embedding_backend = (
276 app_config.llm_embedding_backend or settings.LLM_EMBEDDING_BACKEND
277 )
278 self.llm_embedding_model = (
279 app_config.llm_embedding_model or settings.LLM_EMBEDDING_MODEL
280 )
281 self.llm_embedding_api_key = (
282 app_config.llm_embedding_api_key or settings.LLM_EMBEDDING_API_KEY
283 )
284 self.llm_embedding_endpoint = (
285 app_config.llm_embedding_endpoint or settings.LLM_EMBEDDING_ENDPOINT
286 )
287 self.llm_embedding_chunk_size = (
288 app_config.llm_embedding_chunk_size or settings.LLM_EMBEDDING_CHUNK_SIZE
289 )
290 self.llm_context_size = app_config.llm_context_size or settings.LLM_CONTEXT_SIZE
291 self.llm_request_timeout = (
292 app_config.llm_request_timeout or settings.LLM_REQUEST_TIMEOUT
293 )
294 self.llm_backend = app_config.llm_backend or settings.LLM_BACKEND
295 self.llm_model = app_config.llm_model or settings.LLM_MODEL
296 self.llm_api_key = app_config.llm_api_key or settings.LLM_API_KEY
297 self.llm_endpoint = app_config.llm_endpoint or settings.LLM_ENDPOINT
298 self.llm_output_language = (
299 app_config.llm_output_language or settings.LLM_OUTPUT_LANGUAGE
300 )
301 self.llm_allow_internal_endpoints = settings.LLM_ALLOW_INTERNAL_ENDPOINTS
302 self.llm_extra_params = settings.LLM_EXTRA_PARAMS
304 @property
305 def llm_index_enabled(self) -> bool:
306 return bool(self.ai_enabled and self.llm_embedding_backend)