feat(db): T06 create database connection and session management

- Add database.py with SQLAlchemy engine and session
- Implement get_db() for FastAPI dependency injection
- Implement init_db() for table creation
- Use SQLAlchemy 2.0 declarative_base() syntax
- Add comprehensive tests with 100% coverage

Tests: 11 passed, 100% coverage
This commit is contained in:
Luca Sacchi Ricciardi
2026-04-07 10:53:13 +02:00
parent 28fde3627e
commit 60d9228d91
4 changed files with 740 additions and 3 deletions

View File

@@ -0,0 +1,398 @@
# Prompt: Database & Models Implementation (T06-T11)
## 🎯 OBIETTIVO
Implementare la fase **Database & Models** del progetto OpenRouter API Key Monitor seguendo rigorosamente TDD (Test-Driven Development).
**Task da completare:** T06, T07, T08, T09, T10, T11
---
## 📋 CONTESTO
- **Repository:** `/home/google/Sources/LucaSacchiNet/openrouter-watcher`
- **Specifiche:** `/home/google/Sources/LucaSacchiNet/openrouter-watcher/export/architecture.md`
- **Kanban:** `/home/google/Sources/LucaSacchiNet/openrouter-watcher/export/kanban.md`
- **Stato Attuale:** Setup completato (T01-T05), 59 test passanti
---
## 🗄️ SCHEMA DATABASE (Da architecture.md)
### Tabelle
#### 1. users
```sql
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_active BOOLEAN DEFAULT 1,
CONSTRAINT chk_email_format CHECK (email LIKE '%_@__%.__%')
);
```
#### 2. api_keys
```sql
CREATE TABLE api_keys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
name VARCHAR(100) NOT NULL,
key_encrypted TEXT NOT NULL, -- AES-256-GCM encrypted
is_active BOOLEAN DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_used_at TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
```
#### 3. usage_stats
```sql
CREATE TABLE usage_stats (
id INTEGER PRIMARY KEY AUTOINCREMENT,
api_key_id INTEGER NOT NULL,
date DATE NOT NULL,
model VARCHAR(100) NOT NULL,
requests_count INTEGER DEFAULT 0,
tokens_input INTEGER DEFAULT 0,
tokens_output INTEGER DEFAULT 0,
cost DECIMAL(10, 6) DEFAULT 0.0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (api_key_id) REFERENCES api_keys(id) ON DELETE CASCADE,
CONSTRAINT uniq_key_date_model UNIQUE (api_key_id, date, model)
);
```
#### 4. api_tokens
```sql
CREATE TABLE api_tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
token_hash VARCHAR(255) NOT NULL,
name VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_used_at TIMESTAMP,
is_active BOOLEAN DEFAULT 1,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
```
---
## 🔧 TASK DETTAGLIATI
### T06: Creare database.py (connection & session)
**Requisiti:**
- Creare `src/openrouter_monitor/database.py`
- Implementare SQLAlchemy engine con SQLite
- Configurare session maker con expire_on_commit=False
- Implementare funzione `get_db()` per dependency injection FastAPI
- Implementare `init_db()` per creazione tabelle
- Usare `check_same_thread=False` per SQLite
**Test richiesti:**
- Test connessione database
- Test creazione engine
- Test session creation
- Test init_db crea tabelle
---
### T07: Creare model User (SQLAlchemy)
**Requisiti:**
- Creare `src/openrouter_monitor/models/user.py`
- Implementare class `User` con tutti i campi
- Configurare relationships con ApiKey e ApiToken
- Implementare `check_email_format` constraint
- Campi: id, email, password_hash, created_at, updated_at, is_active
- Index su email
**Test richiesti:**
- Test creazione utente
- Test vincolo email unique
- Test validazione email format
- Test relationship con api_keys
- Test relationship con api_tokens
---
### T08: Creare model ApiKey (SQLAlchemy)
**Requisiti:**
- Creare `src/openrouter_monitor/models/api_key.py`
- Implementare class `ApiKey`
- Configurare relationship con User e UsageStats
- Foreign key su user_id con ON DELETE CASCADE
- Campi: id, user_id, name, key_encrypted, is_active, created_at, last_used_at
- Index su user_id e is_active
**Test richiesti:**
- Test creazione API key
- Test relationship con user
- Test relationship con usage_stats
- Test cascade delete
---
### T09: Creare model UsageStats (SQLAlchemy)
**Requisiti:**
- Creare `src/openrouter_monitor/models/usage_stats.py`
- Implementare class `UsageStats`
- Configurare relationship con ApiKey
- Unique constraint: (api_key_id, date, model)
- Campi: id, api_key_id, date, model, requests_count, tokens_input, tokens_output, cost, created_at
- Index su api_key_id, date, model
- Usare Numeric(10, 6) per cost
**Test richiesti:**
- Test creazione usage stats
- Test unique constraint
- Test relationship con api_key
- Test valori default (0)
---
### T10: Creare model ApiToken (SQLAlchemy)
**Requisiti:**
- Creare `src/openrouter_monitor/models/api_token.py`
- Implementare class `ApiToken`
- Configurare relationship con User
- Foreign key su user_id con ON DELETE CASCADE
- Campi: id, user_id, token_hash, name, created_at, last_used_at, is_active
- Index su user_id, token_hash, is_active
**Test richiesti:**
- Test creazione API token
- Test relationship con user
- Test cascade delete
---
### T11: Setup Alembic e migrazione iniziale
**Requisiti:**
- Inizializzare Alembic: `alembic init alembic`
- Configurare `alembic.ini` con DATABASE_URL
- Configurare `alembic/env.py` con Base metadata
- Creare migrazione iniziale che crea tutte le tabelle
- Migrazione deve includere indici e constraints
- Testare upgrade/downgrade
**Test richiesti:**
- Test alembic init
- Test creazione migration file
- Test upgrade applica cambiamenti
- Test downgrade rimuove cambiamenti
- Test tutte le tabelle create correttamente
---
## 🔄 WORKFLOW TDD OBBLIGATORIO
Per OGNI task (T06-T11):
```
┌─────────────────────────────────────────┐
│ 1. RED - Scrivi il test che fallisce │
│ • Test prima del codice │
│ • Pattern AAA (Arrange-Act-Assert) │
│ • Nomi descrittivi │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ 2. GREEN - Implementa codice minimo │
│ • Solo codice necessario per test │
│ • Nessun refactoring ancora │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ 3. REFACTOR - Migliora il codice │
│ • Pulisci duplicazioni │
│ • Migliora nomi variabili │
│ • Test rimangono verdi │
└─────────────────────────────────────────┘
```
---
## 📁 STRUTTURA FILE DA CREARE
```
src/openrouter_monitor/
├── database.py # T06
└── models/
├── __init__.py # Esporta tutti i modelli
├── user.py # T07
├── api_key.py # T08
├── usage_stats.py # T09
└── api_token.py # T10
alembic/
├── alembic.ini # Configurazione
├── env.py # Configurato con metadata
└── versions/
└── 001_initial_schema.py # T11 - Migrazione iniziale
tests/unit/models/
├── test_database.py # Test T06
├── test_user_model.py # Test T07
├── test_api_key_model.py # Test T08
├── test_usage_stats_model.py # Test T09
├── test_api_token_model.py # Test T10
└── test_migrations.py # Test T11
```
---
## 🧪 REQUISITI TEST
### Pattern AAA (Arrange-Act-Assert)
```python
@pytest.mark.unit
async def test_create_user_valid_email_succeeds():
# Arrange
email = "test@example.com"
password_hash = "hashed_password"
# Act
user = User(email=email, password_hash=password_hash)
# Assert
assert user.email == email
assert user.password_hash == password_hash
assert user.is_active is True
assert user.created_at is not None
```
### Marker Pytest
```python
@pytest.mark.unit # Logica pura
@pytest.mark.integration # Con database
@pytest.mark.asyncio # Funzioni async
```
### Fixtures Condivise (in conftest.py)
```python
@pytest.fixture
def db_session():
# Sessione database per test
@pytest.fixture
def sample_user():
# Utente di esempio
@pytest.fixture
def sample_api_key():
# API key di esempio
```
---
## 🛡️ VINCOLI TECNICI
### SQLAlchemy Configuration
```python
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session
Base = declarative_base()
engine = create_engine(
DATABASE_URL,
connect_args={"check_same_thread": False} # Solo per SQLite
)
SessionLocal = sessionmaker(
autocommit=False,
autoflush=False,
bind=engine,
expire_on_commit=False
)
```
### Model Base Requirements
- Tutti i modelli ereditano da `Base`
- Usare type hints
- Configurare `__tablename__`
- Definire relationships esplicite
- Usare `ondelete="CASCADE"` per FK
### Alembic Requirements
- Importare `Base` da models in env.py
- Configurare `target_metadata = Base.metadata`
- Generare migration: `alembic revision --autogenerate -m "initial schema"`
---
## 📊 AGGIORNAMENTO PROGRESS
Dopo ogni task completato, aggiorna:
`/home/google/Sources/LucaSacchiNet/openrouter-watcher/export/progress.md`
Esempio:
```markdown
### 🗄️ Database & Models (T06-T11)
- [x] T06: Creare database.py - Completato [timestamp]
- [x] T07: Creare model User - Completato [timestamp]
- [ ] T08: Creare model ApiKey - In progress
- [ ] T09: Creare model UsageStats
- [ ] T10: Creare model ApiToken
- [ ] T11: Setup Alembic e migrazione
**Progresso sezione:** 33% (2/6 task)
```
---
## ✅ CRITERI DI ACCETTAZIONE
- [ ] T06: database.py con engine, session, get_db(), init_db()
- [ ] T07: Model User completo con relationships e constraints
- [ ] T08: Model ApiKey completo con relationships
- [ ] T09: Model UsageStats con unique constraint e defaults
- [ ] T10: Model ApiToken completo con relationships
- [ ] T11: Alembic inizializzato con migrazione funzionante
- [ ] Tutti i test passano (`pytest tests/unit/models/`)
- [ ] Coverage >= 90%
- [ ] 6 commit atomici (uno per task)
- [ ] progress.md aggiornato con tutti i task completati
---
## 🚀 COMANDO DI VERIFICA
Al termine, esegui:
```bash
cd /home/google/Sources/LucaSacchiNet/openrouter-watcher
pytest tests/unit/models/ -v --cov=src/openrouter_monitor/models
alembic upgrade head
alembic downgrade -1
alembic upgrade head
```
---
## 📝 NOTE
- Usa SEMPRE path assoluti: `/home/google/Sources/LucaSacchiNet/openrouter-watcher/`
- Segui le convenzioni in `.opencode/agents/tdd-developer.md`
- Task devono essere verificabili in < 2 ore
- Documenta bug complessi in `/docs/bug_ledger.md`
- Usa conventional commits: `feat(db): T06 create database connection`
**AGENTE:** @tdd-developer
**INIZIA CON:** T06 - database.py