global-handler 0.1.0__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.
@@ -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,366 @@
1
+ # Global Handler
2
+
3
+ ## Descripción
4
+
5
+ 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.
6
+
7
+ ### Características Principales
8
+
9
+ - **Validación Automática de Requests**: Usa Pydantic para validar JSON, parámetros de query y headers con decoradores simples.
10
+ - **Sistema de Excepciones HTTP Estructurado**: Excepciones personalizadas con códigos de error consistentes y mensajes descriptivos.
11
+ - **Middlewares de Métricas**: Logging automático de requests con tiempos de ejecución y detalles de contexto.
12
+ - **Manejo Global de Errores**: Respuestas de error estandarizadas en formato JSON.
13
+ - **Integración Fluida con Flask**: Diseñado específicamente para aplicaciones Flask, sin configuración compleja.
14
+
15
+ ## Instalación
16
+
17
+ ### Requisitos
18
+
19
+ - Python 3.13+
20
+ - Flask
21
+ - Pydantic
22
+
23
+ ### Instalación desde PyPI
24
+
25
+ ```bash
26
+ pip install global-handler
27
+ ```
28
+
29
+ ### Instalación desde código fuente
30
+
31
+ ```bash
32
+ git clone https://github.com/tu-usuario/global-handler.git
33
+ cd global-handler
34
+ poetry install
35
+ ```
36
+
37
+ ## Implementación en una Aplicación Flask
38
+
39
+ ### Configuración Básica
40
+
41
+ 1. **Importa los componentes necesarios:**
42
+
43
+ ```python
44
+ from flask import Flask
45
+ from global_handler.decorators.request_decorators import validate_with, validate_query_with, validate_headers_with
46
+ from global_handler.decorators.error_handler import register_error_handlers
47
+ from global_handler.middlewares.metrics_middleware import before_request_metrics, after_request_metrics
48
+ from global_handler.exceptions.http_exceptions import ValidationHttpError
49
+ from pydantic import BaseModel
50
+ ```
51
+
52
+ 2. **Crea tu aplicación Flask:**
53
+
54
+ ```python
55
+ app = Flask(__name__)
56
+
57
+ # Registra los manejadores de errores
58
+ register_error_handlers(app)
59
+
60
+ # Registra los middlewares de métricas
61
+ app.before_request(before_request_metrics)
62
+ app.after_request(after_request_metrics)
63
+ ```
64
+
65
+ ### Uso de Decoradores de Validación
66
+
67
+ #### Validación del Cuerpo JSON (`validate_with`)
68
+
69
+ Valida automáticamente el cuerpo JSON de las requests POST/PUT contra un modelo Pydantic.
70
+
71
+ ```python
72
+ class UserCreateModel(BaseModel):
73
+ name: str
74
+ email: str
75
+ age: int
76
+
77
+ @app.route('/users', methods=['POST'])
78
+ @validate_with(UserCreateModel)
79
+ def create_user(validated_data):
80
+ # validated_data es una instancia de UserCreateModel con datos validados
81
+ user = User(name=validated_data.name, email=validated_data.email, age=validated_data.age)
82
+ # ... lógica para guardar usuario
83
+ return {"message": "Usuario creado", "user_id": user.id}, 201
84
+ ```
85
+
86
+ **Comportamiento:**
87
+ - Si el JSON es inválido: retorna 400 con código "INVALID_JSON"
88
+ - Si falla la validación: retorna 400 con código "VALIDATION_ERROR" y detalles de los errores
89
+
90
+ #### Validación de Parámetros Query (`validate_query_with`)
91
+
92
+ Valida los parámetros de la URL query string.
93
+
94
+ ```python
95
+ class UserQueryModel(BaseModel):
96
+ page: int = 1
97
+ limit: int = 10
98
+ search: str = None
99
+
100
+ @app.route('/users', methods=['GET'])
101
+ @validate_query_with(UserQueryModel)
102
+ def list_users(query_params):
103
+ users = User.query.filter_by(search=query_params.search).paginate(
104
+ page=query_params.page, per_page=query_params.limit
105
+ )
106
+ return {"users": [user.to_dict() for user in users.items]}, 200
107
+ ```
108
+
109
+ **Comportamiento:**
110
+ - Parámetros faltantes: usa valores por defecto del modelo
111
+ - Tipos inválidos: retorna 400 con código "INVALID_PARAMETER"
112
+ - Parámetros requeridos faltantes: retorna 400 con código "MISSING_PARAMETER"
113
+
114
+ #### Validación de Headers (`validate_headers_with`)
115
+
116
+ Valida los headers HTTP de la request.
117
+
118
+ ```python
119
+ from pydantic import Field
120
+
121
+ class AuthHeadersModel(BaseModel):
122
+ authorization: str = Field(alias="Authorization")
123
+ content_type: str = Field(alias="Content-Type", default="application/json")
124
+
125
+ @app.route('/protected', methods=['GET'])
126
+ @validate_headers_with(AuthHeadersModel)
127
+ def protected_route(headers_data):
128
+ token = headers_data.authorization.replace("Bearer ", "")
129
+ # ... lógica de autenticación
130
+ return {"message": "Acceso autorizado"}, 200
131
+ ```
132
+
133
+ **Nota:** Usa `Field(alias="Header-Name")` para mapear nombres de headers con guiones a nombres de campos válidos en Python.
134
+
135
+ ### Middlewares de Métricas
136
+
137
+ Los middlewares registran automáticamente información sobre cada request:
138
+
139
+ - **before_request_metrics**: Registra el inicio de la request con método, path, parámetros query y ID de request.
140
+ - **after_request_metrics**: Registra el final con tiempo de ejecución, código de estado y detalles adicionales.
141
+
142
+ Los logs se envían al sistema logger_tracker configurado.
143
+
144
+ ### Manejo de Errores
145
+
146
+ La librería incluye manejadores de errores globales que convierten excepciones en respuestas JSON estandarizadas:
147
+
148
+ ```json
149
+ {
150
+ "error": {
151
+ "message": "Invalid request body",
152
+ "code": "VALIDATION_ERROR",
153
+ "details": [
154
+ {
155
+ "loc": ["age"],
156
+ "msg": "Input should be a valid integer",
157
+ "type": "int_parsing"
158
+ }
159
+ ]
160
+ }
161
+ }
162
+ ```
163
+
164
+ ### Excepciones Disponibles
165
+
166
+ - `ValidationHttpError`: Errores de validación de datos (400)
167
+ - `InvalidJsonHttpError`: JSON malformado (400)
168
+ - `MissingParameterHttpError`: Parámetros requeridos faltantes (400)
169
+ - `InvalidParameterHttpError`: Parámetros con tipos inválidos (400)
170
+ - `UnauthorizedHttpError`: Autenticación requerida (401)
171
+ - `InvalidCredentialsHttpError`: Credenciales inválidas (401)
172
+ - `TokenExpiredHttpError`: Token expirado (401)
173
+ - `ForbiddenHttpError`: Acceso prohibido (403)
174
+ - `NotFoundHttpError`: Recurso no encontrado (404)
175
+ - Y más...
176
+
177
+ ### Ejemplo Completo de Aplicación
178
+
179
+ ```python
180
+ from flask import Flask
181
+ from global_handler.decorators.request_decorators import validate_with, validate_query_with
182
+ from global_handler.decorators.error_handler import register_error_handlers
183
+ from global_handler.middlewares.metrics_middleware import before_request_metrics, after_request_metrics
184
+ from pydantic import BaseModel
185
+
186
+ app = Flask(__name__)
187
+
188
+ # Configuración
189
+ register_error_handlers(app)
190
+ app.before_request(before_request_metrics)
191
+ app.after_request(after_request_metrics)
192
+
193
+ # Modelos
194
+ class UserCreateModel(BaseModel):
195
+ name: str
196
+ email: str
197
+ age: int
198
+
199
+ class UserQueryModel(BaseModel):
200
+ page: int = 1
201
+ limit: int = 10
202
+
203
+ # Rutas
204
+ @app.route('/users', methods=['POST'])
205
+ @validate_with(UserCreateModel)
206
+ def create_user(validated_data):
207
+ # Lógica de negocio
208
+ return {"message": "Usuario creado", "data": validated_data.model_dump()}, 201
209
+
210
+ @app.route('/users', methods=['GET'])
211
+ @validate_query_with(UserQueryModel)
212
+ def list_users(query_params):
213
+ # Lógica de negocio
214
+ return {"users": [], "pagination": query_params.model_dump()}, 200
215
+
216
+ if __name__ == '__main__':
217
+ app.run(debug=True)
218
+ ```
219
+
220
+ ## Detalles Técnicos
221
+
222
+ - **Lenguaje**: Python 3.13+
223
+ - **Framework**: Flask
224
+ - **Validación**: Pydantic v2
225
+ - **Logging**: Integración con logger_tracker
226
+ - **Build System**: Poetry
227
+ - **Licencia**: MIT
228
+
229
+ ### Dependencias
230
+
231
+ - Flask>=2.0.0
232
+ - Pydantic>=2.0.0
233
+ - logger_tracker==0.1.0
234
+
235
+ ### Estructura del Proyecto
236
+
237
+ ```
238
+ src/global_handler/
239
+ ├── decorators/
240
+ │ ├── error_handler.py # Manejadores de errores globales
241
+ │ └── request_decorators.py # Decoradores de validación
242
+ ├── exceptions/
243
+ │ └── http_exceptions.py # Excepciones HTTP
244
+ └── middlewares/
245
+ └── metrics_middleware.py # Middlewares de métricas
246
+ ```
247
+
248
+ ## Contribución
249
+
250
+ 1. Fork el proyecto
251
+ 2. Crea una rama para tu feature (`git checkout -b feature/AmazingFeature`)
252
+ 3. Commit tus cambios (`git commit -m 'Add some AmazingFeature'`)
253
+ 4. Push a la rama (`git push origin feature/AmazingFeature`)
254
+ 5. Abre un Pull Request
255
+
256
+ ## Licencia
257
+
258
+ Este proyecto está bajo la Licencia MIT. Ver el archivo `LICENSE` para más detalles.
259
+
260
+ ## Soporte
261
+
262
+ Para preguntas o soporte, por favor abre un issue en GitHub o contacta al maintainer.
263
+ pyproject.toml # Configuración del proyecto con Poetry
264
+ ```
265
+
266
+ ### Instalación
267
+
268
+ 1. Clona el repositorio:
269
+ ```bash
270
+ git clone <url-del-repositorio>
271
+ cd global-handler
272
+ ```
273
+
274
+ 2. Instala las dependencias con Poetry:
275
+ ```bash
276
+ poetry install
277
+ ```
278
+
279
+ ### Uso
280
+
281
+ #### Configuración en Flask
282
+
283
+ ```python
284
+ from flask import Flask
285
+ from global_handler.decorators.error_handler import register_error_handlers
286
+ from global_handler.middlewares.metrics_middleware import before_request_metrics, after_request_metrics
287
+
288
+ app = Flask(__name__)
289
+
290
+ # Registrar middlewares
291
+ app.before_request(before_request_metrics)
292
+ app.after_request(after_request_metrics)
293
+
294
+ # Registrar manejadores de errores
295
+ register_error_handlers(app)
296
+ ```
297
+
298
+ #### Uso de Decoradores
299
+
300
+ ```python
301
+ from pydantic import BaseModel
302
+ from global_handler.decorators.request_decorators import validate_with
303
+
304
+ class UserModel(BaseModel):
305
+ name: str
306
+ age: int
307
+
308
+ @app.route('/user', methods=['POST'])
309
+ @validate_with(UserModel)
310
+ def create_user(validated_data):
311
+ # validated_data es una instancia de UserModel
312
+ return {"message": "User created"}
313
+ ```
314
+
315
+ ```python
316
+ from global_handler.decorators.request_decorators import validate_headers_with
317
+
318
+ class HeaderModel(BaseModel):
319
+ Authorization: str
320
+ Content_Type: str
321
+
322
+ @app.route('/protected', methods=['GET'])
323
+ @validate_headers_with(HeaderModel)
324
+ def protected_route(headers_data):
325
+ # headers_data es una instancia de HeaderModel
326
+ return {"message": "Access granted"}
327
+ ```
328
+
329
+ #### Excepciones
330
+
331
+ ```python
332
+ from global_handler.exceptions.http_exceptions import ValidationHttpError
333
+
334
+ raise ValidationHttpError("Invalid data")
335
+ ```
336
+
337
+ ## Tests
338
+
339
+ El proyecto incluye un conjunto de tests unitarios para validar el funcionamiento de los componentes.
340
+
341
+ ### Ejecutar Tests
342
+
343
+ Instala las dependencias de desarrollo:
344
+ ```bash
345
+ poetry install --with dev
346
+ ```
347
+
348
+ Ejecuta los tests con pytest:
349
+ ```bash
350
+ pytest
351
+ ```
352
+
353
+ ### Cobertura de Tests
354
+
355
+ - `test_exceptions.py`: Valida las excepciones HTTP personalizadas.
356
+ - `test_request_decorators.py`: Prueba los decoradores de validación (body, query, headers) con mocks de Flask.
357
+ - `test_error_handler.py`: Verifica el registro y manejo de errores.
358
+ - `test_metrics_middleware.py`: Confirma el logging de métricas.
359
+
360
+ ## Contribución
361
+
362
+ Para contribuir, por favor crea un issue o pull request en el repositorio.
363
+
364
+ ## Licencia
365
+
366
+ Este proyecto está bajo la Licencia MIT. Ver archivo LICENSE para más detalles.
@@ -0,0 +1,27 @@
1
+ [project]
2
+ name = "global-handler"
3
+ version = "0.1.0"
4
+ description = "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
+ authors = [
6
+ {name = "Damian Gonzalez", email = "damian27goa@gmail.com"}
7
+ ]
8
+ readme = "README.md"
9
+ packages = [
10
+ { include = "global_handler", from = "src" }
11
+ ]
12
+
13
+ requires-python = ">=3.13"
14
+ dependencies = [
15
+ "flask",
16
+ "pydantic",
17
+ "logger-tracker (==1.0.9)"
18
+ ]
19
+
20
+
21
+ [tool.poetry.group.dev.dependencies]
22
+ pytest = "^7.0.0"
23
+
24
+
25
+ [build-system]
26
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
27
+ build-backend = "poetry.core.masonry.api"
@@ -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