Coverage for paperless/config.py: 73%

153 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-10 09:07 +0000

1import dataclasses 

2import json 

3from typing import Any 

4 

5from django.conf import settings 

6 

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 

14 

15 

16@dataclasses.dataclass 

17class BaseConfig: 

18 """ 

19 Almost all parsers care about the chosen PDF output format 

20 """ 

21 

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 

30 

31 

32@dataclasses.dataclass 

33class OutputTypeConfig(BaseConfig): 

34 """ 

35 Almost all parsers care about the chosen PDF output format 

36 """ 

37 

38 output_type: OutputTypeChoices = dataclasses.field(init=False) 

39 

40 def __post_init__(self) -> None: 

41 app_config = self._get_config_instance() 

42 

43 self.output_type = app_config.output_type or OutputTypeChoices( 

44 settings.OCR_OUTPUT_TYPE, 

45 ) 

46 

47 

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 """ 

54 

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) 

69 

70 def __post_init__(self) -> None: 

71 super().__post_init__() 

72 

73 app_config = self._get_config_instance() 

74 

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 ) 

102 

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 

112 

113 

114@dataclasses.dataclass 

115class BarcodeConfig(BaseConfig): 

116 """ 

117 Barcodes settings 

118 """ 

119 

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) 

133 

134 def __post_init__(self) -> None: 

135 app_config = self._get_config_instance() 

136 

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 ) 

188 

189 

190@dataclasses.dataclass 

191class GeneralConfig(BaseConfig): 

192 """ 

193 General application settings that require global scope 

194 """ 

195 

196 app_title: str = dataclasses.field(init=False) 

197 app_logo: str = dataclasses.field(init=False) 

198 

199 def __post_init__(self) -> None: 

200 app_config = self._get_config_instance() 

201 

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 

204 

205 

206@dataclasses.dataclass 

207class RemoteOCRConfig(BaseConfig): 

208 """ 

209 Settings for the remote (cloud) OCR parser 

210 """ 

211 

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) 

216 

217 def __post_init__(self) -> None: 

218 app_config = self._get_config_instance() 

219 

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 ) 

232 

233 @property 

234 def remote_ocr_by_default(self) -> bool: 

235 """ 

236 Whether every supported document goes to the remote engine. 

237 

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 

243 

244 

245@dataclasses.dataclass 

246class AIConfig(BaseConfig): 

247 """ 

248 AI related settings that require global scope 

249 """ 

250 

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) 

266 

267 def __post_init__(self) -> None: 

268 app_config = self._get_config_instance() 

269 

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 

303 

304 @property 

305 def llm_index_enabled(self) -> bool: 

306 return bool(self.ai_enabled and self.llm_embedding_backend)