microservice-chassis-grupo2-cc-prod 0.1.8__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_cc_prod-0.1.8/PKG-INFO +56 -0
- microservice_chassis_grupo2_cc_prod-0.1.8/README.md +44 -0
- microservice_chassis_grupo2_cc_prod-0.1.8/microservice_chassis_grupo2/__init__.py +0 -0
- microservice_chassis_grupo2_cc_prod-0.1.8/microservice_chassis_grupo2/core/__init__.py +0 -0
- microservice_chassis_grupo2_cc_prod-0.1.8/microservice_chassis_grupo2/core/config.py +72 -0
- microservice_chassis_grupo2_cc_prod-0.1.8/microservice_chassis_grupo2/core/consul.py +91 -0
- microservice_chassis_grupo2_cc_prod-0.1.8/microservice_chassis_grupo2/core/dependencies.py +64 -0
- microservice_chassis_grupo2_cc_prod-0.1.8/microservice_chassis_grupo2/core/rabbitmq_core.py +184 -0
- microservice_chassis_grupo2_cc_prod-0.1.8/microservice_chassis_grupo2/core/router_utils.py +19 -0
- microservice_chassis_grupo2_cc_prod-0.1.8/microservice_chassis_grupo2/core/secrets.py +113 -0
- microservice_chassis_grupo2_cc_prod-0.1.8/microservice_chassis_grupo2/core/security.py +27 -0
- microservice_chassis_grupo2_cc_prod-0.1.8/microservice_chassis_grupo2/sql/__init__.py +0 -0
- microservice_chassis_grupo2_cc_prod-0.1.8/microservice_chassis_grupo2/sql/database.py +117 -0
- microservice_chassis_grupo2_cc_prod-0.1.8/microservice_chassis_grupo2/sql/models.py +30 -0
- microservice_chassis_grupo2_cc_prod-0.1.8/microservice_chassis_grupo2_cc_prod.egg-info/PKG-INFO +56 -0
- microservice_chassis_grupo2_cc_prod-0.1.8/microservice_chassis_grupo2_cc_prod.egg-info/SOURCES.txt +19 -0
- microservice_chassis_grupo2_cc_prod-0.1.8/microservice_chassis_grupo2_cc_prod.egg-info/dependency_links.txt +1 -0
- microservice_chassis_grupo2_cc_prod-0.1.8/microservice_chassis_grupo2_cc_prod.egg-info/top_level.txt +1 -0
- microservice_chassis_grupo2_cc_prod-0.1.8/pyproject.toml +31 -0
- microservice_chassis_grupo2_cc_prod-0.1.8/setup.cfg +4 -0
- microservice_chassis_grupo2_cc_prod-0.1.8/setup.py +26 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: microservice_chassis_grupo2_cc_prod
|
|
3
|
+
Version: 0.1.8
|
|
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,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,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,91 @@
|
|
|
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
|
+
else:
|
|
48
|
+
with open("/home/pyuser/code/log_consul.txt", "a") as f:
|
|
49
|
+
f.write(f"Consul response code: {response.status_code}\n")
|
|
50
|
+
except Exception as e:
|
|
51
|
+
with open("/home/pyuser/code/log_consul.txt", "a") as f:
|
|
52
|
+
f.write(f"Consul error: {type(e).__name__}: {str(e)}\n")
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
_consul_client = None
|
|
56
|
+
|
|
57
|
+
def create_consul_client() -> ConsulClient:
|
|
58
|
+
global _consul_client
|
|
59
|
+
if _consul_client is None:
|
|
60
|
+
host = os.getenv("CONSUL_HOST", "localhost")
|
|
61
|
+
port = int(os.getenv("CONSUL_PORT", 8501))
|
|
62
|
+
_consul_client = ConsulClient(host, port)
|
|
63
|
+
return _consul_client
|
|
64
|
+
|
|
65
|
+
async def get_service_url(service_name: str, default_url: str = None) -> str:
|
|
66
|
+
consul_client = create_consul_client()
|
|
67
|
+
max_retries = 5
|
|
68
|
+
retry_delay = 1
|
|
69
|
+
last_error: Exception | None = None
|
|
70
|
+
|
|
71
|
+
for attempt in range(max_retries):
|
|
72
|
+
try:
|
|
73
|
+
service_info = await consul_client.discover_service(service_name)
|
|
74
|
+
if service_info:
|
|
75
|
+
return f"https://{service_info['address']}:{service_info['port']}"
|
|
76
|
+
except Exception as e:
|
|
77
|
+
last_error = e
|
|
78
|
+
|
|
79
|
+
if attempt < max_retries - 1:
|
|
80
|
+
await asyncio.sleep(retry_delay)
|
|
81
|
+
|
|
82
|
+
if default_url:
|
|
83
|
+
return default_url
|
|
84
|
+
|
|
85
|
+
if last_error:
|
|
86
|
+
raise RuntimeError(
|
|
87
|
+
f"Could not discover service '{service_name}': "
|
|
88
|
+
f"{last_error.__class__.__name__}: {last_error}"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
raise RuntimeError(f"Could not discover service '{service_name}': no instances found")
|
|
@@ -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}
|
microservice_chassis_grupo2_cc_prod-0.1.8/microservice_chassis_grupo2_cc_prod.egg-info/PKG-INFO
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: microservice_chassis_grupo2_cc_prod
|
|
3
|
+
Version: 0.1.8
|
|
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.
|
microservice_chassis_grupo2_cc_prod-0.1.8/microservice_chassis_grupo2_cc_prod.egg-info/SOURCES.txt
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
setup.py
|
|
4
|
+
microservice_chassis_grupo2/__init__.py
|
|
5
|
+
microservice_chassis_grupo2/core/__init__.py
|
|
6
|
+
microservice_chassis_grupo2/core/config.py
|
|
7
|
+
microservice_chassis_grupo2/core/consul.py
|
|
8
|
+
microservice_chassis_grupo2/core/dependencies.py
|
|
9
|
+
microservice_chassis_grupo2/core/rabbitmq_core.py
|
|
10
|
+
microservice_chassis_grupo2/core/router_utils.py
|
|
11
|
+
microservice_chassis_grupo2/core/secrets.py
|
|
12
|
+
microservice_chassis_grupo2/core/security.py
|
|
13
|
+
microservice_chassis_grupo2/sql/__init__.py
|
|
14
|
+
microservice_chassis_grupo2/sql/database.py
|
|
15
|
+
microservice_chassis_grupo2/sql/models.py
|
|
16
|
+
microservice_chassis_grupo2_cc_prod.egg-info/PKG-INFO
|
|
17
|
+
microservice_chassis_grupo2_cc_prod.egg-info/SOURCES.txt
|
|
18
|
+
microservice_chassis_grupo2_cc_prod.egg-info/dependency_links.txt
|
|
19
|
+
microservice_chassis_grupo2_cc_prod.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
microservice_chassis_grupo2_cc_prod-0.1.8/microservice_chassis_grupo2_cc_prod.egg-info/top_level.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
microservice_chassis_grupo2
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# pyproject.toml
|
|
2
|
+
[build-system]
|
|
3
|
+
requires = [
|
|
4
|
+
"setuptools>=69",
|
|
5
|
+
"wheel",
|
|
6
|
+
"setuptools_scm[toml]>=8"
|
|
7
|
+
]
|
|
8
|
+
build-backend = "setuptools.build_meta"
|
|
9
|
+
|
|
10
|
+
[project]
|
|
11
|
+
name = "microservice_chassis_grupo2_cc_prod"
|
|
12
|
+
dynamic = ["version"]
|
|
13
|
+
description = "A reusable library for microservices"
|
|
14
|
+
readme = "README.md"
|
|
15
|
+
requires-python = ">=3.10"
|
|
16
|
+
authors = [{ name = "Grupo 2" }]
|
|
17
|
+
urls = { "Homepage" = "https://github.com/Grupo-MACC/Chassis" }
|
|
18
|
+
|
|
19
|
+
[tool.setuptools.packages.find]
|
|
20
|
+
where = ["."]
|
|
21
|
+
include = ["microservice_chassis_grupo2*"]
|
|
22
|
+
|
|
23
|
+
[tool.setuptools_scm]
|
|
24
|
+
version_scheme = "no-guess-dev"
|
|
25
|
+
local_scheme = "no-local-version"
|
|
26
|
+
|
|
27
|
+
# Acepta tags tipo v0.1.9, v0.1.5rc10, v1.2.3
|
|
28
|
+
tag_regex = "^(?:v)?(?P<version>\\d+\\.\\d+\\.\\d+(?:(?:a|b|rc)\\d+)?)$"
|
|
29
|
+
|
|
30
|
+
# Escribe versión dentro del paquete (evita problemas al construir desde sdist)
|
|
31
|
+
write_to = "microservice_chassis_grupo2/_version.py"
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
setup.py
|
|
3
|
+
|
|
4
|
+
Este setup.py deja de hardcodear la versión.
|
|
5
|
+
La versión se obtiene de tags Git mediante setuptools_scm (configurado en pyproject.toml).
|
|
6
|
+
|
|
7
|
+
Nota:
|
|
8
|
+
Con pyproject.toml presente, lo normal es construir con:
|
|
9
|
+
python -m build
|
|
10
|
+
y no con setup.py directamente.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from setuptools import setup, find_packages
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
setup(
|
|
17
|
+
name="microservice_chassis_grupo2_cc_prod",
|
|
18
|
+
version="0.1.8",
|
|
19
|
+
packages=find_packages(),
|
|
20
|
+
use_scm_version=True, # <-- clave: versión por tags
|
|
21
|
+
description="A reusable library for microservices",
|
|
22
|
+
url="https://github.com/Grupo-MACC/Chassis",
|
|
23
|
+
author="Grupo 2",
|
|
24
|
+
author_email="",
|
|
25
|
+
install_requires=[]
|
|
26
|
+
)
|