webkitpy 1.0.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.
- webkitpy-1.0.0/LICENSE +21 -0
- webkitpy-1.0.0/PKG-INFO +34 -0
- webkitpy-1.0.0/README.md +10 -0
- webkitpy-1.0.0/pyproject.toml +41 -0
- webkitpy-1.0.0/setup.cfg +4 -0
- webkitpy-1.0.0/src/webkitpy/__init__.py +32 -0
- webkitpy-1.0.0/src/webkitpy/decorators/__init__.py +9 -0
- webkitpy-1.0.0/src/webkitpy/decorators/handle_exceptions.py +234 -0
- webkitpy-1.0.0/src/webkitpy/decorators/measure_performance.py +56 -0
- webkitpy-1.0.0/src/webkitpy/decorators/proccess_formadata.py +11 -0
- webkitpy-1.0.0/src/webkitpy/exceptions/__init__.py +9 -0
- webkitpy-1.0.0/src/webkitpy/exceptions/api.py +80 -0
- webkitpy-1.0.0/src/webkitpy/utils/__init__.py +5 -0
- webkitpy-1.0.0/src/webkitpy/utils/error_formatter.py +93 -0
- webkitpy-1.0.0/src/webkitpy/utils/process_querydict.py +92 -0
- webkitpy-1.0.0/src/webkitpy/utils/responses.py +46 -0
- webkitpy-1.0.0/src/webkitpy.egg-info/PKG-INFO +34 -0
- webkitpy-1.0.0/src/webkitpy.egg-info/SOURCES.txt +19 -0
- webkitpy-1.0.0/src/webkitpy.egg-info/dependency_links.txt +1 -0
- webkitpy-1.0.0/src/webkitpy.egg-info/requires.txt +4 -0
- webkitpy-1.0.0/src/webkitpy.egg-info/top_level.txt +1 -0
webkitpy-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jorge
|
|
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.
|
webkitpy-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: webkitpy
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Toolkit de utilidades para desarrollo web en Python.
|
|
5
|
+
Author-email: Jorge <jorgeluisdcl30@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/jorge/webkitpy
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Provides-Extra: django
|
|
21
|
+
Requires-Dist: Django>=5.0; extra == "django"
|
|
22
|
+
Requires-Dist: djangorestframework>=3.14; extra == "django"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# webkitpy
|
|
26
|
+
|
|
27
|
+
Librería de utilidades para desarrollo web en Python.
|
|
28
|
+
|
|
29
|
+
## Estructura
|
|
30
|
+
|
|
31
|
+
- `pagination`: Utilidades para paginación.
|
|
32
|
+
- `decorators`: Decoradores para métricas, errores y tiempos.
|
|
33
|
+
- `request`: Normalización de peticiones.
|
|
34
|
+
- `metrics`: Contadores y métricas generales.
|
webkitpy-1.0.0/README.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# webkitpy
|
|
2
|
+
|
|
3
|
+
Librería de utilidades para desarrollo web en Python.
|
|
4
|
+
|
|
5
|
+
## Estructura
|
|
6
|
+
|
|
7
|
+
- `pagination`: Utilidades para paginación.
|
|
8
|
+
- `decorators`: Decoradores para métricas, errores y tiempos.
|
|
9
|
+
- `request`: Normalización de peticiones.
|
|
10
|
+
- `metrics`: Contadores y métricas generales.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "webkitpy"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Toolkit de utilidades para desarrollo web en Python."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Jorge", email = "jorgeluisdcl30@gmail.com" }
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
dependencies = []
|
|
17
|
+
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 3 - Alpha",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Operating System :: OS Independent",
|
|
27
|
+
"Topic :: Software Development :: Libraries",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
django = [
|
|
32
|
+
"Django>=5.0",
|
|
33
|
+
"djangorestframework>=3.14",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://github.com/jorge/webkitpy"
|
|
38
|
+
|
|
39
|
+
[tool.setuptools]
|
|
40
|
+
packages = ["webkitpy", "webkitpy.decorators", "webkitpy.exceptions", "webkitpy.utils"]
|
|
41
|
+
package-dir = {"" = "src"}
|
webkitpy-1.0.0/setup.cfg
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WebKitPy – Optimized Web Utilities Toolkit
|
|
3
|
+
|
|
4
|
+
Un toolkit de utilidades optimizadas para desarrollo web en Python,
|
|
5
|
+
especialmente orientado a Django y Django REST Framework.
|
|
6
|
+
|
|
7
|
+
Incluye:
|
|
8
|
+
- Decoradores para medir y limitar consultas por endpoint
|
|
9
|
+
- Decoradores para manejo de errores más expresivo y consistente
|
|
10
|
+
- Conversión automática de request.data (FormData → JSON)
|
|
11
|
+
- Helpers para evitar consultas innecesarias a la base de datos
|
|
12
|
+
- Utilidades para análisis y optimización de queries SQL
|
|
13
|
+
- Excepciones base reutilizables para APIs REST
|
|
14
|
+
|
|
15
|
+
Objetivos:
|
|
16
|
+
- Reducir carga en base de datos
|
|
17
|
+
- Mejorar observabilidad y control de endpoints
|
|
18
|
+
- Centralizar buenas prácticas
|
|
19
|
+
- Mantener código limpio, reutilizable y mantenible
|
|
20
|
+
"""
|
|
21
|
+
from . import decorators
|
|
22
|
+
from . import exceptions
|
|
23
|
+
from . import utils
|
|
24
|
+
|
|
25
|
+
__version__ = "1.0.0"
|
|
26
|
+
__author__ = "Jorge Luis de la Cruz"
|
|
27
|
+
__email__ = "jorgeluisdcl30@gmail.com"
|
|
28
|
+
__all__ = [
|
|
29
|
+
"decorators",
|
|
30
|
+
"exceptions",
|
|
31
|
+
"utils"
|
|
32
|
+
]
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Decoradores utilitarios para las vistas
|
|
3
|
+
"""
|
|
4
|
+
from webkitpy.exceptions.api import APIException
|
|
5
|
+
from functools import wraps
|
|
6
|
+
from rest_framework.exceptions import ValidationError as DRFValidationError
|
|
7
|
+
from django.db import IntegrityError
|
|
8
|
+
from django.core.exceptions import ObjectDoesNotExist
|
|
9
|
+
from django.http import Http404
|
|
10
|
+
from webkitpy.utils.responses import StandardResponse
|
|
11
|
+
from rest_framework import status
|
|
12
|
+
import traceback
|
|
13
|
+
import logging
|
|
14
|
+
from webkitpy.utils.error_formatter import format_validation_errors
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def handle_exceptions(arg=None, *, serializer_class=None):
|
|
20
|
+
"""
|
|
21
|
+
Decorador flexible que funciona de 3 formas:
|
|
22
|
+
|
|
23
|
+
1. Sin parámetros:
|
|
24
|
+
@handle_exceptions
|
|
25
|
+
def post(self, request):
|
|
26
|
+
...
|
|
27
|
+
|
|
28
|
+
2. Con paréntesis vacíos:
|
|
29
|
+
@handle_exceptions()
|
|
30
|
+
def post(self, request):
|
|
31
|
+
...
|
|
32
|
+
|
|
33
|
+
3. Con serializer específico:
|
|
34
|
+
@handle_exceptions(serializer_class=TireInventoryWriteSerializer)
|
|
35
|
+
def post(self, request):
|
|
36
|
+
...
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
# Caso 1: @handle_exceptions (sin paréntesis)
|
|
40
|
+
# arg es la función, serializer_class es None
|
|
41
|
+
if callable(arg):
|
|
42
|
+
view_func = arg
|
|
43
|
+
return _create_wrapper(view_func, None)
|
|
44
|
+
|
|
45
|
+
# Caso 2 y 3: @handle_exceptions() o @handle_exceptions(serializer_class=X)
|
|
46
|
+
# arg puede ser None o un serializer, pero NO es la función
|
|
47
|
+
else:
|
|
48
|
+
# Si pasaron arg (no recomendado), usarlo como serializer_class
|
|
49
|
+
if arg is not None and not callable(arg):
|
|
50
|
+
serializer_class = arg
|
|
51
|
+
|
|
52
|
+
def decorator(view_func):
|
|
53
|
+
return _create_wrapper(view_func, serializer_class)
|
|
54
|
+
|
|
55
|
+
return decorator
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _create_wrapper(view_func, serializer_class):
|
|
59
|
+
"""Crea el wrapper real con la lógica de manejo de excepciones"""
|
|
60
|
+
|
|
61
|
+
@wraps(view_func)
|
|
62
|
+
def _wrapped_view(*args, **kwargs):
|
|
63
|
+
try:
|
|
64
|
+
return view_func(*args, **kwargs)
|
|
65
|
+
|
|
66
|
+
except APIException as e:
|
|
67
|
+
# Imprimir traceback completo para debugging
|
|
68
|
+
print("\n" + "="*80)
|
|
69
|
+
print("🔴 APIException capturada:")
|
|
70
|
+
print(traceback.format_exc())
|
|
71
|
+
print("="*80 + "\n")
|
|
72
|
+
|
|
73
|
+
logger.warning(f"API Exception: {e.code} - {e.message}")
|
|
74
|
+
if hasattr(e, 'errors') and isinstance(e.errors, list):
|
|
75
|
+
return StandardResponse.error(
|
|
76
|
+
message=e.message,
|
|
77
|
+
code=e.code,
|
|
78
|
+
details=e.details,
|
|
79
|
+
status_code=e.status_code,
|
|
80
|
+
errors=e.errors
|
|
81
|
+
)
|
|
82
|
+
return StandardResponse.error(
|
|
83
|
+
message=e.message,
|
|
84
|
+
code=e.code,
|
|
85
|
+
details=e.details,
|
|
86
|
+
status_code=e.status_code
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
except DRFValidationError as e:
|
|
90
|
+
# Imprimir traceback completo para debugging
|
|
91
|
+
print("\n" + "="*80)
|
|
92
|
+
print("🟡 DRFValidationError capturada:")
|
|
93
|
+
print(traceback.format_exc())
|
|
94
|
+
print("="*80 + "\n")
|
|
95
|
+
|
|
96
|
+
logger.warning(f"Validation error: {e}")
|
|
97
|
+
errors = {}
|
|
98
|
+
if hasattr(e, 'detail'):
|
|
99
|
+
if isinstance(e.detail, dict):
|
|
100
|
+
errors = e.detail
|
|
101
|
+
elif isinstance(e.detail, list):
|
|
102
|
+
errors = {"non_field_errors": e.detail}
|
|
103
|
+
|
|
104
|
+
# Obtener modelo para traducir errores
|
|
105
|
+
model = None
|
|
106
|
+
|
|
107
|
+
# Prioridad 1: serializer_class pasado al decorador
|
|
108
|
+
if serializer_class and hasattr(serializer_class, 'Meta'):
|
|
109
|
+
model = getattr(serializer_class.Meta, 'model', None)
|
|
110
|
+
|
|
111
|
+
# Prioridad 2: Auto-detect desde la vista
|
|
112
|
+
if not model:
|
|
113
|
+
try:
|
|
114
|
+
view_instance = args[0] if args else None
|
|
115
|
+
if view_instance and hasattr(view_instance, '__class__'):
|
|
116
|
+
view_class = view_instance.__class__
|
|
117
|
+
view_serializer = getattr(view_class, 'serializer_class', None)
|
|
118
|
+
if view_serializer and hasattr(view_serializer, 'Meta'):
|
|
119
|
+
model = getattr(view_serializer.Meta, 'model', None)
|
|
120
|
+
except Exception as ex:
|
|
121
|
+
logger.warning(f"No se pudo obtener el modelo: {ex}")
|
|
122
|
+
|
|
123
|
+
# Formatear errores usando verbose_name del modelo
|
|
124
|
+
friendly_errors = format_validation_errors(errors, model)
|
|
125
|
+
|
|
126
|
+
return StandardResponse.error(
|
|
127
|
+
message="Por favor revisa los siguientes campos",
|
|
128
|
+
code="VALIDATION_ERROR",
|
|
129
|
+
details="Verifica los datos enviados",
|
|
130
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
131
|
+
errors=errors,
|
|
132
|
+
friendly_errors=friendly_errors
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
except IntegrityError as e:
|
|
136
|
+
# Imprimir traceback completo para debugging
|
|
137
|
+
print("\n" + "="*80)
|
|
138
|
+
print("🔵 IntegrityError capturada:")
|
|
139
|
+
print(traceback.format_exc())
|
|
140
|
+
print("="*80 + "\n")
|
|
141
|
+
|
|
142
|
+
logger.error(f"Database integrity error: {e}")
|
|
143
|
+
error_message = str(e).lower()
|
|
144
|
+
|
|
145
|
+
if 'unique' in error_message or 'duplicada' in error_message:
|
|
146
|
+
return StandardResponse.error(
|
|
147
|
+
message="Ya existe un registro con estos datos",
|
|
148
|
+
code="DUPLICATE_ENTRY",
|
|
149
|
+
details=str(e),
|
|
150
|
+
status_code=status.HTTP_409_CONFLICT
|
|
151
|
+
)
|
|
152
|
+
elif 'foreign key' in error_message:
|
|
153
|
+
return StandardResponse.error(
|
|
154
|
+
message="No se puede completar la operación",
|
|
155
|
+
code="FOREIGN_KEY_VIOLATION",
|
|
156
|
+
details="Existen registros relacionados",
|
|
157
|
+
status_code=status.HTTP_409_CONFLICT
|
|
158
|
+
)
|
|
159
|
+
elif 'not null' in error_message:
|
|
160
|
+
return StandardResponse.error(
|
|
161
|
+
message="Campos requeridos faltantes",
|
|
162
|
+
code="NULL_VIOLATION",
|
|
163
|
+
details="Uno o más campos obligatorios están vacíos",
|
|
164
|
+
status_code=status.HTTP_400_BAD_REQUEST
|
|
165
|
+
)
|
|
166
|
+
else:
|
|
167
|
+
return StandardResponse.error(
|
|
168
|
+
message="Error de integridad de datos",
|
|
169
|
+
code="INTEGRITY_ERROR",
|
|
170
|
+
details=str(e),
|
|
171
|
+
status_code=status.HTTP_400_BAD_REQUEST
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
except (ObjectDoesNotExist, Http404) as e:
|
|
175
|
+
# Imprimir traceback completo para debugging
|
|
176
|
+
print("\n" + "="*80)
|
|
177
|
+
print("🟠 ObjectDoesNotExist capturada:")
|
|
178
|
+
print(traceback.format_exc())
|
|
179
|
+
print("="*80 + "\n")
|
|
180
|
+
|
|
181
|
+
logger.warning(f"Object not found: {e}")
|
|
182
|
+
return StandardResponse.error(
|
|
183
|
+
message="Recurso no encontrado",
|
|
184
|
+
code="NOT_FOUND",
|
|
185
|
+
details=str(e),
|
|
186
|
+
status_code=status.HTTP_404_NOT_FOUND
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
except ValueError as e:
|
|
190
|
+
# Imprimir traceback completo para debugging
|
|
191
|
+
print("\n" + "="*80)
|
|
192
|
+
print("🟣 ValueError capturada:")
|
|
193
|
+
print(traceback.format_exc())
|
|
194
|
+
print("="*80 + "\n")
|
|
195
|
+
|
|
196
|
+
logger.warning(f"Value error: {e}")
|
|
197
|
+
return StandardResponse.error(
|
|
198
|
+
message="Valor inválido",
|
|
199
|
+
code="INVALID_VALUE",
|
|
200
|
+
details=str(e),
|
|
201
|
+
status_code=status.HTTP_400_BAD_REQUEST
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
except TypeError as e:
|
|
205
|
+
# Imprimir traceback completo para debugging
|
|
206
|
+
print("\n" + "="*80)
|
|
207
|
+
print("🟤 TypeError capturada:")
|
|
208
|
+
print(traceback.format_exc())
|
|
209
|
+
print("="*80 + "\n")
|
|
210
|
+
|
|
211
|
+
logger.warning(f"Type error: {e}")
|
|
212
|
+
return StandardResponse.error(
|
|
213
|
+
message="Tipo de dato incorrecto",
|
|
214
|
+
code="TYPE_ERROR",
|
|
215
|
+
details=str(e),
|
|
216
|
+
status_code=status.HTTP_400_BAD_REQUEST
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
except Exception as e:
|
|
220
|
+
# Imprimir traceback completo para debugging
|
|
221
|
+
print("\n" + "="*80)
|
|
222
|
+
print("⚫ Exception NO MANEJADA capturada:")
|
|
223
|
+
print(traceback.format_exc())
|
|
224
|
+
print("="*80 + "\n")
|
|
225
|
+
|
|
226
|
+
logger.critical(f"Unhandled exception: {traceback.format_exc()}")
|
|
227
|
+
return StandardResponse.error(
|
|
228
|
+
message="Error interno del servidor",
|
|
229
|
+
code="INTERNAL_SERVER_ERROR",
|
|
230
|
+
details=str(e),
|
|
231
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
return _wrapped_view
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import functools
|
|
3
|
+
from django.db import connection, reset_queries
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def measure_performance(func):
|
|
7
|
+
"""
|
|
8
|
+
Decorador para medir performance de views.
|
|
9
|
+
Mide: tiempo total, queries, serialización.
|
|
10
|
+
Muestra el listado completo de queries ejecutadas.
|
|
11
|
+
"""
|
|
12
|
+
@functools.wraps(func)
|
|
13
|
+
def wrapper(*args, **kwargs):
|
|
14
|
+
overall_start = time.time()
|
|
15
|
+
reset_queries()
|
|
16
|
+
|
|
17
|
+
response = func(*args, **kwargs)
|
|
18
|
+
|
|
19
|
+
overall_end = time.time()
|
|
20
|
+
total_time = (overall_end - overall_start) * 1000
|
|
21
|
+
queries_time = sum(float(q['time']) for q in connection.queries) * 1000
|
|
22
|
+
queries_count = len(connection.queries)
|
|
23
|
+
processing_time = total_time - queries_time
|
|
24
|
+
|
|
25
|
+
# ===== OUTPUT PRINCIPAL =====
|
|
26
|
+
print(f"\n{'='*60}")
|
|
27
|
+
print(f"PERFORMANCE: {func.__name__}")
|
|
28
|
+
print(f"{'='*60}")
|
|
29
|
+
print(f"Total: {total_time:>8.2f}ms")
|
|
30
|
+
print(f"Queries DB: {queries_time:>8.2f}ms ({queries_count} queries)")
|
|
31
|
+
print(f"Procesamiento: {processing_time:>8.2f}ms")
|
|
32
|
+
print(f"DB/Total ratio: {(queries_time/total_time*100):>8.1f}%")
|
|
33
|
+
print(f"{'='*60}")
|
|
34
|
+
|
|
35
|
+
# ===== LISTADO DE QUERIES =====
|
|
36
|
+
if queries_count > 0:
|
|
37
|
+
print(f"\nLISTADO DE QUERIES ({queries_count} total):")
|
|
38
|
+
print(f"{'-'*60}")
|
|
39
|
+
|
|
40
|
+
for i, query in enumerate(connection.queries, 1):
|
|
41
|
+
query_time = float(query['time']) * 1000
|
|
42
|
+
sql = query['sql'].replace('\n', ' ').strip()
|
|
43
|
+
|
|
44
|
+
# Indicador de query lenta
|
|
45
|
+
speed_icon = "SLOW" if query_time > 10 else "FAST"
|
|
46
|
+
|
|
47
|
+
print(f"\n{i}. [{speed_icon}] {query_time:>6.2f}ms")
|
|
48
|
+
print(f" {sql}")
|
|
49
|
+
|
|
50
|
+
print(f"\n{'-'*60}")
|
|
51
|
+
|
|
52
|
+
print()
|
|
53
|
+
|
|
54
|
+
return response
|
|
55
|
+
|
|
56
|
+
return wrapper
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from functools import wraps
|
|
2
|
+
from rest_framework.request import Request
|
|
3
|
+
from utils.process_querydict import process_querydict
|
|
4
|
+
|
|
5
|
+
def process_formdata(func):
|
|
6
|
+
@wraps(func)
|
|
7
|
+
def wrapper(self, request: Request, *args, **kwargs):
|
|
8
|
+
if hasattr(request, 'data'):
|
|
9
|
+
request._full_data = process_querydict(request.data)
|
|
10
|
+
return func(self, request, *args, **kwargs)
|
|
11
|
+
return wrapper
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Excepciones base para toda la aplicación.
|
|
3
|
+
Estas son las excepciones genéricas que pueden usar todas las apps.
|
|
4
|
+
"""
|
|
5
|
+
from rest_framework import status
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class APIException(Exception):
|
|
10
|
+
"""Excepción base para todas las excepciones de la API"""
|
|
11
|
+
|
|
12
|
+
default_code = "API_ERROR"
|
|
13
|
+
default_message = "Ha ocurrido un error"
|
|
14
|
+
default_status = status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
15
|
+
|
|
16
|
+
def __init__(self, message: Optional[str] = None, code: Optional[str] = None, details: Optional[str] = None, status_code: Optional[int] = None):
|
|
17
|
+
self.message = message or self.default_message
|
|
18
|
+
self.code = code or self.default_code
|
|
19
|
+
self.details = details
|
|
20
|
+
self.status_code = status_code or self.default_status
|
|
21
|
+
super().__init__(self.message)
|
|
22
|
+
|
|
23
|
+
def to_dict(self) -> dict:
|
|
24
|
+
return {
|
|
25
|
+
"code": self.code,
|
|
26
|
+
"message": self.message,
|
|
27
|
+
"details": self.details,
|
|
28
|
+
"status_code": self.status_code
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Excepciones HTTP comunes
|
|
33
|
+
class ValidationException(APIException):
|
|
34
|
+
"""Errores de validación (400)"""
|
|
35
|
+
default_code = "VALIDATION_ERROR"
|
|
36
|
+
default_message = "Error de validación"
|
|
37
|
+
default_status = status.HTTP_400_BAD_REQUEST
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class NotFoundException(APIException):
|
|
41
|
+
"""Recurso no encontrado (404)"""
|
|
42
|
+
default_code = "NOT_FOUND"
|
|
43
|
+
default_message = "Recurso no encontrado"
|
|
44
|
+
default_status = status.HTTP_404_NOT_FOUND
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class PermissionException(APIException):
|
|
48
|
+
"""Sin permisos (403)"""
|
|
49
|
+
default_code = "PERMISSION_DENIED"
|
|
50
|
+
default_message = "No tienes permisos para esta acción"
|
|
51
|
+
default_status = status.HTTP_403_FORBIDDEN
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ConflictException(APIException):
|
|
55
|
+
"""Conflicto de estado (409)"""
|
|
56
|
+
default_code = "CONFLICT"
|
|
57
|
+
default_message = "Conflicto con el estado actual"
|
|
58
|
+
default_status = status.HTTP_409_CONFLICT
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class UnauthorizedException(APIException):
|
|
62
|
+
"""No autenticado (401)"""
|
|
63
|
+
default_code = "UNAUTHORIZED"
|
|
64
|
+
default_message = "No estás autenticado"
|
|
65
|
+
default_status = status.HTTP_401_UNAUTHORIZED
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class MultipleValidationException(APIException):
|
|
69
|
+
"""Excepción para múltiples errores de validación"""
|
|
70
|
+
default_code = "MULTIPLE_VALIDATION_ERRORS"
|
|
71
|
+
default_message = "Se encontraron múltiples errores de validación"
|
|
72
|
+
default_status = status.HTTP_400_BAD_REQUEST
|
|
73
|
+
|
|
74
|
+
def __init__(self, errors: list, message: Optional[str] = None, code: Optional[str] = None):
|
|
75
|
+
self.errors = errors
|
|
76
|
+
self.message = message or self.default_message
|
|
77
|
+
self.code = code or self.default_code
|
|
78
|
+
self.details = f"Se encontraron {len(errors)} error(es) de validación"
|
|
79
|
+
self.status_code = self.default_status
|
|
80
|
+
super().__init__(self.message)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# utils/error_formatter.py
|
|
2
|
+
|
|
3
|
+
def get_field_verbose_name_from_model(model, field_path):
|
|
4
|
+
"""
|
|
5
|
+
Lee el verbose_name directamente del modelo.
|
|
6
|
+
Maneja campos con _id y campos anidados.
|
|
7
|
+
"""
|
|
8
|
+
try:
|
|
9
|
+
# Caso 1: Campo simple sin anidación
|
|
10
|
+
if '.' not in field_path:
|
|
11
|
+
# Si termina en _id, quitarlo (wheel_model_id -> wheel_model)
|
|
12
|
+
if field_path.endswith('_id'):
|
|
13
|
+
field_name = field_path[:-3]
|
|
14
|
+
else:
|
|
15
|
+
field_name = field_path
|
|
16
|
+
|
|
17
|
+
# Buscar el campo en el modelo
|
|
18
|
+
field = model._meta.get_field(field_name)
|
|
19
|
+
|
|
20
|
+
# Retornar verbose_name si existe
|
|
21
|
+
if hasattr(field, 'verbose_name'):
|
|
22
|
+
return field.verbose_name.capitalize()
|
|
23
|
+
|
|
24
|
+
return field_name.replace('_', ' ').title()
|
|
25
|
+
|
|
26
|
+
# Caso 2: Campo anidado (store_parts.store_id)
|
|
27
|
+
parts = field_path.split('.')
|
|
28
|
+
current_model = model
|
|
29
|
+
|
|
30
|
+
for i, part in enumerate(parts):
|
|
31
|
+
# Quitar _id si es el último campo
|
|
32
|
+
if i == len(parts) - 1 and part.endswith('_id'):
|
|
33
|
+
part = part[:-3]
|
|
34
|
+
|
|
35
|
+
# Obtener el campo
|
|
36
|
+
field = current_model._meta.get_field(part)
|
|
37
|
+
|
|
38
|
+
# Si es el último, retornar verbose_name
|
|
39
|
+
if i == len(parts) - 1:
|
|
40
|
+
if hasattr(field, 'verbose_name'):
|
|
41
|
+
return field.verbose_name.capitalize()
|
|
42
|
+
return part.replace('_', ' ').title()
|
|
43
|
+
|
|
44
|
+
# Si no es el último y es una relación, seguir navegando
|
|
45
|
+
if hasattr(field, 'related_model'):
|
|
46
|
+
current_model = field.related_model
|
|
47
|
+
else:
|
|
48
|
+
# No es una relación, no podemos seguir
|
|
49
|
+
return field_path.replace('_', ' ').title()
|
|
50
|
+
|
|
51
|
+
except Exception as e:
|
|
52
|
+
# Si algo falla, usar fallback
|
|
53
|
+
return field_path.replace('_', ' ').title()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def format_validation_errors(errors, model=None):
|
|
57
|
+
"""
|
|
58
|
+
Formatea errores de validación usando verbose_name del modelo.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
errors: Dict con errores de DRF
|
|
62
|
+
model: Modelo de Django para leer verbose_name
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
List de strings amigables
|
|
66
|
+
"""
|
|
67
|
+
friendly_errors = []
|
|
68
|
+
|
|
69
|
+
def flatten(error_dict, parent=''):
|
|
70
|
+
for key, value in error_dict.items():
|
|
71
|
+
current_path = f"{parent}.{key}" if parent else key
|
|
72
|
+
|
|
73
|
+
if isinstance(value, dict):
|
|
74
|
+
# Recursión para objetos anidados
|
|
75
|
+
flatten(value, current_path)
|
|
76
|
+
|
|
77
|
+
elif isinstance(value, list):
|
|
78
|
+
# Obtener label del campo
|
|
79
|
+
if model:
|
|
80
|
+
field_label = get_field_verbose_name_from_model(model, current_path)
|
|
81
|
+
else:
|
|
82
|
+
field_label = current_path.replace('_', ' ').title()
|
|
83
|
+
|
|
84
|
+
# Agregar cada error
|
|
85
|
+
for error_message in value:
|
|
86
|
+
if isinstance(error_message, str):
|
|
87
|
+
if key == 'non_field_errors':
|
|
88
|
+
friendly_errors.append(error_message)
|
|
89
|
+
else:
|
|
90
|
+
friendly_errors.append(f"{field_label}: {error_message}")
|
|
91
|
+
|
|
92
|
+
flatten(errors)
|
|
93
|
+
return friendly_errors
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
import math
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
|
|
6
|
+
def process_querydict(querydict):
|
|
7
|
+
"""
|
|
8
|
+
Procesa completamente un QueryDict multipart/form-data y devuelve
|
|
9
|
+
un dict listo para pasarse al Service.create(**kwargs)
|
|
10
|
+
|
|
11
|
+
Soporta:
|
|
12
|
+
- Campos simples: name=John
|
|
13
|
+
- Arrays: drivers[]=1&drivers[]=2
|
|
14
|
+
- Objetos anidados: address[city]=MX
|
|
15
|
+
- Arrays de objetos: tanks[0][name]=T1&tanks[0][cap]=100
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
result = {}
|
|
19
|
+
nested_objects = defaultdict(dict)
|
|
20
|
+
nested_arrays = defaultdict(lambda: defaultdict(dict))
|
|
21
|
+
simple_arrays = defaultdict(list)
|
|
22
|
+
|
|
23
|
+
array_of_objects_pattern = re.compile(r'^(.+)\[(\d+)\]\[(.+)\]$')
|
|
24
|
+
nested_object_pattern = re.compile(r'^(.+)\[([^\d\]]+)\]$')
|
|
25
|
+
simple_array_pattern = re.compile(r'^(.+)\[\]$')
|
|
26
|
+
|
|
27
|
+
for key in querydict.keys():
|
|
28
|
+
values = querydict.getlist(key)
|
|
29
|
+
|
|
30
|
+
match = array_of_objects_pattern.match(key)
|
|
31
|
+
if match:
|
|
32
|
+
parent_key, index, field = match.groups()
|
|
33
|
+
nested_arrays[parent_key][int(index)][field] = _normalize_value(values[0])
|
|
34
|
+
continue
|
|
35
|
+
|
|
36
|
+
match = nested_object_pattern.match(key)
|
|
37
|
+
if match:
|
|
38
|
+
parent_key, field = match.groups()
|
|
39
|
+
nested_objects[parent_key][field] = _normalize_value(values[0])
|
|
40
|
+
continue
|
|
41
|
+
|
|
42
|
+
match = simple_array_pattern.match(key)
|
|
43
|
+
if match:
|
|
44
|
+
clean_key = match.group(1)
|
|
45
|
+
simple_arrays[clean_key].extend(_normalize_value(v) for v in values)
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
result[key] = _normalize_value(values[0])
|
|
49
|
+
|
|
50
|
+
for key, index_map in nested_arrays.items():
|
|
51
|
+
result[key] = [index_map[i] for i in sorted(index_map)]
|
|
52
|
+
|
|
53
|
+
for key, obj in nested_objects.items():
|
|
54
|
+
result[key] = obj
|
|
55
|
+
|
|
56
|
+
for key, values in simple_arrays.items():
|
|
57
|
+
result[key] = values
|
|
58
|
+
|
|
59
|
+
return result
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _normalize_value(value):
|
|
63
|
+
if value == 'null':
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
if isinstance(value, str):
|
|
67
|
+
lower = value.lower()
|
|
68
|
+
|
|
69
|
+
if lower == 'true':
|
|
70
|
+
return True
|
|
71
|
+
if lower == 'false':
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
if '.' in value:
|
|
76
|
+
f = float(value)
|
|
77
|
+
if math.isfinite(f):
|
|
78
|
+
return f
|
|
79
|
+
else:
|
|
80
|
+
return int(value)
|
|
81
|
+
except ValueError:
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
parsed = json.loads(value)
|
|
86
|
+
if isinstance(parsed, float) and not math.isfinite(parsed):
|
|
87
|
+
return value
|
|
88
|
+
return parsed
|
|
89
|
+
except (json.JSONDecodeError, TypeError):
|
|
90
|
+
return value
|
|
91
|
+
|
|
92
|
+
return value
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from rest_framework.response import Response
|
|
2
|
+
from rest_framework import status
|
|
3
|
+
from django.utils.timezone import now
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class StandardResponse:
|
|
8
|
+
"""Clase para estandarizar todas las respuestas de la API"""
|
|
9
|
+
|
|
10
|
+
@staticmethod
|
|
11
|
+
def success(data: Any = None, message: str = "Operación exitosa", status_code: int = status.HTTP_200_OK, meta: Optional[dict] = None) -> Response:
|
|
12
|
+
"""Respuesta exitosa estandarizada"""
|
|
13
|
+
response_data = {
|
|
14
|
+
"success": True,
|
|
15
|
+
"data": data,
|
|
16
|
+
"message": message,
|
|
17
|
+
"meta": {
|
|
18
|
+
"timestamp": now().isoformat(),
|
|
19
|
+
**(meta or {})
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return Response(response_data, status=status_code)
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def error(message: str, code: str = "ERROR", details: Optional[str] = None, status_code: int = status.HTTP_400_BAD_REQUEST, errors: Optional[dict] = None, friendly_errors: list = None) -> Response:
|
|
26
|
+
"""Respuesta de error estandarizada"""
|
|
27
|
+
error_data = {
|
|
28
|
+
"success": False,
|
|
29
|
+
"error": {
|
|
30
|
+
"code": code,
|
|
31
|
+
"message": message,
|
|
32
|
+
"details": details,
|
|
33
|
+
"timestamp": now().isoformat()
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if errors:
|
|
38
|
+
if isinstance(errors, list):
|
|
39
|
+
error_data["error"]["items"] = errors
|
|
40
|
+
elif isinstance(errors, dict):
|
|
41
|
+
error_data["error"]["fields"] = errors
|
|
42
|
+
|
|
43
|
+
if friendly_errors:
|
|
44
|
+
error_data["error"]["friendly_errors"] = friendly_errors
|
|
45
|
+
|
|
46
|
+
return Response(error_data, status=status_code)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: webkitpy
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Toolkit de utilidades para desarrollo web en Python.
|
|
5
|
+
Author-email: Jorge <jorgeluisdcl30@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/jorge/webkitpy
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Provides-Extra: django
|
|
21
|
+
Requires-Dist: Django>=5.0; extra == "django"
|
|
22
|
+
Requires-Dist: djangorestframework>=3.14; extra == "django"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# webkitpy
|
|
26
|
+
|
|
27
|
+
Librería de utilidades para desarrollo web en Python.
|
|
28
|
+
|
|
29
|
+
## Estructura
|
|
30
|
+
|
|
31
|
+
- `pagination`: Utilidades para paginación.
|
|
32
|
+
- `decorators`: Decoradores para métricas, errores y tiempos.
|
|
33
|
+
- `request`: Normalización de peticiones.
|
|
34
|
+
- `metrics`: Contadores y métricas generales.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/webkitpy/__init__.py
|
|
5
|
+
src/webkitpy.egg-info/PKG-INFO
|
|
6
|
+
src/webkitpy.egg-info/SOURCES.txt
|
|
7
|
+
src/webkitpy.egg-info/dependency_links.txt
|
|
8
|
+
src/webkitpy.egg-info/requires.txt
|
|
9
|
+
src/webkitpy.egg-info/top_level.txt
|
|
10
|
+
src/webkitpy/decorators/__init__.py
|
|
11
|
+
src/webkitpy/decorators/handle_exceptions.py
|
|
12
|
+
src/webkitpy/decorators/measure_performance.py
|
|
13
|
+
src/webkitpy/decorators/proccess_formadata.py
|
|
14
|
+
src/webkitpy/exceptions/__init__.py
|
|
15
|
+
src/webkitpy/exceptions/api.py
|
|
16
|
+
src/webkitpy/utils/__init__.py
|
|
17
|
+
src/webkitpy/utils/error_formatter.py
|
|
18
|
+
src/webkitpy/utils/process_querydict.py
|
|
19
|
+
src/webkitpy/utils/responses.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
webkitpy
|