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 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.
@@ -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,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"}
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,9 @@
1
+ from .handle_exceptions import handle_exceptions
2
+ from .measure_performance import measure_performance
3
+ from .proccess_formadata import proccess_formadata
4
+
5
+ __all__ = [
6
+ "handle_exceptions",
7
+ "measure_performance",
8
+ "proccess_formadata",
9
+ ]
@@ -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,9 @@
1
+ from .handle_exceptions import handle_exceptions
2
+ from .measure_performance import measure_performance
3
+ from .proccess_formadata import proccess_formadata
4
+
5
+ __all__ = [
6
+ "handle_exceptions",
7
+ "measure_performance",
8
+ "proccess_formadata",
9
+ ]
@@ -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,5 @@
1
+ from .api import APIException
2
+
3
+ __all__ = [
4
+ "APIException",
5
+ ]
@@ -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,4 @@
1
+
2
+ [django]
3
+ Django>=5.0
4
+ djangorestframework>=3.14
@@ -0,0 +1 @@
1
+ webkitpy