diff --git a/data/bounties.json b/data/bounties.json new file mode 100644 index 00000000..955e1876 --- /dev/null +++ b/data/bounties.json @@ -0,0 +1,21 @@ +{ + "bounties": [ + { + "id": 1505292, + "category": "Memes", + "winners": 2, + "current_entries": 595, + "max_entries": 1000, + "duration_days": 5, + "reward_per_winner": 18.9, + "total_reward": 51481, + "extra_field": 4041, + "tags": ["recent@Memes", "top@Memes"], + "bounty_types": ["OPEN_BOUNTY", "LOW_COMP", "SELF_POST_OPP"], + "title": "Meme Bounty - Thanks to REUTERS", + "status": "open", + "detected_at": "2026-06-10T08:59:00Z", + "source": "SN" + } + ] +} \ No newline at end of file diff --git a/scripts/process_bounty.py b/scripts/process_bounty.py new file mode 100644 index 00000000..4da51d28 --- /dev/null +++ b/scripts/process_bounty.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +""" +Script para procesar y validar bounties detectados por el radar SN. +""" +import json +import sys +from datetime import datetime +from pathlib import Path +from typing import Dict, Any, List + +BOUNTIES_FILE = Path(__file__).parent.parent / "data" / "bounties.json" + +REQUIRED_FIELDS = [ + "id", + "category", + "winners", + "current_entries", + "max_entries", + "duration_days", + "reward_per_winner", + "total_reward", + "tags", + "bounty_types", + "title", + "status", + "detected_at", + "source" +] + +def load_bounties() -> Dict[str, Any]: + """Carga los bounties desde el archivo JSON.""" + if not BOUNTIES_FILE.exists(): + return {"bounties": []} + with open(BOUNTIES_FILE, 'r', encoding='utf-8') as f: + return json.load(f) + +def save_bounties(data: Dict[str, Any]) -> None: + """Guarda los bounties en el archivo JSON.""" + with open(BOUNTIES_FILE, 'w', encoding='utf-8') as f: + json.dump(data, f, indent=2, ensure_ascii=False) + +def validate_bounty(bounty: Dict[str, Any]) -> List[str]: + """Valida que un bounty tenga todos los campos requeridos.""" + errors = [] + for field in REQUIRED_FIELDS: + if field not in bounty: + errors.append(f"Campo requerido faltante: {field}") + + # Validaciones adicionales + if "id" in bounty and not isinstance(bounty["id"], int): + errors.append("El campo 'id' debe ser un entero") + + if "winners" in bounty and bounty["winners"] <= 0: + errors.append("El número de ganadores debe ser positivo") + + if "current_entries" in bounty and "max_entries" in bounty: + if bounty["current_entries"] > bounty["max_entries"]: + errors.append("Las entradas actuales no pueden exceder el máximo") + + if "status" in bounty and bounty["status"] not in ["open", "closed", "completed"]: + errors.append("Estado inválido. Debe ser: open, closed, o completed") + + return errors + +def add_bounty(bounty: Dict[str, Any]) -> bool: + """Añade un nuevo bounty si no existe y es válido.""" + data = load_bounties() + + # Verificar si ya existe + existing_ids = [b["id"] for b in data["bounties"]] + if bounty["id"] in existing_ids: + print(f"Bounty con ID {bounty['id']} ya existe") + return False + + # Validar + errors = validate_bounty(bounty) + if errors: + print("Errores de validación:") + for error in errors: + print(f" - {error}") + return False + + # Añadir timestamp de procesamiento + bounty["processed_at"] = datetime.utcnow().isoformat() + "Z" + + data["bounties"].append(bounty) + save_bounties(data) + print(f"Bounty {bounty['id']} añadido correctamente") + return True + +def list_open_bounties() -> List[Dict[str, Any]]: + """Lista todos los bounties abiertos.""" + data = load_bounties() + return [b for b in data["bounties"] if b.get("status") == "open"] + +def main() -> int: + """Función principal.""" + if len(sys.argv) < 2: + print("Uso: python process_bounty.py [args...]") + print("Comandos:") + print(" add - Añade un bounty desde archivo JSON") + print(" list - Lista bounties abiertos") + print(" validate - Valida un bounty sin guardarlo") + return 1 + + command = sys.argv[1] + + if command == "add": + if len(sys.argv) < 3: + print("Se requiere archivo JSON") + return 1 + with open(sys.argv[2], 'r', encoding='utf-8') as f: + bounty = json.load(f) + success = add_bounty(bounty) + return 0 if success else 1 + + elif command == "list": + open_bounties = list_open_bounties() + for bounty in open_bounties: + print(f"ID: {bounty['id']} | {bounty['title']} | {bounty['category']} | Entradas: {bounty['current_entries']}/{bounty['max_entries']}") + return 0 + + elif command == "validate": + if len(sys.argv) < 3: + print("Se requiere archivo JSON") + return 1 + with open(sys.argv[2], 'r', encoding='utf-8') as f: + bounty = json.load(f) + errors = validate_bounty(bounty) + if errors: + print("Errores de validación:") + for error in errors: + print(f" - {error}") + return 1 + else: + print("Bounty válido") + return 0 + + else: + print(f"Comando desconocido: {command}") + return 1 + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/test_process_bounty.py b/scripts/test_process_bounty.py new file mode 100644 index 00000000..1d04607a --- /dev/null +++ b/scripts/test_process_bounty.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +""" +Tests unitarios para process_bounty.py +""" +import json +import tempfile +import os +import sys +from pathlib import Path + +# Añadir el directorio scripts al path +sys.path.insert(0, str(Path(__file__).parent)) + +from process_bounty import validate_bounty, load_bounties, save_bounties + +def test_validate_bounty_valid(): + """Test que un bounty válido pasa la validación.""" + bounty = { + "id": 1505292, + "category": "Memes", + "winners": 2, + "current_entries": 595, + "max_entries": 1000, + "duration_days": 5, + "reward_per_winner": 18.9, + "total_reward": 51481, + "tags": ["recent@Memes", "top@Memes"], + "bounty_types": ["OPEN_BOUNTY", "LOW_COMP", "SELF_POST_OPP"], + "title": "Meme Bounty - Thanks to REUTERS", + "status": "open", + "detected_at": "2026-06-10T08:59:00Z", + "source": "SN" + } + errors = validate_bounty(bounty) + assert errors == [], f"Bounty válido no debería tener errores: {errors}" + print("✓ test_validate_bounty_valid passed") + +def test_validate_bounty_missing_fields(): + """Test que se detectan campos faltantes.""" + bounty = { + "id": 1505292, + "category": "Memes" + # Faltan muchos campos requeridos + } + errors = validate_bounty(bounty) + assert len(errors) > 0, "Debería haber errores por campos faltantes" + assert any("winners" in e for e in errors), "Debería detectar 'winners' faltante" + print("✓ test_validate_bounty_missing_fields passed") + +def test_validate_bounty_invalid_winners(): + """Test que se detecta winners inválido.""" + bounty = { + "id": 1505292, + "category": "Memes", + "winners": 0, # Inválido + "current_entries": 595, + "max_entries": 1000, + "duration_days": 5, + "reward_per_winner": 18.9, + "total_reward": 51481, + "tags": ["recent@Memes"], + "bounty_types": ["OPEN_BOUNTY"], + "title": "Test", + "status": "open", + "detected_at": "2026-06-10T08:59:00Z", + "source": "SN" + } + errors = validate_bounty(bounty) + assert any("ganadores" in e.lower() for e in errors), "Debería detectar winners inválido" + print("✓ test_validate_bounty_invalid_winners passed") + +def test_validate_bounty_entries_exceed_max(): + """Test que se detecta cuando current_entries > max_entries.""" + bounty = { + "id": 1505292, + "category": "Memes", + "winners": 2, + "current_entries": 1500, # Excede max_entries + "max_entries": 1000, + "duration_days": 5, + "reward_per_winner": 18.9, + "total_reward": 51481, + "tags": ["recent@Memes"], + "bounty_types": ["OPEN_BOUNTY"], + "title": "Test", + "status": "open", + "detected_at": "2026-06-10T08:59:00Z", + "source": "SN" + } + errors = validate_bounty(bounty) + assert any("exceder" in e.lower() for e in errors), "Debería detectar entries > max" + print("✓ test_validate_bounty_entries_exceed_max passed") + +def test_validate_bounty_invalid_status(): + """Test que se detecta status inválido.""" + bounty = { + "id": 1505292, + "category": "Memes", + "winners": 2, + "current_entries": 595, + "max_entries": 1000, + "duration_days": 5, + "reward_per_winner": 18.9, + "total_reward": 51481, + "tags": ["recent@Memes"], + "bounty_types": ["OPEN_BOUNTY"], + "title": "Test", + "status": "invalid_status", # Inválido + "detected_at": "2026-06-10T08:59:00Z", + "source": "SN" + } + errors = validate_bounty(bounty) + assert any("estado" in e.lower() for e in errors), "Debería detectar status inválido" + print("✓ test_validate_bounty_invalid_status passed") + +def test_load_save_bounties(): + """Test de carga y guardado de bounties.""" + with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f: + temp_file = f.name + + try: + # Monkey patch del archivo + import process_bounty + original_file = process_bounty.BOUNTIES_FILE + process_bounty.BOUNTIES_FILE = Path(temp_file) + + # Guardar datos de prueba + test_data = {"bounties": [{"id": 1, "title": "Test"}]} + save_bounties(test_data) + + # Cargar y verificar + loaded = load_bounties() + assert loaded == test_data, "Los datos cargados deben coincidir con los guardados" + + print("✓ test_load_save_bounties passed") + finally: + process_bounty.BOUNTIES_FILE = original_file + if os.path.exists(temp_file): + os.unlink(temp_file) + +def run_all_tests(): + """Ejecuta todos los tests.""" + test_validate_bounty_valid() + test_validate_bounty_missing_fields() + test_validate_bounty_invalid_winners() + test_validate_bounty_entries_exceed_max() + test_validate_bounty_invalid_status() + test_load_save_bounties() + print("\n✓ Todos los tests pasaron") + +if __name__ == "__main__": + run_all_tests()