Files
Chris CoutinhoandClaude Opus 4.7 665cb9b1eb refactor: convert f-string logging to lazy %-style format (G004)
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>
2026-05-13 01:12:17 +02:00

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