mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-10-21 23:38:13 +00:00
Allow parsers to declare uses remote, and remote ocr_mode (#13634)
* uses_remote_service + allow_remote to allow opt-in / out of remote OCR * Add to parser dev docs * remote_ocr_mode config setting * Checks for remote_ocr_mode and fix import * Update config.component.spec.ts * More tests for remote_ocr_mode * Docs for remote_ocr_mode * Ok, wire up the remote_ocr_mode with allow_remote for consumer * Update consumer.py * Format remote OCR mode check tests * Use get_choice_from_env
This commit is contained in:
1 parent
a1f20c9fe7
commit
7f1609332a
20 files changed
+425
-1
No files matched your search
@@ -468,6 +468,124 @@ class TestParserRegistryGetParserForFile:
|
||||
assert result is AcceptingBuiltin
|
||||
|
||||
|
||||
class TestParserRegistryRemoteParsers:
|
||||
"""Verify the allow_remote filter in ParserRegistry.get_parser_for_file()."""
|
||||
|
||||
@staticmethod
|
||||
def _remote_parser_cls() -> type:
|
||||
class RemoteParser:
|
||||
name = "remote"
|
||||
version = "1.0"
|
||||
author = "A"
|
||||
url = "https://example.com/remote"
|
||||
uses_remote_service = True
|
||||
|
||||
@classmethod
|
||||
def supported_mime_types(cls):
|
||||
return {"text/plain": ".txt"}
|
||||
|
||||
@classmethod
|
||||
def score(cls, mime_type, filename, path=None):
|
||||
return 20
|
||||
|
||||
return RemoteParser
|
||||
|
||||
def test_remote_parser_wins_when_remote_allowed(
|
||||
self,
|
||||
dummy_parser_cls: type,
|
||||
) -> None:
|
||||
"""
|
||||
GIVEN: A remote parser scoring 20 and a local parser scoring 10.
|
||||
WHEN: get_parser_for_file() is called with allow_remote=True.
|
||||
THEN: The remote parser is returned.
|
||||
"""
|
||||
remote_parser_cls = self._remote_parser_cls()
|
||||
registry = ParserRegistry()
|
||||
registry.register_builtin(dummy_parser_cls)
|
||||
registry.register_builtin(remote_parser_cls)
|
||||
|
||||
result = registry.get_parser_for_file(
|
||||
"text/plain",
|
||||
"readme.txt",
|
||||
allow_remote=True,
|
||||
)
|
||||
assert result is remote_parser_cls
|
||||
|
||||
def test_remote_parser_skipped_when_remote_not_allowed(
|
||||
self,
|
||||
dummy_parser_cls: type,
|
||||
) -> None:
|
||||
"""
|
||||
GIVEN: A remote parser scoring 20 and a local parser scoring 10.
|
||||
WHEN: get_parser_for_file() is called with allow_remote=False.
|
||||
THEN: The local parser is returned despite its lower score.
|
||||
"""
|
||||
registry = ParserRegistry()
|
||||
registry.register_builtin(dummy_parser_cls)
|
||||
registry.register_builtin(self._remote_parser_cls())
|
||||
|
||||
result = registry.get_parser_for_file(
|
||||
"text/plain",
|
||||
"readme.txt",
|
||||
allow_remote=False,
|
||||
)
|
||||
assert result is dummy_parser_cls
|
||||
|
||||
def test_no_parser_when_only_remote_available_and_not_allowed(self) -> None:
|
||||
"""
|
||||
GIVEN: A registry whose only candidate declares uses_remote_service.
|
||||
WHEN: get_parser_for_file() is called with allow_remote=False.
|
||||
THEN: None is returned — the remote parser is never used as a
|
||||
fallback when remote processing was not requested.
|
||||
"""
|
||||
registry = ParserRegistry()
|
||||
registry.register_builtin(self._remote_parser_cls())
|
||||
|
||||
result = registry.get_parser_for_file(
|
||||
"text/plain",
|
||||
"readme.txt",
|
||||
allow_remote=False,
|
||||
)
|
||||
assert result is None
|
||||
|
||||
def test_parser_without_attribute_treated_as_local(
|
||||
self,
|
||||
dummy_parser_cls: type,
|
||||
) -> None:
|
||||
"""
|
||||
GIVEN: A third-party parser predating uses_remote_service, so it does
|
||||
not declare the attribute at all.
|
||||
WHEN: get_parser_for_file() is called with allow_remote=False.
|
||||
THEN: It is still considered, i.e. treated as fully local, rather
|
||||
than raising AttributeError.
|
||||
"""
|
||||
assert not hasattr(dummy_parser_cls, "uses_remote_service")
|
||||
|
||||
registry = ParserRegistry()
|
||||
registry.register_builtin(dummy_parser_cls)
|
||||
|
||||
result = registry.get_parser_for_file(
|
||||
"text/plain",
|
||||
"readme.txt",
|
||||
allow_remote=False,
|
||||
)
|
||||
assert result is dummy_parser_cls
|
||||
|
||||
def test_remote_allowed_by_default(self) -> None:
|
||||
"""
|
||||
GIVEN: A registry containing only a remote parser.
|
||||
WHEN: get_parser_for_file() is called without allow_remote.
|
||||
THEN: The remote parser is returned — callers that do not opt in to
|
||||
the filter keep the previous behaviour.
|
||||
"""
|
||||
remote_parser_cls = self._remote_parser_cls()
|
||||
registry = ParserRegistry()
|
||||
registry.register_builtin(remote_parser_cls)
|
||||
|
||||
result = registry.get_parser_for_file("text/plain", "readme.txt")
|
||||
assert result is remote_parser_cls
|
||||
|
||||
|
||||
class TestDiscover:
|
||||
"""Verify entrypoint discovery in ParserRegistry.discover()."""
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import pytest
|
||||
from django.test import override_settings
|
||||
|
||||
from paperless.config import RemoteOCRConfig
|
||||
from paperless.models import RemoteOCRMode
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from unittest.mock import MagicMock
|
||||
@@ -20,6 +21,7 @@ def null_app_config(mocker) -> MagicMock:
|
||||
remote_ocr_engine=None,
|
||||
remote_ocr_api_key=None,
|
||||
remote_ocr_endpoint=None,
|
||||
remote_ocr_mode=None,
|
||||
)
|
||||
|
||||
|
||||
@@ -47,10 +49,12 @@ class TestRemoteOCRConfig:
|
||||
REMOTE_OCR_ENGINE="azureai",
|
||||
REMOTE_OCR_API_KEY="env-key",
|
||||
REMOTE_OCR_ENDPOINT="https://env.cognitiveservices.azure.com",
|
||||
REMOTE_OCR_MODE=RemoteOCRMode.WORKFLOW_ONLY,
|
||||
)
|
||||
assert cfg.remote_ocr_engine == "azureai"
|
||||
assert cfg.remote_ocr_api_key == "env-key"
|
||||
assert cfg.remote_ocr_endpoint == "https://env.cognitiveservices.azure.com"
|
||||
assert cfg.remote_ocr_mode == RemoteOCRMode.WORKFLOW_ONLY
|
||||
|
||||
def test_app_config_takes_precedence(
|
||||
self,
|
||||
@@ -61,16 +65,19 @@ class TestRemoteOCRConfig:
|
||||
remote_ocr_engine="azureai",
|
||||
remote_ocr_api_key="db-key",
|
||||
remote_ocr_endpoint="https://db.cognitiveservices.azure.com",
|
||||
remote_ocr_mode=RemoteOCRMode.WORKFLOW_ONLY,
|
||||
)
|
||||
cfg = make_remote_ocr_config(
|
||||
app_config,
|
||||
REMOTE_OCR_ENGINE=None,
|
||||
REMOTE_OCR_API_KEY="env-key",
|
||||
REMOTE_OCR_ENDPOINT="https://env.cognitiveservices.azure.com",
|
||||
REMOTE_OCR_MODE=RemoteOCRMode.ALWAYS,
|
||||
)
|
||||
assert cfg.remote_ocr_engine == "azureai"
|
||||
assert cfg.remote_ocr_api_key == "db-key"
|
||||
assert cfg.remote_ocr_endpoint == "https://db.cognitiveservices.azure.com"
|
||||
assert cfg.remote_ocr_mode == RemoteOCRMode.WORKFLOW_ONLY
|
||||
|
||||
def test_unset_everywhere(
|
||||
self,
|
||||
@@ -86,3 +93,21 @@ class TestRemoteOCRConfig:
|
||||
assert cfg.remote_ocr_engine is None
|
||||
assert cfg.remote_ocr_api_key is None
|
||||
assert cfg.remote_ocr_endpoint is None
|
||||
|
||||
|
||||
class TestRemoteOCRByDefault:
|
||||
def test_always_mode(self, make_remote_ocr_config, null_app_config) -> None:
|
||||
cfg = make_remote_ocr_config(
|
||||
null_app_config,
|
||||
REMOTE_OCR_MODE=RemoteOCRMode.ALWAYS,
|
||||
)
|
||||
|
||||
assert cfg.remote_ocr_by_default is True
|
||||
|
||||
def test_workflow_only_mode(self, make_remote_ocr_config, null_app_config) -> None:
|
||||
cfg = make_remote_ocr_config(
|
||||
null_app_config,
|
||||
REMOTE_OCR_MODE=RemoteOCRMode.WORKFLOW_ONLY,
|
||||
)
|
||||
|
||||
assert cfg.remote_ocr_by_default is False
|
||||
Reference in new issue
Block a user