From 05a1dafa7d307f3177188e1c8b0ddde64e793ab1 Mon Sep 17 00:00:00 2001 From: Antoine0703 Date: Tue, 9 Dec 2025 14:51:13 +0100 Subject: [PATCH 1/4] feat(server): add complete server for data scrapping --- server/ARCHITECTURE.md | 252 +++++++++++++ server/README.md | 207 +++++++++++ server/__pycache__/config.cpython-312.pyc | Bin 0 -> 3013 bytes server/__pycache__/database.cpython-312.pyc | Bin 0 -> 13800 bytes server/__pycache__/embeddings.cpython-312.pyc | Bin 0 -> 6631 bytes server/__pycache__/main.cpython-312.pyc | Bin 0 -> 13440 bytes server/config.py | 78 ++++ server/database.py | 291 +++++++++++++++ server/embeddings.py | 140 ++++++++ server/examples.py | 152 ++++++++ server/main.py | 332 ++++++++++++++++++ server/requirements.txt | 4 + server/scrapers/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 156 bytes .../__pycache__/arxiv_scraper.cpython-312.pyc | Bin 0 -> 4299 bytes .../scrapers/__pycache__/base.cpython-312.pyc | Bin 0 -> 2876 bytes .../github_scraper.cpython-312.pyc | Bin 0 -> 5670 bytes .../huggingface_scraper.cpython-312.pyc | Bin 0 -> 5470 bytes .../lemonde_scraper.cpython-312.pyc | Bin 0 -> 4869 bytes .../medium_scraper.cpython-312.pyc | Bin 0 -> 4583 bytes server/scrapers/arxiv_scraper.py | 92 +++++ server/scrapers/base.py | 81 +++++ server/scrapers/github_scraper.py | 113 ++++++ server/scrapers/huggingface_scraper.py | 105 ++++++ server/scrapers/lemonde_scraper.py | 108 ++++++ server/scrapers/medium_scraper.py | 101 ++++++ server/test_server.py | 218 ++++++++++++ server/veille_technique.db | Bin 0 -> 196608 bytes 28 files changed, 2274 insertions(+) create mode 100644 server/ARCHITECTURE.md create mode 100644 server/README.md create mode 100644 server/__pycache__/config.cpython-312.pyc create mode 100644 server/__pycache__/database.cpython-312.pyc create mode 100644 server/__pycache__/embeddings.cpython-312.pyc create mode 100644 server/__pycache__/main.cpython-312.pyc create mode 100644 server/config.py create mode 100644 server/database.py create mode 100644 server/embeddings.py create mode 100644 server/examples.py create mode 100644 server/main.py create mode 100644 server/requirements.txt create mode 100644 server/scrapers/__init__.py create mode 100644 server/scrapers/__pycache__/__init__.cpython-312.pyc create mode 100644 server/scrapers/__pycache__/arxiv_scraper.cpython-312.pyc create mode 100644 server/scrapers/__pycache__/base.cpython-312.pyc create mode 100644 server/scrapers/__pycache__/github_scraper.cpython-312.pyc create mode 100644 server/scrapers/__pycache__/huggingface_scraper.cpython-312.pyc create mode 100644 server/scrapers/__pycache__/lemonde_scraper.cpython-312.pyc create mode 100644 server/scrapers/__pycache__/medium_scraper.cpython-312.pyc create mode 100644 server/scrapers/arxiv_scraper.py create mode 100644 server/scrapers/base.py create mode 100644 server/scrapers/github_scraper.py create mode 100644 server/scrapers/huggingface_scraper.py create mode 100644 server/scrapers/lemonde_scraper.py create mode 100644 server/scrapers/medium_scraper.py create mode 100644 server/test_server.py create mode 100644 server/veille_technique.db diff --git a/server/ARCHITECTURE.md b/server/ARCHITECTURE.md new file mode 100644 index 0000000..034c511 --- /dev/null +++ b/server/ARCHITECTURE.md @@ -0,0 +1,252 @@ +# Watch Server - Complete Redesign + +## 📋 Summary of Changes + +The server has been completely redesigned with a modular architecture and two clearly defined operating modes. + +## 🏗️ Architecture + +``` +server/ +├── main.py # Main server with WatchServer (orchestrator) +├── database.py # SQLite database manager +├── embeddings.py # Embeddings manager (vectors) +├── config.py # Centralized configuration +├── examples.py # Usage examples +├── requirements.txt # Python dependencies +├── README.md # Complete documentation +└── scrapers/ + ├── base.py # Abstract BaseScraper interface + ├── arxiv_scraper.py # Scraper for arXiv + ├── github_scraper.py # Scraper for GitHub + ├── medium_scraper.py # Scraper for Medium + ├── lemonde_scraper.py # Scraper for Le Monde + └── huggingface_scraper.py # Scraper for Hugging Face +``` + +## 🎯 Two Operating Modes + +### 1️⃣ Backfill Mode (History) +**When:** At server startup + +**What:** Scrapes all available history from each source + +**How:** +```bash +python main.py backfill --limit 100 +``` + +**Flow:** +1. Scraper calls `scrape_all()` for each source +2. Articles are saved to DB (deduplicated by ID) +3. Embeddings generated and stored for each article +4. Sync recorded in `sync_history` + +### 2️⃣ Watch Mode (Monitoring) +**When:** After backfill or directly + +**What:** Continuously scrapes new articles + +**How:** +```bash +python main.py watch --interval 300 +``` + +**Flow:** +1. Infinite loop (by default, checks every 5 min) +2. Scraper calls `scrape_latest()` for each source +3. New articles detected (ID comparison) +4. Save and create embeddings +5. Wait for next interval + +## 🔧 Main Components + +### BaseScraper (abstract interface) +All scrapers inherit from this class and implement: +- `scrape_latest(limit)` → for watch mode +- `scrape_all(limit)` → for backfill mode +- `normalize_item()` → unified format + +### DatabaseManager +- Manages SQLite persistence +- Tables: articles, embeddings, sync_history +- Automatic deduplication (INSERT OR IGNORE) +- Batch operations for performance + +### EmbeddingManager +- Support for multiple providers (Dummy, SentenceTransformers) +- Vector serialization/deserialization +- Storage as BLOB in DB + +### WatchServer (orchestrator) +- Initializes all scrapers +- Manages both modes +- Detailed operation logging +- Statistics and monitoring + +## 💾 Database Structure + +### Table `articles` +``` +id (TEXT PRIMARY KEY) # Unique identifier per source +source_site (TEXT) # arxiv, github, medium, le_monde, huggingface +title (TEXT) # Article title +description (TEXT) # Summary/content +author_info (TEXT) # Author(s) +keywords (TEXT) # Tags/categories +content_url (TEXT) # Link to source +published_date (TEXT) # Publication date +item_type (TEXT) # article, paper, repository, etc. +created_at (TIMESTAMP) # When we retrieved it +updated_at (TIMESTAMP) # Last update +``` + +### Table `embeddings` +``` +article_id (TEXT UNIQUE) # Link to articles.id +embedding (BLOB) # Serialized vector (pickle) +embedding_model (TEXT) # Which model generated the embedding +created_at (TIMESTAMP) # When created +``` + +### Table `sync_history` +``` +source_site (TEXT) # Which source +sync_mode (TEXT) # "watch" or "backfill" +last_sync_time (TIMESTAMP) # When +items_processed (INTEGER) # How many articles +``` + +## 🚀 Usage + +### Simple startup +```bash +# 1. Fill DB with history +python main.py backfill --limit 50 + +# 2. Then monitor continuously +python main.py watch --interval 300 + +# 3. Check stats +python main.py stats +``` + +### With options +```bash +# Custom backfill +python main.py backfill --limit 200 --db custom.db + +# Watch with 10 min interval +python main.py watch --interval 600 + +# Stats on specific DB +python main.py stats --db custom.db +``` + +## 📊 Complete Flow Example + +``` +Server startup +│ +├─→ BACKFILL Mode (optional) +│ ├─→ ArXiv.scrape_all(100) → 45 articles → DB +│ ├─→ GitHub.scrape_all(100) → 78 articles → DB +│ ├─→ Medium.scrape_all(100) → 23 articles → DB +│ ├─→ LeMonde.scrape_all(100) → 67 articles → DB +│ └─→ HF.scrape_all(100) → 89 articles → DB +│ ↓ All articles receive an embedding +│ → 302 articles in DB with embeddings +│ +└─→ WATCH Mode (infinite loop) + ├─→ Iteration 1 + │ ├─→ ArXiv.scrape_latest(20) → 2 new + │ ├─→ GitHub.scrape_latest(20) → 1 new + │ ├─→ Medium.scrape_latest(20) → 0 new + │ ├─→ LeMonde.scrape_latest(20) → 1 new + │ └─→ HF.scrape_latest(20) → 2 new + │ → 6 new articles added + │ + ├─→ [Wait 5 min] + │ + └─→ Iteration 2 + └─→ ... +``` + +## 🔑 Key Design Points + +### ✓ Modularity +- Each scraper is independent +- Easy to add/remove a source +- Interchangeable embedding providers + +### ✓ Robustness +- Error handling per scraper +- No interruption if a source fails +- Automatic deduplication + +### ✓ Scalability +- Batch operations for DB +- Context manager for connections +- Logging for monitoring + +### ✓ Maintainability +- Clear and documented code +- Centralized configuration +- Usage examples + +## 💻 How to View the Database + +### Option 1: Export and View Locally +```bash +# Export database to local .db file +python main.py export --db veille_technique.db --output veille_export.db + +# View with SQLite Browser or VSCode extension +# Export creates a complete copy of the DB +``` + +### Option 2: Use sqlite3 from Command Line +```bash +# Open the database +sqlite3 veille_technique.db + +# Some useful queries +sqlite> SELECT COUNT(*) FROM articles; -- Total articles +sqlite> SELECT source_site, COUNT(*) FROM articles GROUP BY source_site; -- By source +sqlite> SELECT * FROM articles LIMIT 5; -- View first 5 articles +sqlite> SELECT source_site, COUNT(*) FROM embeddings GROUP BY source_site; -- Embeddings per source +``` + +### Option 3: Use a GUI +- **SQLite Browser**: `brew install sqlitebrowser` (macOS) or `apt install sqlitebrowser` (Linux) +- **VSCode Extension**: "SQLite" extension (officially supported) +- **DBeaver Community**: Free multi-DB application + +### Example: View Articles from One Source +```bash +sqlite3 veille_technique.db << EOF +.headers on +.mode column +SELECT title, author_info, published_date FROM articles +WHERE source_site = 'github' +ORDER BY published_date DESC +LIMIT 10; +EOF +``` + +### Complete DB Structure +```bash +# View all tables +sqlite3 veille_technique.db ".tables" + +# View schema of a table +sqlite3 veille_technique.db ".schema articles" + +# View sync stats +sqlite3 veille_technique.db "SELECT * FROM sync_history ORDER BY created_at DESC LIMIT 5;" +``` + +## 📝 Migration from Old Server + +Old code in the `scrap/` folder remains untouched for reference. +The new server reuses the scraping logic but with a completely restructured architecture. diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..49ab7cd --- /dev/null +++ b/server/README.md @@ -0,0 +1,207 @@ +# Serveur de Veille Technique + +Serveur de scraping et d'indexation avec 2 modes de fonctionnement. + +## Architecture + +Le serveur est organisé autour de plusieurs composants : + +### 1. **Scrapers** (`scrapers/`) +Chaque source a son propre scraper qui implémente l'interface `BaseScraper` : +- `ArxivScraper` : Articles arXiv (cs.LG) +- `GithubScraper` : Repositories GitHub trending +- `MediumScraper` : Articles Medium (AI/ML tags) +- `LeMondeScraper` : Articles Le Monde +- `HuggingFaceScraper` : Models, datasets, spaces sur HuggingFace + +### 2. **DatabaseManager** (`database.py`) +Gère la persistance des données : +- Table `articles` : Articles normalisés +- Table `embeddings` : Vecteurs d'embedding pour chaque article +- Table `sync_history` : Historique des synchronisations + +### 3. **EmbeddingManager** (`embeddings.py`) +Crée les embeddings pour les articles : +- Supports multiple providers (Dummy, SentenceTransformers) +- Sérialise les embeddings en format bytes pour la base de données + +### 4. **WatchServer** (`main.py`) +Orchestrateur principal avec 2 modes. + +## Modes de fonctionnement + +### Mode "Backfill" (Historique) +```bash +python main.py backfill --limit 100 +``` +- Scrape tout l'historique disponible depuis chaque source +- Sauvegarde et crée les embeddings +- S'exécute une seule fois au démarrage +- Idéal pour remplir la base de données initialement + +### Mode "Watch" (Veille) +```bash +python main.py watch --interval 300 +``` +- Lance une boucle infinie +- Scrape les nouvelles articles régulièrement (par défaut toutes les 5 min) +- Ajoute à la base et crée les embeddings automatiquement +- Peut tourner indéfiniment + +### Mode "Stats" +```bash +python main.py stats +``` +- Affiche les statistiques actuelles de la base de données + +## Installation + +```bash +# Créer un environnement virtuel +python -m venv venv +source venv/bin/activate + +# Installer les dépendances +pip install -r requirements.txt +``` + +## Utilisation + +### Démarrage simple +```bash +# Mode backfill pour remplir la BD +python main.py backfill + +# Puis mode watch en continu +python main.py watch +``` + +### Avec options +```bash +# Backfill avec limite 50 par source, base de données custom +python main.py backfill --limit 50 --db custom.db + +# Watch avec intervalle 10 min (600 sec) +python main.py watch --interval 600 + +# Vérifier les stats +python main.py stats --db custom.db +``` + +## Format unifié des articles + +Chaque article scrappé est normalisé dans ce format : + +```python +{ + "id": "unique-identifier", + "source_site": "arxiv|github|medium|le_monde|huggingface", + "title": "Titre de l'article", + "description": "Résumé ou description", + "author_info": "Auteur(s)", + "keywords": "keyword1, keyword2, ...", + "content_url": "https://link-to-original", + "published_date": "2024-01-15T10:30:00Z", + "item_type": "article|paper|repository|..." +} +``` + +## Structure de la base de données + +### Table `articles` +```sql +id (TEXT PRIMARY KEY) +source_site (TEXT) +title (TEXT) +description (TEXT) +author_info (TEXT) +keywords (TEXT) +content_url (TEXT) +published_date (TEXT) +item_type (TEXT) +created_at (TIMESTAMP) +updated_at (TIMESTAMP) +``` + +### Table `embeddings` +```sql +id (INTEGER PRIMARY KEY) +article_id (TEXT UNIQUE) +embedding (BLOB) -- Vecteur sérialisé +embedding_model (TEXT) +created_at (TIMESTAMP) +``` + +### Table `sync_history` +```sql +id (INTEGER PRIMARY KEY) +source_site (TEXT) +sync_mode (TEXT) -- "watch" ou "backfill" +last_sync_time (TIMESTAMP) +items_processed (INTEGER) +created_at (TIMESTAMP) +``` + +## Ajouter une nouvelle source + +1. Créer un nouveau scraper dans `scrapers/` : + +```python +from scrapers.base import BaseScraper + +class NewScraper(BaseScraper): + def __init__(self): + super().__init__("source_name") + + def scrape_latest(self, limit: int = 20) -> List[Dict]: + # Scrape les articles les plus récents + items = [] + # ... votre logique ... + return [self.normalize_item(...) for ... in items] + + def scrape_all(self, limit: int = 100) -> List[Dict]: + # Scrape tout l'historique disponible + items = [] + # ... votre logique ... + return [self.normalize_item(...) for ... in items] +``` + +2. Enregistrer le scraper dans `WatchServer._init_scrapers()` dans `main.py` + +## Configuration des embeddings + +Par défaut, le serveur utilise les embeddings "dummy" (aléatoires mais déterministes) pour la développement. + +Pour utiliser des embeddings réels avec SentenceTransformers : + +```bash +pip install sentence-transformers +``` + +Puis dans `main.py`, modifier le code : + +```python +# Au lieu de : +if use_dummy_embeddings: + embedding_provider = DummyEmbeddingProvider() + +# Utiliser : +from embeddings import SentenceTransformerEmbeddingProvider +embedding_provider = SentenceTransformerEmbeddingProvider("all-MiniLM-L6-v2") +``` + +## Améliorations possibles + +- [ ] API REST pour interroger la base de données +- [ ] Authentification/autorisation +- [ ] Pagination des résultats +- [ ] Filtrage par source, date, keywords +- [ ] Recherche sémantique avec embeddings +- [ ] Notifications pour nouveaux articles +- [ ] Déduplication plus intelligente +- [ ] Cache de résultats +- [ ] Métriques et monitoring + +## Licence + +Voir LICENSE diff --git a/server/__pycache__/config.cpython-312.pyc b/server/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7ff5409a6af7b4c0edcf3a3f262bfcb2022c7d28 GIT binary patch literal 3013 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 literal 0 HcmV?d00001 diff --git a/server/__pycache__/embeddings.cpython-312.pyc b/server/__pycache__/embeddings.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a1a60decc90470149ba40bddda33a30c44793597 GIT binary patch literal 6631 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 literal 0 HcmV?d00001 diff --git a/server/__pycache__/main.cpython-312.pyc b/server/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ea461d9427f045d4b62ac0a13c27cc6714f19213 GIT binary patch literal 13440 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 "ServerConfig": + """Load configuration from JSON/YAML file.""" + import json + + try: + with open(filepath, 'r') as f: + data = json.load(f) + + if "scrapers" in data: + data["scrapers"] = { + name: ScraperConfig(**cfg) + for name, cfg in data["scrapers"].items() + } + + return cls(**data) + except FileNotFoundError: + print(f"Config file {filepath} not found, using default config") + return cls() + + +DEFAULT_CONFIG = ServerConfig() + +DEV_CONFIG = ServerConfig( + db_path="veille_technique_dev.db", + use_dummy_embeddings=True, + watch_interval_seconds=60, +) + +PROD_CONFIG = ServerConfig( + db_path="veille_technique.db", + use_dummy_embeddings=False, + watch_interval_seconds=600, + log_level="WARNING", +) diff --git a/server/database.py b/server/database.py new file mode 100644 index 0000000..f4aacf6 --- /dev/null +++ b/server/database.py @@ -0,0 +1,291 @@ +"""Database manager for the server.""" + +import sqlite3 +from typing import List, Dict, Optional +from datetime import datetime, UTC +from contextlib import contextmanager +import os + + +class DatabaseManager: + """Manages unified database operations.""" + + def __init__(self, db_path: str = "technical_watch.db"): + """ + Initialize the database manager. + + Args: + db_path: Path to the SQLite file + """ + self.db_path = db_path + self.setup_database() + + @contextmanager + def get_connection(self): + """Context manager for database connections.""" + conn = sqlite3.connect(self.db_path) + conn.row_factory = sqlite3.Row + try: + yield conn + finally: + conn.close() + + def setup_database(self): + """Initialize database and create tables.""" + with self.get_connection() as conn: + conn.execute("PRAGMA foreign_keys = ON") + cur = conn.cursor() + + cur.execute(""" + CREATE TABLE IF NOT EXISTS articles ( + id TEXT PRIMARY KEY, + source_site TEXT NOT NULL, + title TEXT NOT NULL, + description TEXT, + author_info TEXT, + keywords TEXT, + content_url TEXT NOT NULL, + published_date TEXT, + item_type TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """) + + cur.execute(""" + CREATE TABLE IF NOT EXISTS embeddings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + article_id TEXT NOT NULL UNIQUE, + embedding BLOB NOT NULL, + embedding_model TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (article_id) REFERENCES articles(id) + ) + """) + + cur.execute(""" + CREATE TABLE IF NOT EXISTS sync_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + source_site TEXT NOT NULL, + sync_mode TEXT NOT NULL, + last_sync_time TIMESTAMP, + items_processed INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """) + + conn.commit() + + def save_article(self, item: Dict, conn: Optional[sqlite3.Connection] = None) -> bool: + """ + Save article to database. + + Args: + item: Dict with unified structure + conn: Optional connection (for transactions) + + Returns: + True if inserted, False if already exists + """ + should_close = False + if conn is None: + conn = sqlite3.connect(self.db_path) + should_close = True + + try: + cur = conn.cursor() + + cur.execute(""" + INSERT OR IGNORE INTO articles + (id, source_site, title, description, author_info, keywords, content_url, published_date, item_type, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, ( + item["id"], + item["source_site"], + item["title"], + item.get("description", ""), + item.get("author_info", ""), + item.get("keywords", ""), + item["content_url"], + item.get("published_date", datetime.now(UTC).isoformat()), + item.get("item_type", "article"), + datetime.now(UTC).isoformat() + )) + + conn.commit() + return cur.rowcount > 0 + + finally: + if should_close: + conn.close() + + def save_articles_batch(self, items: List[Dict]) -> int: + """ + Save multiple articles in one transaction. + + Args: + items: List of dicts with unified structure + + Returns: + Number of articles inserted + """ + with self.get_connection() as conn: + count = 0 + for item in items: + if self.save_article(item, conn): + count += 1 + return count + + def article_exists(self, article_id: str) -> bool: + """Check if article already exists.""" + with self.get_connection() as conn: + cur = conn.cursor() + cur.execute("SELECT 1 FROM articles WHERE id = ?", (article_id,)) + return cur.fetchone() is not None + + def save_embedding(self, article_id: str, embedding: bytes, model: str = "default") -> bool: + """ + Save article embedding. + + Args: + article_id: Article ID + embedding: Embedding in bytes format + model: Name of the model used + + Returns: + True if saved, False otherwise + """ + with self.get_connection() as conn: + cur = conn.cursor() + + try: + cur.execute(""" + INSERT OR IGNORE INTO embeddings + (article_id, embedding, embedding_model) + VALUES (?, ?, ?) + """, (article_id, embedding, model)) + + conn.commit() + return cur.rowcount > 0 + + except sqlite3.IntegrityError: + return False + + def get_articles_without_embeddings(self, limit: int = 100) -> List[Dict]: + """ + Get articles without embeddings. + + Args: + limit: Maximum number of articles to return + + Returns: + List of articles + """ + with self.get_connection() as conn: + cur = conn.cursor() + + cur.execute(""" + SELECT a.* FROM articles a + LEFT JOIN embeddings e ON a.id = e.article_id + WHERE e.id IS NULL + LIMIT ? + """, (limit,)) + + rows = cur.fetchall() + return [dict(row) for row in rows] + + def get_articles_by_source(self, source: str, limit: int = 50) -> List[Dict]: + """Get articles from a specific source.""" + with self.get_connection() as conn: + cur = conn.cursor() + + cur.execute(""" + SELECT * FROM articles + WHERE source_site = ? + ORDER BY published_date DESC + LIMIT ? + """, (source, limit)) + + rows = cur.fetchall() + return [dict(row) for row in rows] + + def get_total_articles(self) -> int: + """Return total number of articles.""" + with self.get_connection() as conn: + cur = conn.cursor() + cur.execute("SELECT COUNT(*) FROM articles") + return cur.fetchone()[0] + + def get_articles_by_source_count(self) -> Dict[str, int]: + """Return number of articles per source.""" + with self.get_connection() as conn: + cur = conn.cursor() + cur.execute(""" + SELECT source_site, COUNT(*) as count + FROM articles + GROUP BY source_site + ORDER BY count DESC + """) + + return {row[0]: row[1] for row in cur.fetchall()} + + def record_sync(self, source: str, mode: str, items_processed: int = 0): + """ + Record a synchronization. + + Args: + source: Source name + mode: "watch" or "backfill" + items_processed: Number of items processed + """ + with self.get_connection() as conn: + cur = conn.cursor() + + cur.execute(""" + INSERT INTO sync_history + (source_site, sync_mode, last_sync_time, items_processed) + VALUES (?, ?, ?, ?) + """, (source, mode, datetime.now(UTC).isoformat(), items_processed)) + + conn.commit() + + def get_last_sync(self, source: str, mode: str) -> Optional[Dict]: + """Get last sync for a source and mode.""" + with self.get_connection() as conn: + cur = conn.cursor() + + cur.execute(""" + SELECT * FROM sync_history + WHERE source_site = ? AND sync_mode = ? + ORDER BY created_at DESC + LIMIT 1 + """, (source, mode)) + + row = cur.fetchone() + return dict(row) if row else None + + def get_stats(self) -> Dict: + """Return database statistics.""" + with self.get_connection() as conn: + cur = conn.cursor() + + cur.execute("SELECT COUNT(*) FROM articles") + total_articles = cur.fetchone()[0] + + cur.execute("SELECT COUNT(*) FROM embeddings") + total_embeddings = cur.fetchone()[0] + + cur.execute(""" + SELECT source_site, COUNT(*) as count + FROM articles + GROUP BY source_site + """) + + articles_by_source = {row[0]: row[1] for row in cur.fetchall()} + + return { + "total_articles": total_articles, + "total_embeddings": total_embeddings, + "articles_by_source": articles_by_source, + "articles_without_embeddings": total_articles - total_embeddings + } diff --git a/server/embeddings.py b/server/embeddings.py new file mode 100644 index 0000000..3c15551 --- /dev/null +++ b/server/embeddings.py @@ -0,0 +1,140 @@ +"""Embedding management and generation.""" + +import pickle +from typing import List, Optional +from abc import ABC, abstractmethod +import numpy as np + + +class EmbeddingProvider(ABC): + """Abstract base class for embedding providers.""" + + @abstractmethod + def embed(self, text: str) -> np.ndarray: + """ + Generate embedding for text. + + Args: + text: Text to embed + + Returns: + Embedding vector (numpy array) + """ + pass + + @abstractmethod + def get_name(self) -> str: + """Return provider name.""" + pass + + +class DummyEmbeddingProvider(EmbeddingProvider): + """Dummy embedding provider for development.""" + + def __init__(self, dimension: int = 384): + """ + Initialize dummy provider. + + Args: + dimension: Embedding dimension + """ + self.dimension = dimension + + def embed(self, text: str) -> np.ndarray: + """Generate deterministic random embedding from text hash.""" + seed = abs(hash(text)) % (2**31) + np.random.seed(seed) + return np.random.randn(self.dimension).astype(np.float32) + + def get_name(self) -> str: + """Return provider name.""" + return "dummy" + + +class SentenceTransformerEmbeddingProvider(EmbeddingProvider): + """Embedding provider using sentence-transformers.""" + + def __init__(self, model_name: str = "all-MiniLM-L6-v2"): + """ + Initialize SentenceTransformers provider. + + Args: + model_name: Model name to use + """ + try: + from sentence_transformers import SentenceTransformer + except ImportError: + raise ImportError( + "sentence-transformers is required. Install it with: " + "pip install sentence-transformers" + ) + + self.model_name = model_name + self.model = SentenceTransformer(model_name) + + def embed(self, text: str) -> np.ndarray: + """Generate embedding with SentenceTransformer.""" + embedding = self.model.encode(text, convert_to_numpy=True) + return embedding.astype(np.float32) + + def get_name(self) -> str: + """Return provider name.""" + return f"sentence-transformers-{self.model_name}" + + +class EmbeddingManager: + """Manage embeddings for articles.""" + + def __init__(self, provider: Optional[EmbeddingProvider] = None): + """ + Initialize embedding manager. + + Args: + provider: Embedding provider to use (default: Dummy) + """ + self.provider = provider or DummyEmbeddingProvider() + + def embed_text(self, text: str) -> bytes: + """ + Generate embedding for text and serialize. + + Args: + text: Text to embed + + Returns: + Serialized embedding in bytes + """ + embedding = self.provider.embed(text) + return pickle.dumps(embedding) + + def embed_article(self, article: dict) -> bytes: + """ + Generate embedding for complete article. + + Args: + article: Dict with title and description + + Returns: + Serialized embedding in bytes + """ + title = article.get("title", "") + description = article.get("description", "") + text = f"{title}\n{description}" + + return self.embed_text(text) + + def deserialize_embedding(self, embedding_bytes: bytes) -> np.ndarray: + """ + Deserialize embedding from bytes. + + Args: + embedding_bytes: Embedding in bytes format + + Returns: + Embedding as numpy array + """ + return pickle.loads(embedding_bytes) + + def get_provider_name(self) -> str: + """Return embedding provider name.""" + return self.provider.get_name() diff --git a/server/examples.py b/server/examples.py new file mode 100644 index 0000000..166b4ab --- /dev/null +++ b/server/examples.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +""" +Usage examples for the watch server. +""" + +import asyncio +from main import WatchServer +from config import DEV_CONFIG, PROD_CONFIG + + +def example_backfill(): + """Example 1: Backfill mode (history).""" + print("\n" + "="*60) + print("EXAMPLE 1 : BACKFILL Mode") + print("="*60) + + server = WatchServer( + db_path="test_backfill.db", + use_dummy_embeddings=True, + check_interval=300 + ) + + print("\n✓ Server created, launching backfill...") + server.run_backfill_mode(limit_per_scraper=10) + + server.print_stats() + + +def example_watch_limited(): + """Example 2: Watch mode with iteration limit (for testing).""" + print("\n" + "="*60) + print("EXAMPLE 2 : WATCH Mode (limited to 3 iterations)") + print("="*60) + + server = WatchServer( + db_path="test_watch.db", + use_dummy_embeddings=True, + check_interval=10 + ) + + print("\n✓ Server created, launching watch (3 iterations)...") + print(" Each iteration scrapes all sources\n") + + try: + import threading + + def stop_after_30s(srv): + import time + time.sleep(30) + srv.running = False + print("\n⏹️ Auto-stopped after 30 seconds") + + thread = threading.Thread(target=stop_after_30s, args=(server,), daemon=True) + thread.start() + + asyncio.run(server.run_watch_mode()) + except KeyboardInterrupt: + print("\n⏹️ Stopped by user") + + server.print_stats() + + +def example_multi_source_stats(): + """Example 3: View stats with multiple sources scraped.""" + print("\n" + "="*60) + print("EXAMPLE 3 : Backfill + Stats") + print("="*60) + + server = WatchServer(db_path="test_multi.db") + + print("\n📥 Scraping each source...") + server.run_backfill_mode(limit_per_scraper=5) + + print("\n📊 Checking stats...") + stats = server.get_stats() + + print(f"\nSummary :") + print(f" Total articles : {stats['total_articles']}") + print(f" Embeddings : {stats['total_embeddings']}") + print(f" Missing : {stats['articles_without_embeddings']}") + + print(f"\nPer source :") + for source, count in sorted(stats['articles_by_source'].items()): + pct = 100 * count / max(1, stats['total_articles']) + print(f" {source:15} : {count:3} ({pct:.1f}%)") + + +def example_custom_config(): + """Example 4: Use custom configuration.""" + print("\n" + "="*60) + print("EXAMPLE 4 : Custom Configuration") + print("="*60) + + from config import ServerConfig, ScraperConfig + + custom_config = ServerConfig( + db_path="test_custom.db", + watch_interval_seconds=120, + use_dummy_embeddings=True, + scrapers={ + "arxiv": ScraperConfig(enabled=True, limit_latest=10, limit_all=30), + "github": ScraperConfig(enabled=True, limit_latest=15, limit_all=50), + "medium": ScraperConfig(enabled=False), + "lemonde": ScraperConfig(enabled=False), + "huggingface": ScraperConfig(enabled=True, limit_latest=10, limit_all=30), + } + ) + + print(f"\n✓ Custom config created") + print(f" DB : {custom_config.db_path}") + print(f" Interval : {custom_config.watch_interval_seconds}s") + print(f" Dummy embeddings : {custom_config.use_dummy_embeddings}") + + print(f"\n✓ Enabled scrapers :") + for name, cfg in custom_config.scrapers.items(): + if cfg.enabled: + print(f" - {name} (latest:{cfg.limit_latest}, all:{cfg.limit_all})") + + +if __name__ == "__main__": + import sys + + print("\n" + "="*60) + print("USAGE EXAMPLES") + print("Watch Server") + print("="*60) + + examples = { + "1": ("Backfill (history)", example_backfill), + "2": ("Watch limited (test)", example_watch_limited), + "3": ("Multi-source stats", example_multi_source_stats), + "4": ("Custom config", example_custom_config), + } + + print("\nChoose an example :") + for key, (desc, _) in examples.items(): + print(f" {key}. {desc}") + + if len(sys.argv) > 1: + choice = sys.argv[1] + else: + choice = input("\nEnter your choice (1-4) : ").strip() + + if choice in examples: + try: + examples[choice][1]() + except Exception as e: + print(f"\n❌ Error : {e}") + import traceback + traceback.print_exc() + else: + print(f"❌ Invalid choice : {choice}") diff --git a/server/main.py b/server/main.py new file mode 100644 index 0000000..1b3cd6a --- /dev/null +++ b/server/main.py @@ -0,0 +1,332 @@ +""" +Watch Server - Technical surveillance with watch and backfill modes. + +Modes: +1. "watch": Scrape new articles continuously +2. "backfill": Retrieve entire available history at startup +""" + +import asyncio +import logging +from typing import Dict, List, Optional +from datetime import datetime, UTC +import argparse + +from database import DatabaseManager +from embeddings import EmbeddingManager, DummyEmbeddingProvider +from scrapers.arxiv_scraper import ArxivScraper +from scrapers.github_scraper import GithubScraper +from scrapers.medium_scraper import MediumScraper +from scrapers.lemonde_scraper import LeMondeScraper +from scrapers.huggingface_scraper import HuggingFaceScraper + + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + + +class WatchServer: + """Technical watch server with watch and backfill modes.""" + + def __init__( + self, + db_path: str = "veille_technique.db", + use_dummy_embeddings: bool = True, + check_interval: int = 300 + ): + """ + Initialize the server. + + Args: + db_path: Path to the database file + use_dummy_embeddings: Use dummy embeddings (dev) or real ones + check_interval: Scraping interval in seconds (watch mode) + """ + self.db_manager = DatabaseManager(db_path) + + if use_dummy_embeddings: + embedding_provider = DummyEmbeddingProvider() + else: + embedding_provider = DummyEmbeddingProvider() + + self.embedding_manager = EmbeddingManager(embedding_provider) + + self.check_interval = check_interval + self.running = False + + self.scrapers = self._init_scrapers() + + def _init_scrapers(self) -> Dict[str, object]: + """Initialize all available scrapers.""" + scrapers = {} + + try: + scrapers["arxiv"] = ArxivScraper(category="cs.LG") + logger.info("✓ ArXiv scraper initialized") + except ImportError as e: + logger.warning(f"✗ ArXiv scraper failed: {e}") + + try: + scrapers["github"] = GithubScraper() + logger.info("✓ GitHub scraper initialized") + except Exception as e: + logger.warning(f"✗ GitHub scraper failed: {e}") + + try: + scrapers["medium"] = MediumScraper() + logger.info("✓ Medium scraper initialized") + except ImportError as e: + logger.warning(f"✗ Medium scraper failed: {e}") + + try: + scrapers["lemonde"] = LeMondeScraper() + logger.info("✓ Le Monde scraper initialized") + except ImportError as e: + logger.warning(f"✗ Le Monde scraper failed: {e}") + + try: + scrapers["huggingface"] = HuggingFaceScraper() + logger.info("✓ Hugging Face scraper initialized") + except Exception as e: + logger.warning(f"✗ Hugging Face scraper failed: {e}") + + return scrapers + + def _process_articles(self, articles: List[Dict]) -> int: + """ + Process articles: save to DB and create embeddings. + + Args: + articles: List of normalized articles + + Returns: + Number of new articles processed + """ + new_count = 0 + + for article in articles: + if self.db_manager.article_exists(article["id"]): + continue + + if self.db_manager.save_article(article): + new_count += 1 + + try: + embedding = self.embedding_manager.embed_article(article) + self.db_manager.save_embedding( + article["id"], + embedding, + model=self.embedding_manager.get_provider_name() + ) + except Exception as e: + logger.error(f"Embedding error for {article['id']}: {e}") + + return new_count + + def run_backfill_mode(self, limit_per_scraper: int = 100): + """ + Launch backfill mode - scrape entire available history. + + Args: + limit_per_scraper: Maximum articles per scraper + """ + logger.info("=" * 60) + logger.info("🔄 BACKFILL MODE START (History)") + logger.info("=" * 60) + + total_new = 0 + + for source_name, scraper in self.scrapers.items(): + logger.info(f"\n📥 Scraping {source_name} (backfill mode)...") + + try: + articles = scraper.scrape_all(limit=limit_per_scraper) + + if not articles: + logger.info(f" ⚠️ No articles found for {source_name}") + continue + + logger.info(f" 📦 {len(articles)} articles received") + + new_count = self._process_articles(articles) + total_new += new_count + + logger.info(f" ✓ {new_count} new articles saved") + + self.db_manager.record_sync(source_name, "backfill", new_count) + + except Exception as e: + logger.error(f" ✗ Error for {source_name}: {e}") + + logger.info("\n" + "=" * 60) + logger.info(f"✓ BACKFILL MODE COMPLETE - {total_new} articles processed") + logger.info("=" * 60) + + async def run_watch_mode(self): + """Launch watch mode - scrape continuously.""" + logger.info("=" * 60) + logger.info("👀 WATCH MODE START (Surveillance)") + logger.info(f"Scraping interval: {self.check_interval}s") + logger.info("=" * 60) + + self.running = True + iteration = 0 + + try: + while self.running: + iteration += 1 + logger.info(f"\n[Iteration {iteration}] {datetime.now(UTC).isoformat()}") + + total_new = 0 + + for source_name, scraper in self.scrapers.items(): + logger.info(f" 📡 Scraping {source_name}...") + + try: + articles = scraper.scrape_latest(limit=20) + + if not articles: + logger.info(f" - No new articles") + continue + + new_count = self._process_articles(articles) + total_new += new_count + + if new_count > 0: + logger.info(f" ✓ {new_count} new articles") + self.db_manager.record_sync(source_name, "watch", new_count) + else: + logger.info(f" - All articles already exist") + + except Exception as e: + logger.error(f" ✗ Error: {e}") + + logger.info(f" 📊 Total: {total_new} new articles") + logger.info(f" ⏳ Waiting {self.check_interval}s...") + + stats = self.db_manager.get_stats() + logger.info(f" 📈 DB: {stats['total_articles']} articles, " + f"{stats['articles_without_embeddings']} without embedding") + + await asyncio.sleep(self.check_interval) + + except KeyboardInterrupt: + logger.info("\n⏹️ Server stopped") + finally: + self.running = False + + def get_stats(self) -> Dict: + """Return database statistics.""" + return self.db_manager.get_stats() + + def print_stats(self): + """Display database statistics.""" + stats = self.get_stats() + + print("\n" + "=" * 60) + print("📊 DATABASE STATISTICS") + print("=" * 60) + print(f"Total articles: {stats['total_articles']}") + print(f"Articles with embedding: {stats['total_embeddings']}") + print(f"Articles without embedding: {stats['articles_without_embeddings']}") + print("\nArticles per source:") + for source, count in stats['articles_by_source'].items(): + print(f" - {source}: {count}") + print("=" * 60) + + def export_database(self, output_path: str) -> bool: + """ + Export database to a new file. + + Args: + output_path: Path where to export the database + + Returns: + True if successful, False otherwise + """ + import shutil + + try: + shutil.copy2(self.db_manager.db_path, output_path) + logger.info(f"✓ Database exported to {output_path}") + + import os + size_mb = os.path.getsize(output_path) / (1024 * 1024) + logger.info(f" File size: {size_mb:.2f} MB") + + return True + except Exception as e: + logger.error(f"✗ Export failed: {e}") + return False + + +def main(): + """Server entry point.""" + parser = argparse.ArgumentParser( + description="Technical watch server with watch and backfill modes" + ) + parser.add_argument( + "mode", + choices=["watch", "backfill", "stats", "export"], + help="Execution mode" + ) + parser.add_argument( + "--db", + default="veille_technique.db", + help="Path to database" + ) + parser.add_argument( + "--interval", + type=int, + default=300, + help="Scraping interval in seconds (watch mode)" + ) + parser.add_argument( + "--limit", + type=int, + default=100, + help="Max articles per source (backfill mode)" + ) + parser.add_argument( + "--output", + default="veille_export.db", + help="Output file path for export mode" + ) + parser.add_argument( + "--no-dummy", + action="store_true", + help="Do not use dummy embeddings" + ) + + args = parser.parse_args() + + server = WatchServer( + db_path=args.db, + use_dummy_embeddings=not args.no_dummy, + check_interval=args.interval + ) + + if args.mode == "backfill": + server.run_backfill_mode(limit_per_scraper=args.limit) + server.print_stats() + + elif args.mode == "watch": + asyncio.run(server.run_watch_mode()) + + elif args.mode == "stats": + server.print_stats() + + elif args.mode == "export": + logger.info(f"Exporting database from {args.db} to {args.output}...") + if server.export_database(args.output): + server.print_stats() + logger.info(f"✓ You can now open {args.output} with SQLite Browser or your preferred tool") + else: + logger.error("Export failed") + + +if __name__ == "__main__": + main() diff --git a/server/requirements.txt b/server/requirements.txt new file mode 100644 index 0000000..6e9ace9 --- /dev/null +++ b/server/requirements.txt @@ -0,0 +1,4 @@ +feedparser==6.0.10 +requests==2.31.0 +arxiv==1.4.8 +numpy==1.26.4 diff --git a/server/scrapers/__init__.py b/server/scrapers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/scrapers/__pycache__/__init__.cpython-312.pyc b/server/scrapers/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..96fed6d9d87636bc9da11774b72d16be477e984e GIT binary patch literal 156 zcmX@j%ge<81ern>nIQTxh(HIQS%4zb87dhx8U0o=6fpsLpFwJVS?g!y=cei>=9T1U z=B4VVq?YLy&M4u=4F<|$LkeT{^GF7 d%}*)KNwq6t1)9YO#Kj=SM`lJw#v*1Q3jjc4C4&F} literal 0 HcmV?d00001 diff --git a/server/scrapers/__pycache__/arxiv_scraper.cpython-312.pyc b/server/scrapers/__pycache__/arxiv_scraper.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d9f5c0a7552a1e3b5e2cbd42a8fad4ff874becb2 GIT binary patch literal 4299 zcmeHK+iw)t89#H|`}(rR_WC|HL}G(khj3}Qg@AFXo5Zx3M)fw%XqY*+$IMpt`!Kz*w!d1Kn$Oo@clNEL4jCMtDb`kk5C1z)PZ=AlR0 zbNSBszWKg$&hPh~{Zlv`Kv4eJ^^UeDh|u5Zpj-sEvq7MZz0u(Ju94JIT4r=_9E=t zhll`ngoJkp-D#UZliU!RU}FAUWKbb;1`}~4O+=aeRJ$I3N>yW=#ebj~rp2Gv6w?Y{ z9{N$A)cb?J-uL?lE<9!E3dRbalMU<|7=W?o5;n5BsW{(uYL7eE7=g~uAxMl83S^22H=S2G(=s^*5O*Hpb^>1EO2{Q<|yCx|YM@ zS$YDZ;haglvZB&COGb?ol@Zl1`+veYy~OOWX% z4dhPSEo_N$RUXBnW{3n|%W4Fxaj`#Tn6j>mnki0b=GX}_qh&d(G*`0=t+5tuT1 zpn5rrUQ{7})opLy7hP<8wj3bH%| zn^}_Da!Y8G_AM#FO#t*VDhmj4cM7x`cCPrJTHJp*0ioxb!-Jy40l+|m|8kz1?8+cmL>qDk+kKzil-*hL^Z6Sl1`a8 zWlCA1TUD9t6%2F=JSKN={I#81#T2h97 znK`V0+taXNdGI8BMYXC@<=7-@yW%!AMp|KSEHA+sT~@H=HL^)+(DGNj0*8z=WR#?A z#yq4JmJ>?g79*2=wB>azb=3-&gOeZ@)1cRyGMXroI=ZMRpe#RgAXX3*QYFq6hQQgG=ga?|Sr|Vzi@J-?T6^ zKeRZrUVjkui!I{4Gk4DvT8`vfj;yyFD>k&1>QK$z4HT#elu*DQ_}BAl)OO0CuY_OM z^&a+Fg03Ysa|;R4U9(B(7d8p`ky#<3-H1npRJ9w`e1U$$PBX94+-@YZ8^OG$u?xwJ zhrqjSblLyS5SmrI^S{4Izf=PK&axS?6fP#!EU74~1b_d{(Nv`c@eU~m> zyfnP!i6A+4PlyA{VJF{u2C+<(F(<`_2fs#ON~-oQx*d%kTV&Ok;4$YRh!LFrhyL{rlgEv<*McHn^dsFSpslM%3OA#euOG` z49AgkFmS<-cR)Psq;5BiT=N!qZ<(Vujo(5wi%NX0zu|Fx^TNmT9|MdQFW(!!JG^*t zt)aWv+W9!zR)`+RM-LRD$MezSkINW4nr}J!!Y?#b&3(UAg&LdYE)<(O?zP`-UvG*% zM@;zL&xK-5<3iPZ)uLJGIGXP`x{B93PW`3kbcuu6k_Uxr3!$!jsOwQ^U$L%vZs6rN z)zRmO1y@3~YkLl@cCOZ~MUFlS9V^z=f7<`=k^obi8|~oo&2#PNI?=;Ue=pBHe6#x8 z8Sde6hW5`0Q2!szutj$H&ZGH&i)Q*Dmaog2E??1c1=Qkx`a%|MijKv}F^GwL52{@A zuFwS|fLn5kF1rBL4sZlq0Kmdr<8H`+iGHg19fn1G>DrRA7J2(o=?b1sY;tL{Ubr&8IoiLBq^CzvpVgEBO-iku={^Q!MZL>bXj^Ko69P;7HV`z=-mDWE?jJ5ezhRYMz5M$C6*o9y&K5)VKe9ajPbwxfNc#h^ zAQ^O&{olzN-m++}p~_$T36{-=^=Bvpw&U1Po~~ zMjuqbSq$?Piv9z2=TZ09sIA0vOzq$JnrA-5*M7~nzVLj=Fb$ud{1(ynWvLqZ>VO8B d+G42YW*=3r-85+O{w3dy9(a#&e_)Pr{{d*3+MNIZ literal 0 HcmV?d00001 diff --git a/server/scrapers/__pycache__/base.cpython-312.pyc b/server/scrapers/__pycache__/base.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a710c0e25f79e5c8f7179993212433e134de9cd3 GIT binary patch literal 2876 zcmd5;&2Jk;6rcUDypjH(^v<0C`5Tv%b__EsUjN@&0z3%Kf zsVyIR$e{<2xJ5`v^w0xUFXeyW#tFe8EQcb2gnB`n1};7E-mJZG+Dg4L(!6=|@n+}! z-q-$EEb0W>57qO|x<<$!I2pCfkU6*t%r2p%L8wG!SMubB>?sXJ!nxwAUZ#=pvW={l zYvjb5>S~_W(7b#jFAI7k=e&pC;q&w3DgSP^iq*rc9L`wUJ?2TBa(?(G1gAei6<# zRhr$&JRFKQbT~2h!in72i2~JjNMnK(^MuaRJX5I-y#RdzdJ%dF`Xuxz=+n?MEZ;2C znXx-cz1iBasBj4&x|U$lKmRh|C~G=xr@d)7?U3=NWwRHtj0%C@F`bPUu*!(`O@_t)_b^bCGWb7t$F@jy>dp zvEFuWbwOS{&cMeRZ?e_R)fJ;^@!QT;6_`yYY;`w~^B8ryUbU9qw^oz+1UxU&!G%K8 zZZZ2NxPTfkw<$IPRL?M;ovXKekJYVq=sRszr);a<@$LG@7I%9gvs?9m@h!&dBj>B5 zW3G03k!G3zZfKhQ@|Yo3N2>+6K9~h^hr}gvV)?H6i?*;=IeAz8a`FI{NtAIN&k3U} zXJMDOk3f(60(upgT_WI(Q{57q8!mWr5E>Q_9ouC=-3gfo!NPrSh}t3O>j9(S1je=% z+AYKLDS+AWT^D?#c6ciNz>B64PrYD#X5DtYu4lBn-Uj1_-!zhshQ5(#kzRrJOea@` zhR<0WgA{LQ``i=00xv`jQmM5E}r>4O{) zFv9}L7x@hEuf+QA^btd>G5uIumg8C*E_)90LjXrGZ&>!trUN1g(k;-`=e_?i(j4<2 zKL+Fo%K8vW(7r$a5{O}a2?a3`Ccvf{0anm~g%f`W%q|Jjy&L0M2KKOp=;@+89G-*x zg3_Xi064MU!2}L|4~`=O+Q4KUgqGJ?U8|`a$`y%}w!h8u!0{50nj{=3z&nk^b*8)G z8gQLaUg*CzuG~~XVUK@p)h%^JDw%c}^4&(+X zv7QwkdU7iA($K}qYHUODP356zcWp>xEETNfBiW(cfYh3g__(JzJS91D&h z7zV*_Kz7Kl>ZMrKGNO1=A-3K?GJN2|8XI5g3WM|;k%F_Pq@r=GVU5*tPf1g{3Gcd+kGBq3LkkiQgVw^`# zN3MX}1eu+Wi^!F5Z4$XDvUnn%Ms9|bkHvT^+^?6O&H;ySoY|Lw6Q&T_uGI=0BQ#R1 z4Vz;`ByeUSVxYW@A&ON%?G92~}^5un?~iX=(DlCyu2rN?CX XC$jvQRR76dktF@ArGE*I;ywQXXF zm{xi3NPF*{d(XM&oICe?=bZgZAmByt{Hg7*@@6eUU(*-+;VOgG0T@gp84V$sksT^C z#8|y!$N{}mbtc&%HpvZfN!O5zK{6|IsyoRK@eFdH<4AU0L9!e33(oR>L!NO*gg5F= z#fX%_L>NvJ;kcro$X@7rKq&?yPLq9A(R7pTQ)0Twzn;;RbV^bmFt8e7Okc00;R<)G zyl1)PD=({)F!&Xyj1j1csG37gxkh$i7IWKRZP<}V!=U<=z>rI3v0LUaC%dqF*d@EK zutU5YlzEu(U=PfCvD@Nt$=)m6kPp^;8|GQr5A%Mx_6PF;m=EO97&BCJ3PrY<0WdR> zy--oz5Po<90>PQIrs!#+V6DsK^z=oXia9EF(T*M>{jHXu`7JYzbm+^^RYwlJ4VK`IW%IQ zyUhqaX_noauY8C;0{#*II^xPu=8wW?+%xWpc#Ugbq4JzeDY_!5ih(QFM9|7Nr>ja* z9rTd6)?Iz08(1gUZF5?0LPxrCqxK1ET9P$kn52`!@sp=doaq%$zy3=9fPhn@!l*N?e?48K-Uz7@iQ`UHO2xAh43cRXtEN*`lcrB6Qc8pSoy5cl>Ud1=jN=q0 z61~qx3&A>k=21!Ql(O+8PU%=)w{AL#6gOS4WlGmfcPt_47t*@n8&*=dQ_rT9RNVAm z%1S9+F{}+uR!tMhWHWeJH$Ad4Je<|2oLWtnE~vP3-Hei?ny%?$dXrgIS3q@1HEOgB z#<4`Fp6;Y71AqBl8&By8tSOqwNm*G*8w~_2DX=bvsq&pFmI&3#2*%RMOjgIFb5yZ3 z*e+!Iv|Yvgrd6x5;T)KogS}MFlR5D4LG61|JRU@F@ueInxE|Co!C5 z0!uO`8pRn6tHb40k0{1eNz+78qy8lb?|$DCO{9}J3P+rVLyyXMG@41rqWuz4$8|WO zXj%1WYuxD;bY~xGFMYl50#n@Jmj2VI^kd8k-zp@_piH4UC%Dc ze;E0EWbu4qUvIwe+N)PzooT(}kE}S6zhM<*A(N#T1JFVb!*szV%#u`$nruG-7+4U~ z4!|=F5HeDM6PtkcYalb>%sHMwQ-u`(AqPYTXWTKtK7p29s7N6~jvZOAPetti;rnRL%L2AHW2kkicf;Bm(1>l?y!KAyo5m+1u1FdeUS;me4u$Zo4MFq5sxX4bBPxjEJoi-7w zPQcbc1c5r~ggK4gJ{sHvf$X9*L2*qAwbp2U3~@Ft@1}cv3(dReekeSBV(P@qxnd}i z_msA^%(9>IpYU@{Md8pLq5GDj5I&L*lzhQ!hp!%_>=Hl zM{(!DyZ%G>>$gq6IrZj@TC9KiPJQ2;{@K|4*ezc%+L!m<57sRQ+m?cDrBHJzykj}s zu@vqoZEcwDnCh7MNpWi%On%H?=VzOX;pm<4vy0Ez6OD6i#c=0x_`p*5z~WQ4o+*Z3 zEPOzv$b4l$I+?w}POu)NS21y9 zeR>9r^sz9ann;!Bta(VFZn_pKD}XTEW=JBtFLuz+538H(u^BeXRTP(skX}t8 zcY_`i{5nknIsS2}F~?azq6z+O{#2#E%v?re&a>z;6A2o7PC=3oOKixch0Bn=32|i< zrv&TEcNu$au#hrJB_pu_L$kP}Rn&k?9FhxNBAUrkB#N*ma|tqsG|>T#2xJoEWiaWuvybJ@&asE*3btQ=~+#TLAq*s`p0Oh1=*s>Wr&i3(u?M15l8!Uq@omJjtU9qKC_>@PIDe7E*^sd3w+=YCz|^z&2Ce{dw zVS|AIuw^F!0B@Vp_Mx|W5rUMB{Se8Hj~rG?>gDTw;I#YTx7Tv|;fKFt6T#2Ha_DDDS@4)I1~2*5BMUCx3Qv{$g=& z_tMVpV$+d)kcRMcQ|BHb&eO}}`%8^GCgX+X-a_aYyoAEbq4uRv`)qdcokFO+7&@Ns zzaMUXgqT1tGs%`}8>efgYG(B1)`Lr};5oPU6kA{VYi-X%CnZ?%&^=e6$O$a-ElYgM zU0x^!8zx6*xcoZ>zQu~k|G(!y`kqHC_gqp{K^m15l~(#2uX~3T`79ub{D=#GU_TMz z(hQKFLrX9;n_NS0<)(pnBfJsP*H&b@!3b@3Lla>JA`E%W8a<#(ksz(vsry0A?iEG& z6C$h9v0oJ7?+vxG;ub|Y9TP>u0gvf~OxUWe2@9`fr*lHwA{6{Aha?IesgyWSsqAAI zN)QpGx52DN^S75QZz42aSq&X%Z$tCl74#Lm_aWQB9b;w^v)c9Kw}_5c_SbRV*}fG7 z-C~{9-3qPJw}*S1xQ5w-D+s#9T~>EXzuosoW0lT4JRIO49IYVe7TLwqpLy` zc3RVO^5UWSk$=%?>!2)!B3^QdD%VV#L1>_28qdI=EM-G2FTj7IghI2$H(x$% z%e?RTqvbPwk&MAQwOiW*&5Dy@n6J>buaW0(DEJTb)Dn8?@2L43SAt<$Ctvv%(cV(! EKce$<(EtDd literal 0 HcmV?d00001 diff --git a/server/scrapers/__pycache__/huggingface_scraper.cpython-312.pyc b/server/scrapers/__pycache__/huggingface_scraper.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8119959c19df01787329bccf25cf43e182326799 GIT binary patch literal 5470 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 Dict: + """Normalize arXiv result.""" + authors = ", ".join([a.name for a in paper.authors]) + link = paper.entry_id + + keywords_list = [paper.primary_category] + if paper.categories: + keywords_list.extend(paper.categories) + + return self.normalize_item( + item_id=link, + source_site=self.source_name, + title=paper.title.replace('\n', ' '), + description=paper.summary.replace('\n', ' '), + author_info=authors, + keywords=", ".join(keywords_list), + content_url=link, + published_date=paper.published.isoformat(), + item_type="paper" + ) + + def scrape_latest(self, limit: int = 20) -> List[Dict]: + """Scrape latest articles.""" + try: + search = arxiv.Search( + query=f"cat:{self.category}", + max_results=limit, + sort_by=arxiv.SortCriterion.SubmittedDate, + sort_order=arxiv.SortOrder.Descending + ) + + results = [] + for paper in search.results(): + results.append(self._normalize_result(paper)) + + self.update_last_check() + return results + + except Exception as e: + print(f"[ERROR] ArXiv scrape_latest: {e}") + return [] + + def scrape_all(self, limit: int = 100) -> List[Dict]: + """Scrape all available articles (with limit).""" + try: + search = arxiv.Search( + query=f"cat:{self.category}", + max_results=limit, + sort_by=arxiv.SortCriterion.SubmittedDate, + sort_order=arxiv.SortOrder.Descending + ) + + results = [] + for paper in search.results(): + results.append(self._normalize_result(paper)) + + self.update_last_check() + return results + + except Exception as e: + print(f"[ERROR] ArXiv scrape_all: {e}") + return [] diff --git a/server/scrapers/base.py b/server/scrapers/base.py new file mode 100644 index 0000000..fb00de8 --- /dev/null +++ b/server/scrapers/base.py @@ -0,0 +1,81 @@ +"""Abstract base class for all scrapers.""" + +from abc import ABC, abstractmethod +from typing import List, Dict, Optional +from datetime import datetime, UTC + + +class BaseScraper(ABC): + """Abstract base class defining interface for all scrapers.""" + + def __init__(self, source_name: str): + """ + Initialize scraper. + + Args: + source_name: Unique source name (e.g., "arxiv", "github", "medium") + """ + self.source_name = source_name + self.last_check = None + + @abstractmethod + def scrape_latest(self, limit: int = 20) -> List[Dict]: + """ + Scrape latest articles/items from source. + Used in watch mode (polling). + + Args: + limit: Maximum number of items to return + + Returns: + List of normalized items + """ + pass + + @abstractmethod + def scrape_all(self, limit: int = 100) -> List[Dict]: + """ + Scrape all available articles/items. + Used in backfill mode (history). + + Args: + limit: Maximum number of items to return + + Returns: + List of normalized items + """ + pass + + def update_last_check(self): + """Update last check timestamp.""" + self.last_check = datetime.now(UTC) + + @staticmethod + def normalize_item( + item_id: str, + source_site: str, + title: str, + description: str, + author_info: str, + keywords: str, + content_url: str, + published_date: str, + item_type: str = "article" + ) -> Dict: + """ + Normalize item to unified format. + + Returns: + Dict with unified structure + """ + return { + "id": item_id, + "source_site": source_site, + "title": title, + "description": description, + "author_info": author_info, + "keywords": keywords, + "content_url": content_url, + "published_date": published_date, + "item_type": item_type, + } diff --git a/server/scrapers/github_scraper.py b/server/scrapers/github_scraper.py new file mode 100644 index 0000000..350f479 --- /dev/null +++ b/server/scrapers/github_scraper.py @@ -0,0 +1,113 @@ +"""Scraper for GitHub.""" + +import os +import requests +from typing import List, Dict, Optional +from .base import BaseScraper + + +class GithubScraper(BaseScraper): + """Scraper for GitHub repositories.""" + + def __init__(self, token: Optional[str] = None): + """ + Initialize GitHub scraper. + + Args: + token: GitHub token (optional, loads from GITHUB_TOKEN env var) + """ + super().__init__("github") + self.token = token or os.getenv("GITHUB_TOKEN") + self.themes = [ + "large-language-model", "llm", "transformer", "text-generation", + "retrieval-augmented-generation", "rag", "agents", "chatbot", + "fine-tuning", "quantization", "lora", "peft", "diffusion", + "stable-diffusion", "image-generation", "multimodal", + "speech-to-text", "speech-synthesis", "audio", + "reinforcement-learning", "computer-vision", + ] + self.headers = { + "Accept": "application/vnd.github+json", + "User-Agent": "server-ai-watcher/1.0" + } + if self.token: + self.headers["Authorization"] = f"Bearer {self.token}" + + def _normalize_repo(self, repo: Dict, theme: str) -> Dict: + """Normalize GitHub repository.""" + full_name = repo.get("full_name") + keywords_list = [theme, repo.get("language") or ""] + if repo.get("topics"): + keywords_list.extend(repo.get("topics")) + + updated_at = repo.get("updated_at") or repo.get("pushed_at") + + return self.normalize_item( + item_id=full_name, + source_site=self.source_name, + title=repo.get("name"), + description=repo.get("description") or "", + author_info=repo.get("owner", {}).get("login", ""), + keywords=", ".join(filter(None, keywords_list)), + content_url=repo.get("html_url") or f"https://github.com/{full_name}", + published_date=updated_at, + item_type="repository" + ) + + def _search_repos(self, query: str, per_page: int = 20) -> List[Dict]: + """Search repositories with given query.""" + url = "https://api.github.com/search/repositories" + params = { + "q": query, + "sort": "stars", + "order": "desc", + "per_page": per_page + } + + try: + resp = requests.get(url, headers=self.headers, params=params, timeout=20) + + if resp.status_code == 403: + retry_after = resp.headers.get("Retry-After") + raise Exception(f"GitHub rate limit hit. Retry after: {retry_after}") + + if resp.status_code != 200: + print(f"[WARN] GitHub API returned {resp.status_code}") + return [] + + data = resp.json() + return data.get("items", []) + + except Exception as e: + print(f"[ERROR] GitHub search: {e}") + return [] + + def scrape_latest(self, limit: int = 20) -> List[Dict]: + """Scrape latest repositories for themes.""" + results = [] + items_per_theme = max(1, limit // len(self.themes)) + + for theme in self.themes: + query = f"{theme} in:name,description,readme stars:>50" + repos = self._search_repos(query, per_page=items_per_theme) + + for repo in repos: + results.append(self._normalize_repo(repo, theme)) + + self.update_last_check() + return results[:limit] + + def scrape_all(self, limit: int = 100) -> List[Dict]: + """Scrape all available repositories (with limit).""" + results = [] + items_per_theme = max(1, limit // len(self.themes)) + + for theme in self.themes: + query = f"{theme} in:name,description,readme stars:>10" + repos = self._search_repos(query, per_page=items_per_theme) + + for repo in repos: + results.append(self._normalize_repo(repo, theme)) + + self.update_last_check() + return results[:limit] diff --git a/server/scrapers/huggingface_scraper.py b/server/scrapers/huggingface_scraper.py new file mode 100644 index 0000000..1a51781 --- /dev/null +++ b/server/scrapers/huggingface_scraper.py @@ -0,0 +1,105 @@ +"""Scraper for Hugging Face.""" + +import requests +from typing import List, Dict, Optional +from datetime import datetime, UTC +from .base import BaseScraper + + +class HuggingFaceScraper(BaseScraper): + """Scraper for Hugging Face Hub.""" + + def __init__(self): + """Initialize Hugging Face scraper.""" + super().__init__("huggingface") + self.endpoints = [ + ("models", "model"), + ("datasets", "dataset"), + ("spaces", "space"), + ("collections", "collection"), + ("papers", "paper"), + ] + + def _build_url(self, item: Dict, item_type: str) -> str: + """Build public URL for item.""" + base = "https://huggingface.co" + item_id = item.get("id") + + if item_type == "model": + return f"{base}/{item.get('modelId')}" + elif item_type in ("dataset", "space", "collection", "paper"): + return f"{base}/{item_id}" + + return base + + def _normalize_item(self, item: Dict, item_type: str) -> Dict: + """Normalize Hugging Face item.""" + item_name = item.get("name") or item.get("modelId") or item.get("id") + item_id = item.get("id") or item.get("modelId") or item.get("name") + + author = item.get("author") or item.get("organization", "") + description = item.get("description", item_name) + + keywords_list = [] + if item.get("tags"): + keywords_list.extend(item.get("tags")) + if item.get("pipeline_tag"): + tag = item.get("pipeline_tag") + keywords_list.append(tag if isinstance(tag, str) else ", ".join(tag)) + + last_modified = item.get("lastModified") or item.get("last_modified") or datetime.now(UTC).isoformat() + + return self.normalize_item( + item_id=item_id, + source_site=self.source_name, + title=item_name, + description=description, + author_info=author, + keywords=", ".join(keywords_list), + content_url=self._build_url(item, item_type), + published_date=last_modified, + item_type=item_type + ) + + def _fetch_endpoint(self, endpoint: str, item_type: str, limit: int = 20) -> List[Dict]: + """Fetch data from specific endpoint.""" + url = f"https://huggingface.co/api/{endpoint}?sort=lastModified&direction=-1&limit={limit}" + + try: + r = requests.get(url, timeout=20) + + if r.status_code == 404: + return [] + + r.raise_for_status() + + items = r.json() + return [self._normalize_item(item, item_type) for item in items] + + except Exception as e: + print(f"[ERROR] HF {item_type}: {e}") + return [] + + def scrape_latest(self, limit: int = 20) -> List[Dict]: + """Scrape latest items.""" + all_items = [] + items_per_endpoint = max(1, limit // len(self.endpoints)) + + for endpoint, item_type in self.endpoints: + items = self._fetch_endpoint(endpoint, item_type, items_per_endpoint) + all_items.extend(items) + + self.update_last_check() + return all_items[:limit] + + def scrape_all(self, limit: int = 100) -> List[Dict]: + """Scrape all available items.""" + all_items = [] + items_per_endpoint = max(1, limit // len(self.endpoints)) + + for endpoint, item_type in self.endpoints: + items = self._fetch_endpoint(endpoint, item_type, items_per_endpoint) + all_items.extend(items) + + self.update_last_check() + return all_items[:limit] diff --git a/server/scrapers/lemonde_scraper.py b/server/scrapers/lemonde_scraper.py new file mode 100644 index 0000000..6219bee --- /dev/null +++ b/server/scrapers/lemonde_scraper.py @@ -0,0 +1,108 @@ +"""Scraper for Le Monde.""" + +from typing import List, Dict +from .base import BaseScraper + +try: + import feedparser +except ImportError: + feedparser = None + + +class LeMondeScraper(BaseScraper): + """Scraper for Le Monde articles.""" + + def __init__(self): + """Initialize Le Monde scraper.""" + super().__init__("le_monde") + self.feeds = [ + "https://www.lemonde.fr/international/rss_full.xml", + "https://www.lemonde.fr/actualite-medias/rss_full.xml", + "https://www.lemonde.fr/en_continu/rss_full.xml" + ] + + if feedparser is None: + raise ImportError("feedparser package is required. Install it with: pip install feedparser") + + def _normalize_entry(self, entry: Dict, feed_url: str) -> Dict: + """Normalize RSS entry from Le Monde.""" + import time + from datetime import datetime + + entry_id = getattr(entry, "id", None) or getattr(entry, "link", None) + + published_date = datetime.utcnow().isoformat() + if getattr(entry, "published_parsed", None): + published_date = datetime.fromtimestamp(time.mktime(entry.published_parsed)).isoformat() + elif getattr(entry, "updated_parsed", None): + published_date = datetime.fromtimestamp(time.mktime(entry.updated_parsed)).isoformat() + + category = "general news" + if "international" in feed_url: + category = "international" + elif "medias" in feed_url: + category = "media news" + elif "continu" in feed_url: + category = "continuous" + + return self.normalize_item( + item_id=entry_id, + source_site=self.source_name, + title=getattr(entry, "title", ""), + description=getattr(entry, "summary", ""), + author_info=getattr(entry, "author", "Le Monde"), + keywords=category, + content_url=getattr(entry, "link", ""), + published_date=published_date, + item_type="article" + ) + + def scrape_latest(self, limit: int = 20) -> List[Dict]: + """Scrape latest articles.""" + import time + + all_items = [] + unique_ids = set() + items_per_feed = limit // len(self.feeds) + 1 + + for feed_url in self.feeds: + try: + feed = feedparser.parse(feed_url) + + for entry in feed.entries[:items_per_feed]: + entry_id = getattr(entry, "id", None) or getattr(entry, "link", None) + if entry_id and entry_id not in unique_ids: + all_items.append(self._normalize_entry(entry, feed_url)) + unique_ids.add(entry_id) + + time.sleep(1) + except Exception as e: + print(f"[ERROR] Le Monde feed {feed_url}: {e}") + + self.update_last_check() + return all_items[:limit] + + def scrape_all(self, limit: int = 100) -> List[Dict]: + """Scrape all available articles.""" + import time + + all_items = [] + unique_ids = set() + items_per_feed = limit // len(self.feeds) + 1 + + for feed_url in self.feeds: + try: + feed = feedparser.parse(feed_url) + + for entry in feed.entries[:items_per_feed]: + entry_id = getattr(entry, "id", None) or getattr(entry, "link", None) + if entry_id and entry_id not in unique_ids: + all_items.append(self._normalize_entry(entry, feed_url)) + unique_ids.add(entry_id) + + time.sleep(1) + except Exception as e: + print(f"[ERROR] Le Monde feed {feed_url}: {e}") + + self.update_last_check() + return all_items[:limit] diff --git a/server/scrapers/medium_scraper.py b/server/scrapers/medium_scraper.py new file mode 100644 index 0000000..875bf6f --- /dev/null +++ b/server/scrapers/medium_scraper.py @@ -0,0 +1,101 @@ +"""Scraper for Medium.""" + +from typing import List, Dict +from .base import BaseScraper + +try: + import feedparser +except ImportError: + feedparser = None + + +class MediumScraper(BaseScraper): + """Scraper for Medium articles.""" + + def __init__(self): + """Initialize Medium scraper.""" + super().__init__("medium") + self.feeds = [ + "https://medium.com/feed/tag/artificial-intelligence", + "https://medium.com/feed/tag/machine-learning", + "https://medium.com/feed/tag/deep-learning", + "https://medium.com/feed/tag/ai", + ] + + if feedparser is None: + raise ImportError("feedparser package is required. Install it with: pip install feedparser") + + def _normalize_entry(self, entry: Dict) -> Dict: + """Normalize RSS entry from Medium.""" + import time + from datetime import datetime + + entry_id = entry.get('link', '') + + published_date = datetime.utcnow().isoformat() + if getattr(entry, "published_parsed", None): + published_date = datetime.fromtimestamp(time.mktime(entry.published_parsed)).isoformat() + + keywords = [tag.term for tag in entry.get('tags', [])] if 'tags' in entry else [] + + return self.normalize_item( + item_id=entry_id, + source_site=self.source_name, + title=entry.get('title', 'N/A'), + description=entry.get('summary', 'N/A'), + author_info=entry.get('author', 'N/A'), + keywords=", ".join(keywords), + content_url=entry_id, + published_date=published_date, + item_type="article" + ) + + def scrape_latest(self, limit: int = 20) -> List[Dict]: + """Scrape latest articles.""" + import time + + all_items = [] + unique_links = set() + items_per_feed = limit // len(self.feeds) + 1 + + for feed_url in self.feeds: + try: + feed = feedparser.parse(feed_url) + + for entry in feed.entries[:items_per_feed]: + link = entry.get('link') + if link and link not in unique_links: + all_items.append(self._normalize_entry(entry)) + unique_links.add(link) + + time.sleep(1) + except Exception as e: + print(f"[ERROR] Medium feed {feed_url}: {e}") + + self.update_last_check() + return all_items[:limit] + + def scrape_all(self, limit: int = 100) -> List[Dict]: + """Scrape all available articles.""" + import time + + all_items = [] + unique_links = set() + items_per_feed = limit // len(self.feeds) + 1 + + for feed_url in self.feeds: + try: + feed = feedparser.parse(feed_url) + + for entry in feed.entries[:items_per_feed]: + link = entry.get('link') + if link and link not in unique_links: + all_items.append(self._normalize_entry(entry)) + unique_links.add(link) + + time.sleep(1) + except Exception as e: + print(f"[ERROR] Medium feed {feed_url}: {e}") + + self.update_last_check() + return all_items[:limit] diff --git a/server/test_server.py b/server/test_server.py new file mode 100644 index 0000000..5e0961c --- /dev/null +++ b/server/test_server.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python3 +""" +Quick server tests - Verify all components are functional. +""" + +import sys +import os + +def test_imports(): + """Test that all imports work.""" + print("🧪 Test 1 : Checking imports...") + try: + from database import DatabaseManager + from embeddings import EmbeddingManager, DummyEmbeddingProvider + from config import ServerConfig + print(" ✓ Core imports OK") + + from scrapers.base import BaseScraper + print(" ✓ BaseScraper OK") + + try: + from scrapers.arxiv_scraper import ArxivScraper + print(" ✓ ArxivScraper OK") + except ImportError as e: + print(f" ⚠️ ArxivScraper : {e}") + + from scrapers.github_scraper import GithubScraper + print(" ✓ GithubScraper OK") + + from scrapers.medium_scraper import MediumScraper + print(" ✓ MediumScraper OK") + + from scrapers.lemonde_scraper import LeMondeScraper + print(" ✓ LeMondeScraper OK") + + from scrapers.huggingface_scraper import HuggingFaceScraper + print(" ✓ HuggingFaceScraper OK") + + return True + except Exception as e: + print(f" ❌ Error : {e}") + return False + + +def test_database(): + """Test that database works.""" + print("\n🧪 Test 2 : Checking database...") + try: + from database import DatabaseManager + + import tempfile + import os + + with tempfile.NamedTemporaryFile(delete=False, suffix='.db') as f: + db_path = f.name + + try: + db = DatabaseManager(db_path) + print(" ✓ DB created") + + with db.get_connection() as conn: + cur = conn.cursor() + cur.execute("SELECT name FROM sqlite_master WHERE type='table'") + tables = [row[0] for row in cur.fetchall()] + + expected = {"articles", "embeddings", "sync_history"} + if expected.issubset(set(tables)): + print(f" ✓ Tables created : {', '.join(sorted(tables))}") + else: + print(f" ❌ Missing tables. Found : {tables}") + return False + + test_article = { + "id": "test-123", + "source_site": "test", + "title": "Test Article", + "description": "Test", + "author_info": "Tester", + "keywords": "test", + "content_url": "http://test.com", + "published_date": "2024-01-01T00:00:00Z", + "item_type": "article" + } + + if db.save_article(test_article): + print(" ✓ Article inserted successfully") + else: + print(" ❌ Error during insertion") + return False + + if db.article_exists("test-123"): + print(" ✓ Existence check OK") + else: + print(" ❌ Article not found after insertion") + return False + + return True + finally: + if os.path.exists(db_path): + os.unlink(db_path) + + except Exception as e: + print(f" ❌ Error : {e}") + import traceback + traceback.print_exc() + return False + + +def test_embeddings(): + """Test that embeddings work.""" + print("\n🧪 Test 3 : Checking embeddings...") + try: + from embeddings import EmbeddingManager, DummyEmbeddingProvider + + provider = DummyEmbeddingProvider(dimension=384) + manager = EmbeddingManager(provider) + print(" ✓ EmbeddingManager created") + + embedding = manager.embed_text("Test text") + print(f" ✓ Embedding generated ({len(embedding)} bytes)") + + test_article = { + "title": "Test Title", + "description": "Test Description" + } + embedding = manager.embed_article(test_article) + print(f" ✓ Article embedded ({len(embedding)} bytes)") + + import pickle + deserialized = pickle.loads(embedding) + print(f" ✓ Embedding deserialized (shape: {deserialized.shape})") + + return True + except Exception as e: + print(f" ❌ Error : {e}") + import traceback + traceback.print_exc() + return False + + +def test_server_creation(): + """Test that server is created correctly.""" + print("\n🧪 Test 4 : Checking server creation...") + try: + from main import WatchServer + + server = WatchServer(check_interval=300) + print(" ✓ Server created") + + if server.scrapers: + print(f" ✓ {len(server.scrapers)} scraper(s) initialized") + for name, scraper in server.scrapers.items(): + print(f" - {name}") + else: + print(" ❌ No scrapers initialized") + return False + + if server.db_manager and server.embedding_manager: + print(" ✓ DatabaseManager and EmbeddingManager OK") + else: + print(" ❌ Managers not initialized") + return False + + return True + except Exception as e: + print(f" ❌ Error : {e}") + import traceback + traceback.print_exc() + return False + + +def main(): + """Run all tests.""" + print("\n" + "="*60) + print("🧪 QUICK TESTS - Watch Server") + print("="*60) + + tests = [ + test_imports, + test_database, + test_embeddings, + test_server_creation, + ] + + results = [] + for test in tests: + try: + result = test() + results.append((test.__name__, result)) + except Exception as e: + print(f"\n❌ Unhandled exception in {test.__name__}: {e}") + import traceback + traceback.print_exc() + results.append((test.__name__, False)) + + print("\n" + "="*60) + print("📊 SUMMARY") + print("="*60) + + passed = sum(1 for _, result in results if result) + total = len(results) + + for name, result in results: + status = "✅" if result else "❌" + print(f"{status} {name}") + + print(f"\nTotal : {passed}/{total} tests passed") + + if passed == total: + print("\n🎉 All tests passed!") + return 0 + else: + print(f"\n⚠️ {total - passed} test(s) failed") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/server/veille_technique.db b/server/veille_technique.db new file mode 100644 index 0000000000000000000000000000000000000000..f5af91064d89fd32542b219dc28a36730c31bb5d GIT binary patch literal 196608 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

