Sweep all 1676 G004 violations across 112 files, converting
`logger.<level>(f"…{x}…")` to `logger.<level>("…%s…", x)`.
Why: ruff rule G004 was added to pyproject.toml to enforce lazy
%-style logging — defers formatting until the log level is enabled
and lets structured log tooling match the unformatted template.
Conversion preserves rendered output byte-for-byte:
- `{x}` → `%s` + `x`
- `{x!r}` / `{x!s}` / `{x!a}` → `%r` / `%s` / `%a`
- Format specs (`{x:.2f}`, `{x:>10}`) → `%s` + `format(x, 'spec')`
(printf-style specs aren't 1:1 with Python format specs, so we
delegate to `format()` to keep identical output)
- Literal `%` → `%%`
- Concatenated f-strings (`f"a {x} " "b"`) flattened
- Trailing kwargs (`exc_info=True`) preserved
Verified:
- `uv run ruff check --select G004` → 0 violations
- `uv run ty check -- nextcloud_mcp_server` → passes
- `uv run pytest tests/unit/` → 1010 passed
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
155 lines
5.0 KiB
Python
155 lines
5.0 KiB
Python
"""Generic HTTP API processor wrapper for custom document processing services."""
|
|
|
|
import logging
|
|
from collections.abc import Awaitable, Callable
|
|
from typing import Any, Optional
|
|
|
|
import httpx
|
|
|
|
from .base import DocumentProcessor, ProcessingResult, ProcessorError
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class CustomHTTPProcessor(DocumentProcessor):
|
|
"""Generic HTTP API processor wrapper.
|
|
|
|
Allows integration with any custom document processing API that follows
|
|
a simple request/response pattern. This makes it easy to integrate your
|
|
own text extraction services without writing a full processor.
|
|
|
|
Expected API Contract:
|
|
- POST request with file as multipart/form-data
|
|
- Response: {"text": "extracted text", "metadata": {...}}
|
|
|
|
Example:
|
|
processor = CustomHTTPProcessor(
|
|
name="my_ocr",
|
|
api_url="https://my-ocr-service.com/process",
|
|
api_key="secret",
|
|
supported_types={"application/pdf", "image/jpeg"},
|
|
)
|
|
result = await processor.process(pdf_bytes, "application/pdf")
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
api_url: str,
|
|
api_key: Optional[str] = None,
|
|
timeout: int = 60,
|
|
supported_types: Optional[set[str]] = None,
|
|
name: str = "custom",
|
|
):
|
|
"""Initialize custom HTTP processor.
|
|
|
|
Args:
|
|
api_url: Your API endpoint (should accept POST with multipart/form-data)
|
|
api_key: Optional API key for authentication (sent as Bearer token)
|
|
timeout: Request timeout in seconds (default: 60)
|
|
supported_types: MIME types your API supports
|
|
name: Unique name for this processor (default: "custom")
|
|
"""
|
|
self.api_url = api_url
|
|
self.api_key = api_key
|
|
self.timeout = timeout
|
|
self._name = name
|
|
self._supported_types = supported_types or set()
|
|
|
|
logger.info("Initialized CustomHTTPProcessor: %s -> %s", name, api_url)
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
return self._name
|
|
|
|
@property
|
|
def supported_mime_types(self) -> set[str]:
|
|
return self._supported_types
|
|
|
|
async def process(
|
|
self,
|
|
content: bytes,
|
|
content_type: str,
|
|
filename: Optional[str] = None,
|
|
options: Optional[dict[str, Any]] = None,
|
|
progress_callback: Optional[
|
|
Callable[[float, Optional[float], Optional[str]], Awaitable[None]]
|
|
] = None,
|
|
) -> ProcessingResult:
|
|
"""Process via custom HTTP API.
|
|
|
|
Args:
|
|
content: Document bytes
|
|
content_type: MIME type
|
|
filename: Optional filename
|
|
options: Custom options (passed as form data to API)
|
|
|
|
Returns:
|
|
ProcessingResult with extracted text and metadata
|
|
|
|
Raises:
|
|
ProcessorError: If API call fails
|
|
"""
|
|
options = options or {}
|
|
|
|
# Prepare request
|
|
files = {"file": (filename or "document", content, content_type)}
|
|
headers = {}
|
|
|
|
if self.api_key:
|
|
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
|
|
try:
|
|
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
response = await client.post(
|
|
self.api_url,
|
|
files=files,
|
|
headers=headers,
|
|
data=options, # Pass options as form data
|
|
)
|
|
response.raise_for_status()
|
|
|
|
# Parse response
|
|
result = response.json()
|
|
text = result.get("text", "")
|
|
metadata = result.get("metadata", {})
|
|
|
|
logger.debug(
|
|
"Custom processor '%s' extracted %s characters",
|
|
self.name,
|
|
len(text),
|
|
)
|
|
|
|
return ProcessingResult(
|
|
text=text,
|
|
metadata=metadata,
|
|
processor=self.name,
|
|
success=True,
|
|
)
|
|
|
|
except httpx.HTTPError as e:
|
|
logger.error("Custom processor '%s' HTTP error: %s", self.name, e)
|
|
raise ProcessorError(f"API call failed: {str(e)}") from e
|
|
except Exception as e:
|
|
logger.error("Custom processor '%s' failed: %s", self.name, e)
|
|
raise ProcessorError(f"Processing failed: {str(e)}") from e
|
|
|
|
async def health_check(self) -> bool:
|
|
"""Check if custom API is available.
|
|
|
|
Returns:
|
|
True if API responds with status < 500
|
|
"""
|
|
try:
|
|
async with httpx.AsyncClient(timeout=5) as client:
|
|
# Try GET request to check availability
|
|
response = await client.get(
|
|
self.api_url,
|
|
headers={"User-Agent": "nextcloud-mcp-server"},
|
|
)
|
|
return response.status_code < 500
|
|
except Exception as e:
|
|
logger.warning(
|
|
"Custom processor '%s' health check failed: %s", self.name, e
|
|
)
|
|
return False
|