microservice-chassis-grupo2 0.1.5rc10__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- microservice_chassis_grupo2-0.1.5rc10/PKG-INFO +10 -0
- microservice_chassis_grupo2-0.1.5rc10/README.md +44 -0
- microservice_chassis_grupo2-0.1.5rc10/microservice_chassis_grupo2/__init__.py +0 -0
- microservice_chassis_grupo2-0.1.5rc10/microservice_chassis_grupo2/core/__init__.py +0 -0
- microservice_chassis_grupo2-0.1.5rc10/microservice_chassis_grupo2/core/config.py +70 -0
- microservice_chassis_grupo2-0.1.5rc10/microservice_chassis_grupo2/core/consul.py +80 -0
- microservice_chassis_grupo2-0.1.5rc10/microservice_chassis_grupo2/core/dependencies.py +64 -0
- microservice_chassis_grupo2-0.1.5rc10/microservice_chassis_grupo2/core/rabbitmq_core.py +159 -0
- microservice_chassis_grupo2-0.1.5rc10/microservice_chassis_grupo2/core/router_utils.py +19 -0
- microservice_chassis_grupo2-0.1.5rc10/microservice_chassis_grupo2/core/security.py +27 -0
- microservice_chassis_grupo2-0.1.5rc10/microservice_chassis_grupo2/sql/__init__.py +0 -0
- microservice_chassis_grupo2-0.1.5rc10/microservice_chassis_grupo2/sql/database.py +138 -0
- microservice_chassis_grupo2-0.1.5rc10/microservice_chassis_grupo2/sql/models.py +30 -0
- microservice_chassis_grupo2-0.1.5rc10/microservice_chassis_grupo2.egg-info/PKG-INFO +10 -0
- microservice_chassis_grupo2-0.1.5rc10/microservice_chassis_grupo2.egg-info/SOURCES.txt +17 -0
- microservice_chassis_grupo2-0.1.5rc10/microservice_chassis_grupo2.egg-info/dependency_links.txt +1 -0
- microservice_chassis_grupo2-0.1.5rc10/microservice_chassis_grupo2.egg-info/top_level.txt +1 -0
- microservice_chassis_grupo2-0.1.5rc10/setup.cfg +4 -0
- microservice_chassis_grupo2-0.1.5rc10/setup.py +12 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: microservice_chassis_grupo2
|
|
3
|
+
Version: 0.1.5rc10
|
|
4
|
+
Summary: A reusable library for microservices
|
|
5
|
+
Home-page: https://github.com/Grupo-MACC/Chassis
|
|
6
|
+
Author: Grupo 2
|
|
7
|
+
Author-email:
|
|
8
|
+
Dynamic: author
|
|
9
|
+
Dynamic: home-page
|
|
10
|
+
Dynamic: summary
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# 🧩 Microservice Chassis Grupo 2
|
|
2
|
+
|
|
3
|
+
**Repository link:** https://github.com/Grupo-MACC/Chassis
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Functionalities
|
|
8
|
+
|
|
9
|
+
This repository provides a **shared chassis** for microservices, containing configuration, database management, authentication, messaging, and helper utilities.
|
|
10
|
+
It allows microservices to reuse common functionalities such as database connections, JWT validation, RabbitMQ communication, and standardized base models.
|
|
11
|
+
|
|
12
|
+
Below is a detailed list of the available functionalities:
|
|
13
|
+
|
|
14
|
+
| **Module** | **Functionality** | **Description** |
|
|
15
|
+
|-------------|------------------|-----------------|
|
|
16
|
+
| `core/config.py` | **Settings Management** | Centralized configuration for the chassis. Defines constants like the encryption algorithm (`RS256`), RabbitMQ connection parameters (`RABBITMQ_HOST`), and exchange name (`broker`). |
|
|
17
|
+
| `core/security.py` | **Token Decoding and Verification** | Handles JWT decoding using a public RSA key. Validates tokens and raises appropriate HTTP errors if invalid or missing. |
|
|
18
|
+
| `core/dependencies.py` | **Database Session & Authentication Dependency** | Provides FastAPI dependencies to manage asynchronous database sessions (`get_db`) and current user retrieval (`get_current_user`) from a JWT token. |
|
|
19
|
+
| `core/rabbitmq.py` | **RabbitMQ Channel & Exchange Management** | Creates asynchronous connections to RabbitMQ using `aio_pika`. Declares durable topic exchanges for inter-service communication. |
|
|
20
|
+
| `core/utils.py` | **Helper Functions and Service URLs** | Defines service URLs (order, machine, delivery, payment, auth) and provides error handling utilities with standardized logging (`raise_and_log_error`). |
|
|
21
|
+
| `sql/database.py` | **Database Engine & Session Configuration** | Configures SQLAlchemy’s asynchronous engine and sessionmaker. Supports `SQLALCHEMY_DATABASE_URL` via environment variable (defaults to SQLite). |
|
|
22
|
+
| `sql/base_class.py` | **Base Model for ORM Classes** | Provides a reusable abstract base class (`BaseModel`) for all database models, including timestamp columns (`creation_date`, `update_date`) and helper methods for converting models to dictionaries. |
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Functionalities per Microservice
|
|
27
|
+
|
|
28
|
+
| **Microservice** | **Used Functionalities** |
|
|
29
|
+
|------------------|--------------------------|
|
|
30
|
+
| `payment` | `core/config`, `core/security`, `core/dependencies`, `core/rabbitmq`, `core/utils`, `sql/database`, `sql/base_class` |
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Summary
|
|
35
|
+
|
|
36
|
+
This chassis allows the `payment` microservice (and potentially others in the future) to operate with consistent patterns for:
|
|
37
|
+
- Secure token validation.
|
|
38
|
+
- Standardized database access and sessions.
|
|
39
|
+
- Centralized configuration.
|
|
40
|
+
- RabbitMQ-based inter-service communication.
|
|
41
|
+
- Common helper utilities and error management.
|
|
42
|
+
- Unified base ORM models.
|
|
43
|
+
|
|
44
|
+
It provides a robust foundation for building scalable and maintainable microservices within the system.
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Configuración del Chassis.
|
|
4
|
+
|
|
5
|
+
Objetivo:
|
|
6
|
+
- Mantener compatibilidad con el sistema actual (AMQP sin TLS por defecto).
|
|
7
|
+
- Permitir activar TLS (AMQPS) sin tocar el código de los microservicios:
|
|
8
|
+
* RABBITMQ_USE_TLS=1
|
|
9
|
+
* RABBITMQ_PORT=5671
|
|
10
|
+
* RABBITMQ_TLS_CA_FILE=/certs/ca.pem
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _env_bool(name: str, default: str = "0") -> bool:
|
|
19
|
+
"""
|
|
20
|
+
Convierte una variable de entorno a bool de forma tolerante.
|
|
21
|
+
|
|
22
|
+
Acepta: 1/true/yes/on/y (case-insensitive).
|
|
23
|
+
"""
|
|
24
|
+
val = os.getenv(name, default)
|
|
25
|
+
return str(val).strip().lower() in {"1", "true", "yes", "on", "y"}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Settings:
|
|
29
|
+
"""
|
|
30
|
+
Settings centralizados.
|
|
31
|
+
|
|
32
|
+
Nota:
|
|
33
|
+
Mantengo el nombre `RABBITMQ_HOST` porque ya lo usa todo el proyecto.
|
|
34
|
+
Aquí pasa a ser una URL completa (amqp/amqps) con puerto.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
ALGORITHM: str = "RS256"
|
|
38
|
+
|
|
39
|
+
EXCHANGE_NAME = "broker"
|
|
40
|
+
EXCHANGE_NAME_COMMAND = "command"
|
|
41
|
+
EXCHANGE_NAME_SAGA = "saga"
|
|
42
|
+
EXCHANGE_NAME_LOGS = "logs"
|
|
43
|
+
|
|
44
|
+
# --- RabbitMQ base ---
|
|
45
|
+
RABBITMQ_USER = os.getenv("RABBITMQ_USER", "guest")
|
|
46
|
+
RABBITMQ_PASSWORD = os.getenv("RABBITMQ_PASSWORD", "guest")
|
|
47
|
+
RABBITMQ_HOSTNAME = os.getenv("RABBITMQ_HOST", "localhost")
|
|
48
|
+
|
|
49
|
+
# --- TLS switch ---
|
|
50
|
+
RABBITMQ_USE_TLS: bool = _env_bool("RABBITMQ_USE_TLS", "0")
|
|
51
|
+
|
|
52
|
+
# Puerto por defecto según TLS
|
|
53
|
+
_default_port = "5671" if RABBITMQ_USE_TLS else "5672"
|
|
54
|
+
RABBITMQ_PORT = int(os.getenv("RABBITMQ_PORT", _default_port))
|
|
55
|
+
|
|
56
|
+
# Override opcional por si alguien quiere pasar una URL completa
|
|
57
|
+
RABBITMQ_URL = os.getenv("RABBITMQ_URL", "").strip()
|
|
58
|
+
|
|
59
|
+
# URL final (compatibilidad con el resto del código)
|
|
60
|
+
if RABBITMQ_URL:
|
|
61
|
+
RABBITMQ_HOST = RABBITMQ_URL
|
|
62
|
+
else:
|
|
63
|
+
scheme = "amqps" if RABBITMQ_USE_TLS else "amqp"
|
|
64
|
+
RABBITMQ_HOST = (
|
|
65
|
+
f"{scheme}://{RABBITMQ_USER}:{RABBITMQ_PASSWORD}"
|
|
66
|
+
f"@{RABBITMQ_HOSTNAME}:{RABBITMQ_PORT}/"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
settings = Settings()
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import asyncio
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
class ConsulClient:
|
|
6
|
+
def __init__(self, host: str, port: str):
|
|
7
|
+
self.host = host
|
|
8
|
+
self.port = port
|
|
9
|
+
self.base_url = f"http://{host}:{port}/v1"
|
|
10
|
+
|
|
11
|
+
async def deregister_service(
|
|
12
|
+
self,
|
|
13
|
+
service_id: str
|
|
14
|
+
) -> bool:
|
|
15
|
+
try:
|
|
16
|
+
async with httpx.AsyncClient() as client:
|
|
17
|
+
response = await client.put(
|
|
18
|
+
f"{self.base_url}/agent/service/deregister/{service_id}",
|
|
19
|
+
timeout=10.0,
|
|
20
|
+
)
|
|
21
|
+
if response.status_code == 200:
|
|
22
|
+
return True
|
|
23
|
+
else:
|
|
24
|
+
return False
|
|
25
|
+
except Exception as e:
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
async def discover_service(
|
|
29
|
+
self,
|
|
30
|
+
service_name: str
|
|
31
|
+
) -> dict:
|
|
32
|
+
try:
|
|
33
|
+
async with httpx.AsyncClient() as client:
|
|
34
|
+
response = await client.get(
|
|
35
|
+
f"{self.base_url}/catalog/service/{service_name}",
|
|
36
|
+
timeout=10.0,
|
|
37
|
+
)
|
|
38
|
+
if response.status_code == 200:
|
|
39
|
+
services = response.json()
|
|
40
|
+
if services:
|
|
41
|
+
service = services[0]
|
|
42
|
+
return {
|
|
43
|
+
"address": service.get("ServiceAddress") or service.get("Address"),
|
|
44
|
+
"port": service.get("ServicePort"),
|
|
45
|
+
}
|
|
46
|
+
except Exception as e:
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
_consul_client = None
|
|
50
|
+
|
|
51
|
+
def create_consul_client() -> ConsulClient:
|
|
52
|
+
global _consul_client
|
|
53
|
+
if _consul_client is None:
|
|
54
|
+
host = os.getenv("CONSUL_HOST", "localhost")
|
|
55
|
+
port = int(os.getenv("CONSUL_PORT", 8500))
|
|
56
|
+
_consul_client = ConsulClient(host, port)
|
|
57
|
+
return _consul_client
|
|
58
|
+
|
|
59
|
+
async def get_service_url(service_name: str, default_url: str = None) -> str:
|
|
60
|
+
"""Get service URL from Consul with retry and fallback."""
|
|
61
|
+
consul_client = create_consul_client()
|
|
62
|
+
max_retries = 5
|
|
63
|
+
retry_delay = 1
|
|
64
|
+
|
|
65
|
+
for attempt in range(max_retries):
|
|
66
|
+
try:
|
|
67
|
+
service_info = await consul_client.discover_service(service_name)
|
|
68
|
+
if service_info:
|
|
69
|
+
url = f"http://{service_info['address']}:{service_info['port']}"
|
|
70
|
+
return url
|
|
71
|
+
except Exception as e:
|
|
72
|
+
pass
|
|
73
|
+
if attempt < max_retries - 1:
|
|
74
|
+
await asyncio.sleep(retry_delay)
|
|
75
|
+
|
|
76
|
+
# Fallback to default
|
|
77
|
+
if default_url:
|
|
78
|
+
return default_url
|
|
79
|
+
|
|
80
|
+
raise Exception(f"Could not discover service: {service_name}")
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from fastapi import Depends, HTTPException, status
|
|
3
|
+
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|
4
|
+
from microservice_chassis_grupo2.core.security import decode_token, PUBLIC_KEY_PATH
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
auth_scheme = HTTPBearer()
|
|
9
|
+
|
|
10
|
+
# Database #########################################################################################
|
|
11
|
+
async def get_db():
|
|
12
|
+
"""Genera sesiones de BD y las cierra al terminar.
|
|
13
|
+
|
|
14
|
+
Nota:
|
|
15
|
+
- init_database() es idempotente: si ya se inicializó, no repite trabajo.
|
|
16
|
+
- Se mantiene commit/rollback automático como en tu implementación original.
|
|
17
|
+
"""
|
|
18
|
+
from microservice_chassis_grupo2.sql.database import init_database, SessionLocal
|
|
19
|
+
|
|
20
|
+
# ✅ Asegura engine y SessionLocal listos antes de usarlos
|
|
21
|
+
await init_database()
|
|
22
|
+
|
|
23
|
+
if SessionLocal is None:
|
|
24
|
+
raise RuntimeError(
|
|
25
|
+
"SessionLocal no inicializada tras init_database(). "
|
|
26
|
+
"Revisa microservice_chassis_grupo2/sql/database.py"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
db = SessionLocal()
|
|
30
|
+
try:
|
|
31
|
+
yield db
|
|
32
|
+
await db.commit()
|
|
33
|
+
except Exception:
|
|
34
|
+
await db.rollback()
|
|
35
|
+
raise
|
|
36
|
+
finally:
|
|
37
|
+
await db.close()
|
|
38
|
+
|
|
39
|
+
async def get_current_user(
|
|
40
|
+
credentials: HTTPAuthorizationCredentials = Depends(auth_scheme)
|
|
41
|
+
):
|
|
42
|
+
"""
|
|
43
|
+
Decodifica el JWT y obtiene el usuario actual desde la base de datos.
|
|
44
|
+
"""
|
|
45
|
+
token = credentials.credentials
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
payload = decode_token(token)
|
|
49
|
+
except ValueError:
|
|
50
|
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token inválido")
|
|
51
|
+
|
|
52
|
+
user_id = payload.get("user_id")
|
|
53
|
+
if not user_id:
|
|
54
|
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token inválido")
|
|
55
|
+
|
|
56
|
+
return user_id
|
|
57
|
+
|
|
58
|
+
def check_public_key():
|
|
59
|
+
if os.path.exists(PUBLIC_KEY_PATH):
|
|
60
|
+
with open(PUBLIC_KEY_PATH, "r", encoding="utf-8") as f:
|
|
61
|
+
f.read()
|
|
62
|
+
return True
|
|
63
|
+
else:
|
|
64
|
+
return False
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
RabbitMQ core del Chassis (aio-pika).
|
|
4
|
+
|
|
5
|
+
Objetivo:
|
|
6
|
+
- Si TLS está activo, usar AMQPS con verificación del servidor por CA.
|
|
7
|
+
- SIN mTLS: no cargamos certificado/clave de cliente (no hace falta).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import ssl
|
|
14
|
+
import inspect
|
|
15
|
+
import logging
|
|
16
|
+
from aio_pika.exceptions import AMQPConnectionError
|
|
17
|
+
from aio_pika import connect_robust, ExchangeType
|
|
18
|
+
|
|
19
|
+
from microservice_chassis_grupo2.core.config import settings
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
# Ruta al public key para verificar JWTs
|
|
24
|
+
PUBLIC_KEY_PATH = os.getenv("PUBLIC_KEY_PATH", "auth_public.pem")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _env_bool(name: str, default: str = "0") -> bool:
|
|
28
|
+
"""
|
|
29
|
+
Convierte una variable de entorno a bool de forma tolerante.
|
|
30
|
+
|
|
31
|
+
Acepta: 1/true/yes/on/y (case-insensitive).
|
|
32
|
+
"""
|
|
33
|
+
val = os.getenv(name, default)
|
|
34
|
+
return str(val).strip().lower() in {"1", "true", "yes", "on", "y"}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _build_ssl_context() -> ssl.SSLContext:
|
|
38
|
+
"""
|
|
39
|
+
Construye un SSLContext para TLS simple (server-auth).
|
|
40
|
+
|
|
41
|
+
Requisitos:
|
|
42
|
+
- RABBITMQ_TLS_CA_FILE debe apuntar al CA que firmó el cert del servidor RabbitMQ.
|
|
43
|
+
- No se carga cert/key de cliente (NO mTLS).
|
|
44
|
+
|
|
45
|
+
Decisiones:
|
|
46
|
+
- TLSv1.2+.
|
|
47
|
+
- check_hostname=False para evitar problemas típicos de CN/SAN en entornos docker.
|
|
48
|
+
(Si tus certs están bien con SAN, puedes ponerlo True sin problema.)
|
|
49
|
+
"""
|
|
50
|
+
ca_file = os.getenv("RABBITMQ_TLS_CA_FILE", "/certs/ca.pem")
|
|
51
|
+
|
|
52
|
+
if not os.path.exists(ca_file):
|
|
53
|
+
raise FileNotFoundError(f"CA file no encontrado: {ca_file}")
|
|
54
|
+
|
|
55
|
+
ctx = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH, cafile=ca_file)
|
|
56
|
+
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
|
|
57
|
+
ctx.check_hostname = False
|
|
58
|
+
|
|
59
|
+
# Verificación estricta del servidor (lo normal en TLS simple)
|
|
60
|
+
ctx.verify_mode = ssl.CERT_REQUIRED
|
|
61
|
+
|
|
62
|
+
return ctx
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
async def _connect(url: str, ssl_ctx: ssl.SSLContext | None):
|
|
66
|
+
"""
|
|
67
|
+
Conecta con aio-pika de forma robusta.
|
|
68
|
+
|
|
69
|
+
Estrategia:
|
|
70
|
+
- Si no hay TLS (ssl_ctx is None): conexión normal.
|
|
71
|
+
- Si hay TLS: pasar el SSLContext usando el parámetro correcto
|
|
72
|
+
para la versión de aio-pika instalada.
|
|
73
|
+
|
|
74
|
+
Compatibilidad:
|
|
75
|
+
- Algunas versiones soportan `ssl_context=SSLContext`.
|
|
76
|
+
- Otras usan `ssl=True` + `ssl_options` (dict u objeto SSLOptions).
|
|
77
|
+
"""
|
|
78
|
+
if ssl_ctx is None:
|
|
79
|
+
return await connect_robust(url)
|
|
80
|
+
|
|
81
|
+
sig = inspect.signature(connect_robust)
|
|
82
|
+
params = sig.parameters
|
|
83
|
+
|
|
84
|
+
# Opción 1 (preferida): ssl_context=
|
|
85
|
+
if "ssl_context" in params:
|
|
86
|
+
return await connect_robust(url, ssl_context=ssl_ctx)
|
|
87
|
+
|
|
88
|
+
# Opción 2: ssl=True + ssl_options=dict(...)
|
|
89
|
+
if "ssl_options" in params:
|
|
90
|
+
# Para aiormq, suele funcionar pasando el contexto dentro de ssl_options.
|
|
91
|
+
# Si tu versión exige keys tipo cafile/certfile/keyfile,
|
|
92
|
+
# aquí también podrías pasar {"cafile": "..."} en vez de context.
|
|
93
|
+
return await connect_robust(url, ssl=True, ssl_options={"context": ssl_ctx})
|
|
94
|
+
|
|
95
|
+
# Último recurso: algunas versiones aceptan ssl como contexto
|
|
96
|
+
return await connect_robust(url, ssl=ssl_ctx)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
async def get_channel():
|
|
100
|
+
"""
|
|
101
|
+
Devuelve (connection, channel) listo para usar.
|
|
102
|
+
|
|
103
|
+
Nota profesional:
|
|
104
|
+
- Abrir/cerrar conexión por cada publish es caro.
|
|
105
|
+
Para producción, lo suyo es pool o conexión global por proceso.
|
|
106
|
+
- Aquí mantengo tu contrato actual por compatibilidad.
|
|
107
|
+
"""
|
|
108
|
+
use_tls = _env_bool("RABBITMQ_USE_TLS", "0")
|
|
109
|
+
|
|
110
|
+
ssl_ctx = _build_ssl_context() if use_tls else None
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
connection = await _connect(settings.RABBITMQ_HOST, ssl_ctx)
|
|
114
|
+
channel = await connection.channel()
|
|
115
|
+
return connection, channel
|
|
116
|
+
except AMQPConnectionError as e:
|
|
117
|
+
# Re-lanzamos con contexto útil (no lo escondas con un fallback “mágico”).
|
|
118
|
+
raise RuntimeError(f"RabbitMQ connection failed (TLS={use_tls}) to {settings.RABBITMQ_HOST}: {e}") from e
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
async def declare_exchange(channel):
|
|
122
|
+
"""Declara el exchange general (broker)."""
|
|
123
|
+
return await channel.declare_exchange(
|
|
124
|
+
settings.EXCHANGE_NAME,
|
|
125
|
+
ExchangeType.TOPIC,
|
|
126
|
+
durable=True,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
async def declare_exchange_command(channel):
|
|
131
|
+
"""Declara el exchange de comandos (command)."""
|
|
132
|
+
return await channel.declare_exchange(
|
|
133
|
+
settings.EXCHANGE_NAME_COMMAND,
|
|
134
|
+
ExchangeType.TOPIC,
|
|
135
|
+
durable=True,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
async def declare_exchange_saga(channel):
|
|
140
|
+
"""Declara el exchange de saga (saga)."""
|
|
141
|
+
return await channel.declare_exchange(
|
|
142
|
+
settings.EXCHANGE_NAME_SAGA,
|
|
143
|
+
ExchangeType.TOPIC,
|
|
144
|
+
durable=True,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
async def declare_exchange_logs(channel):
|
|
149
|
+
"""
|
|
150
|
+
Declara el exchange de logs (logs) y asegura la cola telegraf_metrics.
|
|
151
|
+
"""
|
|
152
|
+
exchange = await channel.declare_exchange(
|
|
153
|
+
settings.EXCHANGE_NAME_LOGS,
|
|
154
|
+
ExchangeType.TOPIC,
|
|
155
|
+
durable=True,
|
|
156
|
+
)
|
|
157
|
+
queue = await channel.declare_queue("telegraf_metrics", durable=True)
|
|
158
|
+
await queue.bind(exchange, routing_key="#")
|
|
159
|
+
return exchange
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Util/Helper functions for router definitions."""
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
from fastapi import HTTPException
|
|
6
|
+
|
|
7
|
+
ORDER_SERVICE_URL = f"{os.getenv('ORDER_SERVICE', 'http://localhost')}:5000"
|
|
8
|
+
MACHINE_SERVICE_URL = f"{os.getenv('MACHINE_SERVICE', 'http://localhost')}:5001"
|
|
9
|
+
DELIVERY_SERVICE_URL = f"{os.getenv('DELIVERY_SERVICE', 'http://localhost')}:5002"
|
|
10
|
+
PAYMENT_SERVICE_URL = f"{os.getenv('PAYMENT_SERVICE', 'http://localhost')}:5003"
|
|
11
|
+
AUTH_SERVICE_URL = f"{os.getenv('AUTH_SERVICE', 'http://localhost')}:5004"
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def raise_and_log_error(my_logger, status_code: int, message: str):
|
|
17
|
+
"""Raises HTTPException and logs an error."""
|
|
18
|
+
my_logger.error(message)
|
|
19
|
+
raise HTTPException(status_code, message)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import jwt
|
|
2
|
+
import os
|
|
3
|
+
from fastapi import HTTPException
|
|
4
|
+
from microservice_chassis_grupo2.core.config import settings
|
|
5
|
+
|
|
6
|
+
#"/home/pyuser/code/auth_public.pem"
|
|
7
|
+
PUBLIC_KEY_PATH = os.getenv("PUBLIC_KEY_PATH", "auth_public.pem")
|
|
8
|
+
|
|
9
|
+
def read_public_pem():
|
|
10
|
+
if not os.path.exists(PUBLIC_KEY_PATH):
|
|
11
|
+
print("no hay")
|
|
12
|
+
print(PUBLIC_KEY_PATH)
|
|
13
|
+
raise ValueError(f"Public key not found at {PUBLIC_KEY_PATH}")
|
|
14
|
+
with open(PUBLIC_KEY_PATH, "r", encoding="utf-8") as f:
|
|
15
|
+
return f.read()
|
|
16
|
+
|
|
17
|
+
def decode_token(token: str) -> dict:
|
|
18
|
+
try:
|
|
19
|
+
public_pem = read_public_pem()
|
|
20
|
+
payload = jwt.decode(token, public_pem, algorithms=[settings.ALGORITHM])
|
|
21
|
+
return payload
|
|
22
|
+
except FileNotFoundError as e:
|
|
23
|
+
raise HTTPException(status_code=500, detail="Public key file not found")
|
|
24
|
+
except ValueError as e:
|
|
25
|
+
raise HTTPException(status_code=401, detail=str(e))
|
|
26
|
+
except Exception as e:
|
|
27
|
+
raise HTTPException(status_code=500, detail=f"Unexpected error: {str(e)}")
|
|
File without changes
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""microservice_chassis_grupo2.sql.database
|
|
3
|
+
|
|
4
|
+
Inicialización de SQLAlchemy Async.
|
|
5
|
+
|
|
6
|
+
Compatibilidad:
|
|
7
|
+
- Si existe SQLALCHEMY_DATABASE_URL, inicializa engine/SessionLocal al importar
|
|
8
|
+
(evita `engine=None` en microservicios que usan `database.engine` directamente).
|
|
9
|
+
|
|
10
|
+
Modo “avanzado” (AWS/Consul):
|
|
11
|
+
- Si no existe SQLALCHEMY_DATABASE_URL, se puede usar `await init_database()`
|
|
12
|
+
para resolver RDS (por RDS_HOST o descubriendo servicio `rds` en Consul).
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import asyncio
|
|
18
|
+
import logging
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
from sqlalchemy.orm import sessionmaker, declarative_base
|
|
22
|
+
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, AsyncEngine
|
|
23
|
+
|
|
24
|
+
from microservice_chassis_grupo2.core.consul import get_service_url
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
# Estado global
|
|
29
|
+
engine: Optional[AsyncEngine] = None
|
|
30
|
+
SessionLocal: Optional[sessionmaker] = None
|
|
31
|
+
Base = declarative_base()
|
|
32
|
+
|
|
33
|
+
_db_initialized: bool = False
|
|
34
|
+
_init_lock = asyncio.Lock()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _safe_url_hint(url: str) -> str:
|
|
38
|
+
"""Evita volcar credenciales en logs."""
|
|
39
|
+
try:
|
|
40
|
+
return url.split("://", 1)[0] + "://***"
|
|
41
|
+
except Exception:
|
|
42
|
+
return "***"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _create_engine_and_session(database_url: str) -> None:
|
|
46
|
+
"""Crea engine y SessionLocal (sin I/O)."""
|
|
47
|
+
global engine, SessionLocal, _db_initialized
|
|
48
|
+
|
|
49
|
+
engine = create_async_engine(
|
|
50
|
+
database_url,
|
|
51
|
+
echo=False,
|
|
52
|
+
pool_pre_ping=True,
|
|
53
|
+
future=True,
|
|
54
|
+
)
|
|
55
|
+
SessionLocal = sessionmaker(
|
|
56
|
+
bind=engine,
|
|
57
|
+
class_=AsyncSession,
|
|
58
|
+
expire_on_commit=False,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
_db_initialized = True
|
|
62
|
+
logger.info("[DATABASE] Engine creado (%s)", _safe_url_hint(database_url))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
async def get_database_url() -> str:
|
|
66
|
+
"""Resuelve la URL de BD.
|
|
67
|
+
|
|
68
|
+
Prioridad:
|
|
69
|
+
1) SQLALCHEMY_DATABASE_URL
|
|
70
|
+
2) RDS_HOST + DB_USER/DB_PASSWORD + DB_NAME (MySQL)
|
|
71
|
+
3) DB_NAME + Consul(service=rds) + DB_USER/DB_PASSWORD (MySQL)
|
|
72
|
+
4) fallback sqlite default.db
|
|
73
|
+
"""
|
|
74
|
+
env_url = os.getenv("SQLALCHEMY_DATABASE_URL")
|
|
75
|
+
if env_url:
|
|
76
|
+
return env_url
|
|
77
|
+
|
|
78
|
+
db_name = os.getenv("DB_NAME")
|
|
79
|
+
db_user = os.getenv("DB_USER")
|
|
80
|
+
db_password = os.getenv("DB_PASSWORD")
|
|
81
|
+
|
|
82
|
+
if not db_name:
|
|
83
|
+
return "sqlite+aiosqlite:///./default.db"
|
|
84
|
+
|
|
85
|
+
rds_host = os.getenv("RDS_HOST")
|
|
86
|
+
if rds_host:
|
|
87
|
+
rds_port = os.getenv("RDS_PORT", "3306")
|
|
88
|
+
if not db_user or not db_password:
|
|
89
|
+
raise RuntimeError("Faltan DB_USER/DB_PASSWORD para RDS_HOST.")
|
|
90
|
+
return f"mysql+aiomysql://{db_user}:{db_password}@{rds_host}:{rds_port}/{db_name}"
|
|
91
|
+
|
|
92
|
+
# Consul discovery
|
|
93
|
+
rds_info = await get_service_url(service_name="rds", default_url=None)
|
|
94
|
+
if rds_info.startswith("http://"):
|
|
95
|
+
rds_info = rds_info.replace("http://", "")
|
|
96
|
+
elif rds_info.startswith("https://"):
|
|
97
|
+
rds_info = rds_info.replace("https://", "")
|
|
98
|
+
|
|
99
|
+
if not db_user or not db_password:
|
|
100
|
+
raise RuntimeError("Faltan DB_USER/DB_PASSWORD para RDS via Consul.")
|
|
101
|
+
|
|
102
|
+
return f"mysql+aiomysql://{db_user}:{db_password}@{rds_info}/{db_name}"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
async def init_database() -> None:
|
|
106
|
+
"""Inicializa engine y SessionLocal (idempotente y async-safe)."""
|
|
107
|
+
global _db_initialized
|
|
108
|
+
|
|
109
|
+
if _db_initialized:
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
async with _init_lock:
|
|
113
|
+
if _db_initialized:
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
database_url = await get_database_url()
|
|
117
|
+
_create_engine_and_session(database_url)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
async def dispose_database() -> None:
|
|
121
|
+
"""Libera recursos de BD de forma segura."""
|
|
122
|
+
global engine, SessionLocal, _db_initialized
|
|
123
|
+
|
|
124
|
+
if engine is not None:
|
|
125
|
+
await engine.dispose()
|
|
126
|
+
|
|
127
|
+
engine = None
|
|
128
|
+
SessionLocal = None
|
|
129
|
+
_db_initialized = False
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ✅ Compatibilidad: init eager si existe SQLALCHEMY_DATABASE_URL
|
|
133
|
+
_env_url = os.getenv("SQLALCHEMY_DATABASE_URL")
|
|
134
|
+
if _env_url:
|
|
135
|
+
try:
|
|
136
|
+
_create_engine_and_session(_env_url)
|
|
137
|
+
except Exception:
|
|
138
|
+
logger.exception("[DATABASE] Falló la inicialización eager desde SQLALCHEMY_DATABASE_URL.")
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from sqlalchemy import Column, DateTime
|
|
2
|
+
from sqlalchemy.sql import func
|
|
3
|
+
from microservice_chassis_grupo2.sql.database import Base
|
|
4
|
+
|
|
5
|
+
class BaseModel(Base):
|
|
6
|
+
"""Base database table representation to reuse."""
|
|
7
|
+
__abstract__ = True
|
|
8
|
+
creation_date = Column(DateTime(timezone=True), server_default=func.now())
|
|
9
|
+
update_date = Column(DateTime, nullable=False, server_default=func.now(), onupdate=func.now())
|
|
10
|
+
|
|
11
|
+
def __repr__(self):
|
|
12
|
+
fields = ""
|
|
13
|
+
for column in self.__table__.columns:
|
|
14
|
+
if fields == "":
|
|
15
|
+
fields = f"{column.name}='{getattr(self, column.name)}'"
|
|
16
|
+
# fields = "{}='{}'".format(column.name, getattr(self, column.name))
|
|
17
|
+
else:
|
|
18
|
+
fields = f"{fields}, {column.name}='{getattr(self, column.name)}'"
|
|
19
|
+
# fields = "{}, {}='{}'".format(fields, column.name, getattr(self, column.name))
|
|
20
|
+
return f"<{self.__class__.__name__}({fields})>"
|
|
21
|
+
# return "<{}({})>".format(self.__class__.__name__, fields)
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def list_as_dict(items):
|
|
25
|
+
"""Returns list of items as dict."""
|
|
26
|
+
return [i.as_dict() for i in items]
|
|
27
|
+
|
|
28
|
+
def as_dict(self):
|
|
29
|
+
"""Return the item as dict."""
|
|
30
|
+
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: microservice_chassis_grupo2
|
|
3
|
+
Version: 0.1.5rc10
|
|
4
|
+
Summary: A reusable library for microservices
|
|
5
|
+
Home-page: https://github.com/Grupo-MACC/Chassis
|
|
6
|
+
Author: Grupo 2
|
|
7
|
+
Author-email:
|
|
8
|
+
Dynamic: author
|
|
9
|
+
Dynamic: home-page
|
|
10
|
+
Dynamic: summary
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
setup.py
|
|
3
|
+
microservice_chassis_grupo2/__init__.py
|
|
4
|
+
microservice_chassis_grupo2.egg-info/PKG-INFO
|
|
5
|
+
microservice_chassis_grupo2.egg-info/SOURCES.txt
|
|
6
|
+
microservice_chassis_grupo2.egg-info/dependency_links.txt
|
|
7
|
+
microservice_chassis_grupo2.egg-info/top_level.txt
|
|
8
|
+
microservice_chassis_grupo2/core/__init__.py
|
|
9
|
+
microservice_chassis_grupo2/core/config.py
|
|
10
|
+
microservice_chassis_grupo2/core/consul.py
|
|
11
|
+
microservice_chassis_grupo2/core/dependencies.py
|
|
12
|
+
microservice_chassis_grupo2/core/rabbitmq_core.py
|
|
13
|
+
microservice_chassis_grupo2/core/router_utils.py
|
|
14
|
+
microservice_chassis_grupo2/core/security.py
|
|
15
|
+
microservice_chassis_grupo2/sql/__init__.py
|
|
16
|
+
microservice_chassis_grupo2/sql/database.py
|
|
17
|
+
microservice_chassis_grupo2/sql/models.py
|
microservice_chassis_grupo2-0.1.5rc10/microservice_chassis_grupo2.egg-info/dependency_links.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
microservice_chassis_grupo2
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="microservice_chassis_grupo2",
|
|
5
|
+
version="0.1.5rc10",
|
|
6
|
+
packages=find_packages(),
|
|
7
|
+
install_requires=[],
|
|
8
|
+
author="Grupo 2",
|
|
9
|
+
author_email="",
|
|
10
|
+
description="A reusable library for microservices",
|
|
11
|
+
url="https://github.com/Grupo-MACC/Chassis",
|
|
12
|
+
)
|