@5PQOA05?MjGhkk90;{DPYSb*!zew^+ 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 TW&W{PJY6h)TQBSbxtsk9hCQ^C literal 0 HcmV?d00001 From 814bafbce32c93dde67fdbf210b99458f6afdd9b Mon Sep 17 00:00:00 2001 From: Antoine0703 Date: Tue, 9 Dec 2025 15:57:29 +0100 Subject: [PATCH 2/4] feat(server): update full_content scrapping --- server/__pycache__/config.cpython-312.pyc | Bin 3013 -> 0 bytes server/__pycache__/database.cpython-312.pyc | Bin 13800 -> 0 bytes server/__pycache__/embeddings.cpython-312.pyc | Bin 6631 -> 0 bytes server/__pycache__/main.cpython-312.pyc | Bin 13440 -> 0 bytes server/database.py | 49 +++++++++++- server/embeddings.py | 14 +++- server/main.py | 9 +++ server/requirements.txt | 2 + .../__pycache__/__init__.cpython-312.pyc | Bin 156 -> 0 bytes .../__pycache__/arxiv_scraper.cpython-312.pyc | Bin 4299 -> 0 bytes .../scrapers/__pycache__/base.cpython-312.pyc | Bin 2876 -> 0 bytes .../github_scraper.cpython-312.pyc | Bin 5670 -> 0 bytes .../huggingface_scraper.cpython-312.pyc | Bin 5470 -> 0 bytes .../lemonde_scraper.cpython-312.pyc | Bin 4869 -> 0 bytes .../medium_scraper.cpython-312.pyc | Bin 4583 -> 0 bytes server/scrapers/arxiv_scraper.py | 74 ++++++++++++++++++ server/scrapers/base.py | 16 +++- server/scrapers/github_scraper.py | 21 ++++- server/scrapers/huggingface_scraper.py | 19 +++++ server/scrapers/lemonde_scraper.py | 29 ++++++- server/scrapers/medium_scraper.py | 24 +++++- server/veille_export.db | Bin 0 -> 40960 bytes server/veille_technique.db | Bin 196608 -> 598016 bytes 23 files changed, 245 insertions(+), 12 deletions(-) delete mode 100644 server/__pycache__/config.cpython-312.pyc delete mode 100644 server/__pycache__/database.cpython-312.pyc delete mode 100644 server/__pycache__/embeddings.cpython-312.pyc delete mode 100644 server/__pycache__/main.cpython-312.pyc delete mode 100644 server/scrapers/__pycache__/__init__.cpython-312.pyc delete mode 100644 server/scrapers/__pycache__/arxiv_scraper.cpython-312.pyc delete mode 100644 server/scrapers/__pycache__/base.cpython-312.pyc delete mode 100644 server/scrapers/__pycache__/github_scraper.cpython-312.pyc delete mode 100644 server/scrapers/__pycache__/huggingface_scraper.cpython-312.pyc delete mode 100644 server/scrapers/__pycache__/lemonde_scraper.cpython-312.pyc delete mode 100644 server/scrapers/__pycache__/medium_scraper.cpython-312.pyc create mode 100644 server/veille_export.db diff --git a/server/__pycache__/config.cpython-312.pyc b/server/__pycache__/config.cpython-312.pyc deleted file mode 100644 index 7ff5409a6af7b4c0edcf3a3f262bfcb2022c7d28..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3013 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 diff --git a/server/__pycache__/embeddings.cpython-312.pyc b/server/__pycache__/embeddings.cpython-312.pyc deleted file mode 100644 index a1a60decc90470149ba40bddda33a30c44793597..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6631 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 diff --git a/server/__pycache__/main.cpython-312.pyc b/server/__pycache__/main.cpython-312.pyc deleted file mode 100644 index ea461d9427f045d4b62ac0a13c27cc6714f19213..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13440 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 bool: @@ -87,6 +92,8 @@ def save_article(self, item: Dict, conn: Optional[sqlite3.Connection] = None) -> Returns: True if inserted, False if already exists """ + import hashlib + should_close = False if conn is None: conn = sqlite3.connect(self.db_path) @@ -95,15 +102,20 @@ def save_article(self, item: Dict, conn: Optional[sqlite3.Connection] = None) -> try: cur = conn.cursor() + full_content = item.get("full_content", item.get("description", "")) + content_hash = hashlib.sha256(full_content.encode()).hexdigest() + cur.execute(""" INSERT OR IGNORE INTO articles - (id, source_site, title, description, author_info, keywords, content_url, published_date, item_type, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + (id, source_site, title, description, full_content, content_hash, author_info, keywords, content_url, published_date, item_type, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( item["id"], item["source_site"], item["title"], item.get("description", ""), + full_content, + content_hash, item.get("author_info", ""), item.get("keywords", ""), item["content_url"], @@ -137,12 +149,43 @@ def save_articles_batch(self, items: List[Dict]) -> int: return count def article_exists(self, article_id: str) -> bool: - """Check if article already exists.""" + """Check if article already exists by ID.""" with self.get_connection() as conn: cur = conn.cursor() cur.execute("SELECT 1 FROM articles WHERE id = ?", (article_id,)) return cur.fetchone() is not None + def article_exists_by_hash(self, content_hash: str) -> bool: + """ + Check if article already exists by content hash. + + Args: + content_hash: SHA256 hash of article content + + Returns: + True if article with same content exists + """ + with self.get_connection() as conn: + cur = conn.cursor() + cur.execute("SELECT 1 FROM articles WHERE content_hash = ?", (content_hash,)) + return cur.fetchone() is not None + + def get_article_by_hash(self, content_hash: str) -> Optional[Dict]: + """ + Get article by content hash. + + Args: + content_hash: SHA256 hash of article content + + Returns: + Article dict if found, None otherwise + """ + with self.get_connection() as conn: + cur = conn.cursor() + cur.execute("SELECT * FROM articles WHERE content_hash = ?", (content_hash,)) + row = cur.fetchone() + return dict(row) if row else None + def save_embedding(self, article_id: str, embedding: bytes, model: str = "default") -> bool: """ Save article embedding. diff --git a/server/embeddings.py b/server/embeddings.py index 3c15551..99f2c22 100644 --- a/server/embeddings.py +++ b/server/embeddings.py @@ -111,15 +111,21 @@ def embed_article(self, article: dict) -> bytes: """ Generate embedding for complete article. + Uses full_content if available, otherwise combines title and description. + Args: - article: Dict with title and description + article: Dict with title, description, and optional full_content Returns: Serialized embedding in bytes """ - title = article.get("title", "") - description = article.get("description", "") - text = f"{title}\n{description}" + full_content = article.get("full_content") + if full_content: + text = full_content + else: + title = article.get("title", "") + description = article.get("description", "") + text = f"{title}\n{description}" return self.embed_text(text) diff --git a/server/main.py b/server/main.py index 1b3cd6a..cb19748 100644 --- a/server/main.py +++ b/server/main.py @@ -105,12 +105,21 @@ def _process_articles(self, articles: List[Dict]) -> int: Returns: Number of new articles processed """ + import hashlib + new_count = 0 for article in articles: if self.db_manager.article_exists(article["id"]): continue + full_content = article.get("full_content", article.get("description", "")) + content_hash = hashlib.sha256(full_content.encode()).hexdigest() + + if self.db_manager.article_exists_by_hash(content_hash): + logger.debug(f"Article {article['id']} is duplicate (same content hash)") + continue + if self.db_manager.save_article(article): new_count += 1 diff --git a/server/requirements.txt b/server/requirements.txt index 6e9ace9..ca8dd9a 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -2,3 +2,5 @@ feedparser==6.0.10 requests==2.31.0 arxiv==1.4.8 numpy==1.26.4 +beautifulsoup4==4.12.2 +PyPDF2==3.0.1 diff --git a/server/scrapers/__pycache__/__init__.cpython-312.pyc b/server/scrapers/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 96fed6d9d87636bc9da11774b72d16be477e984e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 156 zcmX@j%ge<81ern>nIQTxh(HIQS%4zb87dhx8U0o=6fpsLpFwJVS?g!y=cei>=9T1U z=B4VVq?YLy&M4u=4F<|$LkeT{^GF7 d%}*)KNwq6t1)9YO#Kj=SM`lJw#v*1Q3jjc4C4&F} diff --git a/server/scrapers/__pycache__/arxiv_scraper.cpython-312.pyc b/server/scrapers/__pycache__/arxiv_scraper.cpython-312.pyc deleted file mode 100644 index d9f5c0a7552a1e3b5e2cbd42a8fad4ff874becb2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4299 zcmeHK+iw)t89#H|`}(rR_WC|HL}G(khj3}Qg@AFXo5Zx3M)fw%XqY*+$IMpt`!Kz*w!d1Kn$Oo@clNEL4jCMtDb`kk5C1z)PZ=AlR0 zbNSBszWKg$&hPh~{Zlv`Kv4eJ^^UeDh|u5Zpj-sEvq7MZz0u(Ju94JIT4r=_9E=t zhll`ngoJkp-D#UZliU!RU}FAUWKbb;1`}~4O+=aeRJ$I3N>yW=#ebj~rp2Gv6w?Y{ z9{N$A)cb?J-uL?lE<9!E3dRbalMU<|7=W?o5;n5BsW{(uYL7eE7=g~uAxMl83S^22H=S2G(=s^*5O*Hpb^>1EO2{Q<|yCx|YM@ zS$YDZ;haglvZB&COGb?ol@Zl1`+veYy~OOWX% z4dhPSEo_N$RUXBnW{3n|%W4Fxaj`#Tn6j>mnki0b=GX}_qh&d(G*`0=t+5tuT1 zpn5rrUQ{7})opLy7hP<8wj3bH%| zn^}_Da!Y8G_AM#FO#t*VDhmj4cM7x`cCPrJTHJp*0ioxb!-Jy40l+|m|8kz1?8+cmL>qDk+kKzil-*hL^Z6Sl1`a8 zWlCA1TUD9t6%2F=JSKN={I#81#T2h97 znK`V0+taXNdGI8BMYXC@<=7-@yW%!AMp|KSEHA+sT~@H=HL^)+(DGNj0*8z=WR#?A z#yq4JmJ>?g79*2=wB>azb=3-&gOeZ@)1cRyGMXroI=ZMRpe#RgAXX3*QYFq6hQQgG=ga?|Sr|Vzi@J-?T6^ zKeRZrUVjkui!I{4Gk4DvT8`vfj;yyFD>k&1>QK$z4HT#elu*DQ_}BAl)OO0CuY_OM z^&a+Fg03Ysa|;R4U9(B(7d8p`ky#<3-H1npRJ9w`e1U$$PBX94+-@YZ8^OG$u?xwJ zhrqjSblLyS5SmrI^S{4Izf=PK&axS?6fP#!EU74~1b_d{(Nv`c@eU~m> zyfnP!i6A+4PlyA{VJF{u2C+<(F(<`_2fs#ON~-oQx*d%kTV&Ok;4$YRh!LFrhyL{rlgEv<*McHn^dsFSpslM%3OA#euOG` z49AgkFmS<-cR)Psq;5BiT=N!qZ<(Vujo(5wi%NX0zu|Fx^TNmT9|MdQFW(!!JG^*t zt)aWv+W9!zR)`+RM-LRD$MezSkINW4nr}J!!Y?#b&3(UAg&LdYE)<(O?zP`-UvG*% zM@;zL&xK-5<3iPZ)uLJGIGXP`x{B93PW`3kbcuu6k_Uxr3!$!jsOwQ^U$L%vZs6rN z)zRmO1y@3~YkLl@cCOZ~MUFlS9V^z=f7<`=k^obi8|~oo&2#PNI?=;Ue=pBHe6#x8 z8Sde6hW5`0Q2!szutj$H&ZGH&i)Q*Dmaog2E??1c1=Qkx`a%|MijKv}F^GwL52{@A zuFwS|fLn5kF1rBL4sZlq0Kmdr<8H`+iGHg19fn1G>DrRA7J2(o=?b1sY;tL{Ubr&8IoiLBq^CzvpVgEBO-iku={^Q!MZL>bXj^Ko69P;7HV`z=-mDWE?jJ5ezhRYMz5M$C6*o9y&K5)VKe9ajPbwxfNc#h^ zAQ^O&{olzN-m++}p~_$T36{-=^=Bvpw&U1Po~~ zMjuqbSq$?Piv9z2=TZ09sIA0vOzq$JnrA-5*M7~nzVLj=Fb$ud{1(ynWvLqZ>VO8B d+G42YW*=3r-85+O{w3dy9(a#&e_)Pr{{d*3+MNIZ diff --git a/server/scrapers/__pycache__/base.cpython-312.pyc b/server/scrapers/__pycache__/base.cpython-312.pyc deleted file mode 100644 index a710c0e25f79e5c8f7179993212433e134de9cd3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2876 zcmd5;&2Jk;6rcUDypjH(^v<0C`5Tv%b__EsUjN@&0z3%Kf zsVyIR$e{<2xJ5`v^w0xUFXeyW#tFe8EQcb2gnB`n1};7E-mJZG+Dg4L(!6=|@n+}! z-q-$EEb0W>57qO|x<<$!I2pCfkU6*t%r2p%L8wG!SMubB>?sXJ!nxwAUZ#=pvW={l zYvjb5>S~_W(7b#jFAI7k=e&pC;q&w3DgSP^iq*rc9L`wUJ?2TBa(?(G1gAei6<# zRhr$&JRFKQbT~2h!in72i2~JjNMnK(^MuaRJX5I-y#RdzdJ%dF`Xuxz=+n?MEZ;2C znXx-cz1iBasBj4&x|U$lKmRh|C~G=xr@d)7?U3=NWwRHtj0%C@F`bPUu*!(`O@_t)_b^bCGWb7t$F@jy>dp zvEFuWbwOS{&cMeRZ?e_R)fJ;^@!QT;6_`yYY;`w~^B8ryUbU9qw^oz+1UxU&!G%K8 zZZZ2NxPTfkw<$IPRL?M;ovXKekJYVq=sRszr);a<@$LG@7I%9gvs?9m@h!&dBj>B5 zW3G03k!G3zZfKhQ@|Yo3N2>+6K9~h^hr}gvV)?H6i?*;=IeAz8a`FI{NtAIN&k3U} zXJMDOk3f(60(upgT_WI(Q{57q8!mWr5E>Q_9ouC=-3gfo!NPrSh}t3O>j9(S1je=% z+AYKLDS+AWT^D?#c6ciNz>B64PrYD#X5DtYu4lBn-Uj1_-!zhshQ5(#kzRrJOea@` zhR<0WgA{LQ``i=00xv`jQmM5E}r>4O{) zFv9}L7x@hEuf+QA^btd>G5uIumg8C*E_)90LjXrGZ&>!trUN1g(k;-`=e_?i(j4<2 zKL+Fo%K8vW(7r$a5{O}a2?a3`Ccvf{0anm~g%f`W%q|Jjy&L0M2KKOp=;@+89G-*x zg3_Xi064MU!2}L|4~`=O+Q4KUgqGJ?U8|`a$`y%}w!h8u!0{50nj{=3z&nk^b*8)G z8gQLaUg*CzuG~~XVUK@p)h%^JDw%c}^4&(+X zv7QwkdU7iA($K}qYHUODP356zcWp>xEETNfBiW(cfYh3g__(JzJS91D&h z7zV*_Kz7Kl>ZMrKGNO1=A-3K?GJN2|8XI5g3WM|;k%F_Pq@r=GVU5*tPf1g{3Gcd+kGBq3LkkiQgVw^`# zN3MX}1eu+Wi^!F5Z4$XDvUnn%Ms9|bkHvT^+^?6O&H;ySoY|Lw6Q&T_uGI=0BQ#R1 z4Vz;`ByeUSVxYW@A&ON%?G92~}^5un?~iX=(DlCyu2rN?CX XC$jvQRR76dktF@ArGE*I;ywQXXF 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 EKce$<(EtDd diff --git a/server/scrapers/__pycache__/huggingface_scraper.cpython-312.pyc b/server/scrapers/__pycache__/huggingface_scraper.cpython-312.pyc deleted file mode 100644 index 8119959c19df01787329bccf25cf43e182326799..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5470 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 str: + """Download and extract text from arXiv PDF.""" + if PyPDF2 is None: + return "" + + try: + response = requests.get(pdf_url, timeout=30) + if response.status_code != 200: + return "" + + pdf_file = io.BytesIO(response.content) + pdf_reader = PyPDF2.PdfReader(pdf_file) + + text_parts = [] + for page in pdf_reader.pages: + text = page.extract_text() + if text: + text_parts.append(text) + + full_text = '\n'.join(text_parts) + return full_text + + except Exception as e: + print(f"[WARN] PDF extraction failed for {pdf_url}: {e}") + return "" + + def _fetch_full_content(self, paper) -> str: + """Fetch full paper content from arXiv PDF.""" + try: + pdf_url = paper.pdf_url + pdf_text = self._extract_pdf_text(pdf_url) + + if pdf_text: + full_content = f"""{paper.title} + +Authors: {', '.join([a.name for a in paper.authors])} + +Category: {paper.primary_category} + +Published: {paper.published.strftime('%Y-%m-%d')} + +Abstract: +{paper.summary} + +Full Paper Content: +{pdf_text}""" + return full_content + else: + full_content = f"""{paper.title} + +Authors: {', '.join([a.name for a in paper.authors])} + +Category: {paper.primary_category} + +{paper.summary}""" + return full_content + except Exception as e: + print(f"[WARN] Content fetch failed: {e}") + return paper.summary + return "" + def _normalize_result(self, paper) -> Dict: """Normalize arXiv result.""" authors = ", ".join([a.name for a in paper.authors]) @@ -37,11 +106,16 @@ def _normalize_result(self, paper) -> Dict: if paper.categories: keywords_list.extend(paper.categories) + full_content = self._fetch_full_content(paper) + if not full_content: + full_content = paper.summary.replace('\n', ' ') + return self.normalize_item( item_id=link, source_site=self.source_name, title=paper.title.replace('\n', ' '), description=paper.summary.replace('\n', ' '), + full_content=full_content, author_info=authors, keywords=", ".join(keywords_list), content_url=link, diff --git a/server/scrapers/base.py b/server/scrapers/base.py index fb00de8..8c1fa66 100644 --- a/server/scrapers/base.py +++ b/server/scrapers/base.py @@ -60,11 +60,24 @@ def normalize_item( keywords: str, content_url: str, published_date: str, - item_type: str = "article" + item_type: str = "article", + full_content: str = "" ) -> Dict: """ Normalize item to unified format. + Args: + item_id: Unique item identifier + source_site: Source name + title: Item title + description: Short description/summary + author_info: Author information + keywords: Tags/keywords + content_url: URL to source + published_date: Publication date + item_type: Type of item + full_content: Complete article text (optional) + Returns: Dict with unified structure """ @@ -73,6 +86,7 @@ def normalize_item( "source_site": source_site, "title": title, "description": description, + "full_content": full_content or description, "author_info": author_info, "keywords": keywords, "content_url": content_url, diff --git a/server/scrapers/github_scraper.py b/server/scrapers/github_scraper.py index 350f479..c6782e3 100644 --- a/server/scrapers/github_scraper.py +++ b/server/scrapers/github_scraper.py @@ -33,6 +33,20 @@ def __init__(self, token: Optional[str] = None): if self.token: self.headers["Authorization"] = f"Bearer {self.token}" + def _fetch_readme(self, full_name: str) -> str: + """Fetch README content from repository.""" + try: + url = f"https://api.github.com/repos/{full_name}/readme" + headers = self.headers.copy() + headers["Accept"] = "application/vnd.github.v3.raw" + + resp = requests.get(url, headers=headers, timeout=10) + if resp.status_code == 200: + return resp.text + except Exception: + pass + return "" + def _normalize_repo(self, repo: Dict, theme: str) -> Dict: """Normalize GitHub repository.""" full_name = repo.get("full_name") @@ -41,12 +55,17 @@ def _normalize_repo(self, repo: Dict, theme: str) -> Dict: keywords_list.extend(repo.get("topics")) updated_at = repo.get("updated_at") or repo.get("pushed_at") + description = repo.get("description") or "" + + readme = self._fetch_readme(full_name) + full_content = f"{description}\n\n{readme}" if readme else description return self.normalize_item( item_id=full_name, source_site=self.source_name, title=repo.get("name"), - description=repo.get("description") or "", + description=description, + full_content=full_content, author_info=repo.get("owner", {}).get("login", ""), keywords=", ".join(filter(None, keywords_list)), content_url=repo.get("html_url") or f"https://github.com/{full_name}", diff --git a/server/scrapers/huggingface_scraper.py b/server/scrapers/huggingface_scraper.py index 1a51781..9a32abc 100644 --- a/server/scrapers/huggingface_scraper.py +++ b/server/scrapers/huggingface_scraper.py @@ -32,6 +32,21 @@ def _build_url(self, item: Dict, item_type: str) -> str: return base + def _fetch_model_card(self, item_id: str, item_type: str) -> str: + """Fetch model card or README from Hugging Face.""" + try: + if item_type == "model": + url = f"https://huggingface.co/{item_id}/raw/main/README.md" + else: + url = f"https://huggingface.co/{item_id}/raw/main/README.md" + + resp = requests.get(url, timeout=10) + if resp.status_code == 200: + return resp.text + except Exception: + pass + return "" + def _normalize_item(self, item: Dict, item_type: str) -> Dict: """Normalize Hugging Face item.""" item_name = item.get("name") or item.get("modelId") or item.get("id") @@ -49,11 +64,15 @@ def _normalize_item(self, item: Dict, item_type: str) -> Dict: last_modified = item.get("lastModified") or item.get("last_modified") or datetime.now(UTC).isoformat() + model_card = self._fetch_model_card(item_id, item_type) + full_content = model_card if model_card else description + return self.normalize_item( item_id=item_id, source_site=self.source_name, title=item_name, description=description, + full_content=full_content, author_info=author, keywords=", ".join(keywords_list), content_url=self._build_url(item, item_type), diff --git a/server/scrapers/lemonde_scraper.py b/server/scrapers/lemonde_scraper.py index 6219bee..6a3b4d5 100644 --- a/server/scrapers/lemonde_scraper.py +++ b/server/scrapers/lemonde_scraper.py @@ -2,6 +2,8 @@ from typing import List, Dict from .base import BaseScraper +import requests +from bs4 import BeautifulSoup try: import feedparser @@ -24,6 +26,22 @@ def __init__(self): if feedparser is None: raise ImportError("feedparser package is required. Install it with: pip install feedparser") + def _fetch_article_content(self, url: str) -> str: + """Fetch full article content from Le Monde.""" + try: + resp = requests.get(url, timeout=10) + if resp.status_code == 200: + soup = BeautifulSoup(resp.content, 'html.parser') + article = soup.find('article') + if article: + for script in article(["script", "style"]): + script.decompose() + text = article.get_text(separator='\n', strip=True) + return text + except Exception: + pass + return "" + def _normalize_entry(self, entry: Dict, feed_url: str) -> Dict: """Normalize RSS entry from Le Monde.""" import time @@ -45,14 +63,21 @@ def _normalize_entry(self, entry: Dict, feed_url: str) -> Dict: elif "continu" in feed_url: category = "continuous" + summary = getattr(entry, "summary", "") + link = getattr(entry, "link", "") + + article_content = self._fetch_article_content(link) + full_content = article_content if article_content else summary + return self.normalize_item( item_id=entry_id, source_site=self.source_name, title=getattr(entry, "title", ""), - description=getattr(entry, "summary", ""), + description=summary, + full_content=full_content, author_info=getattr(entry, "author", "Le Monde"), keywords=category, - content_url=getattr(entry, "link", ""), + content_url=link, published_date=published_date, item_type="article" ) diff --git a/server/scrapers/medium_scraper.py b/server/scrapers/medium_scraper.py index 875bf6f..bd2db3c 100644 --- a/server/scrapers/medium_scraper.py +++ b/server/scrapers/medium_scraper.py @@ -2,6 +2,8 @@ from typing import List, Dict from .base import BaseScraper +import requests +from bs4 import BeautifulSoup try: import feedparser @@ -25,6 +27,21 @@ def __init__(self): if feedparser is None: raise ImportError("feedparser package is required. Install it with: pip install feedparser") + def _fetch_article_content(self, url: str) -> str: + """Fetch full article content from Medium.""" + try: + resp = requests.get(url, timeout=10) + if resp.status_code == 200: + soup = BeautifulSoup(resp.content, 'html.parser') + article = soup.find('article') + if article: + paragraphs = article.find_all('p') + content = '\n'.join([p.get_text() for p in paragraphs]) + return content + except Exception: + pass + return "" + def _normalize_entry(self, entry: Dict) -> Dict: """Normalize RSS entry from Medium.""" import time @@ -37,12 +54,17 @@ def _normalize_entry(self, entry: Dict) -> Dict: published_date = datetime.fromtimestamp(time.mktime(entry.published_parsed)).isoformat() keywords = [tag.term for tag in entry.get('tags', [])] if 'tags' in entry else [] + summary = entry.get('summary', 'N/A') + + article_content = self._fetch_article_content(entry_id) + full_content = article_content if article_content else summary return self.normalize_item( item_id=entry_id, source_site=self.source_name, title=entry.get('title', 'N/A'), - description=entry.get('summary', 'N/A'), + description=summary, + full_content=full_content, author_info=entry.get('author', 'N/A'), keywords=", ".join(keywords), content_url=entry_id, diff --git a/server/veille_export.db b/server/veille_export.db new file mode 100644 index 0000000000000000000000000000000000000000..2a0b8469815c61f484b503851421cd77fef20bda GIT binary patch literal 40960 zcmeI&O>f#r7{GD6Ng$+((i zw(5R?{Wg2vue8S)Hb$ zG)-Glj}`TZm&x{GyrEw8#Cq21l6LduU#n~XX{Ay@Tm570-?hK5epp?;dcX8f>6vJdt}S0V_A4v!#(qy)f%eJ;y`9?*5J&oM++)%oq)$a!VxJPL&bh1?{c5+AcZCEUpH#YS5 zTVeM^o#MIMv-(aDdj5HGefcuHS=(+HBDqIwu82<-$5taWjXk3+4%+qoTKiZ$GLA*9 zW47u|6=>gRn%C!gC59|a+kDPOgTJ}w(Yoo z2D7!LRHWJazbu89#b~JlNM%rTn)SyWW1et&4r05}+WtburwyaD52En%;l3{;^Zj>Q zZKJ-|jH-2WHkhht8@q-|=Z4i(fDNG1bsEV zZ8ga=;!Q3ZR(L+j+W)ilI37(7{$p2|TMLEqP9>`v<4Usb#%qhWu6$F6J8_2v^+CHl z`LJD`JYAbSu^@l|0tg_000IagfB*srAb>z#1)?ALx&O~=dKni22q1s}0tg_000Iag zfB*uK0QdhC0s;sifB*srAb#r1^x#-Z2Im1 literal 0 HcmV?d00001 diff --git a/server/veille_technique.db b/server/veille_technique.db index f5af91064d89fd32542b219dc28a36730c31bb5d..8e883f154a38c7f7fbd0cb72ca94a73a6806f439 100644 GIT binary patch literal 598016 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