Added everything
This commit is contained in:
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
0
src/application/__init__.py
Normal file
0
src/application/__init__.py
Normal file
0
src/application/ports/__init__.py
Normal file
0
src/application/ports/__init__.py
Normal file
46
src/application/ports/report_repository.py
Normal file
46
src/application/ports/report_repository.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from domain.reports import Report
|
||||
from typing import List, Optional
|
||||
|
||||
class ReportRepository(ABC):
|
||||
"""Puerto (interfaz) para el repositorio de Reportes"""
|
||||
|
||||
@abstractmethod
|
||||
def save(self, report: Report) -> Report:
|
||||
"""Guarda un reporte en la base de datos"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def find_by_id(self, report_id: str) -> Optional[Report]:
|
||||
"""Obtiene un reporte por ID"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def find_by_user_id(self, user_id: int) -> List[Report]:
|
||||
"""Obtiene todos los reportes de un usuario"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def find_all(self) -> List[Report]:
|
||||
"""Obtiene todos los reportes"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def find_by_visibility_range(self, min_visibility: float, max_visibility: float) -> List[Report]:
|
||||
"""Obtiene reportes dentro de un rango de visibilidad"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def update_visibility(self, report_id: str, new_visibility: float) -> None:
|
||||
"""Actualiza la visibilidad de un reporte"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def delete(self, report_id: str) -> bool:
|
||||
"""Elimina un reporte"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def find_shadowbanned(self, visibility_threshold: float = 20) -> List[Report]:
|
||||
"""Obtiene reportes con baja visibilidad (shadowbaneados)"""
|
||||
pass
|
||||
46
src/application/ports/user_repository.py
Normal file
46
src/application/ports/user_repository.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from domain.users import User
|
||||
from typing import List, Optional
|
||||
|
||||
class UserRepository(ABC):
|
||||
"""Puerto (interfaz) para el repositorio de Usuarios"""
|
||||
|
||||
@abstractmethod
|
||||
def save(self, user: User) -> User:
|
||||
"""Guarda un usuario en la base de datos"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def find_by_id(self, user_id: int) -> Optional[User]:
|
||||
"""Obtiene un usuario por ID"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def find_by_email(self, email: str) -> Optional[User]:
|
||||
"""Obtiene un usuario por email"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def find_all(self) -> List[User]:
|
||||
"""Obtiene todos los usuarios"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def update(self, user: User) -> User:
|
||||
"""Actualiza un usuario"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def delete(self, user_id: int) -> bool:
|
||||
"""Elimina un usuario"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def increment_reports(self, user_id: int) -> None:
|
||||
"""Incrementa el contador de reportes de un usuario"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def update_rating(self, user_id: int, new_rating: float) -> None:
|
||||
"""Actualiza la calificación de un usuario"""
|
||||
pass
|
||||
0
src/application/services/__init__.py
Normal file
0
src/application/services/__init__.py
Normal file
115
src/application/services/report_services.py
Normal file
115
src/application/services/report_services.py
Normal file
@@ -0,0 +1,115 @@
|
||||
from domain.reports import Report
|
||||
from application.ports.report_repository import ReportRepository
|
||||
from application.ports.user_repository import UserRepository
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from uuid import uuid4
|
||||
|
||||
class CreateReport:
|
||||
"""Use case para crear un nuevo reporte"""
|
||||
def __init__(self, repo: ReportRepository, user_repo: UserRepository):
|
||||
if not isinstance(repo, ReportRepository):
|
||||
raise TypeError("repo must implement ReportRepository")
|
||||
if not isinstance(user_repo, UserRepository):
|
||||
raise TypeError("user_repo must implement UserRepository")
|
||||
self.repo = repo
|
||||
self.user_repo = user_repo
|
||||
|
||||
def execute(self, id_usuario: int, tipo_reporte: int, descripcion: str,
|
||||
ubicacion: Optional[str] = None) -> Report:
|
||||
# Verificar que el usuario existe
|
||||
user = self.user_repo.find_by_id(id_usuario)
|
||||
if not user:
|
||||
raise ValueError(f"Usuario con ID {id_usuario} no existe")
|
||||
|
||||
report = Report(
|
||||
id_reporte=str(uuid4()),
|
||||
id_usuario=id_usuario,
|
||||
tipo_reporte=tipo_reporte,
|
||||
descripcion=descripcion,
|
||||
ubicacion=ubicacion,
|
||||
visibilidad=50.0, # Visibilidad inicial neutral
|
||||
fecha_creacion=datetime.now()
|
||||
)
|
||||
|
||||
# Incrementar contador de reportes del usuario
|
||||
self.user_repo.increment_reports(id_usuario)
|
||||
|
||||
return self.repo.save(report)
|
||||
|
||||
class GetReportById:
|
||||
"""Use case para obtener un reporte por ID"""
|
||||
def __init__(self, repo: ReportRepository):
|
||||
if not isinstance(repo, ReportRepository):
|
||||
raise TypeError("repo must implement ReportRepository")
|
||||
self.repo = repo
|
||||
|
||||
def execute(self, report_id: str) -> Optional[Report]:
|
||||
return self.repo.find_by_id(report_id)
|
||||
|
||||
class GetReportsByUser:
|
||||
"""Use case para obtener todos los reportes de un usuario"""
|
||||
def __init__(self, repo: ReportRepository):
|
||||
if not isinstance(repo, ReportRepository):
|
||||
raise TypeError("repo must implement ReportRepository")
|
||||
self.repo = repo
|
||||
|
||||
def execute(self, user_id: int) -> List[Report]:
|
||||
return self.repo.find_by_user_id(user_id)
|
||||
|
||||
class ListAllReports:
|
||||
"""Use case para obtener todos los reportes"""
|
||||
def __init__(self, repo: ReportRepository):
|
||||
if not isinstance(repo, ReportRepository):
|
||||
raise TypeError("repo must implement ReportRepository")
|
||||
self.repo = repo
|
||||
|
||||
def execute(self) -> List[Report]:
|
||||
return self.repo.find_all()
|
||||
|
||||
class UpdateReportVisibility:
|
||||
"""Use case para actualizar la visibilidad de un reporte basado en votación comunitaria"""
|
||||
def __init__(self, repo: ReportRepository, user_repo: UserRepository):
|
||||
if not isinstance(repo, ReportRepository):
|
||||
raise TypeError("repo must implement ReportRepository")
|
||||
self.repo = repo
|
||||
self.user_repo = user_repo
|
||||
|
||||
def execute(self, report_id: str, new_visibility: float, penalize_author: bool = False) -> None:
|
||||
# Validar rango de visibilidad
|
||||
if new_visibility < 0 or new_visibility > 100:
|
||||
raise ValueError("La visibilidad debe estar entre 0 y 100")
|
||||
|
||||
report = self.repo.find_by_id(report_id)
|
||||
if not report:
|
||||
raise ValueError(f"Reporte con ID {report_id} no existe")
|
||||
|
||||
self.repo.update_visibility(report_id, new_visibility)
|
||||
|
||||
# Si la visibilidad es muy baja (shadowban), penalizar al autor
|
||||
if penalize_author and new_visibility < 20:
|
||||
user = self.user_repo.find_by_id(report.id_usuario)
|
||||
if user:
|
||||
# Reducir calificación del usuario
|
||||
new_rating = max(0, user.calificacion - 5)
|
||||
self.user_repo.update_rating(report.id_usuario, new_rating)
|
||||
|
||||
class GetShadowbannedReports:
|
||||
"""Use case para obtener reportes shadowbaneados (baja visibilidad)"""
|
||||
def __init__(self, repo: ReportRepository):
|
||||
if not isinstance(repo, ReportRepository):
|
||||
raise TypeError("repo must implement ReportRepository")
|
||||
self.repo = repo
|
||||
|
||||
def execute(self, visibility_threshold: float = 20) -> List[Report]:
|
||||
return self.repo.find_shadowbanned(visibility_threshold)
|
||||
|
||||
class DeleteReport:
|
||||
"""Use case para eliminar un reporte"""
|
||||
def __init__(self, repo: ReportRepository):
|
||||
if not isinstance(repo, ReportRepository):
|
||||
raise TypeError("repo must implement ReportRepository")
|
||||
self.repo = repo
|
||||
|
||||
def execute(self, report_id: str) -> bool:
|
||||
return self.repo.delete(report_id)
|
||||
90
src/application/services/user_services.py
Normal file
90
src/application/services/user_services.py
Normal file
@@ -0,0 +1,90 @@
|
||||
from domain.users import User
|
||||
from application.ports.user_repository import UserRepository
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
class CreateUser:
|
||||
"""Use case para crear un nuevo usuario"""
|
||||
def __init__(self, repo: UserRepository):
|
||||
if not isinstance(repo, UserRepository):
|
||||
raise TypeError("repo must implement UserRepository")
|
||||
self.repo = repo
|
||||
|
||||
def execute(self, nombre: str, apellido: str, email: str,
|
||||
fecha_nacimiento: datetime, url_foto_perfil: Optional[str] = None,
|
||||
biografia: Optional[str] = None) -> User:
|
||||
user = User(
|
||||
user_id=0,
|
||||
nombre=nombre,
|
||||
apellido=apellido,
|
||||
email=email,
|
||||
fecha_nacimiento=fecha_nacimiento,
|
||||
fecha_creacion=datetime.now(),
|
||||
calificacion=50.0, # Puntuación inicial
|
||||
numero_reportes=0,
|
||||
url_foto_perfil=url_foto_perfil,
|
||||
biografia=biografia
|
||||
)
|
||||
return self.repo.save(user)
|
||||
|
||||
class GetUserById:
|
||||
"""Use case para obtener un usuario por ID"""
|
||||
def __init__(self, repo: UserRepository):
|
||||
if not isinstance(repo, UserRepository):
|
||||
raise TypeError("repo must implement UserRepository")
|
||||
self.repo = repo
|
||||
|
||||
def execute(self, user_id: int) -> Optional[User]:
|
||||
return self.repo.find_by_id(user_id)
|
||||
|
||||
class GetUserByEmail:
|
||||
"""Use case para obtener un usuario por email"""
|
||||
def __init__(self, repo: UserRepository):
|
||||
if not isinstance(repo, UserRepository):
|
||||
raise TypeError("repo must implement UserRepository")
|
||||
self.repo = repo
|
||||
|
||||
def execute(self, email: str) -> Optional[User]:
|
||||
return self.repo.find_by_email(email)
|
||||
|
||||
class ListAllUsers:
|
||||
"""Use case para obtener todos los usuarios"""
|
||||
def __init__(self, repo: UserRepository):
|
||||
if not isinstance(repo, UserRepository):
|
||||
raise TypeError("repo must implement UserRepository")
|
||||
self.repo = repo
|
||||
|
||||
def execute(self) -> List[User]:
|
||||
return self.repo.find_all()
|
||||
|
||||
class UpdateUser:
|
||||
"""Use case para actualizar un usuario"""
|
||||
def __init__(self, repo: UserRepository):
|
||||
if not isinstance(repo, UserRepository):
|
||||
raise TypeError("repo must implement UserRepository")
|
||||
self.repo = repo
|
||||
|
||||
def execute(self, user_id: int, nombre: str = None, apellido: str = None,
|
||||
url_foto_perfil: str = None, biografia: str = None) -> Optional[User]:
|
||||
user = self.repo.find_by_id(user_id)
|
||||
if user:
|
||||
if nombre:
|
||||
user.nombre = nombre
|
||||
if apellido:
|
||||
user.apellido = apellido
|
||||
if url_foto_perfil is not None:
|
||||
user.url_foto_perfil = url_foto_perfil
|
||||
if biografia is not None:
|
||||
user.biografia = biografia
|
||||
return self.repo.update(user)
|
||||
return None
|
||||
|
||||
class DeleteUser:
|
||||
"""Use case para eliminar un usuario"""
|
||||
def __init__(self, repo: UserRepository):
|
||||
if not isinstance(repo, UserRepository):
|
||||
raise TypeError("repo must implement UserRepository")
|
||||
self.repo = repo
|
||||
|
||||
def execute(self, user_id: int) -> bool:
|
||||
return self.repo.delete(user_id)
|
||||
0
src/core/__init__.py
Normal file
0
src/core/__init__.py
Normal file
37
src/core/config.py
Normal file
37
src/core/config.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from pydantic_settings import BaseSettings
|
||||
from pydantic import Field
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""Configuración de la aplicación VoxPopuli Microservices"""
|
||||
|
||||
# Base de datos MySQL
|
||||
mysql_url: str = Field(
|
||||
default="mysql+pymysql://user:password@localhost/voxpopuli_users",
|
||||
description="URL de conexión a MySQL para API de Usuarios"
|
||||
)
|
||||
|
||||
# Base de datos MongoDB
|
||||
mongodb_url: str = Field(
|
||||
default="mongodb://localhost:27017",
|
||||
description="URL de conexión a MongoDB para API de Reportes"
|
||||
)
|
||||
mongodb_db: str = Field(
|
||||
default="voxpopuli_reports",
|
||||
description="Base de datos MongoDB"
|
||||
)
|
||||
|
||||
# API
|
||||
api_title: str = "VoxPopuli Microservices"
|
||||
api_version: str = "1.0.0"
|
||||
api_description: str = "Plataforma de reportes comunitarios con usuarios y gestión de reportes"
|
||||
|
||||
# Server
|
||||
host: str = "0.0.0.0"
|
||||
log_level: str = "info"
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
case_sensitive = False
|
||||
|
||||
|
||||
ConfSettings = Settings()
|
||||
0
src/domain/__init__.py
Normal file
0
src/domain/__init__.py
Normal file
14
src/domain/reports.py
Normal file
14
src/domain/reports.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
@dataclass
|
||||
class Report:
|
||||
"""Modelo de dominio para Reporte"""
|
||||
id_reporte: str
|
||||
id_usuario: int
|
||||
tipo_reporte: int # Número que representa el tipo
|
||||
descripcion: str
|
||||
ubicacion: Optional[str]
|
||||
visibilidad: float # 0-100 (puntuación comunitaria)
|
||||
fecha_creacion: Optional[datetime] = None
|
||||
17
src/domain/users.py
Normal file
17
src/domain/users.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
@dataclass
|
||||
class User:
|
||||
"""Modelo de dominio para Usuario"""
|
||||
user_id: int
|
||||
nombre: str
|
||||
apellido: str
|
||||
email: str
|
||||
fecha_nacimiento: datetime
|
||||
fecha_creacion: datetime
|
||||
calificacion: float # 0-100
|
||||
numero_reportes: int
|
||||
url_foto_perfil: Optional[str]
|
||||
biografia: Optional[str]
|
||||
0
src/infrastructure/__init__.py
Normal file
0
src/infrastructure/__init__.py
Normal file
0
src/infrastructure/adapters/__init__.py
Normal file
0
src/infrastructure/adapters/__init__.py
Normal file
0
src/infrastructure/adapters/persistence/__init__.py
Normal file
0
src/infrastructure/adapters/persistence/__init__.py
Normal file
22
src/infrastructure/adapters/persistence/db.py
Normal file
22
src/infrastructure/adapters/persistence/db.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import declarative_base, sessionmaker
|
||||
from core.config import ConfSettings
|
||||
|
||||
# Base de datos MySQL para Usuarios
|
||||
engine = create_engine(
|
||||
ConfSettings.mysql_url,
|
||||
echo=False,
|
||||
pool_pre_ping=True,
|
||||
)
|
||||
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
def get_db():
|
||||
"""Obtiene una sesión de base de datos"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
18
src/infrastructure/adapters/persistence/models.py
Normal file
18
src/infrastructure/adapters/persistence/models.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from sqlalchemy import Column, Integer, String, Float, DateTime
|
||||
from infrastructure.adapters.persistence.db import Base
|
||||
from datetime import datetime
|
||||
|
||||
class UserModel(Base):
|
||||
"""Modelo SQLAlchemy para Usuario (MySQL)"""
|
||||
__tablename__ = "usuarios"
|
||||
|
||||
user_id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||
nombre = Column(String(100), nullable=False, index=True)
|
||||
apellido = Column(String(100), nullable=False, index=True)
|
||||
email = Column(String(255), unique=True, nullable=False, index=True)
|
||||
fecha_nacimiento = Column(DateTime, nullable=False)
|
||||
fecha_creacion = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
calificacion = Column(Float, default=50.0, nullable=False) # 0-100
|
||||
numero_reportes = Column(Integer, default=0, nullable=False)
|
||||
url_foto_perfil = Column(String(500), nullable=True)
|
||||
biografia = Column(String(1000), nullable=True)
|
||||
11
src/infrastructure/adapters/persistence/mongodb.py
Normal file
11
src/infrastructure/adapters/persistence/mongodb.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from pymongo import MongoClient
|
||||
from pymongo.collection import Collection
|
||||
from core.config import ConfSettings
|
||||
|
||||
# Conexión a MongoDB para Reportes
|
||||
mongo_client = MongoClient(ConfSettings.mongodb_url)
|
||||
mongodb = mongo_client[ConfSettings.mongodb_db]
|
||||
|
||||
def get_reports_collection() -> Collection:
|
||||
"""Obtiene la colección de reportes desde MongoDB"""
|
||||
return mongodb["reportes"]
|
||||
@@ -0,0 +1,81 @@
|
||||
from application.ports.report_repository import ReportRepository
|
||||
from domain.reports import Report
|
||||
from infrastructure.adapters.persistence.mongodb import get_reports_collection
|
||||
from typing import List, Optional
|
||||
from bson import ObjectId
|
||||
from datetime import datetime
|
||||
|
||||
class ReportRepositoryMongo(ReportRepository):
|
||||
"""Implementación del repositorio de Reportes usando MongoDB"""
|
||||
|
||||
def __init__(self):
|
||||
self.collection = get_reports_collection()
|
||||
|
||||
def save(self, report: Report) -> Report:
|
||||
"""Guarda un nuevo reporte"""
|
||||
report_dict = {
|
||||
"id_reporte": report.id_reporte,
|
||||
"id_usuario": report.id_usuario,
|
||||
"tipo_reporte": report.tipo_reporte,
|
||||
"descripcion": report.descripcion,
|
||||
"ubicacion": report.ubicacion,
|
||||
"visibilidad": report.visibilidad,
|
||||
"fecha_creacion": report.fecha_creacion or datetime.utcnow()
|
||||
}
|
||||
result = self.collection.insert_one(report_dict)
|
||||
return report
|
||||
|
||||
def find_by_id(self, report_id: str) -> Optional[Report]:
|
||||
"""Obtiene un reporte por ID"""
|
||||
doc = self.collection.find_one({"id_reporte": report_id})
|
||||
if doc:
|
||||
return self._to_domain(doc)
|
||||
return None
|
||||
|
||||
def find_by_user_id(self, user_id: int) -> List[Report]:
|
||||
"""Obtiene todos los reportes de un usuario"""
|
||||
docs = self.collection.find({"id_usuario": user_id})
|
||||
return [self._to_domain(doc) for doc in docs]
|
||||
|
||||
def find_all(self) -> List[Report]:
|
||||
"""Obtiene todos los reportes"""
|
||||
docs = self.collection.find()
|
||||
return [self._to_domain(doc) for doc in docs]
|
||||
|
||||
def find_by_visibility_range(self, min_visibility: float, max_visibility: float) -> List[Report]:
|
||||
"""Obtiene reportes dentro de un rango de visibilidad"""
|
||||
docs = self.collection.find({
|
||||
"visibilidad": {"$gte": min_visibility, "$lte": max_visibility}
|
||||
})
|
||||
return [self._to_domain(doc) for doc in docs]
|
||||
|
||||
def update_visibility(self, report_id: str, new_visibility: float) -> None:
|
||||
"""Actualiza la visibilidad de un reporte"""
|
||||
self.collection.update_one(
|
||||
{"id_reporte": report_id},
|
||||
{"$set": {"visibilidad": new_visibility}}
|
||||
)
|
||||
|
||||
def delete(self, report_id: str) -> bool:
|
||||
"""Elimina un reporte"""
|
||||
result = self.collection.delete_one({"id_reporte": report_id})
|
||||
return result.deleted_count > 0
|
||||
|
||||
def find_shadowbanned(self, visibility_threshold: float = 20) -> List[Report]:
|
||||
"""Obtiene reportes con baja visibilidad (shadowbaneados)"""
|
||||
docs = self.collection.find({
|
||||
"visibilidad": {"$lt": visibility_threshold}
|
||||
})
|
||||
return [self._to_domain(doc) for doc in docs]
|
||||
|
||||
def _to_domain(self, doc: dict) -> Report:
|
||||
"""Convierte un documento de MongoDB a un objeto de dominio"""
|
||||
return Report(
|
||||
id_reporte=doc.get("id_reporte"),
|
||||
id_usuario=doc.get("id_usuario"),
|
||||
tipo_reporte=doc.get("tipo_reporte"),
|
||||
descripcion=doc.get("descripcion"),
|
||||
ubicacion=doc.get("ubicacion"),
|
||||
visibilidad=doc.get("visibilidad"),
|
||||
fecha_creacion=doc.get("fecha_creacion")
|
||||
)
|
||||
104
src/infrastructure/adapters/persistence/user_repository_sql.py
Normal file
104
src/infrastructure/adapters/persistence/user_repository_sql.py
Normal file
@@ -0,0 +1,104 @@
|
||||
from application.ports.user_repository import UserRepository
|
||||
from domain.users import User
|
||||
from infrastructure.adapters.persistence.models import UserModel
|
||||
from infrastructure.adapters.persistence.db import SessionLocal
|
||||
from typing import List, Optional
|
||||
|
||||
class UserRepositorySQL(UserRepository):
|
||||
"""Implementación del repositorio de Usuarios usando SQLAlchemy (MySQL)"""
|
||||
|
||||
def __init__(self, db_session=None):
|
||||
self.db = db_session or SessionLocal()
|
||||
|
||||
def save(self, user: User) -> User:
|
||||
"""Guarda un nuevo usuario"""
|
||||
db_user = UserModel(
|
||||
nombre=user.nombre,
|
||||
apellido=user.apellido,
|
||||
email=user.email,
|
||||
fecha_nacimiento=user.fecha_nacimiento,
|
||||
fecha_creacion=user.fecha_creacion,
|
||||
calificacion=user.calificacion,
|
||||
numero_reportes=user.numero_reportes,
|
||||
url_foto_perfil=user.url_foto_perfil,
|
||||
biografia=user.biografia
|
||||
)
|
||||
self.db.add(db_user)
|
||||
self.db.commit()
|
||||
self.db.refresh(db_user)
|
||||
|
||||
# Convertir de vuelta a dominio
|
||||
return self._to_domain(db_user)
|
||||
|
||||
def find_by_id(self, user_id: int) -> Optional[User]:
|
||||
"""Obtiene un usuario por ID"""
|
||||
db_user = self.db.query(UserModel).filter(UserModel.user_id == user_id).first()
|
||||
if db_user:
|
||||
return self._to_domain(db_user)
|
||||
return None
|
||||
|
||||
def find_by_email(self, email: str) -> Optional[User]:
|
||||
"""Obtiene un usuario por email"""
|
||||
db_user = self.db.query(UserModel).filter(UserModel.email == email).first()
|
||||
if db_user:
|
||||
return self._to_domain(db_user)
|
||||
return None
|
||||
|
||||
def find_all(self) -> List[User]:
|
||||
"""Obtiene todos los usuarios"""
|
||||
db_users = self.db.query(UserModel).all()
|
||||
return [self._to_domain(user) for user in db_users]
|
||||
|
||||
def update(self, user: User) -> User:
|
||||
"""Actualiza un usuario"""
|
||||
db_user = self.db.query(UserModel).filter(UserModel.user_id == user.user_id).first()
|
||||
if db_user:
|
||||
db_user.nombre = user.nombre
|
||||
db_user.apellido = user.apellido
|
||||
db_user.calificacion = user.calificacion
|
||||
db_user.numero_reportes = user.numero_reportes
|
||||
db_user.url_foto_perfil = user.url_foto_perfil
|
||||
db_user.biografia = user.biografia
|
||||
self.db.commit()
|
||||
self.db.refresh(db_user)
|
||||
return self._to_domain(db_user)
|
||||
return user
|
||||
|
||||
def delete(self, user_id: int) -> bool:
|
||||
"""Elimina un usuario"""
|
||||
db_user = self.db.query(UserModel).filter(UserModel.user_id == user_id).first()
|
||||
if db_user:
|
||||
self.db.delete(db_user)
|
||||
self.db.commit()
|
||||
return True
|
||||
return False
|
||||
|
||||
def increment_reports(self, user_id: int) -> None:
|
||||
"""Incrementa el contador de reportes de un usuario"""
|
||||
db_user = self.db.query(UserModel).filter(UserModel.user_id == user_id).first()
|
||||
if db_user:
|
||||
db_user.numero_reportes += 1
|
||||
self.db.commit()
|
||||
|
||||
def update_rating(self, user_id: int, new_rating: float) -> None:
|
||||
"""Actualiza la calificación de un usuario"""
|
||||
db_user = self.db.query(UserModel).filter(UserModel.user_id == user_id).first()
|
||||
if db_user:
|
||||
# Asegurar que la calificación esté en el rango 0-100
|
||||
db_user.calificacion = max(0, min(100, new_rating))
|
||||
self.db.commit()
|
||||
|
||||
def _to_domain(self, db_user: UserModel) -> User:
|
||||
"""Convierte un modelo SQLAlchemy a un objeto de dominio"""
|
||||
return User(
|
||||
user_id=db_user.user_id,
|
||||
nombre=db_user.nombre,
|
||||
apellido=db_user.apellido,
|
||||
email=db_user.email,
|
||||
fecha_nacimiento=db_user.fecha_nacimiento,
|
||||
fecha_creacion=db_user.fecha_creacion,
|
||||
calificacion=db_user.calificacion,
|
||||
numero_reportes=db_user.numero_reportes,
|
||||
url_foto_perfil=db_user.url_foto_perfil,
|
||||
biografia=db_user.biografia
|
||||
)
|
||||
0
src/infrastructure/api/__init__.py
Normal file
0
src/infrastructure/api/__init__.py
Normal file
0
src/infrastructure/api/reports/__init__.py
Normal file
0
src/infrastructure/api/reports/__init__.py
Normal file
13
src/infrastructure/api/reports/app.py
Normal file
13
src/infrastructure/api/reports/app.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from fastapi import FastAPI
|
||||
from core.config import ConfSettings
|
||||
from infrastructure.api.reports.router import router
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
"""Factory para crear la aplicación de Reportes"""
|
||||
app = FastAPI(
|
||||
title="Reportes Microservice",
|
||||
version="1.0.0",
|
||||
description="Microservicio de gestión de reportes comunitarios"
|
||||
)
|
||||
app.include_router(router)
|
||||
return app
|
||||
99
src/infrastructure/api/reports/reports.py
Normal file
99
src/infrastructure/api/reports/reports.py
Normal file
@@ -0,0 +1,99 @@
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from infrastructure.api.reports.schemas import ReportCreateRequest, ReportUpdateVisibilityRequest, ReportResponse
|
||||
from application.services.report_services import (
|
||||
CreateReport, GetReportById, GetReportsByUser, ListAllReports,
|
||||
UpdateReportVisibility, GetShadowbannedReports, DeleteReport
|
||||
)
|
||||
from infrastructure.adapters.persistence.report_repository_mongo import ReportRepositoryMongo
|
||||
from infrastructure.adapters.persistence.user_repository_sql import UserRepositorySQL
|
||||
|
||||
router = APIRouter()
|
||||
report_repo = ReportRepositoryMongo()
|
||||
user_repo = UserRepositorySQL()
|
||||
|
||||
@router.post("/", response_model=ReportResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_report(report_data: ReportCreateRequest):
|
||||
"""Crea un nuevo reporte"""
|
||||
try:
|
||||
create_use_case = CreateReport(report_repo, user_repo)
|
||||
report = create_use_case.execute(
|
||||
id_usuario=report_data.id_usuario,
|
||||
tipo_reporte=report_data.tipo_reporte,
|
||||
descripcion=report_data.descripcion,
|
||||
ubicacion=report_data.ubicacion
|
||||
)
|
||||
return report
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e)
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Error al crear reporte: {str(e)}"
|
||||
)
|
||||
|
||||
@router.get("/{report_id}", response_model=ReportResponse)
|
||||
async def get_report(report_id: str):
|
||||
"""Obtiene un reporte por ID"""
|
||||
get_use_case = GetReportById(report_repo)
|
||||
report = get_use_case.execute(report_id)
|
||||
if not report:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Reporte con ID {report_id} no encontrado"
|
||||
)
|
||||
return report
|
||||
|
||||
@router.get("/user/{user_id}", response_model=list[ReportResponse])
|
||||
async def get_user_reports(user_id: int):
|
||||
"""Obtiene todos los reportes de un usuario"""
|
||||
get_use_case = GetReportsByUser(report_repo)
|
||||
reports = get_use_case.execute(user_id)
|
||||
return reports
|
||||
|
||||
@router.get("/", response_model=list[ReportResponse])
|
||||
async def list_reports():
|
||||
"""Obtiene todos los reportes"""
|
||||
list_use_case = ListAllReports(report_repo)
|
||||
return list_use_case.execute()
|
||||
|
||||
@router.get("/shadowbanned/list", response_model=list[ReportResponse])
|
||||
async def get_shadowbanned_reports(threshold: float = 20):
|
||||
"""Obtiene reportes shadowbaneados (baja visibilidad)"""
|
||||
get_use_case = GetShadowbannedReports(report_repo)
|
||||
return get_use_case.execute(threshold)
|
||||
|
||||
@router.put("/{report_id}/visibility", status_code=status.HTTP_200_OK)
|
||||
async def update_report_visibility(report_id: str, visibility_data: ReportUpdateVisibilityRequest):
|
||||
"""Actualiza la visibilidad de un reporte (basado en votación comunitaria)"""
|
||||
try:
|
||||
update_use_case = UpdateReportVisibility(report_repo, user_repo)
|
||||
update_use_case.execute(
|
||||
report_id=report_id,
|
||||
new_visibility=visibility_data.new_visibility,
|
||||
penalize_author=visibility_data.penalize_author
|
||||
)
|
||||
return {
|
||||
"message": "Visibilidad actualizada exitosamente",
|
||||
"report_id": report_id,
|
||||
"new_visibility": visibility_data.new_visibility
|
||||
}
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e)
|
||||
)
|
||||
|
||||
@router.delete("/{report_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_report(report_id: str):
|
||||
"""Elimina un reporte"""
|
||||
delete_use_case = DeleteReport(report_repo)
|
||||
success = delete_use_case.execute(report_id)
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Reporte con ID {report_id} no encontrado"
|
||||
)
|
||||
return None
|
||||
19
src/infrastructure/api/reports/root.py
Normal file
19
src/infrastructure/api/reports/root.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/health")
|
||||
async def health_check():
|
||||
"""Verifica el estado de la API"""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"service": "Reportes API"
|
||||
}
|
||||
|
||||
@router.get("/")
|
||||
async def root():
|
||||
"""Endpoint raíz"""
|
||||
return {
|
||||
"message": "API de Reportes - VoxPopuli",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
17
src/infrastructure/api/reports/router.py
Normal file
17
src/infrastructure/api/reports/router.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from fastapi import APIRouter
|
||||
from infrastructure.api.reports.reports import router as reports_router
|
||||
from infrastructure.api.reports.root import router as root_router
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
router.include_router(
|
||||
reports_router,
|
||||
prefix="/reports",
|
||||
tags=["reports"]
|
||||
)
|
||||
|
||||
router.include_router(
|
||||
root_router,
|
||||
prefix='',
|
||||
tags=["root"]
|
||||
)
|
||||
28
src/infrastructure/api/reports/schemas.py
Normal file
28
src/infrastructure/api/reports/schemas.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
class ReportCreateRequest(BaseModel):
|
||||
"""Solicitud para crear un reporte"""
|
||||
id_usuario: int
|
||||
tipo_reporte: int
|
||||
descripcion: str
|
||||
ubicacion: Optional[str] = None
|
||||
|
||||
class ReportUpdateVisibilityRequest(BaseModel):
|
||||
"""Solicitud para actualizar la visibilidad de un reporte"""
|
||||
new_visibility: float
|
||||
penalize_author: bool = False
|
||||
|
||||
class ReportResponse(BaseModel):
|
||||
"""Respuesta con datos de reporte"""
|
||||
id_reporte: str
|
||||
id_usuario: int
|
||||
tipo_reporte: int
|
||||
descripcion: str
|
||||
ubicacion: Optional[str]
|
||||
visibilidad: float
|
||||
fecha_creacion: Optional[datetime]
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
0
src/infrastructure/api/users/__init__.py
Normal file
0
src/infrastructure/api/users/__init__.py
Normal file
13
src/infrastructure/api/users/app.py
Normal file
13
src/infrastructure/api/users/app.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from fastapi import FastAPI
|
||||
from core.config import ConfSettings
|
||||
from infrastructure.api.users.router import router
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
"""Factory para crear la aplicación de Usuarios"""
|
||||
app = FastAPI(
|
||||
title="Usuarios Microservice",
|
||||
version="1.0.0",
|
||||
description="Microservicio de gestión de usuarios y autenticación"
|
||||
)
|
||||
app.include_router(router)
|
||||
return app
|
||||
19
src/infrastructure/api/users/root.py
Normal file
19
src/infrastructure/api/users/root.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/health")
|
||||
async def health_check():
|
||||
"""Verifica el estado de la API"""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"service": "Usuarios API"
|
||||
}
|
||||
|
||||
@router.get("/")
|
||||
async def root():
|
||||
"""Endpoint raíz"""
|
||||
return {
|
||||
"message": "API de Usuarios - VoxPopuli",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
17
src/infrastructure/api/users/router.py
Normal file
17
src/infrastructure/api/users/router.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from fastapi import APIRouter
|
||||
from infrastructure.api.users.users import router as users_router
|
||||
from infrastructure.api.users.root import router as root_router
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
router.include_router(
|
||||
users_router,
|
||||
prefix="/users",
|
||||
tags=["users"]
|
||||
)
|
||||
|
||||
router.include_router(
|
||||
root_router,
|
||||
prefix='',
|
||||
tags=["root"]
|
||||
)
|
||||
35
src/infrastructure/api/users/schemas.py
Normal file
35
src/infrastructure/api/users/schemas.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
class UserCreateRequest(BaseModel):
|
||||
"""Solicitud para crear un usuario"""
|
||||
nombre: str
|
||||
apellido: str
|
||||
email: str
|
||||
fecha_nacimiento: datetime
|
||||
url_foto_perfil: Optional[str] = None
|
||||
biografia: Optional[str] = None
|
||||
|
||||
class UserUpdateRequest(BaseModel):
|
||||
"""Solicitud para actualizar un usuario"""
|
||||
nombre: Optional[str] = None
|
||||
apellido: Optional[str] = None
|
||||
url_foto_perfil: Optional[str] = None
|
||||
biografia: Optional[str] = None
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
"""Respuesta con datos de usuario"""
|
||||
user_id: int
|
||||
nombre: str
|
||||
apellido: str
|
||||
email: str
|
||||
fecha_nacimiento: datetime
|
||||
fecha_creacion: datetime
|
||||
calificacion: float
|
||||
numero_reportes: int
|
||||
url_foto_perfil: Optional[str]
|
||||
biografia: Optional[str]
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
89
src/infrastructure/api/users/users.py
Normal file
89
src/infrastructure/api/users/users.py
Normal file
@@ -0,0 +1,89 @@
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from infrastructure.api.users.schemas import UserCreateRequest, UserUpdateRequest, UserResponse
|
||||
from application.services.user_services import (
|
||||
CreateUser, GetUserById, GetUserByEmail, ListAllUsers, UpdateUser, DeleteUser
|
||||
)
|
||||
from infrastructure.adapters.persistence.user_repository_sql import UserRepositorySQL
|
||||
|
||||
router = APIRouter()
|
||||
user_repo = UserRepositorySQL()
|
||||
|
||||
@router.post("/", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_user(user_data: UserCreateRequest):
|
||||
"""Crea un nuevo usuario"""
|
||||
try:
|
||||
create_use_case = CreateUser(user_repo)
|
||||
user = create_use_case.execute(
|
||||
nombre=user_data.nombre,
|
||||
apellido=user_data.apellido,
|
||||
email=user_data.email,
|
||||
fecha_nacimiento=user_data.fecha_nacimiento,
|
||||
url_foto_perfil=user_data.url_foto_perfil,
|
||||
biografia=user_data.biografia
|
||||
)
|
||||
return user
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Error al crear usuario: {str(e)}"
|
||||
)
|
||||
|
||||
@router.get("/{user_id}", response_model=UserResponse)
|
||||
async def get_user(user_id: int):
|
||||
"""Obtiene un usuario por ID"""
|
||||
get_use_case = GetUserById(user_repo)
|
||||
user = get_use_case.execute(user_id)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Usuario con ID {user_id} no encontrado"
|
||||
)
|
||||
return user
|
||||
|
||||
@router.get("/email/{email}", response_model=UserResponse)
|
||||
async def get_user_by_email(email: str):
|
||||
"""Obtiene un usuario por email"""
|
||||
get_use_case = GetUserByEmail(user_repo)
|
||||
user = get_use_case.execute(email)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Usuario con email {email} no encontrado"
|
||||
)
|
||||
return user
|
||||
|
||||
@router.get("/", response_model=list[UserResponse])
|
||||
async def list_users():
|
||||
"""Obtiene todos los usuarios"""
|
||||
list_use_case = ListAllUsers(user_repo)
|
||||
return list_use_case.execute()
|
||||
|
||||
@router.put("/{user_id}", response_model=UserResponse)
|
||||
async def update_user(user_id: int, user_data: UserUpdateRequest):
|
||||
"""Actualiza un usuario"""
|
||||
update_use_case = UpdateUser(user_repo)
|
||||
user = update_use_case.execute(
|
||||
user_id=user_id,
|
||||
nombre=user_data.nombre,
|
||||
apellido=user_data.apellido,
|
||||
url_foto_perfil=user_data.url_foto_perfil,
|
||||
biografia=user_data.biografia
|
||||
)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Usuario con ID {user_id} no encontrado"
|
||||
)
|
||||
return user
|
||||
|
||||
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_user(user_id: int):
|
||||
"""Elimina un usuario"""
|
||||
delete_use_case = DeleteUser(user_repo)
|
||||
success = delete_use_case.execute(user_id)
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Usuario con ID {user_id} no encontrado"
|
||||
)
|
||||
return None
|
||||
60
src/main.py
Normal file
60
src/main.py
Normal file
@@ -0,0 +1,60 @@
|
||||
"""
|
||||
Punto de entrada principal para VoxPopuli Microservices
|
||||
Ejecuta dos APIs en paralelo: Usuarios (puerto 8000) y Reportes (puerto 8001)
|
||||
"""
|
||||
from infrastructure.api.users.app import create_app as create_users_app
|
||||
from infrastructure.api.reports.app import create_app as create_reports_app
|
||||
from core.config import ConfSettings
|
||||
import threading
|
||||
import uvicorn
|
||||
|
||||
def run_users_api():
|
||||
"""Ejecuta la API de Usuarios en puerto 8000"""
|
||||
app_users = create_users_app()
|
||||
uvicorn.run(
|
||||
app_users,
|
||||
host=ConfSettings.host,
|
||||
port=8000,
|
||||
reload=False,
|
||||
log_level=ConfSettings.log_level,
|
||||
)
|
||||
|
||||
def run_reports_api():
|
||||
"""Ejecuta la API de Reportes en puerto 8001"""
|
||||
app_reports = create_reports_app()
|
||||
uvicorn.run(
|
||||
app_reports,
|
||||
host=ConfSettings.host,
|
||||
port=8001,
|
||||
reload=False,
|
||||
log_level=ConfSettings.log_level,
|
||||
)
|
||||
|
||||
def run():
|
||||
"""Inicia ambas APIs en threads separados"""
|
||||
print("=" * 60)
|
||||
print("Iniciando VoxPopuli Microservices...")
|
||||
print("=" * 60)
|
||||
|
||||
users_thread = threading.Thread(target=run_users_api, daemon=True, name="Users-API")
|
||||
reports_thread = threading.Thread(target=run_reports_api, daemon=True, name="Reports-API")
|
||||
|
||||
users_thread.start()
|
||||
reports_thread.start()
|
||||
|
||||
print("\n✓ API de Usuarios ejecutándose en http://0.0.0.0:8000")
|
||||
print("✓ API de Reportes ejecutándose en http://0.0.0.0:8001")
|
||||
print("\nDocumentación disponible en:")
|
||||
print(" - Usuarios: http://localhost:8000/docs")
|
||||
print(" - Reportes: http://localhost:8001/docs")
|
||||
print("\n" + "=" * 60 + "\n")
|
||||
|
||||
try:
|
||||
users_thread.join()
|
||||
reports_thread.join()
|
||||
except KeyboardInterrupt:
|
||||
print("\n\nRecibiendo señal de salida...")
|
||||
print("Cerrando APIs...")
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
Reference in New Issue
Block a user