Files
office_translator/_bmad-output/implementation-artifacts/2-1-abstraction-provider-base-registry.md
Sepehr Ramezani 26bd096a06 feat: production deployment - full update with providers, admin, glossaries, pricing, tests
Major changes across backend, frontend, infrastructure:
- Provider system with model selection (Google, DeepL, OpenAI, Ollama, Google Cloud)
- Admin panel: user management, pricing, settings
- Glossary system with CSV import/export
- Subscription and tier quota management
- Security hardening (rate limiting, API key auth, path traversal fixes)
- Docker compose for dev, prod, and IONOS deployment
- Alembic migrations for new tables
- Frontend: dashboard, pricing page, landing page, i18n (en/fr)
- Test suite and verification scripts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-25 15:01:47 +02:00

360 lines
14 KiB
Markdown

# Story 2.1: Abstraction Provider (Base + Registry)
Status: done
## Story
As a **Developer**,
I want **to create an abstract TranslationProvider base class with a ProviderRegistry**,
so that **multiple translation providers can be plugged in with fallback support and clean architecture compliance**.
## Acceptance Criteria
1. **AC1: Abstract Base Class** - `TranslationProvider` defines abstract methods: `translate_text()`, `get_name()`, `is_available()`
2. **AC2: ProviderRegistry** - Registry can register, retrieve, and list providers by name
3. **AC3: Environment Configuration** - Providers configured via environment variables (API keys, URLs)
4. **AC4: Health Check** - Each provider has `is_available()` method returning `True`/`False`
5. **AC5: Unit Tests** - Tests verify provider interface and registry functionality
## Tasks / Subtasks
- [x] **Task 1: Create Provider Base Class** (AC: 1, 4)
- [x] 1.1 Create `services/providers/__init__.py`
- [x] 1.2 Create `services/providers/base.py` with abstract `TranslationProvider`
- [x] 1.3 Define abstract methods: `translate_text()`, `get_name()`, `is_available()`
- [x] 1.4 Add optional methods: `translate_batch()`, `health_check()`
- [x] 1.5 Add Pydantic models for request/response in `services/providers/schemas.py`
- [x] **Task 2: Create Provider Registry** (AC: 2)
- [x] 2.1 Create `services/providers/registry.py` with `ProviderRegistry` class
- [x] 2.2 Implement `register(name, provider_class)` method
- [x] 2.3 Implement `get(name) -> TranslationProvider` method
- [x] 2.4 Implement `list_available() -> List[str]` method
- [x] 2.5 Implement `get_first_available(names: List[str]) -> TranslationProvider` for fallback
- [x] 2.6 Add singleton pattern for global registry
- [x] **Task 3: Migrate Existing Google Provider** (AC: 1, 3, 4)
- [x] 3.1 Create `services/providers/google_provider.py`
- [x] 3.2 Extend `TranslationProvider` base class
- [x] 3.3 Implement `is_available()` using a simple ping/test
- [x] 3.4 Migrate caching logic from existing `GoogleTranslationProvider`
- [x] 3.5 Register in registry on import
- [x] **Task 4: Environment Configuration** (AC: 3)
- [x] 4.1 Add provider settings to `config.py` (or create `services/providers/config.py`)
- [x] 4.2 Load API keys from environment: `GOOGLE_API_KEY`, `DEEPL_API_KEY`, `OPENAI_API_KEY`, `OLLAMA_BASE_URL`
- [x] 4.3 Add provider enable/disable flags
- [x] 4.4 Update `.env.example` with new variables
- [x] **Task 5: Unit Tests** (AC: 5)
- [x] 5.1 Create `tests/test_providers/test_base.py`
- [x] 5.2 Create `tests/test_providers/test_registry.py`
- [x] 5.3 Create `tests/test_providers/test_google_provider.py`
- [x] 5.4 Test abstract class cannot be instantiated directly
- [x] 5.5 Test registry registration and retrieval
- [x] 5.6 Test fallback chain logic
## Dev Notes
### 🚨 BROWNFIELD CONTEXT - CRITICAL
This is a **refactoring story**. The project already has provider implementations in `services/translation_service.py`:
- `TranslationProvider` (ABC) - lines 116-157
- `GoogleTranslationProvider` - lines 159-282
- `DeepLTranslationProvider` - lines 285-340
- `OllamaTranslationProvider` - lines 397-517
- `OpenAITranslationProvider` - lines 667-767
- `OpenRouterTranslationProvider` - lines 520-654
**Strategy:** Extract and reorganize into clean module structure, keeping backward compatibility.
### Target Module Structure (from Architecture)
```
services/
├── providers/
│ ├── __init__.py # Exports + registry setup
│ ├── base.py # Abstract TranslationProvider
│ ├── registry.py # ProviderRegistry singleton
│ ├── schemas.py # Pydantic request/response models
│ ├── config.py # Provider settings from env
│ └── google_provider.py # Google implementation (migrated)
├── translation_service.py # Keep for now, refactor later
└── ...
```
### Existing Code to Preserve
**From `services/translation_service.py`:**
| Component | Lines | Action |
|-----------|-------|--------|
| `TranslationCache` | 53-113 | Keep in `services/translation_service.py` (used by all providers) |
| `retry_with_backoff` | 27-50 | Keep as utility |
| `TranslationProvider` (ABC) | 116-157 | Refactor to `providers/base.py` |
| `GoogleTranslationProvider` | 159-282 | Migrate to `providers/google_provider.py` |
| Other providers | Various | Migrate in subsequent stories (2.2-2.5) |
### Abstract Base Class Design
```python
# services/providers/base.py
from abc import ABC, abstractmethod
from typing import Optional, List
from pydantic import BaseModel
class TranslationRequest(BaseModel):
text: str
target_language: str
source_language: str = "auto"
class TranslationResponse(BaseModel):
translated_text: str
provider_name: str
from_cache: bool = False
class TranslationProvider(ABC):
"""Abstract base class for translation providers."""
@abstractmethod
def translate_text(self, request: TranslationRequest) -> TranslationResponse:
"""Translate a single text string."""
pass
@abstractmethod
def get_name(self) -> str:
"""Return provider name for logging and registry."""
pass
@abstractmethod
def is_available(self) -> bool:
"""Check if provider is configured and reachable."""
pass
def translate_batch(self, requests: List[TranslationRequest]) -> List[TranslationResponse]:
"""Default batch implementation using individual calls."""
return [self.translate_text(req) for req in requests]
def health_check(self) -> dict:
"""Return health status details."""
return {
"name": self.get_name(),
"available": self.is_available()
}
```
### Provider Registry Design
```python
# services/providers/registry.py
from typing import Dict, List, Optional, Type
from .base import TranslationProvider
class ProviderRegistry:
"""Singleton registry for translation providers."""
_instance: Optional['ProviderRegistry'] = None
def __new__(cls) -> 'ProviderRegistry':
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._providers: Dict[str, TranslationProvider] = {}
return cls._instance
def register(self, name: str, provider: TranslationProvider) -> None:
self._providers[name] = provider
def get(self, name: str) -> Optional[TranslationProvider]:
return self._providers.get(name)
def list_available(self) -> List[str]:
return [name for name, p in self._providers.items() if p.is_available()]
def get_first_available(self, names: List[str]) -> Optional[TranslationProvider]:
"""Get first available provider from a list of names (fallback chain)."""
for name in names:
provider = self.get(name)
if provider and provider.is_available():
return provider
return None
# Global registry instance
registry = ProviderRegistry()
```
### Environment Variables
```bash
# .env.example additions
GOOGLE_TRANSLATE_ENABLED=true
DEEPL_ENABLED=false
DEEPL_API_KEY=
OPENAI_ENABLED=false
OPENAI_API_KEY=
OLLAMA_ENABLED=false
OLLAMA_BASE_URL=http://localhost:11434
OPENROUTER_ENABLED=false
OPENROUTER_API_KEY=
# Provider fallback chain (comma-separated)
PROVIDER_FALLBACK_CHAIN=google,deepl,ollama,openrouter
```
### Backward Compatibility
During transition, keep existing `TranslationService` in `services/translation_service.py` working:
```python
# services/translation_service.py (modified)
from services.providers.registry import registry
from services.providers.google_provider import GoogleTranslationProvider
# For backward compatibility, existing code continues to work
# New code uses registry.get("google").translate_text(...)
```
### Files to Create
| File | Action | Description |
|------|--------|-------------|
| `services/providers/__init__.py` | CREATE | Package init, exports |
| `services/providers/base.py` | CREATE | Abstract TranslationProvider |
| `services/providers/registry.py` | CREATE | ProviderRegistry singleton |
| `services/providers/schemas.py` | CREATE | Pydantic models |
| `services/providers/config.py` | CREATE | Provider settings from env |
| `services/providers/google_provider.py` | CREATE | Google provider (migrated) |
| `tests/test_providers/__init__.py` | CREATE | Test package |
| `tests/test_providers/test_base.py` | CREATE | Base class tests |
| `tests/test_providers/test_registry.py` | CREATE | Registry tests |
| `tests/test_providers/test_google_provider.py` | CREATE | Google provider tests |
### Files NOT to Modify (Yet)
- `services/translation_service.py` - Keep existing classes, we'll deprecate later
- `translators/*.py` - File processors, different concern
- `routes/*.py` - Will be updated in subsequent stories
- `main.py` - No changes in this story
### Testing Commands
```bash
# Run tests
pytest tests/test_providers/ -v
# Test specific file
pytest tests/test_providers/test_registry.py -v
# With coverage
pytest tests/test_providers/ --cov=services/providers
```
### Project Structure Notes
- Python files: `snake_case` (e.g., `google_provider.py`)
- Classes: `PascalCase` (e.g., `TranslationProvider`, `ProviderRegistry`)
- Variables/functions: `snake_case`
- Follow existing patterns in `services/` folder
### Architecture Compliance
Per `_bmad-output/planning-artifacts/architecture.md`:
- [x] Uses Clean Architecture pattern (providers as pluggable adapters)
- [x] Follows naming conventions (snake_case files, PascalCase classes)
- [x] Configuration via environment variables
- [x] Async support preparation (future story will add async)
### References
- [Source: _bmad-output/planning-artifacts/architecture.md#Data Architecture]
- [Source: _bmad-output/planning-artifacts/architecture.md#Implementation Patterns]
- [Source: _bmad-output/planning-artifacts/epics.md#Story 2.1]
- [Source: _bmad-output/planning-artifacts/prd.md#FR6-FR7 Translation Providers]
- [Source: _bmad-output/planning-artifacts/prd.md#NFR12-NFR13 Provider Fallback]
### Previous Story Learnings (Epic 1)
From Story 1.1:
- Use `pytest` with `pytest-asyncio` for async tests
- Create `conftest.py` with shared fixtures
- Add `__init__.py` to test directories
- Keep backward compatibility during refactoring
## Dev Agent Record
### Agent Model Used
claude-3-5-sonnet (claude-sonnet-4-20250514)
### Debug Log References
None - implementation completed without issues.
### Completion Notes List
- **Task 1 Complete**: Created `services/providers/` module with abstract base class, schemas, and Pydantic models for requests/responses
- **Task 2 Complete**: Implemented thread-safe singleton `ProviderRegistry` with register, get, list_available, and get_first_available methods
- **Task 3 Complete**: Migrated Google provider to new architecture, preserving caching support via existing `_translation_cache`
- **Task 4 Complete**: Created `services/providers/config.py` with environment-based configuration for all providers; updated `.env.example`
- **Task 5 Complete**: 45 unit tests covering base class, registry, and Google provider functionality
- **All 141 tests pass** including new provider tests and existing regression tests
- **Backward compatibility maintained**: Existing `services/translation_service.py` unchanged
### File List
**New Files Created:**
- `services/providers/__init__.py`
- `services/providers/base.py`
- `services/providers/registry.py`
- `services/providers/schemas.py`
- `services/providers/config.py`
- `services/providers/google_provider.py`
- `tests/test_providers/__init__.py`
- `tests/test_providers/test_base.py`
- `tests/test_providers/test_registry.py`
- `tests/test_providers/test_google_provider.py`
**Modified Files:**
- `.env.example` - Added provider enable flags and fallback chain configuration
## Change Log
- 2026-02-20: Story 2.1 completed - Provider abstraction layer with registry, base class, schemas, config, and Google provider migration. All 45 new tests pass. Total test suite: 141 tests passing.
- 2026-02-20: **Code Review (AI)** - Fixed 3 HIGH + 4 MEDIUM issues:
- [FIXED] Auto-registration of Google provider in `__init__.py`
- [FIXED] Added `error` field to `TranslationResponse` for silent failure detection
- [FIXED] Added `success` property to `TranslationResponse`
- [FIXED] Language code validation in `TranslationRequest` (ISO 639-1 format)
- [FIXED] Improved dotenv loading in `config.py`
- [ADDED] 3 new tests for error handling and validation
- Total tests: 47 passing
## Senior Developer Review (AI)
**Review Date:** 2026-02-20
**Reviewer:** AI Code Review (adversarial mode)
**Outcome:** ✅ APPROVED (after fixes)
### Issues Found and Resolved
| Severity | Issue | File:Line | Status |
|----------|-------|-----------|--------|
| HIGH | Google provider not auto-registered | `__init__.py:42-50` | ✅ Fixed |
| HIGH | Silent failure returns untranslated text | `google_provider.py:135-141` | ✅ Fixed |
| HIGH | No error indication on translation failure | `schemas.py:21-36` | ✅ Fixed |
| MEDIUM | No language code validation | `schemas.py:9-22` | ✅ Fixed |
| MEDIUM | load_dotenv side effect at module level | `config.py:12-19` | ✅ Fixed |
| MEDIUM | Circular import risk (deferred) | `google_provider.py:51` | ⚠️ Acceptable |
| MEDIUM | No integration tests | `tests/` | ⚠️ Deferred |
| LOW | Class-level lock pattern | `registry.py:24-25` | ⚠️ Acceptable |
| LOW | Two provider instance patterns | `google_provider.py:178-186` | ⚠️ Acceptable |
### Git vs Story Discrepancy Note
9 modified files not in File List (from Story 1.7 in-progress):
- `alembic/env.py`, `database/`, `main.py`, `models/`, `requirements.txt`, `routes/`, `services/auth_service.py`
### Test Coverage
- 47 unit tests for providers (all passing)
- Coverage: base class, registry, Google provider, schemas, error handling