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>
360 lines
14 KiB
Markdown
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
|