# 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)