fix(ocr): round-2 review — lazy store lock, mode enum normalization, type hints

Round 2 review (PR #910):
- BLOCKING: BatchOcrJobStore._shared_lock is now lazy-init (anyio.Lock | None,
  created on first shared() call) instead of at class-definition time — matches
  the CLAUDE.md "no anyio primitives at import time" rule and OcrProcessor's
  pattern. The None-check->assign has no await between, so it's race-free.
- document_ocr_mode now normalizes via _enum_fields (case-insensitive, like
  document_ocr_provider) instead of a strict dynaconf is_in Validator, so
  DOCUMENT_OCR_MODE=Batch normalizes to "batch" rather than erroring. Tests for
  case-normalization + invalid-value rejection.
- TYPE_CHECKING-gated GatewayBatchOcrClient import so build_gateway_batch_client
  / _get_batch_client are typed `GatewayBatchOcrClient | None` instead of Any
  (runtime import stays lazy to avoid the import cycle).
- Rename ocr_options -> doc_identity_options (it's threaded to all tiers; only
  OCR reads it) + clarify the comment.
- Drop the redundant forward-ref quotes on _shared_instance.
- Add direct _batch_identity unit tests (partial/empty options branches).

Left as follow-up: reusing one httpx.AsyncClient across submit/poll (same
per-call pattern as the existing sync _GatewayOcrBackend; no clean aclose hook
on the cached client today).

1653 unit tests pass; ruff + ty green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-06-15 10:45:10 +02:00
co-authored by Claude Opus 4.8
parent 2b7dfc8535
commit 995e810d89
6 changed files with 75 additions and 16 deletions
@@ -21,7 +21,7 @@ import logging
import time
from abc import ABC, abstractmethod
from collections.abc import Awaitable, Callable
from typing import Any
from typing import TYPE_CHECKING, Any
import anyio
import httpx
@@ -30,6 +30,12 @@ from nextcloud_mcp_server.config import Settings, get_settings
from .base import DocumentProcessor, ProcessingResult
if TYPE_CHECKING:
# Annotation-only import (the runtime import is lazy, inside
# build_gateway_batch_client, to avoid a document_processors -> embedding
# cycle at load).
from ..embedding.gateway_batch_client import GatewayBatchOcrClient
logger = logging.getLogger(__name__)
# Connect timeout for the OCR backend request. The overall (read) timeout is
@@ -197,7 +203,7 @@ def _build_gateway_token_provider(settings: Settings) -> Any:
)
def build_gateway_batch_client(settings: Settings) -> Any:
def build_gateway_batch_client(settings: Settings) -> "GatewayBatchOcrClient | None":
"""Build a ``GatewayBatchOcrClient`` when the gateway is the OCR backend, else
``None`` (so batch mode falls back to sync for provider=mistral / no gateway).
Batch OCR is gateway-only — Mistral's Batch API is reached *through* the
@@ -373,7 +379,7 @@ class OcrProcessor(DocumentProcessor):
processor=self.name,
)
async def _get_batch_client(self) -> Any:
async def _get_batch_client(self) -> "GatewayBatchOcrClient | None":
"""Cached gateway batch client (or ``None`` when batch isn't applicable —
provider=mistral / no gateway). Resolved once under the backend lock so the
token provider's M2M cache survives across documents."""