Added everything

This commit is contained in:
Juan M. Ley
2026-03-16 21:05:52 -06:00
parent 00e997debf
commit b526e23149
44 changed files with 2147 additions and 3 deletions

0
src/__init__.py Normal file
View File

View File

View File

View 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

View 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

View File

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

View 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
View File

37
src/core/config.py Normal file
View 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
View File

14
src/domain/reports.py Normal file
View 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
View 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]

View File

View File

View 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()

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

View 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"]

View File

@@ -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")
)

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

View File

View 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

View 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

View 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"
}

View 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"]
)

View 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

View File

View 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

View 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"
}

View 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"]
)

View 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

View 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
View 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()