Added everything
This commit is contained in:
12
.env.example
Normal file
12
.env.example
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# Variables de entorno para VoxPopuli Microservices
|
||||||
|
|
||||||
|
# Base de Datos MySQL (API de Usuarios)
|
||||||
|
MYSQL_URL=mysql+pymysql://user:password@localhost/voxpopuli_users
|
||||||
|
|
||||||
|
# Base de Datos MongoDB (API de Reportes)
|
||||||
|
MONGODB_URL=mongodb://localhost:27017
|
||||||
|
MONGODB_DB=voxpopuli_reports
|
||||||
|
|
||||||
|
# Configuración de Server
|
||||||
|
HOST=0.0.0.0
|
||||||
|
LOG_LEVEL=info
|
||||||
69
.gitignore
vendored
Normal file
69
.gitignore
vendored
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# Gitignore para VoxPopuli Microservices
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
.DS_Store
|
||||||
|
*.code-workspace
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Bases de Datos
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
test.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
|
||||||
|
# Cache
|
||||||
|
.cache/
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
.tox/
|
||||||
|
.hypothesis/
|
||||||
|
|
||||||
|
# Otros
|
||||||
|
*.pyc
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
node_modules/
|
||||||
|
*.pem
|
||||||
|
credentials.json
|
||||||
290
API_EXAMPLES.json
Normal file
290
API_EXAMPLES.json
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
{
|
||||||
|
"endpoints": {
|
||||||
|
"usuarios": {
|
||||||
|
"crear_usuario": {
|
||||||
|
"metodo": "POST",
|
||||||
|
"url": "http://localhost:8000/users/",
|
||||||
|
"solicitud": {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"body": {
|
||||||
|
"nombre": "Juan",
|
||||||
|
"apellido": "Pérez",
|
||||||
|
"email": "juan.perez@example.com",
|
||||||
|
"fecha_nacimiento": "1990-05-15T00:00:00",
|
||||||
|
"url_foto_perfil": "https://example.com/photos/juan.jpg",
|
||||||
|
"biografia": "Soy un usuario activo de VoxPopuli"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"respuesta_exitosa": {
|
||||||
|
"codigo": 201,
|
||||||
|
"body": {
|
||||||
|
"user_id": 1,
|
||||||
|
"nombre": "Juan",
|
||||||
|
"apellido": "Pérez",
|
||||||
|
"email": "juan.perez@example.com",
|
||||||
|
"fecha_nacimiento": "1990-05-15T00:00:00",
|
||||||
|
"fecha_creacion": "2024-01-15T10:30:00",
|
||||||
|
"calificacion": 50.0,
|
||||||
|
"numero_reportes": 0,
|
||||||
|
"url_foto_perfil": "https://example.com/photos/juan.jpg",
|
||||||
|
"biografia": "Soy un usuario activo de VoxPopuli"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"obtener_usuario_por_id": {
|
||||||
|
"metodo": "GET",
|
||||||
|
"url": "http://localhost:8000/users/1",
|
||||||
|
"respuesta": {
|
||||||
|
"codigo": 200,
|
||||||
|
"body": {
|
||||||
|
"user_id": 1,
|
||||||
|
"nombre": "Juan",
|
||||||
|
"apellido": "Pérez",
|
||||||
|
"email": "juan.perez@example.com",
|
||||||
|
"fecha_nacimiento": "1990-05-15T00:00:00",
|
||||||
|
"fecha_creacion": "2024-01-15T10:30:00",
|
||||||
|
"calificacion": 50.0,
|
||||||
|
"numero_reportes": 0,
|
||||||
|
"url_foto_perfil": "https://example.com/photos/juan.jpg",
|
||||||
|
"biografia": "Soy un usuario activo de VoxPopuli"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"obtener_usuario_por_email": {
|
||||||
|
"metodo": "GET",
|
||||||
|
"url": "http://localhost:8000/users/email/juan.perez@example.com",
|
||||||
|
"respuesta": {
|
||||||
|
"codigo": 200,
|
||||||
|
"body": {
|
||||||
|
"user_id": 1,
|
||||||
|
"nombre": "Juan",
|
||||||
|
"apellido": "Pérez",
|
||||||
|
"email": "juan.perez@example.com",
|
||||||
|
"fecha_nacimiento": "1990-05-15T00:00:00",
|
||||||
|
"fecha_creacion": "2024-01-15T10:30:00",
|
||||||
|
"calificacion": 50.0,
|
||||||
|
"numero_reportes": 0,
|
||||||
|
"url_foto_perfil": "https://example.com/photos/juan.jpg",
|
||||||
|
"biografia": "Soy un usuario activo de VoxPopuli"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"listar_todos_usuarios": {
|
||||||
|
"metodo": "GET",
|
||||||
|
"url": "http://localhost:8000/users/",
|
||||||
|
"respuesta": {
|
||||||
|
"codigo": 200,
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"user_id": 1,
|
||||||
|
"nombre": "Juan",
|
||||||
|
"apellido": "Pérez",
|
||||||
|
"email": "juan.perez@example.com",
|
||||||
|
"fecha_nacimiento": "1990-05-15T00:00:00",
|
||||||
|
"fecha_creacion": "2024-01-15T10:30:00",
|
||||||
|
"calificacion": 50.0,
|
||||||
|
"numero_reportes": 0,
|
||||||
|
"url_foto_perfil": "https://example.com/photos/juan.jpg",
|
||||||
|
"biografia": "Soy un usuario activo de VoxPopuli"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"actualizar_usuario": {
|
||||||
|
"metodo": "PUT",
|
||||||
|
"url": "http://localhost:8000/users/1",
|
||||||
|
"solicitud": {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"body": {
|
||||||
|
"nombre": "Juan Carlos",
|
||||||
|
"apellido": "Pérez García",
|
||||||
|
"url_foto_perfil": "https://example.com/photos/juan-new.jpg",
|
||||||
|
"biografia": "Actualizando mi perfil"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"respuesta_exitosa": {
|
||||||
|
"codigo": 200,
|
||||||
|
"body": {
|
||||||
|
"user_id": 1,
|
||||||
|
"nombre": "Juan Carlos",
|
||||||
|
"apellido": "Pérez García",
|
||||||
|
"email": "juan.perez@example.com",
|
||||||
|
"fecha_nacimiento": "1990-05-15T00:00:00",
|
||||||
|
"fecha_creacion": "2024-01-15T10:30:00",
|
||||||
|
"calificacion": 50.0,
|
||||||
|
"numero_reportes": 0,
|
||||||
|
"url_foto_perfil": "https://example.com/photos/juan-new.jpg",
|
||||||
|
"biografia": "Actualizando mi perfil"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"eliminar_usuario": {
|
||||||
|
"metodo": "DELETE",
|
||||||
|
"url": "http://localhost:8000/users/1",
|
||||||
|
"respuesta_exitosa": {
|
||||||
|
"codigo": 204,
|
||||||
|
"body": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"reportes": {
|
||||||
|
"crear_reporte": {
|
||||||
|
"metodo": "POST",
|
||||||
|
"url": "http://localhost:8001/reports/",
|
||||||
|
"solicitud": {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"body": {
|
||||||
|
"id_usuario": 1,
|
||||||
|
"tipo_reporte": 1,
|
||||||
|
"descripcion": "Se encontró un bache en la calle principal que pone en riesgo el tráfico",
|
||||||
|
"ubicacion": "Calle Principal, entre Av. Central y Av. Norte, Cuadra 5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"respuesta_exitosa": {
|
||||||
|
"codigo": 201,
|
||||||
|
"body": {
|
||||||
|
"id_reporte": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"id_usuario": 1,
|
||||||
|
"tipo_reporte": 1,
|
||||||
|
"descripcion": "Se encontró un bache en la calle principal que pone en riesgo el tráfico",
|
||||||
|
"ubicacion": "Calle Principal, entre Av. Central y Av. Norte, Cuadra 5",
|
||||||
|
"visibilidad": 50.0,
|
||||||
|
"fecha_creacion": "2024-01-15T10:35:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"obtener_reporte_por_id": {
|
||||||
|
"metodo": "GET",
|
||||||
|
"url": "http://localhost:8001/reports/550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"respuesta": {
|
||||||
|
"codigo": 200,
|
||||||
|
"body": {
|
||||||
|
"id_reporte": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"id_usuario": 1,
|
||||||
|
"tipo_reporte": 1,
|
||||||
|
"descripcion": "Se encontró un bache en la calle principal que pone en riesgo el tráfico",
|
||||||
|
"ubicacion": "Calle Principal, entre Av. Central y Av. Norte, Cuadra 5",
|
||||||
|
"visibilidad": 50.0,
|
||||||
|
"fecha_creacion": "2024-01-15T10:35:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"obtener_reportes_de_usuario": {
|
||||||
|
"metodo": "GET",
|
||||||
|
"url": "http://localhost:8001/reports/user/1",
|
||||||
|
"respuesta": {
|
||||||
|
"codigo": 200,
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"id_reporte": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"id_usuario": 1,
|
||||||
|
"tipo_reporte": 1,
|
||||||
|
"descripcion": "Se encontró un bache en la calle principal que pone en riesgo el tráfico",
|
||||||
|
"ubicacion": "Calle Principal, entre Av. Central y Av. Norte, Cuadra 5",
|
||||||
|
"visibilidad": 50.0,
|
||||||
|
"fecha_creacion": "2024-01-15T10:35:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"listar_todos_reportes": {
|
||||||
|
"metodo": "GET",
|
||||||
|
"url": "http://localhost:8001/reports/",
|
||||||
|
"respuesta": {
|
||||||
|
"codigo": 200,
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"id_reporte": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"id_usuario": 1,
|
||||||
|
"tipo_reporte": 1,
|
||||||
|
"descripcion": "Se encontró un bache en la calle principal que pone en riesgo el tráfico",
|
||||||
|
"ubicacion": "Calle Principal, entre Av. Central y Av. Norte, Cuadra 5",
|
||||||
|
"visibilidad": 50.0,
|
||||||
|
"fecha_creacion": "2024-01-15T10:35:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"obtener_reportes_shadowbaneados": {
|
||||||
|
"metodo": "GET",
|
||||||
|
"url": "http://localhost:8001/reports/shadowbanned/list?threshold=20",
|
||||||
|
"respuesta": {
|
||||||
|
"codigo": 200,
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"id_reporte": "550e8400-e29b-41d4-a716-446655440001",
|
||||||
|
"id_usuario": 2,
|
||||||
|
"tipo_reporte": 2,
|
||||||
|
"descripcion": "Reporte spam con baja visibilidad",
|
||||||
|
"ubicacion": "Ubicación desconocida",
|
||||||
|
"visibilidad": 10.0,
|
||||||
|
"fecha_creacion": "2024-01-14T08:00:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"actualizar_visibilidad": {
|
||||||
|
"metodo": "PUT",
|
||||||
|
"url": "http://localhost:8001/reports/550e8400-e29b-41d4-a716-446655440000/visibility",
|
||||||
|
"solicitud": {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"body": {
|
||||||
|
"new_visibility": 85.0,
|
||||||
|
"penalize_author": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"respuesta_exitosa": {
|
||||||
|
"codigo": 200,
|
||||||
|
"body": {
|
||||||
|
"message": "Visibilidad actualizada exitosamente",
|
||||||
|
"report_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"new_visibility": 85.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"actualizar_visibilidad_con_penalizacion": {
|
||||||
|
"metodo": "PUT",
|
||||||
|
"url": "http://localhost:8001/reports/550e8400-e29b-41d4-a716-446655440001/visibility",
|
||||||
|
"solicitud": {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"body": {
|
||||||
|
"new_visibility": 15.0,
|
||||||
|
"penalize_author": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"descripcion": "Reduce la visibilidad a shadowban y penaliza la calificación del autor si se establece penalize_author=true",
|
||||||
|
"respuesta_exitosa": {
|
||||||
|
"codigo": 200,
|
||||||
|
"body": {
|
||||||
|
"message": "Visibilidad actualizada exitosamente",
|
||||||
|
"report_id": "550e8400-e29b-41d4-a716-446655440001",
|
||||||
|
"new_visibility": 15.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"eliminar_reporte": {
|
||||||
|
"metodo": "DELETE",
|
||||||
|
"url": "http://localhost:8001/reports/550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"respuesta_exitosa": {
|
||||||
|
"codigo": 204,
|
||||||
|
"body": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tipos_reporte": {
|
||||||
|
"1": "Infraestructura/Vía pública",
|
||||||
|
"2": "Inseguridad",
|
||||||
|
"3": "Contaminación",
|
||||||
|
"4": "Servicios públicos",
|
||||||
|
"5": "Otro"
|
||||||
|
},
|
||||||
|
"codigos_estado_http": {
|
||||||
|
"200": "OK - La solicitud fue exitosa",
|
||||||
|
"201": "Created - Recurso creado exitosamente",
|
||||||
|
"204": "No Content - Eliminación exitosa",
|
||||||
|
"400": "Bad Request - Error en la solicitud",
|
||||||
|
"404": "Not Found - Recurso no encontrado",
|
||||||
|
"500": "Internal Server Error - Error en el servidor"
|
||||||
|
}
|
||||||
|
}
|
||||||
247
ARCHITECTURE.md
Normal file
247
ARCHITECTURE.md
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
# Arquitectura de VoxPopuli Microservices
|
||||||
|
|
||||||
|
## Patrones Arquitectónicos Usados
|
||||||
|
|
||||||
|
### 1. Clean Architecture (Arquitectura Limpia)
|
||||||
|
|
||||||
|
La aplicación está organizada en capas independientes:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ API REST (FastAPI) │
|
||||||
|
│ Infraestructura API (HTTP Handlers) │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ Application Layer (Servicios) │
|
||||||
|
│ - Use Cases / Application Services │
|
||||||
|
│ - Lógica de negocio de la aplicación │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ Domain Layer (Dominio) │
|
||||||
|
│ - Entidades de negocio puras │
|
||||||
|
│ - Reglas de negocio independientes │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ Infrastructure Layer (Infraestructura) │
|
||||||
|
│ - Adaptadores (Repositorios) │
|
||||||
|
│ - Acceso a Datos (MySQL, MongoDB) │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Microservicios
|
||||||
|
|
||||||
|
Dos microservicios independientes:
|
||||||
|
|
||||||
|
- **Usuarios API** (Puerto 8000) - Gestión de usuarios y credenciales
|
||||||
|
- **Reportes API** (Puerto 8001) - Gestión de reportes comunitarios
|
||||||
|
|
||||||
|
Cada microservicio:
|
||||||
|
- Tiene su propia base de datos
|
||||||
|
- Es escalable independientemente
|
||||||
|
- Se puede desplegar por separado
|
||||||
|
- Expone su propia API REST
|
||||||
|
|
||||||
|
### 3. Patrón Repository
|
||||||
|
|
||||||
|
Abstracción para acceso a datos:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────┐
|
||||||
|
│ Service Layer │
|
||||||
|
└────────┬─────────┘
|
||||||
|
│ depends on
|
||||||
|
▼
|
||||||
|
┌──────────────────────────┐
|
||||||
|
│ Port (Interface) │
|
||||||
|
│ UserRepository │
|
||||||
|
│ ReportRepository │
|
||||||
|
└────────┬─────────────────┘
|
||||||
|
│ implemented by
|
||||||
|
▼
|
||||||
|
┌──────────────────────────┐
|
||||||
|
│ Concrete Adapters │
|
||||||
|
│ UserRepositorySQL │
|
||||||
|
│ ReportRepositoryMongo │
|
||||||
|
└──────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Beneficios:**
|
||||||
|
- Independencia de la implementación de BD
|
||||||
|
- Fácil de testear (usar mocks)
|
||||||
|
- Reutilizable en diferentes contextos
|
||||||
|
|
||||||
|
### 4. Inversión de Dependencias (Dependency Inversion)
|
||||||
|
|
||||||
|
Los servicios dependen de **abstracciones (interfaces)**, no de implementaciones concretas:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class CreateUser:
|
||||||
|
def __init__(self, repo: UserRepository): # Depende de interfaz
|
||||||
|
self.repo = repo
|
||||||
|
|
||||||
|
# Puede usar cualquier implementación de UserRepository
|
||||||
|
service = CreateUser(UserRepositorySQL()) # Implementación SQL
|
||||||
|
# o
|
||||||
|
service = CreateUser(UserRepositoryMock()) # Para testing
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flujo de Solicitud
|
||||||
|
|
||||||
|
### Crear Usuario:
|
||||||
|
|
||||||
|
```
|
||||||
|
1. HTTP POST /users/
|
||||||
|
└─> FastAPI Handler
|
||||||
|
└─> CreateUser Use Case
|
||||||
|
└─> UserRepository.save()
|
||||||
|
└─> UserRepositorySQL
|
||||||
|
└─> SQLAlchemy
|
||||||
|
└─> MySQL Database
|
||||||
|
```
|
||||||
|
|
||||||
|
### Crear Reporte:
|
||||||
|
|
||||||
|
```
|
||||||
|
1. HTTP POST /reports/
|
||||||
|
└─> FastAPI Handler
|
||||||
|
└─> CreateReport Use Case
|
||||||
|
└─> ReportRepository.save()
|
||||||
|
└─> UserRepository.increment_reports()
|
||||||
|
└─> ReportRepositoryMongo
|
||||||
|
└─> UserRepositorySQL
|
||||||
|
└─> MongoDB
|
||||||
|
└─> MySQL
|
||||||
|
```
|
||||||
|
|
||||||
|
## Capas Detalladas
|
||||||
|
|
||||||
|
### Domain Layer (src/domain/)
|
||||||
|
|
||||||
|
**Entidades puras de negocio**:
|
||||||
|
- `User`: Representa un usuario del sistema
|
||||||
|
- `Report`: Representa un reporte comunitario
|
||||||
|
|
||||||
|
No tienen dependencias externas. Solo representan conceptos de negocio.
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclass
|
||||||
|
class User:
|
||||||
|
user_id: int
|
||||||
|
nombre: str
|
||||||
|
email: str
|
||||||
|
# ... más campos
|
||||||
|
```
|
||||||
|
|
||||||
|
### Application Layer (src/application/)
|
||||||
|
|
||||||
|
**Use Cases y Servicios**:
|
||||||
|
|
||||||
|
#### Ports (Interfaces):
|
||||||
|
- `UserRepository`: Contrato para acceso a datos de usuarios
|
||||||
|
- `ReportRepository`: Contrato para acceso a datos de reportes
|
||||||
|
|
||||||
|
#### Services (Use Cases):
|
||||||
|
- `CreateUser`, `GetUserById`, `UpdateUser`, `DeleteUser`
|
||||||
|
- `CreateReport`, `GetReportById`, `UpdateReportVisibility`
|
||||||
|
|
||||||
|
Contienen la lógica de negocio:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class CreateReport:
|
||||||
|
def execute(self, id_usuario, tipo_reporte, ...):
|
||||||
|
# 1. Validar que usuario existe
|
||||||
|
user = self.user_repo.find_by_id(id_usuario)
|
||||||
|
|
||||||
|
# 2. Crear reporte
|
||||||
|
report = Report(...)
|
||||||
|
|
||||||
|
# 3. Guardar en BD
|
||||||
|
self.repo.save(report)
|
||||||
|
|
||||||
|
# 4. Actualizar contador de usuario
|
||||||
|
self.user_repo.increment_reports(id_usuario)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Infrastructure Layer (src/infrastructure/)
|
||||||
|
|
||||||
|
#### Persistence Adapters:
|
||||||
|
|
||||||
|
**user_repository_sql.py**:
|
||||||
|
- Implementa `UserRepository` usando SQLAlchemy
|
||||||
|
- Convierte entre modelo de dominio y modelo de BD
|
||||||
|
|
||||||
|
**report_repository_mongo.py**:
|
||||||
|
- Implementa `ReportRepository` usando PyMongo
|
||||||
|
- Convierte entre modelo de dominio y documento MongoDB
|
||||||
|
|
||||||
|
#### API Handlers:
|
||||||
|
|
||||||
|
**users/users.py**:
|
||||||
|
- Endpoints HTTP para gestión de usuarios
|
||||||
|
- Usa esquemas Pydantic para validación
|
||||||
|
|
||||||
|
**reports/reports.py**:
|
||||||
|
- Endpoints HTTP para gestión de reportes
|
||||||
|
- Mapeo de solicitudes a use cases
|
||||||
|
|
||||||
|
### Core Layer (src/core/)
|
||||||
|
|
||||||
|
Configuración centralizada:
|
||||||
|
- Variables de entorno
|
||||||
|
- Configuración de logging
|
||||||
|
- Configuración de bases de datos
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
La arquitectura facilita el testing:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Mock Repository para testing
|
||||||
|
class UserRepositoryMock(UserRepository):
|
||||||
|
def __init__(self):
|
||||||
|
self.users = {}
|
||||||
|
|
||||||
|
def save(self, user):
|
||||||
|
self.users[user.user_id] = user
|
||||||
|
return user
|
||||||
|
|
||||||
|
# Usar en tests
|
||||||
|
def test_create_user():
|
||||||
|
mock_repo = UserRepositoryMock()
|
||||||
|
service = CreateUser(mock_repo)
|
||||||
|
user = service.execute(...)
|
||||||
|
assert mock_repo.users[1] is not None
|
||||||
|
```
|
||||||
|
|
||||||
|
## Escalabilidad
|
||||||
|
|
||||||
|
Cada componente puede escalar independientemente:
|
||||||
|
|
||||||
|
1. **Usuarios API**: Escalar horizontal con load balancer
|
||||||
|
2. **Reportes API**: Escalar horizontal con load balancer
|
||||||
|
3. **MySQL**: Replicación master-slave
|
||||||
|
4. **MongoDB**: Sharding automático
|
||||||
|
|
||||||
|
## Despliegue
|
||||||
|
|
||||||
|
Cada microservicio puede desplegarse:
|
||||||
|
|
||||||
|
- **Docker**: Contenedores independientes
|
||||||
|
- **Kubernetes**: Pods independientes
|
||||||
|
- **Serverless**: Funciones Lambda independientes
|
||||||
|
|
||||||
|
## Monitoreo
|
||||||
|
|
||||||
|
Cada API expone:
|
||||||
|
- `/health` - Health check
|
||||||
|
- `/docs` - Swagger UI
|
||||||
|
- Logging estructurado
|
||||||
|
- Métricas por endpoint
|
||||||
|
|
||||||
|
## Futuras Mejoras
|
||||||
|
|
||||||
|
1. **Autenticación**: JWT tokens en API
|
||||||
|
2. **Autorización**: RBAC (Role-Based Access Control)
|
||||||
|
3. **Rate Limiting**: Proteger contra abuso
|
||||||
|
4. **Caché**: Redis para datos frecuentes
|
||||||
|
5. **Message Queue**: RabbitMQ para comunicación asíncrona
|
||||||
|
6. **Logging Centralizado**: ELK Stack
|
||||||
|
7. **Observabilidad**: Prometheus + Grafana
|
||||||
|
8. **Tracing**: Jaeger para rastreo de solicitudes
|
||||||
173
DATABASE.md
Normal file
173
DATABASE.md
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
# Configuración de Base de Datos - VoxPopuli Microservices
|
||||||
|
|
||||||
|
## MySQL (API de Usuarios)
|
||||||
|
|
||||||
|
### Crear la base de datos:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE DATABASE voxpopuli_users CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
USE voxpopuli_users;
|
||||||
|
|
||||||
|
CREATE TABLE usuarios (
|
||||||
|
user_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
nombre VARCHAR(100) NOT NULL,
|
||||||
|
apellido VARCHAR(100) NOT NULL,
|
||||||
|
email VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
fecha_nacimiento DATETIME NOT NULL,
|
||||||
|
fecha_creacion DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
calificacion FLOAT DEFAULT 50.0,
|
||||||
|
numero_reportes INT DEFAULT 0,
|
||||||
|
url_foto_perfil VARCHAR(500),
|
||||||
|
biografia VARCHAR(1000),
|
||||||
|
INDEX idx_nombre (nombre),
|
||||||
|
INDEX idx_email (email),
|
||||||
|
INDEX idx_fecha_creacion (fecha_creacion)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuración en .env:
|
||||||
|
|
||||||
|
```
|
||||||
|
MYSQL_URL=mysql+pymysql://usuario:contraseña@localhost:3306/voxpopuli_users
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nota sobre SQLAlchemy:
|
||||||
|
|
||||||
|
El proyecto usa SQLAlchemy para mapeo objeto-relacional (ORM). Las tablas se crean automáticamente cuando se ejecuta el código si usas:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from infrastructure.adapters.persistence.db import Base, engine
|
||||||
|
|
||||||
|
# Crear todas las tablas definidas en los modelos
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
```
|
||||||
|
|
||||||
|
## MongoDB (API de Reportes)
|
||||||
|
|
||||||
|
### Crear la base de datos y colección:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
use voxpopuli_reports
|
||||||
|
|
||||||
|
db.createCollection("reportes")
|
||||||
|
|
||||||
|
// Crear índices para mejor rendimiento
|
||||||
|
db.reportes.createIndex({ "id_usuario": 1 })
|
||||||
|
db.reportes.createIndex({ "id_reporte": 1 }, { unique: true })
|
||||||
|
db.reportes.createIndex({ "visibilidad": 1 })
|
||||||
|
db.reportes.createIndex({ "fecha_creacion": -1 })
|
||||||
|
db.reportes.createIndex({ "tipo_reporte": 1 })
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuración en .env:
|
||||||
|
|
||||||
|
```
|
||||||
|
MONGODB_URL=mongodb://localhost:27017
|
||||||
|
MONGODB_DB=voxpopuli_reports
|
||||||
|
```
|
||||||
|
|
||||||
|
### Estructura de documento en MongoDB:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"_id": ObjectId("..."),
|
||||||
|
"id_reporte": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"id_usuario": 1,
|
||||||
|
"tipo_reporte": 1,
|
||||||
|
"descripcion": "Descripción del reporte",
|
||||||
|
"ubicacion": "Ubicación específica",
|
||||||
|
"visibilidad": 50.0,
|
||||||
|
"fecha_creacion": ISODate("2024-01-15T10:30:00Z")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Variables de Entorno
|
||||||
|
|
||||||
|
Archivo `.env` requerido:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# MySQL
|
||||||
|
MYSQL_URL=mysql+pymysql://user:password@localhost/voxpopuli_users
|
||||||
|
|
||||||
|
# MongoDB
|
||||||
|
MONGODB_URL=mongodb://localhost:27017
|
||||||
|
MONGODB_DB=voxpopuli_reports
|
||||||
|
|
||||||
|
# Server
|
||||||
|
HOST=0.0.0.0
|
||||||
|
LOG_LEVEL=info
|
||||||
|
```
|
||||||
|
|
||||||
|
## Inicializar Bases de Datos
|
||||||
|
|
||||||
|
### Opción 1: Automático (Recomendado)
|
||||||
|
|
||||||
|
Crear un script `init_db.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from infrastructure.adapters.persistence.db import Base, engine
|
||||||
|
from infrastructure.adapters.persistence.models import UserModel
|
||||||
|
|
||||||
|
# Crear todas las tablas de MySQL
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
|
# Para MongoDB, la colección se crea automáticamente
|
||||||
|
print("Bases de datos inicializadas correctamente")
|
||||||
|
```
|
||||||
|
|
||||||
|
Ejecutar:
|
||||||
|
```bash
|
||||||
|
python init_db.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Opción 2: Manual
|
||||||
|
|
||||||
|
1. Crear MySQL database y tabla (ver scripts arriba)
|
||||||
|
2. Crear MongoDB database y colección (ver scripts arriba)
|
||||||
|
|
||||||
|
## Verificar Conexiones
|
||||||
|
|
||||||
|
### MySQL:
|
||||||
|
```python
|
||||||
|
from infrastructure.adapters.persistence.db import SessionLocal
|
||||||
|
|
||||||
|
db = SessionLocal()
|
||||||
|
print(db.execute("SELECT 1"))
|
||||||
|
db.close()
|
||||||
|
```
|
||||||
|
|
||||||
|
### MongoDB:
|
||||||
|
```python
|
||||||
|
from infrastructure.adapters.persistence.mongodb import get_reports_collection
|
||||||
|
|
||||||
|
collection = get_reports_collection()
|
||||||
|
print(collection.find_one())
|
||||||
|
```
|
||||||
|
|
||||||
|
## Respaldo y Restauración
|
||||||
|
|
||||||
|
### MySQL:
|
||||||
|
```bash
|
||||||
|
# Respaldo
|
||||||
|
mysqldump -u user -p voxpopuli_users > backup.sql
|
||||||
|
|
||||||
|
# Restauración
|
||||||
|
mysql -u user -p voxpopuli_users < backup.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### MongoDB:
|
||||||
|
```bash
|
||||||
|
# Respaldo
|
||||||
|
mongodump --db voxpopuli_reports --out ./backup
|
||||||
|
|
||||||
|
# Restauración
|
||||||
|
mongorestore --db voxpopuli_reports ./backup/voxpopuli_reports
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas Importantes
|
||||||
|
|
||||||
|
1. **Contraseñas**: Nunca guardes contraseñas reales en el repositorio. Usa variables de entorno.
|
||||||
|
2. **Índices**: Los índices mejoran significativamente el rendimiento en consultas frecuentes.
|
||||||
|
3. **Conexiones**: Los pools de conexión están configurados en `db.py` y `mongodb.py`.
|
||||||
|
4. **Unicode**: MySQL está configurado con UTF-8 para soportar múltiples idiomas correctamente.
|
||||||
52
enable-venv.bat
Normal file
52
enable-venv.bat
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
@echo off
|
||||||
|
REM Script para habilitar el entorno virtual de VoxPopuli Microservices (Windows)
|
||||||
|
|
||||||
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ========================================
|
||||||
|
echo VoxPopuli Microservices Setup
|
||||||
|
echo ========================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM Crear entorno virtual si no existe
|
||||||
|
if not exist "venv" (
|
||||||
|
echo Creando entorno virtual...
|
||||||
|
python -m venv venv
|
||||||
|
echo [OK] Entorno virtual creado
|
||||||
|
) else (
|
||||||
|
echo [OK] Entorno virtual ya existe
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Activar entorno virtual
|
||||||
|
echo Activando entorno virtual...
|
||||||
|
call venv\Scripts\activate.bat
|
||||||
|
echo [OK] Entorno virtual activado
|
||||||
|
|
||||||
|
REM Instalar dependencias
|
||||||
|
if exist "requirements.txt" (
|
||||||
|
echo Instalando dependencias...
|
||||||
|
pip install -r requirements.txt
|
||||||
|
echo [OK] Dependencias instaladas
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Verificar archivo .env
|
||||||
|
if not exist ".env" (
|
||||||
|
if exist ".env.example" (
|
||||||
|
echo Creando archivo .env desde .env.example...
|
||||||
|
copy .env.example .env
|
||||||
|
echo [ATENCION] Por favor, actualiza .env con tus credenciales
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ========================================
|
||||||
|
echo [OK] Setup completado
|
||||||
|
echo ========================================
|
||||||
|
echo.
|
||||||
|
echo Para ejecutar los microservicios:
|
||||||
|
echo cd src
|
||||||
|
echo python main.py
|
||||||
|
echo.
|
||||||
|
|
||||||
|
pause
|
||||||
52
enable-venv.sh
Normal file
52
enable-venv.sh
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Script para habilitar el entorno virtual de VoxPopuli Microservices
|
||||||
|
|
||||||
|
# Colores para output
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
echo -e "${YELLOW}================================${NC}"
|
||||||
|
echo -e "${YELLOW}VoxPopuli Microservices Setup${NC}"
|
||||||
|
echo -e "${YELLOW}================================${NC}"
|
||||||
|
|
||||||
|
# Crear entorno virtual si no existe
|
||||||
|
if [ ! -d "venv" ]; then
|
||||||
|
echo -e "${YELLOW}Creando entorno virtual...${NC}"
|
||||||
|
python3 -m venv venv
|
||||||
|
echo -e "${GREEN}✓ Entorno virtual creado${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${GREEN}✓ Entorno virtual ya existe${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Activar entorno virtual
|
||||||
|
echo -e "${YELLOW}Activando entorno virtual...${NC}"
|
||||||
|
source venv/bin/activate
|
||||||
|
echo -e "${GREEN}✓ Entorno virtual activado${NC}"
|
||||||
|
|
||||||
|
# Instalar dependencias
|
||||||
|
if [ -f "requirements.txt" ]; then
|
||||||
|
echo -e "${YELLOW}Instalando dependencias...${NC}"
|
||||||
|
pip install -r requirements.txt
|
||||||
|
echo -e "${GREEN}✓ Dependencias instaladas${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verificar archivo .env
|
||||||
|
if [ ! -f ".env" ]; then
|
||||||
|
if [ -f ".env.example" ]; then
|
||||||
|
echo -e "${YELLOW}Creando archivo .env desde .env.example...${NC}"
|
||||||
|
cp .env.example .env
|
||||||
|
echo -e "${YELLOW}⚠ Por favor, actualiza .env con tus credenciales${NC}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo -e "${GREEN}================================${NC}"
|
||||||
|
echo -e "${GREEN}✓ Setup completado${NC}"
|
||||||
|
echo -e "${GREEN}================================${NC}"
|
||||||
|
echo
|
||||||
|
echo -e "${YELLOW}Para ejecutar los microservicios:${NC}"
|
||||||
|
echo "cd src"
|
||||||
|
echo "python main.py"
|
||||||
|
echo
|
||||||
237
readme.md
237
readme.md
@@ -1,3 +1,234 @@
|
|||||||
# VOX POPULI
|
# VoxPopuli Microservices
|
||||||
## INFRAESTRUCTURA PARA "VOZ CIUDADANA"
|
|
||||||
#### Proyecto de HOKZAAP SOFTWARE S. de R.L. de C.V.
|
Plataforma de reportes comunitarios con arquitectura de microservicios usando FastAPI.
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
VoxPopuli es una infraestructura de microservicios que permite a usuarios crear y reportar incidentes en la comunidad, con un sistema de calificación y visibilidad basado en votación comunitaria.
|
||||||
|
|
||||||
|
### Características principales:
|
||||||
|
|
||||||
|
- **API de Usuarios**: Gestión de usuarios y autenticación (MySQL)
|
||||||
|
- **API de Reportes**: Gestión de reportes comunitarios (MongoDB)
|
||||||
|
- Sistema de calificación de usuarios (0-100)
|
||||||
|
- Sistema de visibilidad de reportes con shadowban para contenido de baja calidad
|
||||||
|
- Penalización de usuarios que crean reportes shadowbaneados
|
||||||
|
|
||||||
|
## Estructura del Proyecto
|
||||||
|
|
||||||
|
```
|
||||||
|
VoxPopuli_Microservices/
|
||||||
|
├── src/
|
||||||
|
│ ├── domain/ # Modelos de dominio (entidades de negocio)
|
||||||
|
│ │ ├── users.py # Modelo User
|
||||||
|
│ │ └── reports.py # Modelo Report
|
||||||
|
│ ├── application/ # Capa de aplicación (use cases)
|
||||||
|
│ │ ├── ports/ # Interfaces (abstracciones)
|
||||||
|
│ │ │ ├── user_repository.py
|
||||||
|
│ │ │ └── report_repository.py
|
||||||
|
│ │ └── services/ # Casos de uso
|
||||||
|
│ │ ├── user_services.py
|
||||||
|
│ │ └── report_services.py
|
||||||
|
│ ├── infrastructure/ # Capa de infraestructura
|
||||||
|
│ │ ├── adapters/
|
||||||
|
│ │ │ └── persistence/ # Implementaciones de repositorios
|
||||||
|
│ │ │ ├── db.py # Configuración MySQL
|
||||||
|
│ │ │ ├── mongodb.py # Configuración MongoDB
|
||||||
|
│ │ │ ├── models.py # Modelos SQLAlchemy
|
||||||
|
│ │ │ ├── user_repository_sql.py
|
||||||
|
│ │ │ └── report_repository_mongo.py
|
||||||
|
│ │ └── api/ # APIs REST
|
||||||
|
│ │ ├── users/ # Microservicio de Usuarios
|
||||||
|
│ │ │ ├── app.py # Factory de la app
|
||||||
|
│ │ │ ├── router.py # Enrutador principal
|
||||||
|
│ │ │ ├── users.py # Endpoints de usuarios
|
||||||
|
│ │ │ ├── schemas.py # Esquemas Pydantic
|
||||||
|
│ │ │ └── root.py # Endpoints raíz
|
||||||
|
│ │ └── reports/ # Microservicio de Reportes
|
||||||
|
│ │ ├── app.py
|
||||||
|
│ │ ├── router.py
|
||||||
|
│ │ ├── reports.py
|
||||||
|
│ │ ├── schemas.py
|
||||||
|
│ │ └── root.py
|
||||||
|
│ ├── core/
|
||||||
|
│ │ └── config.py # Configuración de la aplicación
|
||||||
|
│ └── main.py # Punto de entrada principal
|
||||||
|
├── .env.example # Variables de entorno de ejemplo
|
||||||
|
└── requirements.txt # Dependencias de Python
|
||||||
|
```
|
||||||
|
|
||||||
|
## Requisitos
|
||||||
|
|
||||||
|
- Python 3.10+
|
||||||
|
- MySQL 8.0+
|
||||||
|
- MongoDB 4.4+
|
||||||
|
|
||||||
|
## Instalación
|
||||||
|
|
||||||
|
1. **Clonar el repositorio** (o extraer los archivos)
|
||||||
|
|
||||||
|
2. **Crear entorno virtual**:
|
||||||
|
```bash
|
||||||
|
python -m venv venv
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Activar entorno virtual**:
|
||||||
|
- En Windows:
|
||||||
|
```bash
|
||||||
|
venv\Scripts\activate
|
||||||
|
```
|
||||||
|
- En Linux/Mac:
|
||||||
|
```bash
|
||||||
|
source venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Instalar dependencias**:
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Configurar variables de entorno**:
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# Editar .env con tus credenciales de MySQL y MongoDB
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Crear base de datos MySQL**:
|
||||||
|
```sql
|
||||||
|
CREATE DATABASE voxpopuli_users CHARACTER SET utf8mb4;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ejecución
|
||||||
|
|
||||||
|
### Ejecutar ambos microservicios:
|
||||||
|
```bash
|
||||||
|
cd src
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Esto iniciará:
|
||||||
|
- **API de Usuarios**: http://localhost:8000
|
||||||
|
- **API de Reportes**: http://localhost:8001
|
||||||
|
|
||||||
|
### Documentación interactiva:
|
||||||
|
- Usuarios: http://localhost:8000/docs
|
||||||
|
- Reportes: http://localhost:8001/docs
|
||||||
|
|
||||||
|
## Endpoints Principales
|
||||||
|
|
||||||
|
### API de Usuarios (Puerto 8000)
|
||||||
|
|
||||||
|
| Método | Endpoint | Descripción |
|
||||||
|
|--------|----------|-------------|
|
||||||
|
| POST | `/users/` | Crear usuario |
|
||||||
|
| GET | `/users/{user_id}` | Obtener usuario por ID |
|
||||||
|
| GET | `/users/email/{email}` | Obtener usuario por email |
|
||||||
|
| GET | `/users/` | Listar todos los usuarios |
|
||||||
|
| PUT | `/users/{user_id}` | Actualizar usuario |
|
||||||
|
| DELETE | `/users/{user_id}` | Eliminar usuario |
|
||||||
|
|
||||||
|
### API de Reportes (Puerto 8001)
|
||||||
|
|
||||||
|
| Método | Endpoint | Descripción |
|
||||||
|
|--------|----------|-------------|
|
||||||
|
| POST | `/reports/` | Crear reporte |
|
||||||
|
| GET | `/reports/{report_id}` | Obtener reporte por ID |
|
||||||
|
| GET | `/reports/user/{user_id}` | Obtener reportes de un usuario |
|
||||||
|
| GET | `/reports/` | Listar todos los reportes |
|
||||||
|
| GET | `/reports/shadowbanned/list` | Listar reportes shadowbaneados |
|
||||||
|
| PUT | `/reports/{report_id}/visibility` | Actualizar visibilidad |
|
||||||
|
| DELETE | `/reports/{report_id}` | Eliminar reporte |
|
||||||
|
|
||||||
|
## Modelos de Datos
|
||||||
|
|
||||||
|
### Usuario (MySQL)
|
||||||
|
```python
|
||||||
|
- user_id: int (PK)
|
||||||
|
- nombre: str
|
||||||
|
- apellido: str
|
||||||
|
- email: str (único)
|
||||||
|
- fecha_nacimiento: datetime
|
||||||
|
- fecha_creacion: datetime
|
||||||
|
- calificacion: float (0-100)
|
||||||
|
- numero_reportes: int
|
||||||
|
- url_foto_perfil: str (opcional)
|
||||||
|
- biografia: str (opcional)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reporte (MongoDB)
|
||||||
|
```python
|
||||||
|
- id_reporte: str (UUID)
|
||||||
|
- id_usuario: int
|
||||||
|
- tipo_reporte: int (número que representa el tipo)
|
||||||
|
- descripcion: str
|
||||||
|
- ubicacion: str (opcional)
|
||||||
|
- visibilidad: float (0-100, basado en votación comunitaria)
|
||||||
|
- fecha_creacion: datetime
|
||||||
|
```
|
||||||
|
|
||||||
|
## Arquitectura
|
||||||
|
|
||||||
|
La arquitectura sigue el patrón de **Arquitectura Limpia (Clean Architecture)**:
|
||||||
|
|
||||||
|
- **Domain**: Modelos de negocio puros (entidades)
|
||||||
|
- **Application**: Lógica de negocio (casos de uso/servicios)
|
||||||
|
- **Infrastructure**: Adaptadores para bases de datos y APIs
|
||||||
|
|
||||||
|
Esto permite:
|
||||||
|
- Independencia de frameworks
|
||||||
|
- Facilidad de testing
|
||||||
|
- Separación de responsabilidades
|
||||||
|
- Reutilización de lógica de dominio
|
||||||
|
|
||||||
|
## Ejemplo de Uso
|
||||||
|
|
||||||
|
### Crear un usuario:
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://localhost:8000/users/" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"nombre": "Juan",
|
||||||
|
"apellido": "Pérez",
|
||||||
|
"email": "juan@example.com",
|
||||||
|
"fecha_nacimiento": "1990-01-15T00:00:00",
|
||||||
|
"url_foto_perfil": "https://example.com/photo.jpg",
|
||||||
|
"biografia": "Usuario de VoxPopuli"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Crear un reporte:
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://localhost:8001/reports/" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"id_usuario": 1,
|
||||||
|
"tipo_reporte": 1,
|
||||||
|
"descripcion": "Bache en la calle principal",
|
||||||
|
"ubicacion": "Calle Principal, Cuadra 5"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Desarrollo
|
||||||
|
|
||||||
|
### Ejecutar con hot reload:
|
||||||
|
Edita `src/main.py` y cambia `reload=False` a `reload=True`
|
||||||
|
|
||||||
|
### Testing:
|
||||||
|
(Próximamente se añadirán tests unitarios e integración)
|
||||||
|
|
||||||
|
## Contribuciones
|
||||||
|
|
||||||
|
Las contribuciones son bienvenidas. Por favor:
|
||||||
|
1. Fork el repositorio
|
||||||
|
2. Crea una rama para tu feature (`git checkout -b feature/AmazingFeature`)
|
||||||
|
3. Commit tus cambios (`git commit -m 'Add some AmazingFeature'`)
|
||||||
|
4. Push a la rama (`git push origin feature/AmazingFeature`)
|
||||||
|
5. Abre un Pull Request
|
||||||
|
|
||||||
|
## Licencia
|
||||||
|
|
||||||
|
Este proyecto está bajo la licencia MIT - ver archivo LICENSE para más detalles.
|
||||||
|
|
||||||
|
## Autor
|
||||||
|
|
||||||
|
Desarrollado como parte de la plataforma VoxPopuli para reportes comunitarios.
|
||||||
|
|||||||
8
requirements.txt
Normal file
8
requirements.txt
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fastapi
|
||||||
|
uvicorn
|
||||||
|
sqlalchemy
|
||||||
|
pymysql
|
||||||
|
pydantic
|
||||||
|
pydantic-settings
|
||||||
|
pymongo
|
||||||
|
python-dotenv
|
||||||
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