All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 3m22s
315 lines
11 KiB
Python
315 lines
11 KiB
Python
"""
|
|
Track C1.1 — Provider interface adapters.
|
|
|
|
Two adapter classes that bridge the legacy `TranslationProvider`
|
|
interface (translate(text, target, source) -> str) and the new
|
|
TranslationProvider interface (translate_text(request) -> Response).
|
|
|
|
Why?
|
|
The codebase has TWO provider interfaces:
|
|
1. New: services/providers/base.py
|
|
- translate_text(TranslationRequest) -> TranslationResponse
|
|
- translate_batch(List[TranslationRequest]) -> List[TranslationResponse]
|
|
2. Legacy: services/translation_service.py
|
|
- translate(text, target, source) -> str
|
|
- translate_batch(texts, target, source) -> List[str]
|
|
|
|
Both are used in production. Translators sometimes receive the new
|
|
interface, sometimes the legacy one. Previously the translators
|
|
duck-typed the provider to figure out which interface it supported
|
|
(see e.g. pptx_translator._translate_with_provider). That was hacky.
|
|
|
|
These adapters give a single, explicit way to bridge the two
|
|
interfaces, so translators can always treat the provider as
|
|
whichever interface they prefer.
|
|
|
|
Usage:
|
|
# Wrap a new-style provider as legacy
|
|
legacy = NewProviderAsLegacyAdapter(new_provider)
|
|
legacy.translate("Hello", "fr", "auto") # returns str
|
|
|
|
# Wrap a legacy-style provider as new
|
|
new = LegacyProviderAsNewAdapter(legacy_provider)
|
|
new.translate_text(TranslationRequest(...)) # returns TranslationResponse
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import List, Optional
|
|
|
|
from core.logging import get_logger
|
|
|
|
from .base import TranslationProvider as NewTranslationProvider
|
|
from .schemas import TranslationRequest, TranslationResponse, ProviderHealthStatus
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
# =============================================================================
|
|
# New-style → Legacy
|
|
# =============================================================================
|
|
|
|
class NewProviderAsLegacyAdapter:
|
|
"""Adapt a new-style provider (translate_text) to the legacy interface.
|
|
|
|
The legacy interface is what most translators and the legacy
|
|
`translation_service` expect:
|
|
translate(text, target_language, source_language="auto") -> str
|
|
translate_batch(texts, target, source) -> List[str]
|
|
get_name() -> str
|
|
is_available() -> bool
|
|
|
|
This adapter is for INTERNAL use — it adapts at the boundary so
|
|
that a single provider instance can be passed to any code path.
|
|
"""
|
|
|
|
def __init__(self, new_provider: NewTranslationProvider):
|
|
self._new = new_provider
|
|
|
|
# ---- Core legacy methods ----
|
|
|
|
def translate(
|
|
self, text: str, target_language: str, source_language: str = "auto"
|
|
) -> str:
|
|
"""Translate a single text — legacy signature."""
|
|
# Lazy import to avoid circular import
|
|
try:
|
|
from .schemas import TranslationRequest
|
|
request = TranslationRequest(
|
|
text=text,
|
|
target_language=target_language,
|
|
source_language=source_language,
|
|
)
|
|
response = self._new.translate_text(request)
|
|
if response.error:
|
|
raise Exception(
|
|
f"[{response.error_code or 'UNKNOWN'}] {response.error}"
|
|
)
|
|
return response.translated_text
|
|
except Exception as e:
|
|
# If the new provider raised, re-raise so the caller
|
|
# can decide how to handle it (legacy code expects exceptions).
|
|
logger.debug(
|
|
"new_provider_as_legacy_translate_error",
|
|
error=str(e)[:200],
|
|
)
|
|
raise
|
|
|
|
def translate_batch(
|
|
self,
|
|
texts: List[str],
|
|
target_language: str,
|
|
source_language: str = "auto",
|
|
) -> List[str]:
|
|
"""Translate multiple texts — legacy signature."""
|
|
if not texts:
|
|
return []
|
|
|
|
try:
|
|
from .schemas import TranslationRequest
|
|
requests = [
|
|
TranslationRequest(
|
|
text=t,
|
|
target_language=target_language,
|
|
source_language=source_language,
|
|
)
|
|
for t in texts
|
|
]
|
|
responses = self._new.translate_batch(requests)
|
|
return [r.translated_text for r in responses]
|
|
except Exception as e:
|
|
logger.debug(
|
|
"new_provider_as_legacy_batch_error",
|
|
error=str(e)[:200],
|
|
count=len(texts),
|
|
)
|
|
raise
|
|
|
|
# ---- Identity methods (pass-through) ----
|
|
|
|
def get_name(self) -> str:
|
|
return self._new.get_name()
|
|
|
|
def is_available(self) -> bool:
|
|
try:
|
|
return bool(self._new.is_available())
|
|
except Exception:
|
|
return False
|
|
|
|
def health_check(self) -> dict:
|
|
"""Best-effort health check, legacy-shaped (plain dict)."""
|
|
try:
|
|
status = self._new.health_check()
|
|
return {
|
|
"name": status.name,
|
|
"available": status.available,
|
|
"latency_ms": status.latency_ms,
|
|
"error": status.error,
|
|
}
|
|
except Exception as e:
|
|
return {
|
|
"name": self.get_name(),
|
|
"available": False,
|
|
"error": str(e)[:200],
|
|
}
|
|
|
|
|
|
# =============================================================================
|
|
# Legacy-style → New
|
|
# =============================================================================
|
|
|
|
class LegacyProviderAsNewAdapter(NewTranslationProvider):
|
|
"""Adapt a legacy-style provider (translate(text, ...)) to the new interface.
|
|
|
|
Most new code expects the new interface:
|
|
translate_text(TranslationRequest) -> TranslationResponse
|
|
translate_batch(List[TranslationRequest]) -> List[TranslationResponse]
|
|
|
|
A "legacy" provider is anything that has:
|
|
translate(text, target, source) -> str
|
|
translate_batch(texts, target, source) -> List[str]
|
|
get_name() -> str
|
|
is_available() -> bool
|
|
"""
|
|
|
|
def __init__(self, legacy_provider, provider_name: str = None):
|
|
self._legacy = legacy_provider
|
|
# If a custom name is provided, use it; otherwise inherit from the
|
|
# legacy provider.
|
|
if provider_name is not None:
|
|
self._name = provider_name
|
|
else:
|
|
try:
|
|
self._name = legacy_provider.get_name()
|
|
except Exception:
|
|
self._name = "legacy"
|
|
|
|
# ---- Implement the abstract methods of NewTranslationProvider ----
|
|
|
|
def translate_text(self, request: TranslationRequest) -> TranslationResponse:
|
|
"""Translate a single text — new interface."""
|
|
start = time.time()
|
|
try:
|
|
translated = self._legacy.translate(
|
|
text=request.text,
|
|
target_language=request.target_language,
|
|
source_language=request.source_language or "auto",
|
|
)
|
|
elapsed_ms = round((time.time() - start) * 1000, 2)
|
|
return TranslationResponse(
|
|
translated_text=translated or "",
|
|
provider_name=self._name,
|
|
source_language=request.source_language or "auto",
|
|
target_language=request.target_language,
|
|
elapsed_ms=elapsed_ms,
|
|
)
|
|
except Exception as e:
|
|
elapsed_ms = round((time.time() - start) * 1000, 2)
|
|
return TranslationResponse(
|
|
translated_text="",
|
|
provider_name=self._name,
|
|
source_language=request.source_language or "auto",
|
|
target_language=request.target_language,
|
|
elapsed_ms=elapsed_ms,
|
|
error=str(e)[:200],
|
|
error_code="LEGACY_PROVIDER_ERROR",
|
|
)
|
|
|
|
def translate_batch(
|
|
self, requests: List[TranslationRequest]
|
|
) -> List[TranslationResponse]:
|
|
"""Translate a batch of texts — new interface."""
|
|
if not requests:
|
|
return []
|
|
|
|
# Group by language pair so we can call the legacy
|
|
# translate_batch efficiently (legacy signature is per-language-pair).
|
|
from collections import defaultdict
|
|
groups: dict = defaultdict(list)
|
|
for i, r in enumerate(requests):
|
|
key = (r.target_language, r.source_language or "auto")
|
|
groups[key].append((i, r))
|
|
|
|
results: List[TranslationResponse] = [None] * len(requests) # type: ignore
|
|
|
|
for (target_lang, source_lang), items in groups.items():
|
|
texts = [r.text for _, r in items]
|
|
try:
|
|
translated = self._legacy.translate_batch(
|
|
texts=texts,
|
|
target_language=target_lang,
|
|
source_language=source_lang,
|
|
)
|
|
except Exception as e:
|
|
# If legacy batch fails, fall back to per-text
|
|
logger.debug(
|
|
"legacy_batch_fallback_to_individual",
|
|
error=str(e)[:200],
|
|
)
|
|
translated = []
|
|
for t in texts:
|
|
try:
|
|
translated.append(
|
|
self._legacy.translate(t, target_lang, source_lang)
|
|
)
|
|
except Exception as individual_err:
|
|
translated.append("") # legacy code expects str
|
|
|
|
for (orig_idx, r), t in zip(items, translated):
|
|
results[orig_idx] = TranslationResponse(
|
|
translated_text=t or "",
|
|
provider_name=self._name,
|
|
source_language=source_lang,
|
|
target_language=target_lang,
|
|
)
|
|
|
|
return results
|
|
|
|
def get_name(self) -> str:
|
|
# Always prefer the explicit name set at construction. Only fall
|
|
# back to the legacy provider's get_name if no name was set.
|
|
if hasattr(self, "_name") and self._name:
|
|
return self._name
|
|
try:
|
|
return self._legacy.get_name()
|
|
except Exception:
|
|
return "legacy"
|
|
|
|
def is_available(self) -> bool:
|
|
try:
|
|
return bool(self._legacy.is_available())
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
# =============================================================================
|
|
# Auto-detect helper
|
|
# =============================================================================
|
|
|
|
import time # noqa: E402
|
|
|
|
|
|
def coerce_to_legacy(provider) -> NewProviderAsLegacyAdapter:
|
|
"""Coerce any provider to the legacy interface.
|
|
|
|
If the provider is already a legacy provider, return it as-is.
|
|
If it's a new-style provider, wrap it in NewProviderAsLegacyAdapter.
|
|
"""
|
|
if isinstance(provider, NewProviderAsLegacyAdapter):
|
|
return provider
|
|
if isinstance(provider, NewTranslationProvider):
|
|
return NewProviderAsLegacyAdapter(provider)
|
|
# Already legacy-shaped — pass through
|
|
return provider
|
|
|
|
|
|
def coerce_to_new(provider, name: str = None) -> NewTranslationProvider:
|
|
"""Coerce any provider to the new interface.
|
|
|
|
If the provider is already a new-style provider, return it as-is.
|
|
If it's a legacy provider, wrap it in LegacyProviderAsNewAdapter.
|
|
"""
|
|
if isinstance(provider, NewTranslationProvider):
|
|
return provider
|
|
return LegacyProviderAsNewAdapter(provider, provider_name=name)
|