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.
Files changed (19) hide show
  1. microservice_chassis_grupo2-0.1.5rc10/PKG-INFO +10 -0
  2. microservice_chassis_grupo2-0.1.5rc10/README.md +44 -0
  3. microservice_chassis_grupo2-0.1.5rc10/microservice_chassis_grupo2/__init__.py +0 -0
  4. microservice_chassis_grupo2-0.1.5rc10/microservice_chassis_grupo2/core/__init__.py +0 -0
  5. microservice_chassis_grupo2-0.1.5rc10/microservice_chassis_grupo2/core/config.py +70 -0
  6. microservice_chassis_grupo2-0.1.5rc10/microservice_chassis_grupo2/core/consul.py +80 -0
  7. microservice_chassis_grupo2-0.1.5rc10/microservice_chassis_grupo2/core/dependencies.py +64 -0
  8. microservice_chassis_grupo2-0.1.5rc10/microservice_chassis_grupo2/core/rabbitmq_core.py +159 -0
  9. microservice_chassis_grupo2-0.1.5rc10/microservice_chassis_grupo2/core/router_utils.py +19 -0
  10. microservice_chassis_grupo2-0.1.5rc10/microservice_chassis_grupo2/core/security.py +27 -0
  11. microservice_chassis_grupo2-0.1.5rc10/microservice_chassis_grupo2/sql/__init__.py +0 -0
  12. microservice_chassis_grupo2-0.1.5rc10/microservice_chassis_grupo2/sql/database.py +138 -0
  13. microservice_chassis_grupo2-0.1.5rc10/microservice_chassis_grupo2/sql/models.py +30 -0
  14. microservice_chassis_grupo2-0.1.5rc10/microservice_chassis_grupo2.egg-info/PKG-INFO +10 -0
  15. microservice_chassis_grupo2-0.1.5rc10/microservice_chassis_grupo2.egg-info/SOURCES.txt +17 -0
  16. microservice_chassis_grupo2-0.1.5rc10/microservice_chassis_grupo2.egg-info/dependency_links.txt +1 -0
  17. microservice_chassis_grupo2-0.1.5rc10/microservice_chassis_grupo2.egg-info/top_level.txt +1 -0
  18. microservice_chassis_grupo2-0.1.5rc10/setup.cfg +4 -0
  19. 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.
@@ -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)}")
@@ -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
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ )