342 lines
12 KiB
Markdown
342 lines
12 KiB
Markdown
# Story 2.1: Implémenter le scraper Twitter avec rate limiting
|
|
|
|
Status: review
|
|
|
|
## Story
|
|
|
|
As a développeur,
|
|
I want implémenter un scraper Twitter avec gestion des rate limits,
|
|
So que je peux collecter des tweets sur les matchs de football sans dépasser les limites API.
|
|
|
|
## Acceptance Criteria
|
|
|
|
**Given** une clé API Twitter est configurée
|
|
**When** le scraper Twitter est exécuté
|
|
**Then** il collecte des tweets pour un match donné avec mots-clés pertinents
|
|
**And** il respecte la limite de 1000 requêtes/heure
|
|
**And** il gère les erreurs de rate limit avec retry avec backoff exponentiel
|
|
**And** les tweets collectés sont stockés avec timestamp, texte, engagement (retweets, likes)
|
|
|
|
**Given** le scraper détecte un rate limit
|
|
**When** la limite est atteinte (>90% utilisation)
|
|
**Then** une alerte est loggée
|
|
**And** le scraper passe en mode priorisation (matchs VIP uniquement)
|
|
**And** les données collectées sont sauvegardées avant arrêt
|
|
|
|
## Tasks / Subtasks
|
|
|
|
- [x] Installer les dépendances Twitter API (AC: #1)
|
|
- [x] Installer `tweepy` ou `tweepy-async` pour Twitter API
|
|
- [x] Configurer les credentials Twitter API
|
|
- [x] Créer le module scraper dans `backend/app/scrapers/`
|
|
- [x] Configurer la connexion Twitter API
|
|
- [x] Vérifier l'authentification Twitter
|
|
|
|
- [x] Implémenter le collecteur de tweets (AC: #1)
|
|
- [x] Créer la fonction de recherche de tweets par mots-clés
|
|
- [x] Extraire les données: texte, timestamp, retweets, likes
|
|
- [x] Implémenter le parsing des données Twitter
|
|
- [x] Stocker les tweets dans la base de données
|
|
- [x] Gérer les erreurs de connexion/timeout
|
|
|
|
- [x] Implémenter le rate limiting (AC: #1, #2)
|
|
- [x] Configurer le rate limiter pour 1000 req/heure
|
|
- [x] Implémenter le suivi de l'utilisation API
|
|
- [x] Implémenter l'alerte quand utilisation > 90%
|
|
- [x] Implémenter le retry avec backoff exponentiel
|
|
- [x] Configurer le mode priorisation (matchs VIP)
|
|
|
|
- [x] Implémenter le mode dégradé (AC: #2)
|
|
- [x] Définir les matchs VIP dans la configuration
|
|
- [x] Implémenter la priorisation dynamique des matchs
|
|
- [x] Sauvegarder les données collectées avant arrêt
|
|
- [x] Logger l'alerte de mode dégradé
|
|
- [x] Vérifier que le scraper continue avec autres sources
|
|
|
|
- [x] Créer les schémas de base de données pour tweets (AC: #1)
|
|
- [x] Créer la table `tweets` dans SQLite
|
|
- [x] Définir les colonnes: id, text, created_at, retweet_count, like_count
|
|
- [x] Ajouter les colonnes pour match_id et source
|
|
- [x] Créer les indexes appropriés
|
|
- [x] Générer et appliquer les migrations
|
|
|
|
- [x] Tester le scraper Twitter (AC: #1, #2)
|
|
- [x] Tester la collecte de tweets pour un match
|
|
- [x] Vérifier que le rate limiting fonctionne
|
|
- [x] Tester le mode priorisation VIP
|
|
- [x] Vérifier que les données sont stockées correctement
|
|
- [x] Tester le retry avec backoff exponentiel
|
|
|
|
## Dev Notes
|
|
|
|
### Architecture Patterns et Contraintes
|
|
|
|
**Stack Technique Imposé:**
|
|
- **Twitter API:** Tweepy ou tweepy-async
|
|
- **Rate Limiting:** 1000 requêtes/heure (gratuit)
|
|
- **Database:** SQLite avec Drizzle (Next.js) et SQLAlchemy (FastAPI)
|
|
- **Queue:** RabbitMQ pour traitement asynchrone (Phase 2+)
|
|
- **Logging:** Structuré avec alertes pour rate limits
|
|
|
|
**Configuration Requise:**
|
|
- Credentials Twitter API: Bearer token ou OAuth 1.0a
|
|
- Rate limiter: 1000 req/heure avec alerte à 90%
|
|
- Mode dégradé: Priorisation des matchs VIP
|
|
- Stockage: Table `tweets` dans SQLite
|
|
|
|
**Intégration avec Architecture Globale:**
|
|
- Part de la pondération: Twitter 60% (Reddit 25%, RSS 15%)
|
|
- Queue RabbitMQ pour découplage scraping et analyse
|
|
- Même structure de données que Reddit et RSS
|
|
- Format cohérent pour fusion multi-sources
|
|
|
|
**Conventions de Nommage:**
|
|
- Table: `tweets` (snake_case pluriel)
|
|
- Colonnes: `tweet_id`, `text`, `created_at`, `match_id` (snake_case)
|
|
- Functions: `scrape_twitter_match()`, `handle_rate_limit()` (snake_case)
|
|
- Variables: `max_tweets_per_hour`, `is_vip_match` (snake_case/UPPER_SNAKE_CASE)
|
|
|
|
### Source Tree Components à Toucher
|
|
|
|
**Fichiers à créer:**
|
|
1. `backend/app/scrapers/` (répertoire scrapers)
|
|
2. `backend/app/scrapers/__init__.py`
|
|
3. `backend/app/scrapers/twitter_scraper.py` (module Twitter)
|
|
4. `backend/app/models/tweet.py` (modèle SQLAlchemy pour tweets)
|
|
5. `backend/app/schemas/tweet.py` (schéma Pydantic pour tweets)
|
|
|
|
**Fichiers à modifier:**
|
|
1. `backend/requirements.txt` (ajouter tweepy)
|
|
2. `src/db/schema.ts` (ajouter schéma Drizzle pour tweets - Next.js)
|
|
|
|
### Project Structure Notes
|
|
|
|
**Alignment with unified project structure:**
|
|
- ✅ Twitter scraper dans `backend/app/scrapers/` comme spécifié
|
|
- ✅ Rate limiting à 1000 req/heure comme spécifié
|
|
- ✅ Pondération Twitter 60% comme spécifié dans epics
|
|
- ✅ Mode dégradé avec priorisation VIP comme spécifié
|
|
|
|
**Conventions de code à respecter:**
|
|
- Gestion asynchrone avec async/await (si tweepy-async)
|
|
- Logging structuré pour monitoring
|
|
- Retry avec backoff exponentiel
|
|
- Mode dégradé avec priorisation
|
|
|
|
**Intégration avec architecture existante:**
|
|
- SQLite partagé avec Next.js et FastAPI
|
|
- Même convention de nommage `snake_case`
|
|
- Préparation pour intégration RabbitMQ (Phase 2+)
|
|
|
|
### Technical Requirements
|
|
|
|
**Configuration Twitter API:**
|
|
```python
|
|
import tweepy
|
|
|
|
# Configuration Twitter API
|
|
auth = tweepy.BearerToken("YOUR_BEARER_TOKEN")
|
|
client = tweepy.Client(bearer_token=auth)
|
|
|
|
# Rate limiting configuration
|
|
MAX_TWEETS_PER_HOUR = 1000
|
|
RATE_LIMIT_ALERT_THRESHOLD = 0.9 # 90%
|
|
VIP_MATCH_IDS = [1, 2, 3] # IDs des matchs VIP
|
|
```
|
|
|
|
**Scraper Twitter:**
|
|
```python
|
|
async def scrape_twitter_match(match_id: str, keywords: List[str]):
|
|
# Collecte des tweets pour un match
|
|
tweets = client.search_recent_tweets(
|
|
query=f"{' OR '.join(keywords)}",
|
|
max_results=100,
|
|
tweet_fields=['created_at', 'public_metrics', 'text']
|
|
)
|
|
|
|
# Stockage dans la base de données
|
|
for tweet in tweets.data:
|
|
save_tweet_to_db(tweet, match_id)
|
|
```
|
|
|
|
**Rate Limiting:**
|
|
```python
|
|
from time import sleep
|
|
from math import exp
|
|
|
|
async def handle_rate_limit(api_calls_remaining: int):
|
|
if api_calls_remaining < (MAX_TWEETS_PER_HOUR * (1 - RATE_LIMIT_ALERT_THRESHOLD)):
|
|
log_alert("Rate limit approaching 90%")
|
|
|
|
if api_calls_remaining == 0:
|
|
# Mode dégradé: priorisation VIP
|
|
enable_vip_mode_only()
|
|
wait_time = 3600 # Attendre 1 heure
|
|
sleep(wait_time)
|
|
```
|
|
|
|
**Schéma Table Tweets:**
|
|
```python
|
|
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
|
|
from sqlalchemy.orm import relationship
|
|
|
|
class Tweet(Base):
|
|
__tablename__ = "tweets"
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
tweet_id = Column(String, unique=True, index=True)
|
|
text = Column(String, nullable=False)
|
|
created_at = Column(DateTime, nullable=False, index=True)
|
|
retweet_count = Column(Integer, default=0)
|
|
like_count = Column(Integer, default=0)
|
|
match_id = Column(Integer, ForeignKey("matches.id"))
|
|
source = Column(String, default="twitter") # twitter, reddit, rss
|
|
```
|
|
|
|
### Architecture Compliance
|
|
|
|
**Conformité avec Architecture Decision Document:**
|
|
|
|
✅ **External API Management:**
|
|
- Rate limiting à 1000 req/heure
|
|
- Alertes prédictives (>90% utilisation)
|
|
- Mode dégradé avec priorisation VIP
|
|
|
|
✅ **Data Architecture:**
|
|
- Table `tweets` dans SQLite
|
|
- Conventions `snake_case`
|
|
- Indexes optimisés
|
|
|
|
✅ **Code Organization:**
|
|
- Module dans `backend/app/scrapers/`
|
|
- Séparation clear des responsabilités
|
|
|
|
### Library/Framework Requirements
|
|
|
|
**Packages:**
|
|
- `tweepy` ou `tweepy-async`
|
|
|
|
### File Structure Requirements
|
|
|
|
```
|
|
backend/
|
|
├── app/
|
|
│ ├── scrapers/
|
|
│ │ ├── __init__.py
|
|
│ │ └── twitter_scraper.py
|
|
│ ├── models/
|
|
│ │ └── tweet.py
|
|
│ └── schemas/
|
|
│ └── tweet.py
|
|
```
|
|
|
|
### Testing Requirements
|
|
|
|
- Tests de collecte de tweets
|
|
- Tests de rate limiting
|
|
- Tests de mode dégradé
|
|
- Tests de stockage en base de données
|
|
|
|
### References
|
|
|
|
- [Source: _bmad-output/planning-artifacts/epics.md#Story-2.1]
|
|
|
|
## Dev Agent Record
|
|
|
|
### Agent Model Used
|
|
|
|
GLM-4.7
|
|
|
|
### Completion Notes List
|
|
|
|
- ✅ **Dependencies Twitter API**: Tweepy 4.14.0 ajouté à requirements.txt
|
|
- ✅ **Twitter Scraper Module**: Module complet avec rate limiting et mode dégradé
|
|
- Classe `TwitterScraper` avec authentification Twitter API
|
|
- Tracking en temps réel des appels API (max 1000/heure)
|
|
- Alertes prédictives à 90% d'utilisation
|
|
- Mode dégradé automatique pour matchs VIP uniquement
|
|
- Retry avec backoff exponentiel
|
|
- Logging structuré pour monitoring
|
|
- ✅ **Database Schema**: Table `tweets` créée dans SQLite
|
|
- Colonnes: id, tweet_id, text, created_at, retweet_count, like_count, match_id, source
|
|
- Indexes: tweet_id (unique), created_at, match_id, composite (match_id, source)
|
|
- Migration Alembic: 20260117_0001_create_tweets_table.py
|
|
- ✅ **Pydantic Schemas**: Schémas de validation pour tweets
|
|
- TweetBase, TweetCreate, TweetResponse
|
|
- TweetListResponse avec pagination
|
|
- TweetStatsResponse pour statistiques
|
|
- ✅ **Drizzle Schema**: Schéma pour Next.js (src/db/schema.ts)
|
|
- Table `tweets` avec conventions TypeScript
|
|
- Mapping snake_case ↔ camelCase
|
|
- ✅ **Tests Unitaires**: Tests complets pour scraper et modèle
|
|
- test_twitter_scraper.py: 10+ tests pour rate limiting, mode VIP, scraping
|
|
- test_tweet_model.py: 8+ tests pour modèle SQLAlchemy
|
|
- ✅ **Documentation**: README complet dans backend/app/scrapers/
|
|
- Guide d'installation et configuration
|
|
- Exemples d'utilisation
|
|
- Documentation API et dépannage
|
|
|
|
### Technical Decisions
|
|
|
|
**Pourquoi Tweepy vs tweepy-async:**
|
|
- Choisie `tweepy` synchrone pour simplicité initiale
|
|
- Peut migrer vers `tweepy-async` dans Phase 2+ si performance nécessaire
|
|
- Rate limiting natif de Tweepy suffisant pour Phase 1
|
|
|
|
**Rate Limiting Strategy:**
|
|
- Tracking côté client (self.api_calls_made)
|
|
- Alertes prédictives à 90% pour éviter blocages
|
|
- Mode dégradé automatique: matchs VIP seulement
|
|
- Backoff exponentiel: minimum 1 minute, maximum 1 heure
|
|
|
|
**Database Schema Design:**
|
|
- Convention `snake_case` cohérente avec SQLAlchemy
|
|
- Indexes optimisés pour requêtes fréquentes:
|
|
- `tweet_id` unique pour éviter doublons
|
|
- `created_at` pour tri temporel
|
|
- `match_id` et `match_id+source` pour filtrage
|
|
- `source` colonne pour multi-source future (Reddit, RSS)
|
|
|
|
**Mode Dégradé Implementation:**
|
|
- VIP match IDs configurables dans scraper
|
|
- Activation automatique quand rate limit atteint
|
|
- Alertes loggées avec niveau WARNING
|
|
- Sauvegarde automatique avant arrêt
|
|
|
|
### Integration Points
|
|
|
|
**Backend Integration:**
|
|
- Module accessible via `app.scrapers.twitter_scraper`
|
|
- Factory function: `create_twitter_scraper()`
|
|
- Compatible avec SQLAlchemy sessions via `save_tweets_to_db()`
|
|
|
|
**Frontend Integration (Next.js):**
|
|
- Schéma Drizzle dans `src/db/schema.ts`
|
|
- Partage de la même base SQLite
|
|
- Conventions TypeScript (camelCase) automatiques
|
|
|
|
**Future Integration (Phase 2+):**
|
|
- RabbitMQ queue pour découplage scraping/analyse
|
|
- Workers asynchrones pour multi-source
|
|
- Dashboard monitoring temps réel
|
|
|
|
### File List
|
|
|
|
**Fichiers créés:**
|
|
- `backend/app/scrapers/__init__.py`
|
|
- `backend/app/scrapers/twitter_scraper.py`
|
|
- `backend/app/scrapers/README.md`
|
|
- `backend/app/models/tweet.py`
|
|
- `backend/app/schemas/tweet.py`
|
|
- `backend/alembic/versions/20260117_0001_create_tweets_table.py`
|
|
- `backend/tests/__init__.py`
|
|
- `backend/tests/test_twitter_scraper.py`
|
|
- `backend/tests/test_tweet_model.py`
|
|
- `backend/tests/run_tests.py`
|
|
|
|
**Fichiers modifiés:**
|
|
- `backend/requirements.txt` (ajouté tweepy==4.14.0)
|
|
- `backend/app/models/__init__.py` (ajouté Tweet import)
|
|
- `backend/app/schemas/__init__.py` (ajouté Tweet schemas)
|
|
- `chartbastan/src/db/schema.ts` (ajouté tweets table)
|