global-handler 0.1.0__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.
- global_handler/decorators/error_handler.py +55 -0
- global_handler/decorators/request_decorators.py +118 -0
- global_handler/exceptions/http_exceptions.py +121 -0
- global_handler/middlewares/metrics_middleware.py +32 -0
- global_handler-0.1.0.dist-info/LICENSE +21 -0
- global_handler-0.1.0.dist-info/METADATA +380 -0
- global_handler-0.1.0.dist-info/RECORD +8 -0
- global_handler-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
|
|
2
|
+
from src.controllers.base_controller import BaseController
|
|
3
|
+
from src.global_handler.exceptions.http_exceptions import HttpError
|
|
4
|
+
from logger_tracker import logger, _request_uuid
|
|
5
|
+
from src.config import DEBUG
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def register_error_handlers(app):
|
|
9
|
+
base_controller = BaseController()
|
|
10
|
+
|
|
11
|
+
@app.errorhandler(HttpError)
|
|
12
|
+
def handle_http_error(error: HttpError):
|
|
13
|
+
request_id = getattr(_request_uuid, "id", None)
|
|
14
|
+
extra={
|
|
15
|
+
"status_code": error.status_code,
|
|
16
|
+
"details": error.details,
|
|
17
|
+
"request_id": request_id
|
|
18
|
+
}
|
|
19
|
+
logger.logg_warning(
|
|
20
|
+
f"HTTP Error {error.error_code}"
|
|
21
|
+
)
|
|
22
|
+
logger.logg_debug(
|
|
23
|
+
f"HTTP Error details: {extra}"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
return base_controller.error(
|
|
27
|
+
message=error.message,
|
|
28
|
+
details=error.details,
|
|
29
|
+
status_code=error.status_code,
|
|
30
|
+
error_code=error.error_code,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
@app.errorhandler(Exception)
|
|
34
|
+
def handle_unexpected_error(error: Exception):
|
|
35
|
+
request_id = getattr(_request_uuid, "id", None)
|
|
36
|
+
extra={
|
|
37
|
+
"request_id": request_id,
|
|
38
|
+
"exception_type": type(error).__name__,
|
|
39
|
+
"error_details": str(error)
|
|
40
|
+
},
|
|
41
|
+
logger.logg_error(
|
|
42
|
+
"Unhandled exception"
|
|
43
|
+
)
|
|
44
|
+
logger.logg_debug(
|
|
45
|
+
f"Unhandled exception details: {extra}"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
message = "Internal server error"
|
|
49
|
+
details = str(error) if DEBUG else None
|
|
50
|
+
|
|
51
|
+
return base_controller.error(
|
|
52
|
+
message=message,
|
|
53
|
+
details=details,
|
|
54
|
+
status_code=500
|
|
55
|
+
)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
from functools import wraps
|
|
2
|
+
from flask import request
|
|
3
|
+
from werkzeug.exceptions import BadRequest
|
|
4
|
+
from pydantic import ValidationError
|
|
5
|
+
|
|
6
|
+
from logger_tracker import logger
|
|
7
|
+
from src.global_handler.exceptions.http_exceptions import (
|
|
8
|
+
ValidationHttpError,
|
|
9
|
+
InvalidJsonHttpError,
|
|
10
|
+
MissingParameterHttpError,
|
|
11
|
+
InvalidParameterHttpError,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def normalize_validation_errors(exc):
|
|
16
|
+
"""Normalize Pydantic validation errors to a consistent format."""
|
|
17
|
+
return exc.errors()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def validate_with(model):
|
|
21
|
+
def decorator(f):
|
|
22
|
+
@wraps(f)
|
|
23
|
+
def wrapper(*args, **kwargs):
|
|
24
|
+
if not request.is_json:
|
|
25
|
+
raise InvalidJsonHttpError("Request body must be a valid JSON")
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
validated = model(**request.get_json())
|
|
29
|
+
return f(validated_data=validated, *args, **kwargs)
|
|
30
|
+
|
|
31
|
+
except BadRequest:
|
|
32
|
+
raise InvalidJsonHttpError("Request body must be a valid JSON")
|
|
33
|
+
|
|
34
|
+
except ValidationError as exc:
|
|
35
|
+
extra = {"errors": exc.errors()}
|
|
36
|
+
logger.logg_warning(
|
|
37
|
+
f"Validation error on {model.__name__}"
|
|
38
|
+
)
|
|
39
|
+
logger.logg_debug(
|
|
40
|
+
f"Validation error details: {extra}"
|
|
41
|
+
)
|
|
42
|
+
logger.logg_info("Aqui estamos")
|
|
43
|
+
raise ValidationHttpError(normalize_validation_errors(exc))
|
|
44
|
+
|
|
45
|
+
return wrapper
|
|
46
|
+
return decorator
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def validate_headers_with(model):
|
|
50
|
+
def decorator(f):
|
|
51
|
+
@wraps(f)
|
|
52
|
+
def wrapper(*args, **kwargs):
|
|
53
|
+
try:
|
|
54
|
+
# Convertir headers a dict y validar
|
|
55
|
+
headers_dict = dict(request.headers)
|
|
56
|
+
validated = model(**headers_dict)
|
|
57
|
+
return f(headers_data=validated, *args, **kwargs)
|
|
58
|
+
|
|
59
|
+
except ValidationError as exc:
|
|
60
|
+
extra = {"errors": exc.errors()}
|
|
61
|
+
logger.logg_warning(
|
|
62
|
+
f"Header validation error on {model.__name__}"
|
|
63
|
+
)
|
|
64
|
+
logger.logg_debug(
|
|
65
|
+
f"Header validation error details: {extra}"
|
|
66
|
+
)
|
|
67
|
+
raise ValidationHttpError(normalize_validation_errors(exc))
|
|
68
|
+
|
|
69
|
+
return wrapper
|
|
70
|
+
return decorator
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def validate_query_with(model):
|
|
74
|
+
def decorator(f):
|
|
75
|
+
@wraps(f)
|
|
76
|
+
def wrapper(*args, **kwargs):
|
|
77
|
+
try:
|
|
78
|
+
validated = model(**request.args.to_dict())
|
|
79
|
+
return f(query_params=validated, *args, **kwargs)
|
|
80
|
+
|
|
81
|
+
except ValidationError as exc:
|
|
82
|
+
extra = {"errors": exc.errors()}
|
|
83
|
+
logger.logg_warning(
|
|
84
|
+
f"Query validation error on {model.__name__}"
|
|
85
|
+
)
|
|
86
|
+
logger.logg_debug(
|
|
87
|
+
f"Query validation error details: {extra}"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
for error in exc.errors():
|
|
91
|
+
error_type = error.get("type")
|
|
92
|
+
|
|
93
|
+
if error_type == "missing":
|
|
94
|
+
raise MissingParameterHttpError(error)
|
|
95
|
+
|
|
96
|
+
raise InvalidParameterHttpError(error)
|
|
97
|
+
|
|
98
|
+
raise ValidationHttpError(exc.errors())
|
|
99
|
+
|
|
100
|
+
return wrapper
|
|
101
|
+
return decorator
|
|
102
|
+
|
|
103
|
+
def normalize_validation_errors(exc: ValidationError) -> list[dict]:
|
|
104
|
+
normalized_errors = []
|
|
105
|
+
|
|
106
|
+
for err in exc.errors():
|
|
107
|
+
field = ".".join(str(loc) for loc in err.get("loc", []))
|
|
108
|
+
error_type = err.get("type")
|
|
109
|
+
|
|
110
|
+
normalized_errors.append(
|
|
111
|
+
{
|
|
112
|
+
"field": field,
|
|
113
|
+
"error_type": error_type,
|
|
114
|
+
"error": err.get("msg"),
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
return normalized_errors
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
class HttpError(Exception):
|
|
2
|
+
status_code: int = 400
|
|
3
|
+
message: str = "Bad Request"
|
|
4
|
+
error_code = "BAD_REQUEST"
|
|
5
|
+
|
|
6
|
+
def __init__(self, details=None, *, message: str | None = None):
|
|
7
|
+
self.details = details
|
|
8
|
+
if message:
|
|
9
|
+
self.message = message
|
|
10
|
+
super().__init__(self.message)
|
|
11
|
+
|
|
12
|
+
# -------------------------
|
|
13
|
+
# Errores 400 Errores del cliente
|
|
14
|
+
# -------------------------
|
|
15
|
+
class BadRequestHttpError(HttpError):
|
|
16
|
+
status_code = 400
|
|
17
|
+
message = "Bad request"
|
|
18
|
+
|
|
19
|
+
class ValidationHttpError(HttpError):
|
|
20
|
+
status_code = 400
|
|
21
|
+
message = "Invalid request body"
|
|
22
|
+
error_code = "VALIDATION_ERROR"
|
|
23
|
+
|
|
24
|
+
class InvalidJsonHttpError(HttpError):
|
|
25
|
+
status_code = 400
|
|
26
|
+
message = "Invalid JSON payload"
|
|
27
|
+
error_code = "INVALID_JSON"
|
|
28
|
+
|
|
29
|
+
class MissingParameterHttpError(HttpError):
|
|
30
|
+
status_code = 400
|
|
31
|
+
message = "Missing required parameter"
|
|
32
|
+
error_code = "MISSING_PARAMETER"
|
|
33
|
+
|
|
34
|
+
class InvalidParameterHttpError(HttpError):
|
|
35
|
+
status_code = 400
|
|
36
|
+
message = "Invalid parameter value"
|
|
37
|
+
error_code = "INVALID_PARAMETER"
|
|
38
|
+
|
|
39
|
+
# -------------------------
|
|
40
|
+
# Errores 401 Autenticacion
|
|
41
|
+
# -------------------------
|
|
42
|
+
class UnauthorizedHttpError(HttpError):
|
|
43
|
+
status_code = 401
|
|
44
|
+
message = "Unauthorized"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class InvalidCredentialsHttpError(HttpError):
|
|
48
|
+
status_code = 401
|
|
49
|
+
message = "Invalid credentials"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class TokenExpiredHttpError(HttpError):
|
|
53
|
+
status_code = 401
|
|
54
|
+
message = "Authentication token expired"
|
|
55
|
+
|
|
56
|
+
# -------------------------
|
|
57
|
+
# Errores 403 Authorizacion
|
|
58
|
+
# -------------------------
|
|
59
|
+
class ForbiddenHttpError(HttpError):
|
|
60
|
+
status_code = 403
|
|
61
|
+
message = "Forbidden"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class InsufficientPermissionsHttpError(HttpError):
|
|
65
|
+
status_code = 403
|
|
66
|
+
message = "Insufficient permissions"
|
|
67
|
+
|
|
68
|
+
# -------------------------
|
|
69
|
+
# Errores 404 Recurso no encontrado
|
|
70
|
+
# -------------------------
|
|
71
|
+
class NotFoundHttpError(HttpError):
|
|
72
|
+
status_code = 404
|
|
73
|
+
message = "Resource not found"
|
|
74
|
+
|
|
75
|
+
class EndpointNotFoundHttpError(NotFoundHttpError):
|
|
76
|
+
message = "Endpoint not found"
|
|
77
|
+
|
|
78
|
+
# -------------------------
|
|
79
|
+
# Errores 409 Conflicto
|
|
80
|
+
# -------------------------
|
|
81
|
+
class ConflictHttpError(HttpError):
|
|
82
|
+
status_code = 409
|
|
83
|
+
message = "Conflict"
|
|
84
|
+
|
|
85
|
+
class ResourceAlreadyExistsHttpError(ConflictHttpError):
|
|
86
|
+
message = "Resource already exists"
|
|
87
|
+
|
|
88
|
+
# -------------------------
|
|
89
|
+
# Errores 422 Entidad no procesable
|
|
90
|
+
# -------------------------
|
|
91
|
+
class UnprocessableEntityHttpError(HttpError):
|
|
92
|
+
status_code = 422
|
|
93
|
+
message = "Unprocessable entity"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class InvalidStateHttpError(UnprocessableEntityHttpError):
|
|
97
|
+
message = "Invalid resource state"
|
|
98
|
+
|
|
99
|
+
# -------------------------
|
|
100
|
+
# Errores 429 Rate limit
|
|
101
|
+
# -------------------------
|
|
102
|
+
class TooManyRequestsHttpError(HttpError):
|
|
103
|
+
status_code = 429
|
|
104
|
+
message = "Too many requests"
|
|
105
|
+
|
|
106
|
+
# -------------------------
|
|
107
|
+
# Errores 500 Errores del servidor
|
|
108
|
+
# -------------------------
|
|
109
|
+
class InternalServerHttpError(HttpError):
|
|
110
|
+
status_code = 500
|
|
111
|
+
message = "Internal server error"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class ServiceUnavailableHttpError(HttpError):
|
|
115
|
+
status_code = 503
|
|
116
|
+
message = "Service unavailable"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class TimeoutHttpError(HttpError):
|
|
120
|
+
status_code = 504
|
|
121
|
+
message = "Gateway timeout"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from flask import request, g
|
|
3
|
+
from logger_tracker import logger, _request_uuid
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def before_request_metrics():
|
|
7
|
+
g.start_time = time.perf_counter()
|
|
8
|
+
extra={
|
|
9
|
+
"query_params": request.args.to_dict(),
|
|
10
|
+
"request_id": getattr(_request_uuid, "id", None)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
logger.logg_info(
|
|
14
|
+
f"📦 Method: {request.method} Path: {request.path}"
|
|
15
|
+
)
|
|
16
|
+
logger.logg_debug(extra)
|
|
17
|
+
|
|
18
|
+
def after_request_metrics(response):
|
|
19
|
+
elapsed = time.perf_counter() - g.start_time
|
|
20
|
+
extra={
|
|
21
|
+
"request_id": getattr(_request_uuid, "id", None),
|
|
22
|
+
"method": request.method,
|
|
23
|
+
"path": request.path,
|
|
24
|
+
"status_code": response.status_code,
|
|
25
|
+
"duration_ms": round(elapsed * 1000, 2)
|
|
26
|
+
}
|
|
27
|
+
logger.logg_debug(extra)
|
|
28
|
+
logger.logg_info(
|
|
29
|
+
"HTTP request completed",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
return response
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Damian Gonzalez
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: global-handler
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Global Handler es una librería Python diseñada para facilitar el manejo de errores, validaciones de requests y métricas en aplicaciones web
|
|
5
|
+
Author: Damian Gonzalez
|
|
6
|
+
Author-email: damian27goa@gmail.com
|
|
7
|
+
Requires-Python: >=3.13
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
10
|
+
Requires-Dist: flask
|
|
11
|
+
Requires-Dist: logger-tracker (==1.0.9)
|
|
12
|
+
Requires-Dist: pydantic
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# Global Handler
|
|
16
|
+
|
|
17
|
+
## Descripción
|
|
18
|
+
|
|
19
|
+
Global Handler es una librería Python completa para el manejo robusto de aplicaciones web basadas en Flask. Proporciona herramientas esenciales para validar datos de entrada, gestionar errores de manera elegante, y monitorear el rendimiento de tus endpoints. Diseñada para desarrolladores que buscan una solución integrada y fácil de usar para mejorar la calidad y mantenibilidad de sus APIs REST.
|
|
20
|
+
|
|
21
|
+
### Características Principales
|
|
22
|
+
|
|
23
|
+
- **Validación Automática de Requests**: Usa Pydantic para validar JSON, parámetros de query y headers con decoradores simples.
|
|
24
|
+
- **Sistema de Excepciones HTTP Estructurado**: Excepciones personalizadas con códigos de error consistentes y mensajes descriptivos.
|
|
25
|
+
- **Middlewares de Métricas**: Logging automático de requests con tiempos de ejecución y detalles de contexto.
|
|
26
|
+
- **Manejo Global de Errores**: Respuestas de error estandarizadas en formato JSON.
|
|
27
|
+
- **Integración Fluida con Flask**: Diseñado específicamente para aplicaciones Flask, sin configuración compleja.
|
|
28
|
+
|
|
29
|
+
## Instalación
|
|
30
|
+
|
|
31
|
+
### Requisitos
|
|
32
|
+
|
|
33
|
+
- Python 3.13+
|
|
34
|
+
- Flask
|
|
35
|
+
- Pydantic
|
|
36
|
+
|
|
37
|
+
### Instalación desde PyPI
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install global-handler
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Instalación desde código fuente
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
git clone https://github.com/tu-usuario/global-handler.git
|
|
47
|
+
cd global-handler
|
|
48
|
+
poetry install
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Implementación en una Aplicación Flask
|
|
52
|
+
|
|
53
|
+
### Configuración Básica
|
|
54
|
+
|
|
55
|
+
1. **Importa los componentes necesarios:**
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from flask import Flask
|
|
59
|
+
from global_handler.decorators.request_decorators import validate_with, validate_query_with, validate_headers_with
|
|
60
|
+
from global_handler.decorators.error_handler import register_error_handlers
|
|
61
|
+
from global_handler.middlewares.metrics_middleware import before_request_metrics, after_request_metrics
|
|
62
|
+
from global_handler.exceptions.http_exceptions import ValidationHttpError
|
|
63
|
+
from pydantic import BaseModel
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
2. **Crea tu aplicación Flask:**
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
app = Flask(__name__)
|
|
70
|
+
|
|
71
|
+
# Registra los manejadores de errores
|
|
72
|
+
register_error_handlers(app)
|
|
73
|
+
|
|
74
|
+
# Registra los middlewares de métricas
|
|
75
|
+
app.before_request(before_request_metrics)
|
|
76
|
+
app.after_request(after_request_metrics)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Uso de Decoradores de Validación
|
|
80
|
+
|
|
81
|
+
#### Validación del Cuerpo JSON (`validate_with`)
|
|
82
|
+
|
|
83
|
+
Valida automáticamente el cuerpo JSON de las requests POST/PUT contra un modelo Pydantic.
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
class UserCreateModel(BaseModel):
|
|
87
|
+
name: str
|
|
88
|
+
email: str
|
|
89
|
+
age: int
|
|
90
|
+
|
|
91
|
+
@app.route('/users', methods=['POST'])
|
|
92
|
+
@validate_with(UserCreateModel)
|
|
93
|
+
def create_user(validated_data):
|
|
94
|
+
# validated_data es una instancia de UserCreateModel con datos validados
|
|
95
|
+
user = User(name=validated_data.name, email=validated_data.email, age=validated_data.age)
|
|
96
|
+
# ... lógica para guardar usuario
|
|
97
|
+
return {"message": "Usuario creado", "user_id": user.id}, 201
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Comportamiento:**
|
|
101
|
+
- Si el JSON es inválido: retorna 400 con código "INVALID_JSON"
|
|
102
|
+
- Si falla la validación: retorna 400 con código "VALIDATION_ERROR" y detalles de los errores
|
|
103
|
+
|
|
104
|
+
#### Validación de Parámetros Query (`validate_query_with`)
|
|
105
|
+
|
|
106
|
+
Valida los parámetros de la URL query string.
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
class UserQueryModel(BaseModel):
|
|
110
|
+
page: int = 1
|
|
111
|
+
limit: int = 10
|
|
112
|
+
search: str = None
|
|
113
|
+
|
|
114
|
+
@app.route('/users', methods=['GET'])
|
|
115
|
+
@validate_query_with(UserQueryModel)
|
|
116
|
+
def list_users(query_params):
|
|
117
|
+
users = User.query.filter_by(search=query_params.search).paginate(
|
|
118
|
+
page=query_params.page, per_page=query_params.limit
|
|
119
|
+
)
|
|
120
|
+
return {"users": [user.to_dict() for user in users.items]}, 200
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Comportamiento:**
|
|
124
|
+
- Parámetros faltantes: usa valores por defecto del modelo
|
|
125
|
+
- Tipos inválidos: retorna 400 con código "INVALID_PARAMETER"
|
|
126
|
+
- Parámetros requeridos faltantes: retorna 400 con código "MISSING_PARAMETER"
|
|
127
|
+
|
|
128
|
+
#### Validación de Headers (`validate_headers_with`)
|
|
129
|
+
|
|
130
|
+
Valida los headers HTTP de la request.
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from pydantic import Field
|
|
134
|
+
|
|
135
|
+
class AuthHeadersModel(BaseModel):
|
|
136
|
+
authorization: str = Field(alias="Authorization")
|
|
137
|
+
content_type: str = Field(alias="Content-Type", default="application/json")
|
|
138
|
+
|
|
139
|
+
@app.route('/protected', methods=['GET'])
|
|
140
|
+
@validate_headers_with(AuthHeadersModel)
|
|
141
|
+
def protected_route(headers_data):
|
|
142
|
+
token = headers_data.authorization.replace("Bearer ", "")
|
|
143
|
+
# ... lógica de autenticación
|
|
144
|
+
return {"message": "Acceso autorizado"}, 200
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**Nota:** Usa `Field(alias="Header-Name")` para mapear nombres de headers con guiones a nombres de campos válidos en Python.
|
|
148
|
+
|
|
149
|
+
### Middlewares de Métricas
|
|
150
|
+
|
|
151
|
+
Los middlewares registran automáticamente información sobre cada request:
|
|
152
|
+
|
|
153
|
+
- **before_request_metrics**: Registra el inicio de la request con método, path, parámetros query y ID de request.
|
|
154
|
+
- **after_request_metrics**: Registra el final con tiempo de ejecución, código de estado y detalles adicionales.
|
|
155
|
+
|
|
156
|
+
Los logs se envían al sistema logger_tracker configurado.
|
|
157
|
+
|
|
158
|
+
### Manejo de Errores
|
|
159
|
+
|
|
160
|
+
La librería incluye manejadores de errores globales que convierten excepciones en respuestas JSON estandarizadas:
|
|
161
|
+
|
|
162
|
+
```json
|
|
163
|
+
{
|
|
164
|
+
"error": {
|
|
165
|
+
"message": "Invalid request body",
|
|
166
|
+
"code": "VALIDATION_ERROR",
|
|
167
|
+
"details": [
|
|
168
|
+
{
|
|
169
|
+
"loc": ["age"],
|
|
170
|
+
"msg": "Input should be a valid integer",
|
|
171
|
+
"type": "int_parsing"
|
|
172
|
+
}
|
|
173
|
+
]
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Excepciones Disponibles
|
|
179
|
+
|
|
180
|
+
- `ValidationHttpError`: Errores de validación de datos (400)
|
|
181
|
+
- `InvalidJsonHttpError`: JSON malformado (400)
|
|
182
|
+
- `MissingParameterHttpError`: Parámetros requeridos faltantes (400)
|
|
183
|
+
- `InvalidParameterHttpError`: Parámetros con tipos inválidos (400)
|
|
184
|
+
- `UnauthorizedHttpError`: Autenticación requerida (401)
|
|
185
|
+
- `InvalidCredentialsHttpError`: Credenciales inválidas (401)
|
|
186
|
+
- `TokenExpiredHttpError`: Token expirado (401)
|
|
187
|
+
- `ForbiddenHttpError`: Acceso prohibido (403)
|
|
188
|
+
- `NotFoundHttpError`: Recurso no encontrado (404)
|
|
189
|
+
- Y más...
|
|
190
|
+
|
|
191
|
+
### Ejemplo Completo de Aplicación
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
from flask import Flask
|
|
195
|
+
from global_handler.decorators.request_decorators import validate_with, validate_query_with
|
|
196
|
+
from global_handler.decorators.error_handler import register_error_handlers
|
|
197
|
+
from global_handler.middlewares.metrics_middleware import before_request_metrics, after_request_metrics
|
|
198
|
+
from pydantic import BaseModel
|
|
199
|
+
|
|
200
|
+
app = Flask(__name__)
|
|
201
|
+
|
|
202
|
+
# Configuración
|
|
203
|
+
register_error_handlers(app)
|
|
204
|
+
app.before_request(before_request_metrics)
|
|
205
|
+
app.after_request(after_request_metrics)
|
|
206
|
+
|
|
207
|
+
# Modelos
|
|
208
|
+
class UserCreateModel(BaseModel):
|
|
209
|
+
name: str
|
|
210
|
+
email: str
|
|
211
|
+
age: int
|
|
212
|
+
|
|
213
|
+
class UserQueryModel(BaseModel):
|
|
214
|
+
page: int = 1
|
|
215
|
+
limit: int = 10
|
|
216
|
+
|
|
217
|
+
# Rutas
|
|
218
|
+
@app.route('/users', methods=['POST'])
|
|
219
|
+
@validate_with(UserCreateModel)
|
|
220
|
+
def create_user(validated_data):
|
|
221
|
+
# Lógica de negocio
|
|
222
|
+
return {"message": "Usuario creado", "data": validated_data.model_dump()}, 201
|
|
223
|
+
|
|
224
|
+
@app.route('/users', methods=['GET'])
|
|
225
|
+
@validate_query_with(UserQueryModel)
|
|
226
|
+
def list_users(query_params):
|
|
227
|
+
# Lógica de negocio
|
|
228
|
+
return {"users": [], "pagination": query_params.model_dump()}, 200
|
|
229
|
+
|
|
230
|
+
if __name__ == '__main__':
|
|
231
|
+
app.run(debug=True)
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Detalles Técnicos
|
|
235
|
+
|
|
236
|
+
- **Lenguaje**: Python 3.13+
|
|
237
|
+
- **Framework**: Flask
|
|
238
|
+
- **Validación**: Pydantic v2
|
|
239
|
+
- **Logging**: Integración con logger_tracker
|
|
240
|
+
- **Build System**: Poetry
|
|
241
|
+
- **Licencia**: MIT
|
|
242
|
+
|
|
243
|
+
### Dependencias
|
|
244
|
+
|
|
245
|
+
- Flask>=2.0.0
|
|
246
|
+
- Pydantic>=2.0.0
|
|
247
|
+
- logger_tracker==0.1.0
|
|
248
|
+
|
|
249
|
+
### Estructura del Proyecto
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
src/global_handler/
|
|
253
|
+
├── decorators/
|
|
254
|
+
│ ├── error_handler.py # Manejadores de errores globales
|
|
255
|
+
│ └── request_decorators.py # Decoradores de validación
|
|
256
|
+
├── exceptions/
|
|
257
|
+
│ └── http_exceptions.py # Excepciones HTTP
|
|
258
|
+
└── middlewares/
|
|
259
|
+
└── metrics_middleware.py # Middlewares de métricas
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Contribución
|
|
263
|
+
|
|
264
|
+
1. Fork el proyecto
|
|
265
|
+
2. Crea una rama para tu feature (`git checkout -b feature/AmazingFeature`)
|
|
266
|
+
3. Commit tus cambios (`git commit -m 'Add some AmazingFeature'`)
|
|
267
|
+
4. Push a la rama (`git push origin feature/AmazingFeature`)
|
|
268
|
+
5. Abre un Pull Request
|
|
269
|
+
|
|
270
|
+
## Licencia
|
|
271
|
+
|
|
272
|
+
Este proyecto está bajo la Licencia MIT. Ver el archivo `LICENSE` para más detalles.
|
|
273
|
+
|
|
274
|
+
## Soporte
|
|
275
|
+
|
|
276
|
+
Para preguntas o soporte, por favor abre un issue en GitHub o contacta al maintainer.
|
|
277
|
+
pyproject.toml # Configuración del proyecto con Poetry
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Instalación
|
|
281
|
+
|
|
282
|
+
1. Clona el repositorio:
|
|
283
|
+
```bash
|
|
284
|
+
git clone <url-del-repositorio>
|
|
285
|
+
cd global-handler
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
2. Instala las dependencias con Poetry:
|
|
289
|
+
```bash
|
|
290
|
+
poetry install
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Uso
|
|
294
|
+
|
|
295
|
+
#### Configuración en Flask
|
|
296
|
+
|
|
297
|
+
```python
|
|
298
|
+
from flask import Flask
|
|
299
|
+
from global_handler.decorators.error_handler import register_error_handlers
|
|
300
|
+
from global_handler.middlewares.metrics_middleware import before_request_metrics, after_request_metrics
|
|
301
|
+
|
|
302
|
+
app = Flask(__name__)
|
|
303
|
+
|
|
304
|
+
# Registrar middlewares
|
|
305
|
+
app.before_request(before_request_metrics)
|
|
306
|
+
app.after_request(after_request_metrics)
|
|
307
|
+
|
|
308
|
+
# Registrar manejadores de errores
|
|
309
|
+
register_error_handlers(app)
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
#### Uso de Decoradores
|
|
313
|
+
|
|
314
|
+
```python
|
|
315
|
+
from pydantic import BaseModel
|
|
316
|
+
from global_handler.decorators.request_decorators import validate_with
|
|
317
|
+
|
|
318
|
+
class UserModel(BaseModel):
|
|
319
|
+
name: str
|
|
320
|
+
age: int
|
|
321
|
+
|
|
322
|
+
@app.route('/user', methods=['POST'])
|
|
323
|
+
@validate_with(UserModel)
|
|
324
|
+
def create_user(validated_data):
|
|
325
|
+
# validated_data es una instancia de UserModel
|
|
326
|
+
return {"message": "User created"}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
```python
|
|
330
|
+
from global_handler.decorators.request_decorators import validate_headers_with
|
|
331
|
+
|
|
332
|
+
class HeaderModel(BaseModel):
|
|
333
|
+
Authorization: str
|
|
334
|
+
Content_Type: str
|
|
335
|
+
|
|
336
|
+
@app.route('/protected', methods=['GET'])
|
|
337
|
+
@validate_headers_with(HeaderModel)
|
|
338
|
+
def protected_route(headers_data):
|
|
339
|
+
# headers_data es una instancia de HeaderModel
|
|
340
|
+
return {"message": "Access granted"}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
#### Excepciones
|
|
344
|
+
|
|
345
|
+
```python
|
|
346
|
+
from global_handler.exceptions.http_exceptions import ValidationHttpError
|
|
347
|
+
|
|
348
|
+
raise ValidationHttpError("Invalid data")
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
## Tests
|
|
352
|
+
|
|
353
|
+
El proyecto incluye un conjunto de tests unitarios para validar el funcionamiento de los componentes.
|
|
354
|
+
|
|
355
|
+
### Ejecutar Tests
|
|
356
|
+
|
|
357
|
+
Instala las dependencias de desarrollo:
|
|
358
|
+
```bash
|
|
359
|
+
poetry install --with dev
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Ejecuta los tests con pytest:
|
|
363
|
+
```bash
|
|
364
|
+
pytest
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### Cobertura de Tests
|
|
368
|
+
|
|
369
|
+
- `test_exceptions.py`: Valida las excepciones HTTP personalizadas.
|
|
370
|
+
- `test_request_decorators.py`: Prueba los decoradores de validación (body, query, headers) con mocks de Flask.
|
|
371
|
+
- `test_error_handler.py`: Verifica el registro y manejo de errores.
|
|
372
|
+
- `test_metrics_middleware.py`: Confirma el logging de métricas.
|
|
373
|
+
|
|
374
|
+
## Contribución
|
|
375
|
+
|
|
376
|
+
Para contribuir, por favor crea un issue o pull request en el repositorio.
|
|
377
|
+
|
|
378
|
+
## Licencia
|
|
379
|
+
|
|
380
|
+
Este proyecto está bajo la Licencia MIT. Ver archivo LICENSE para más detalles.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
global_handler/decorators/error_handler.py,sha256=l6SSiDgx5MM3PpGi5-_tEFA6cIRbR8QU16YLkEhIww4,1642
|
|
2
|
+
global_handler/decorators/request_decorators.py,sha256=KxkarF0wDzZ9oc0jvfHfqqUT6XQQ0_KTmVraDJXJ3n0,3650
|
|
3
|
+
global_handler/exceptions/http_exceptions.py,sha256=hjdgrGqUJb86NcgBXj8Lw9Qowr52Z6tXCLeMyGq0n0A,3133
|
|
4
|
+
global_handler/middlewares/metrics_middleware.py,sha256=PH5Mc-rmB8oL8Xjfe5DKW6ZO1nByICpfT-6OTBWdwik,868
|
|
5
|
+
global_handler-0.1.0.dist-info/LICENSE,sha256=-YS_OFJ3Tg-QOF_XKA56RibdopNB1UL0OYEv1k6JAsA,1071
|
|
6
|
+
global_handler-0.1.0.dist-info/METADATA,sha256=2lKTKClUZRwioxuEWz3ga4YyypVvhk22AwvGTSicNCs,11044
|
|
7
|
+
global_handler-0.1.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
8
|
+
global_handler-0.1.0.dist-info/RECORD,,
|