microservice-chassis-grupo2-cc-prod 0.1.6__py3-none-any.whl

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.
File without changes
File without changes
@@ -0,0 +1,72 @@
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
+ RABBITMQ_USER = os.getenv("RABBITMQ_USER", "guest")
39
+ EXCHANGE_NAME = "broker"
40
+ EXCHANGE_NAME_COMMAND = "command"
41
+ EXCHANGE_NAME_SAGA = "saga"
42
+ EXCHANGE_NAME_LOGS = "logs"
43
+ EXCHANGE_NAME_SAGA_CANCEL_CMD = "saga_cancel_cmd"
44
+ EXCHANGE_NAME_SAGA_CANCEL_EVT = "saga_cancel_evt"
45
+
46
+ # --- RabbitMQ base ---
47
+ RABBITMQ_USER = os.getenv("RABBITMQ_USER", "guest")
48
+ RABBITMQ_PASSWORD = os.getenv("RABBITMQ_PASSWORD", "guest")
49
+ RABBITMQ_HOSTNAME = os.getenv("RABBITMQ_HOST", "localhost")
50
+
51
+ # --- TLS switch ---
52
+ RABBITMQ_USE_TLS: bool = _env_bool("RABBITMQ_USE_TLS", "0")
53
+
54
+ # Puerto por defecto según TLS
55
+ _default_port = "5671" if RABBITMQ_USE_TLS else "5672"
56
+ RABBITMQ_PORT = int(os.getenv("RABBITMQ_PORT", _default_port))
57
+
58
+ # Override opcional por si alguien quiere pasar una URL completa
59
+ RABBITMQ_URL = os.getenv("RABBITMQ_URL", "").strip()
60
+
61
+ # URL final (compatibilidad con el resto del código)
62
+ if RABBITMQ_URL:
63
+ RABBITMQ_HOST = RABBITMQ_URL
64
+ else:
65
+ scheme = "amqps" if RABBITMQ_USE_TLS else "amqp"
66
+ RABBITMQ_HOST = (
67
+ f"{scheme}://{RABBITMQ_USER}:{RABBITMQ_PASSWORD}"
68
+ f"@{RABBITMQ_HOSTNAME}:{RABBITMQ_PORT}/"
69
+ )
70
+
71
+
72
+ settings = Settings()
@@ -0,0 +1,81 @@
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"https://{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
+ verify=False
38
+ )
39
+ if response.status_code == 200:
40
+ services = response.json()
41
+ if services:
42
+ service = services[0]
43
+ return {
44
+ "address": service.get("ServiceAddress") or service.get("Address"),
45
+ "port": service.get("ServicePort"),
46
+ }
47
+ except Exception as e:
48
+ return None
49
+
50
+ _consul_client = None
51
+
52
+ def create_consul_client() -> ConsulClient:
53
+ global _consul_client
54
+ if _consul_client is None:
55
+ host = os.getenv("CONSUL_HOST", "localhost")
56
+ port = int(os.getenv("CONSUL_PORT", 8501))
57
+ _consul_client = ConsulClient(host, port)
58
+ return _consul_client
59
+
60
+ async def get_service_url(service_name: str, default_url: str = None) -> str:
61
+ """Get service URL from Consul with retry and fallback."""
62
+ consul_client = create_consul_client()
63
+ max_retries = 5
64
+ retry_delay = 1
65
+
66
+ for attempt in range(max_retries):
67
+ try:
68
+ service_info = await consul_client.discover_service(service_name)
69
+ if service_info:
70
+ url = f"https://{service_info['address']}:{service_info['port']}"
71
+ return url
72
+ except Exception as e:
73
+ pass
74
+ if attempt < max_retries - 1:
75
+ await asyncio.sleep(retry_delay)
76
+
77
+ # Fallback to default
78
+ if default_url:
79
+ return default_url
80
+
81
+ raise Exception(f"Could not discover service: {e.__class__.__name__}: {str(e)}")
@@ -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,184 @@
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
+ from microservice_chassis_grupo2.core.consul import get_service_url
21
+ from microservice_chassis_grupo2.core.secrets import SSMSecrets
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # Ruta al public key para verificar JWTs
26
+ PUBLIC_KEY_PATH = os.getenv("PUBLIC_KEY_PATH", "auth_public.pem")
27
+
28
+ ssm = SSMSecrets(region=os.getenv("AWS_REGION", "us-east-1"))
29
+
30
+ def _env_bool(name: str, default: str = "0") -> bool:
31
+ """
32
+ Convierte una variable de entorno a bool de forma tolerante.
33
+
34
+ Acepta: 1/true/yes/on/y (case-insensitive).
35
+ """
36
+ val = os.getenv(name, default)
37
+ return str(val).strip().lower() in {"1", "true", "yes", "on", "y"}
38
+
39
+
40
+ def _build_ssl_context() -> ssl.SSLContext:
41
+ """
42
+ Construye un SSLContext para TLS simple (server-auth).
43
+
44
+ Requisitos:
45
+ - RABBITMQ_TLS_CA_FILE debe apuntar al CA que firmó el cert del servidor RabbitMQ.
46
+ - No se carga cert/key de cliente (NO mTLS).
47
+
48
+ Decisiones:
49
+ - TLSv1.2+.
50
+ - check_hostname=False para evitar problemas típicos de CN/SAN en entornos docker.
51
+ (Si tus certs están bien con SAN, puedes ponerlo True sin problema.)
52
+ """
53
+ ca_file = os.getenv("RABBITMQ_TLS_CA_FILE", "/certs/ca.pem")
54
+
55
+ if not os.path.exists(ca_file):
56
+ raise FileNotFoundError(f"CA file no encontrado: {ca_file}")
57
+
58
+ ctx = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH, cafile=ca_file)
59
+ ctx.minimum_version = ssl.TLSVersion.TLSv1_2
60
+ ctx.check_hostname = False
61
+
62
+ # Verificación estricta del servidor (lo normal en TLS simple)
63
+ ctx.verify_mode = ssl.CERT_REQUIRED
64
+
65
+ return ctx
66
+
67
+
68
+ async def _connect(url: str, ssl_ctx: ssl.SSLContext | None):
69
+ """
70
+ Conecta con aio-pika de forma robusta.
71
+
72
+ Estrategia:
73
+ - Si no hay TLS (ssl_ctx is None): conexión normal.
74
+ - Si hay TLS: pasar el SSLContext usando el parámetro correcto
75
+ para la versión de aio-pika instalada.
76
+
77
+ Compatibilidad:
78
+ - Algunas versiones soportan `ssl_context=SSLContext`.
79
+ - Otras usan `ssl=True` + `ssl_options` (dict u objeto SSLOptions).
80
+ """
81
+ if ssl_ctx is None:
82
+ return await connect_robust(url)
83
+
84
+ sig = inspect.signature(connect_robust)
85
+ params = sig.parameters
86
+
87
+ # Opción 1 (preferida): ssl_context=
88
+ if "ssl_context" in params:
89
+ return await connect_robust(url, ssl_context=ssl_ctx)
90
+
91
+ # Opción 2: ssl=True + ssl_options=dict(...)
92
+ if "ssl_options" in params:
93
+ # Para aiormq, suele funcionar pasando el contexto dentro de ssl_options.
94
+ # Si tu versión exige keys tipo cafile/certfile/keyfile,
95
+ # aquí también podrías pasar {"cafile": "..."} en vez de context.
96
+ return await connect_robust(url, ssl=True, ssl_options={"context": ssl_ctx})
97
+
98
+ # Último recurso: algunas versiones aceptan ssl como contexto
99
+ return await connect_robust(url, ssl=ssl_ctx)
100
+
101
+
102
+ async def get_channel():
103
+ """
104
+ Devuelve (connection, channel) listo para usar.
105
+
106
+ Nota profesional:
107
+ - Abrir/cerrar conexión por cada publish es caro.
108
+ Para producción, lo suyo es pool o conexión global por proceso.
109
+ - Aquí mantengo tu contrato actual por compatibilidad.
110
+ """
111
+
112
+ ssl_ctx = _build_ssl_context()
113
+
114
+
115
+ try:
116
+ service_url = await get_service_url("rabbitmq")
117
+ if service_url:
118
+ address = service_url.split("//")[1].split(":")[0]
119
+ port = service_url.split(":")[2]
120
+ #rabbitmq_url = f"amqp://{settings.RABBITMQ_USER}:{ssm.get_parameter('/infrastructure/dev/rabbitmq/password')}@{address}:{port}/"
121
+ rabbitmq_url = f"amqp://{settings.RABBITMQ_USER}:guest@{address}:{port}/"
122
+ connection = await _connect(rabbitmq_url, ssl_ctx)
123
+ channel = await connection.channel()
124
+ return connection, channel
125
+ except AMQPConnectionError as e:
126
+ # Re-lanzamos con contexto útil (no lo escondas con un fallback “mágico”).
127
+ raise RuntimeError(f"RabbitMQ connection failed (TLS={ssl_ctx is not None}) to {rabbitmq_url}: {e}") from e
128
+
129
+
130
+ async def declare_exchange(channel):
131
+ """Declara el exchange general (broker)."""
132
+ return await channel.declare_exchange(
133
+ settings.EXCHANGE_NAME,
134
+ ExchangeType.TOPIC,
135
+ durable=True,
136
+ )
137
+
138
+
139
+ async def declare_exchange_command(channel):
140
+ """Declara el exchange de comandos (command)."""
141
+ return await channel.declare_exchange(
142
+ settings.EXCHANGE_NAME_COMMAND,
143
+ ExchangeType.TOPIC,
144
+ durable=True,
145
+ )
146
+
147
+
148
+ async def declare_exchange_saga(channel):
149
+ """Declara el exchange de saga (saga)."""
150
+ return await channel.declare_exchange(
151
+ settings.EXCHANGE_NAME_SAGA,
152
+ ExchangeType.TOPIC,
153
+ durable=True,
154
+ )
155
+
156
+ async def declare_exchange_saga_cancelation_commands(channel):
157
+ """Declara el exchange de comandos (command)."""
158
+ return await channel.declare_exchange(
159
+ settings.EXCHANGE_NAME_SAGA_CANCEL_CMD,
160
+ ExchangeType.TOPIC,
161
+ durable=True,
162
+ )
163
+
164
+
165
+ async def declare_exchange_saga_cancelation_events(channel):
166
+ """Declara el exchange de saga (saga)."""
167
+ return await channel.declare_exchange(
168
+ settings.EXCHANGE_NAME_SAGA_CANCEL_EVT,
169
+ ExchangeType.TOPIC,
170
+ durable=True,
171
+ )
172
+
173
+ async def declare_exchange_logs(channel):
174
+ """
175
+ Declara el exchange de logs (logs) y asegura la cola telegraf_metrics.
176
+ """
177
+ exchange = await channel.declare_exchange(
178
+ settings.EXCHANGE_NAME_LOGS,
179
+ ExchangeType.TOPIC,
180
+ durable=True,
181
+ )
182
+ queue = await channel.declare_queue("telegraf_metrics", durable=True)
183
+ await queue.bind(exchange, routing_key="#")
184
+ 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,113 @@
1
+ import base64
2
+ import boto3
3
+ from typing import Dict, Optional
4
+
5
+
6
+ class SecretsError(Exception):
7
+ """Excepción base para errores de secretos"""
8
+ pass
9
+
10
+
11
+ class SSMSecrets:
12
+ def __init__(self, region: Optional[str] = None):
13
+ self.client = boto3.client("ssm", region_name=region)
14
+
15
+ def get_parameter(self, name: str, decrypt: bool = True) -> str:
16
+ """
17
+ Obtiene un parámetro individual de SSM
18
+ """
19
+ try:
20
+ response = self.client.get_parameter(
21
+ Name=name,
22
+ WithDecryption=decrypt
23
+ )
24
+ return response["Parameter"]["Value"]
25
+ except self.client.exceptions.ParameterNotFound:
26
+ raise SecretsError(f"SSM parameter not found: {name}")
27
+ except Exception as e:
28
+ raise SecretsError(f"Error getting SSM parameter {name}: {e}")
29
+
30
+ def get_parameters_by_path(
31
+ self,
32
+ path: str,
33
+ recursive: bool = True,
34
+ decrypt: bool = True
35
+ ) -> Dict[str, str]:
36
+ """
37
+ Obtiene todos los parámetros bajo un path
38
+ Devuelve un dict {nombre_parametro: valor}
39
+ """
40
+ parameters = {}
41
+ next_token = None
42
+
43
+ try:
44
+ while True:
45
+ kwargs = {
46
+ "Path": path,
47
+ "Recursive": recursive,
48
+ "WithDecryption": decrypt,
49
+ }
50
+ if next_token:
51
+ kwargs["NextToken"] = next_token
52
+
53
+ response = self.client.get_parameters_by_path(**kwargs)
54
+
55
+ for param in response["Parameters"]:
56
+ key = param["Name"].split("/")[-1]
57
+ parameters[key] = param["Value"]
58
+
59
+ next_token = response.get("NextToken")
60
+ if not next_token:
61
+ break
62
+
63
+ return parameters
64
+
65
+ except Exception as e:
66
+ raise SecretsError(f"Error getting SSM parameters by path {path}: {e}")
67
+
68
+
69
+ class SecretsManager:
70
+ def __init__(self, region: Optional[str] = None):
71
+ self.client = boto3.client("secretsmanager", region_name=region)
72
+
73
+ def get_secret(self, secret_id: str) -> str:
74
+ """
75
+ Obtiene un secreto de Secrets Manager
76
+ """
77
+ try:
78
+ response = self.client.get_secret_value(
79
+ SecretId=secret_id
80
+ )
81
+
82
+ if "SecretString" in response:
83
+ return response["SecretString"]
84
+
85
+ # Secret binario (poco común)
86
+ return base64.b64decode(response["SecretBinary"]).decode("utf-8")
87
+
88
+ except self.client.exceptions.ResourceNotFoundException:
89
+ raise SecretsError(f"Secret not found: {secret_id}")
90
+ except Exception as e:
91
+ raise SecretsError(f"Error getting secret {secret_id}: {e}")
92
+
93
+
94
+ class KMSService:
95
+ def __init__(self, region: Optional[str] = None):
96
+ self.client = boto3.client("kms", region_name=region)
97
+
98
+ def decrypt(self, ciphertext_base64: str) -> str:
99
+ """
100
+ Descifra manualmente un ciphertext usando KMS.
101
+ Útil si guardas datos cifrados fuera de SSM/Secrets Manager.
102
+ """
103
+ try:
104
+ ciphertext = base64.b64decode(ciphertext_base64)
105
+
106
+ response = self.client.decrypt(
107
+ CiphertextBlob=ciphertext
108
+ )
109
+
110
+ return response["Plaintext"].decode("utf-8")
111
+
112
+ except Exception as e:
113
+ raise SecretsError(f"Error decrypting with KMS: {e}")
@@ -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,117 @@
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
+ from sqlalchemy.orm import sessionmaker, declarative_base
19
+ from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
20
+ from microservice_chassis_grupo2.core.consul import get_service_url
21
+ import logging
22
+ from microservice_chassis_grupo2.core.secrets import SSMSecrets
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ try:
27
+ ssm = SSMSecrets(region=os.getenv("AWS_REGION", "us-east-1"))
28
+ except Exception as e:
29
+ logger.error(f"Error initializing SSMSecrets: {type(e).__name__}: {str(e)}")
30
+ ssm = None # Fallback si SSM no está disponible
31
+
32
+ async def get_database_url():
33
+ """Get database URL from Consul or fallback to environment variable."""
34
+ print("[DATABASE] Starting get_database_url()")
35
+
36
+ db_user = os.getenv('DB_USER', 'admin')
37
+ #db_password = ssm.get_parameter('/infrastructure/dev/rds/password')
38
+ db_password = os.getenv('DB_PASSWORD', 'maccadmin')
39
+ db_name = os.getenv('DB_NAME')
40
+
41
+ if not db_name:
42
+ raise ValueError("DB_NAME environment variable is required")
43
+
44
+ print(f"[DATABASE] Using DB_NAME: {db_name}")
45
+
46
+ # ✅ Primero intentar RDS_HOST directo
47
+ rds_host = os.getenv('RDS_HOST')
48
+ if rds_host:
49
+ rds_port = os.getenv('RDS_PORT', '3306')
50
+ direct_url = f"mysql+aiomysql://{db_user}:{db_password}@{rds_host}:{rds_port}/{db_name}"
51
+ print(f"[DATABASE] Using direct RDS connection: {rds_host}:{rds_port}")
52
+ logger.info(f"Using direct RDS connection: {rds_host}:{rds_port}/{db_name}")
53
+ return direct_url
54
+
55
+ try:
56
+ print("[DATABASE] Attempting to get RDS from Consul...")
57
+ rds_info = await get_service_url(
58
+ service_name="rds",
59
+ default_url=None
60
+ )
61
+
62
+ print(f"[DATABASE] Got RDS info from Consul: {rds_info}")
63
+
64
+ # ✅ CORRECCIÓN: Remover el prefijo http:// si existe
65
+ if rds_info.startswith('http://'):
66
+ rds_info = rds_info.replace('http://', '')
67
+ elif rds_info.startswith('https://'):
68
+ rds_info = rds_info.replace('https://', '')
69
+
70
+ print(f"[DATABASE] Cleaned RDS info: {rds_info}")
71
+
72
+ # Construir URL de conexión MySQL
73
+ database_url = f"mysql+aiomysql://{db_user}:{db_password}@{rds_info}/{db_name}"
74
+ print(f"[DATABASE] Using RDS from Consul for database: {db_name}")
75
+ logger.info(f"Using RDS from Consul: {rds_info} for database: {db_name}")
76
+ return database_url
77
+
78
+ except Exception as e:
79
+ print(f"[DATABASE] Error getting RDS from Consul: {type(e).__name__}: {str(e)}")
80
+ fallback_url = os.getenv('SQLALCHEMY_DATABASE_URL', 'sqlite+aiosqlite:///./test.db')
81
+ print(f"[DATABASE] Using fallback: {fallback_url}")
82
+ logger.warning(f"Could not get RDS from Consul: {str(e)}, using fallback: {fallback_url}")
83
+ return fallback_url
84
+
85
+ # Variables globales
86
+ engine = None
87
+ SessionLocal = None
88
+ Base = declarative_base()
89
+ _db_initialized = False
90
+
91
+ async def init_database():
92
+ global engine, SessionLocal, _db_initialized
93
+
94
+ print("[DATABASE] init_database() called")
95
+
96
+ if _db_initialized:
97
+ print("[DATABASE] Database already initialized")
98
+ return
99
+
100
+ database_url = await get_database_url()
101
+
102
+ print("[DATABASE] Creating engine...")
103
+
104
+ engine = create_async_engine(
105
+ database_url,
106
+ echo=False,
107
+ pool_pre_ping=True,
108
+ future=True,
109
+ )
110
+ SessionLocal = sessionmaker(
111
+ bind=engine,
112
+ class_=AsyncSession,
113
+ expire_on_commit=False,
114
+ )
115
+
116
+ _db_initialized = True
117
+ print("[DATABASE] Engine and session created successfully")
@@ -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,56 @@
1
+ Metadata-Version: 2.4
2
+ Name: microservice_chassis_grupo2_cc_prod
3
+ Version: 0.1.6
4
+ Summary: A reusable library for microservices
5
+ Home-page: https://github.com/Grupo-MACC/Chassis
6
+ Author: Grupo 2
7
+ Author-email:
8
+ Project-URL: Homepage, https://github.com/Grupo-MACC/Chassis
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ Dynamic: home-page
12
+
13
+ # 🧩 Microservice Chassis Grupo 2
14
+
15
+ **Repository link:** https://github.com/Grupo-MACC/Chassis
16
+
17
+ ---
18
+
19
+ ## Functionalities
20
+
21
+ This repository provides a **shared chassis** for microservices, containing configuration, database management, authentication, messaging, and helper utilities.
22
+ It allows microservices to reuse common functionalities such as database connections, JWT validation, RabbitMQ communication, and standardized base models.
23
+
24
+ Below is a detailed list of the available functionalities:
25
+
26
+ | **Module** | **Functionality** | **Description** |
27
+ |-------------|------------------|-----------------|
28
+ | `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`). |
29
+ | `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. |
30
+ | `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. |
31
+ | `core/rabbitmq.py` | **RabbitMQ Channel & Exchange Management** | Creates asynchronous connections to RabbitMQ using `aio_pika`. Declares durable topic exchanges for inter-service communication. |
32
+ | `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`). |
33
+ | `sql/database.py` | **Database Engine & Session Configuration** | Configures SQLAlchemy’s asynchronous engine and sessionmaker. Supports `SQLALCHEMY_DATABASE_URL` via environment variable (defaults to SQLite). |
34
+ | `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. |
35
+
36
+ ---
37
+
38
+ ## Functionalities per Microservice
39
+
40
+ | **Microservice** | **Used Functionalities** |
41
+ |------------------|--------------------------|
42
+ | `payment` | `core/config`, `core/security`, `core/dependencies`, `core/rabbitmq`, `core/utils`, `sql/database`, `sql/base_class` |
43
+
44
+ ---
45
+
46
+ ## Summary
47
+
48
+ This chassis allows the `payment` microservice (and potentially others in the future) to operate with consistent patterns for:
49
+ - Secure token validation.
50
+ - Standardized database access and sessions.
51
+ - Centralized configuration.
52
+ - RabbitMQ-based inter-service communication.
53
+ - Common helper utilities and error management.
54
+ - Unified base ORM models.
55
+
56
+ It provides a robust foundation for building scalable and maintainable microservices within the system.
@@ -0,0 +1,16 @@
1
+ microservice_chassis_grupo2/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ microservice_chassis_grupo2/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ microservice_chassis_grupo2/core/config.py,sha256=fCXb0ZdlKqYKGlFecEDyHVq__EqptuHnUcNj_iiJITo,2167
4
+ microservice_chassis_grupo2/core/consul.py,sha256=gnjNoDXu7dAcj8z-f1E2A40lxeJwbRMcnERy7C644Qc,2625
5
+ microservice_chassis_grupo2/core/dependencies.py,sha256=l2g1p5b9b104SOHVCe6WJcz5_c6vyU6OdDewAmxOyiQ,1988
6
+ microservice_chassis_grupo2/core/rabbitmq_core.py,sha256=-PEqhaPZnUl6B1jDQ3Or5kMlb47MijEm7DOHopKoO4o,6124
7
+ microservice_chassis_grupo2/core/router_utils.py,sha256=BczZZO9LDWWTWNxCDYYJXHe-E1Cqew7TbLzr5hKnxQc,766
8
+ microservice_chassis_grupo2/core/secrets.py,sha256=ifG9OZ9UeoY6JByCDTBxm08XA6Q0u12KftWvfcMIefI,3479
9
+ microservice_chassis_grupo2/core/security.py,sha256=e4uGs299JW8hdPFi3hqdRB7TRylQ9smiJT6h5x8neUg,1002
10
+ microservice_chassis_grupo2/sql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ microservice_chassis_grupo2/sql/database.py,sha256=EHRdFFbHUQx2Kmnu1Y8Gkipu_dd8W52fF7nDMV5oIrk,4157
12
+ microservice_chassis_grupo2/sql/models.py,sha256=jhRS2NBhVpwJGHt620N3gcgdszngzzqS3R_I3NOKK8o,1289
13
+ microservice_chassis_grupo2_cc_prod-0.1.6.dist-info/METADATA,sha256=BZ_XCcBgSXRDqs6v4RYkvvhHKA3Tg91LWEJMaZzcE08,3230
14
+ microservice_chassis_grupo2_cc_prod-0.1.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
15
+ microservice_chassis_grupo2_cc_prod-0.1.6.dist-info/top_level.txt,sha256=gAbQlQALJPI7wCWahNa12XvGlherC3Jf9C2X4Y80DXU,28
16
+ microservice_chassis_grupo2_cc_prod-0.1.6.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ microservice_chassis_grupo2