django-cfg 1.5.29__py3-none-any.whl → 1.5.31__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of django-cfg might be problematic. Click here for more details.

Files changed (24) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/integrations/centrifugo/services/logging.py +49 -20
  3. django_cfg/apps/integrations/centrifugo/views/testing_api.py +16 -6
  4. django_cfg/apps/integrations/centrifugo/views/wrapper.py +16 -6
  5. django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +2 -1
  6. django_cfg/apps/integrations/grpc/services/discovery/registry.py +40 -39
  7. django_cfg/modules/django_client/core/generator/typescript/files_generator.py +12 -0
  8. django_cfg/modules/django_client/core/generator/typescript/generator.py +8 -0
  9. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/function.ts.jinja +22 -0
  10. django_cfg/modules/django_client/core/generator/typescript/templates/main_index.ts.jinja +4 -0
  11. django_cfg/modules/django_client/core/generator/typescript/templates/utils/validation-events.ts.jinja +133 -0
  12. django_cfg/modules/django_client/urls.py +38 -5
  13. django_cfg/modules/django_twilio/email_otp.py +3 -1
  14. django_cfg/modules/django_twilio/sms.py +3 -1
  15. django_cfg/modules/django_twilio/unified.py +6 -2
  16. django_cfg/modules/django_twilio/whatsapp.py +3 -1
  17. django_cfg/pyproject.toml +1 -1
  18. django_cfg/static/frontend/admin.zip +0 -0
  19. django_cfg/templates/admin/index.html +17 -18
  20. {django_cfg-1.5.29.dist-info → django_cfg-1.5.31.dist-info}/METADATA +1 -1
  21. {django_cfg-1.5.29.dist-info → django_cfg-1.5.31.dist-info}/RECORD +24 -23
  22. {django_cfg-1.5.29.dist-info → django_cfg-1.5.31.dist-info}/WHEEL +0 -0
  23. {django_cfg-1.5.29.dist-info → django_cfg-1.5.31.dist-info}/entry_points.txt +0 -0
  24. {django_cfg-1.5.29.dist-info → django_cfg-1.5.31.dist-info}/licenses/LICENSE +0 -0
django_cfg/__init__.py CHANGED
@@ -32,7 +32,7 @@ Example:
32
32
  default_app_config = "django_cfg.apps.DjangoCfgConfig"
33
33
 
34
34
  # Version information
35
- __version__ = "1.5.29"
35
+ __version__ = "1.5.31"
36
36
  __license__ = "MIT"
37
37
 
38
38
  # Setup warnings debug early (checks env var only at this point)
@@ -8,6 +8,7 @@ Mirrors RPCLogger patterns from legacy WebSocket solution for easy migration.
8
8
  import time
9
9
  from typing import Any, Optional
10
10
 
11
+ from django.utils import timezone
11
12
  from django_cfg.modules.django_logging import get_logger
12
13
 
13
14
  logger = get_logger("centrifugo")
@@ -87,11 +88,11 @@ class CentrifugoLogger:
87
88
 
88
89
  logger.info(f"✅ Creating CentrifugoLog entry for {message_id} (async)")
89
90
  try:
90
- from asgiref.sync import sync_to_async
91
91
  from ..models import CentrifugoLog
92
92
 
93
- # Wrap ORM call in sync_to_async
94
- log_entry = await sync_to_async(CentrifugoLog.objects.create)(
93
+ # Use Django 5.2+ async ORM instead of sync_to_async
94
+ # This prevents connection leaks from sync_to_async threads
95
+ log_entry = await CentrifugoLog.objects.acreate(
95
96
  message_id=message_id,
96
97
  channel=channel,
97
98
  data=data,
@@ -234,14 +235,17 @@ class CentrifugoLogger:
234
235
  return
235
236
 
236
237
  try:
237
- from asgiref.sync import sync_to_async
238
238
  from ..models import CentrifugoLog
239
239
 
240
- await sync_to_async(CentrifugoLog.objects.mark_success)(
241
- log_instance=log_entry,
242
- acks_received=acks_received,
243
- duration_ms=duration_ms,
244
- )
240
+ # ✅ Use Django 5.2+ async ORM instead of sync_to_async
241
+ log_entry.status = CentrifugoLog.StatusChoices.SUCCESS
242
+ log_entry.acks_received = acks_received
243
+ log_entry.completed_at = timezone.now()
244
+
245
+ if duration_ms is not None:
246
+ log_entry.duration_ms = duration_ms
247
+
248
+ await log_entry.asave(update_fields=["status", "acks_received", "completed_at", "duration_ms"])
245
249
 
246
250
  logger.info(
247
251
  f"Centrifugo publish successful: {log_entry.message_id}",
@@ -420,14 +424,25 @@ class CentrifugoLogger:
420
424
  return
421
425
 
422
426
  try:
423
- from asgiref.sync import sync_to_async
424
427
  from ..models import CentrifugoLog
425
428
 
426
- await sync_to_async(CentrifugoLog.objects.mark_failed)(
427
- log_instance=log_entry,
428
- error_code=error_code,
429
- error_message=error_message,
430
- duration_ms=duration_ms,
429
+ # ✅ Use Django 5.2+ async ORM instead of sync_to_async
430
+ log_entry.status = CentrifugoLog.StatusChoices.FAILED
431
+ log_entry.error_code = error_code
432
+ log_entry.error_message = error_message
433
+ log_entry.completed_at = timezone.now()
434
+
435
+ if duration_ms is not None:
436
+ log_entry.duration_ms = duration_ms
437
+
438
+ await log_entry.asave(
439
+ update_fields=[
440
+ "status",
441
+ "error_code",
442
+ "error_message",
443
+ "completed_at",
444
+ "duration_ms",
445
+ ]
431
446
  )
432
447
 
433
448
  logger.error(
@@ -465,13 +480,27 @@ class CentrifugoLogger:
465
480
  return
466
481
 
467
482
  try:
468
- from asgiref.sync import sync_to_async
469
483
  from ..models import CentrifugoLog
470
484
 
471
- await sync_to_async(CentrifugoLog.objects.mark_timeout)(
472
- log_instance=log_entry,
473
- acks_received=acks_received,
474
- duration_ms=duration_ms,
485
+ # ✅ Use Django 5.2+ async ORM instead of sync_to_async
486
+ log_entry.status = CentrifugoLog.StatusChoices.TIMEOUT
487
+ log_entry.acks_received = acks_received
488
+ log_entry.error_code = "timeout"
489
+ log_entry.error_message = f"Timeout after {log_entry.ack_timeout}s"
490
+ log_entry.completed_at = timezone.now()
491
+
492
+ if duration_ms is not None:
493
+ log_entry.duration_ms = duration_ms
494
+
495
+ await log_entry.asave(
496
+ update_fields=[
497
+ "status",
498
+ "acks_received",
499
+ "error_code",
500
+ "error_message",
501
+ "completed_at",
502
+ "duration_ms",
503
+ ]
475
504
  )
476
505
 
477
506
  logger.warning(
@@ -12,6 +12,7 @@ from typing import Any, Dict
12
12
  import httpx
13
13
  import jwt
14
14
  from django.conf import settings
15
+ from django.utils import timezone
15
16
  from django_cfg.modules.django_logging import get_logger
16
17
  from drf_spectacular.utils import extend_schema
17
18
  from pydantic import BaseModel, Field
@@ -284,14 +285,23 @@ class CentrifugoTestingAPIViewSet(AdminAPIMixin, viewsets.ViewSet):
284
285
 
285
286
  # Mark as failed
286
287
  if log_entry:
287
- from asgiref.sync import sync_to_async
288
288
  from ..models import CentrifugoLog
289
289
 
290
- await sync_to_async(CentrifugoLog.objects.mark_failed)(
291
- log_instance=log_entry,
292
- error_code=type(e).__name__,
293
- error_message=str(e),
294
- duration_ms=duration_ms,
290
+ # ✅ Use Django 5.2+ async ORM instead of sync_to_async
291
+ log_entry.status = CentrifugoLog.StatusChoices.FAILED
292
+ log_entry.error_code = type(e).__name__
293
+ log_entry.error_message = str(e)
294
+ log_entry.completed_at = timezone.now()
295
+ log_entry.duration_ms = duration_ms
296
+
297
+ await log_entry.asave(
298
+ update_fields=[
299
+ "status",
300
+ "error_code",
301
+ "error_message",
302
+ "completed_at",
303
+ "duration_ms",
304
+ ]
295
305
  )
296
306
 
297
307
  raise
@@ -12,6 +12,7 @@ from typing import Any, Dict
12
12
  import httpx
13
13
  from django.db import transaction
14
14
  from django.http import JsonResponse
15
+ from django.utils import timezone
15
16
  from django.utils.decorators import method_decorator
16
17
  from django.views import View
17
18
  from django.views.decorators.csrf import csrf_exempt
@@ -173,14 +174,23 @@ class PublishWrapperView(View):
173
174
 
174
175
  # Mark as failed
175
176
  if log_entry:
176
- from asgiref.sync import sync_to_async
177
177
  from ..models import CentrifugoLog
178
178
 
179
- await sync_to_async(CentrifugoLog.objects.mark_failed)(
180
- log_instance=log_entry,
181
- error_code=type(e).__name__,
182
- error_message=str(e),
183
- duration_ms=duration_ms,
179
+ # ✅ Use Django 5.2+ async ORM instead of sync_to_async
180
+ log_entry.status = CentrifugoLog.StatusChoices.FAILED
181
+ log_entry.error_code = type(e).__name__
182
+ log_entry.error_message = str(e)
183
+ log_entry.completed_at = timezone.now()
184
+ log_entry.duration_ms = duration_ms
185
+
186
+ await log_entry.asave(
187
+ update_fields=[
188
+ "status",
189
+ "error_code",
190
+ "error_message",
191
+ "completed_at",
192
+ "duration_ms",
193
+ ]
184
194
  )
185
195
 
186
196
  raise
@@ -781,7 +781,8 @@ class Command(BaseCommand):
781
781
  # Mark server as stopping (sync context - signal handlers are sync)
782
782
  if server_status:
783
783
  try:
784
- server_status.mark_stopping()
784
+ # ✅ Use Django 5.2+ async ORM instead of sync_to_async
785
+ asyncio.create_task(server_status.amark_stopping())
785
786
  except Exception as e:
786
787
  self.logger.warning(f"Could not mark server as stopping: {e}")
787
788
 
@@ -181,12 +181,29 @@ class ServiceRegistryManager:
181
181
  >>> stats['success_rate']
182
182
  96.67
183
183
  """
184
- # Django 5.2: Native async aggregate is not available yet (Django 5.2 limitation)
185
- # Use asgiref.sync.sync_to_async as recommended by Django docs
186
- from asgiref.sync import sync_to_async
184
+ # Django 5.2+ async ORM: Use native async aggregate
185
+ stats = await (
186
+ GRPCRequestLog.objects.filter(service_name=service_name)
187
+ .recent(hours)
188
+ .aaggregate(
189
+ total=Count("id"),
190
+ successful=Count("id", filter=models.Q(status="success")),
191
+ errors=Count("id", filter=models.Q(status="error")),
192
+ avg_duration=Avg("duration_ms"),
193
+ )
194
+ )
187
195
 
188
- # Wrap the sync version in sync_to_async
189
- return await sync_to_async(self.get_service_statistics)(service_name, hours)
196
+ total = stats["total"] or 0
197
+ successful = stats["successful"] or 0
198
+ success_rate = (successful / total * 100) if total > 0 else 0.0
199
+
200
+ return {
201
+ "total": total,
202
+ "successful": successful,
203
+ "errors": stats["errors"] or 0,
204
+ "success_rate": round(success_rate, 2),
205
+ "avg_duration_ms": round(stats["avg_duration"] or 0, 2),
206
+ }
190
207
 
191
208
  def get_all_services_with_stats(self, hours: int = 24) -> List[Dict]:
192
209
  """
@@ -274,16 +291,15 @@ class ServiceRegistryManager:
274
291
  services = self.get_all_services()
275
292
  services_with_stats = []
276
293
 
277
- # Django 5.2: Use sync_to_async for aggregate queries that aren't natively async yet
278
- from asgiref.sync import sync_to_async
294
+ # Django 5.2+ async ORM: Use native async aggregate
295
+ for service in services:
296
+ service_name = service.get("name")
279
297
 
280
- # Create async version of the aggregate operation
281
- @sync_to_async
282
- def get_service_stats_sync(service_name: str):
283
- return (
298
+ # Get stats from GRPCRequestLog using native async aggregate
299
+ stats = await (
284
300
  GRPCRequestLog.objects.filter(service_name=service_name)
285
301
  .recent(hours)
286
- .aggregate(
302
+ .aaggregate(
287
303
  total=Count("id"),
288
304
  successful=Count("id", filter=models.Q(status="success")),
289
305
  avg_duration=Avg("duration_ms"),
@@ -291,12 +307,6 @@ class ServiceRegistryManager:
291
307
  )
292
308
  )
293
309
 
294
- for service in services:
295
- service_name = service.get("name")
296
-
297
- # Get stats from GRPCRequestLog (async)
298
- stats = await get_service_stats_sync(service_name)
299
-
300
310
  # Calculate success rate
301
311
  total = stats["total"] or 0
302
312
  successful = stats["successful"] or 0
@@ -429,39 +439,30 @@ class ServiceRegistryManager:
429
439
  if not service:
430
440
  return []
431
441
 
432
- # Django 5.2: Use sync_to_async for complex queries with aggregates and values_list
433
- from asgiref.sync import sync_to_async
434
-
435
- @sync_to_async
436
- def get_method_durations(svc_name: str, method_name: str):
437
- return list(
442
+ # Django 5.2+ async ORM: Use native async operations
443
+ methods_list = []
444
+ for method_name in service.get("methods", []):
445
+ # Get durations for percentile calculation using async list comprehension
446
+ durations = [
447
+ duration async for duration in
438
448
  GRPCRequestLog.objects.filter(
439
- service_name=svc_name,
449
+ service_name=service_name,
440
450
  method_name=method_name,
441
451
  duration_ms__isnull=False,
442
452
  ).values_list("duration_ms", flat=True)
443
- )
453
+ ]
444
454
 
445
- @sync_to_async
446
- def get_method_stats(svc_name: str, method_name: str):
447
- return GRPCRequestLog.objects.filter(
448
- service_name=svc_name,
455
+ # Get aggregate stats using native async aggregate
456
+ stats = await GRPCRequestLog.objects.filter(
457
+ service_name=service_name,
449
458
  method_name=method_name,
450
- ).aggregate(
459
+ ).aaggregate(
451
460
  total=Count("id"),
452
461
  successful=Count("id", filter=models.Q(status="success")),
453
462
  errors=Count("id", filter=models.Q(status="error")),
454
463
  avg_duration=Avg("duration_ms"),
455
464
  )
456
465
 
457
- methods_list = []
458
- for method_name in service.get("methods", []):
459
- # Get durations for percentile calculation (async)
460
- durations = await get_method_durations(service_name, method_name)
461
-
462
- # Get aggregate stats (async)
463
- stats = await get_method_stats(service_name, method_name)
464
-
465
466
  # Calculate percentiles
466
467
  p50, p95, p99 = self._calculate_percentiles(durations)
467
468
 
@@ -143,6 +143,18 @@ class FilesGenerator:
143
143
  description="Retry utilities with p-retry",
144
144
  )
145
145
 
146
+ def generate_validation_events_file(self):
147
+ """Generate validation-events.ts with browser CustomEvent integration."""
148
+
149
+ template = self.jinja_env.get_template('utils/validation-events.ts.jinja')
150
+ content = template.render()
151
+
152
+ return GeneratedFile(
153
+ path="validation-events.ts",
154
+ content=content,
155
+ description="Zod validation error events for browser integration",
156
+ )
157
+
146
158
  def generate_api_instance_file(self):
147
159
  """Generate api-instance.ts with global singleton."""
148
160
 
@@ -98,6 +98,10 @@ class TypeScriptGenerator(BaseGenerator):
98
98
  # Generate retry.ts with p-retry
99
99
  files.append(self.files_gen.generate_retry_file())
100
100
 
101
+ # Generate validation-events.ts (browser CustomEvent for Zod errors)
102
+ if self.generate_zod_schemas:
103
+ files.append(self.files_gen.generate_validation_events_file())
104
+
101
105
  # Generate api-instance.ts singleton (needed for fetchers/hooks)
102
106
  if self.generate_fetchers:
103
107
  files.append(self.files_gen.generate_api_instance_file())
@@ -146,6 +150,10 @@ class TypeScriptGenerator(BaseGenerator):
146
150
  # Generate retry.ts with p-retry
147
151
  files.append(self.files_gen.generate_retry_file())
148
152
 
153
+ # Generate validation-events.ts (browser CustomEvent for Zod errors)
154
+ if self.generate_zod_schemas:
155
+ files.append(self.files_gen.generate_validation_events_file())
156
+
149
157
  # Generate api-instance.ts singleton (needed for fetchers/hooks)
150
158
  if self.generate_fetchers:
151
159
  files.append(self.files_gen.generate_api_instance_file())
@@ -43,6 +43,28 @@ export async function {{ func_name }}(
43
43
 
44
44
  consola.error('Response data:', response);
45
45
 
46
+ // Dispatch browser CustomEvent (only if window is defined)
47
+ if (typeof window !== 'undefined' && error instanceof Error && 'issues' in error) {
48
+ try {
49
+ const event = new CustomEvent('zod-validation-error', {
50
+ detail: {
51
+ operation: '{{ func_name }}',
52
+ path: '{{ operation.path }}',
53
+ method: '{{ operation.http_method }}',
54
+ error: error,
55
+ response: response,
56
+ timestamp: new Date(),
57
+ },
58
+ bubbles: true,
59
+ cancelable: false,
60
+ });
61
+ window.dispatchEvent(event);
62
+ } catch (eventError) {
63
+ // Silently fail - event dispatch should never crash the app
64
+ consola.warn('Failed to dispatch validation error event:', eventError);
65
+ }
66
+ }
67
+
46
68
  // Re-throw the error
47
69
  throw error;
48
70
  }
@@ -55,6 +55,10 @@ export * as Enums from "./enums";
55
55
 
56
56
  // Re-export Zod schemas for runtime validation
57
57
  export * as Schemas from "./_utils/schemas";
58
+
59
+ // Re-export Zod validation events for browser integration
60
+ export type { ValidationErrorDetail, ValidationErrorEvent } from "./validation-events";
61
+ export { dispatchValidationError, onValidationError, formatZodError } from "./validation-events";
58
62
  {% endif %}
59
63
  {% if generate_fetchers %}
60
64
 
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Zod Validation Events - Browser CustomEvent integration
3
+ *
4
+ * Dispatches browser CustomEvents when Zod validation fails, allowing
5
+ * React/frontend apps to listen and handle validation errors globally.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * // In your React app
10
+ * window.addEventListener('zod-validation-error', (event) => {
11
+ * const { operation, path, method, error, response } = event.detail;
12
+ * console.error(`Validation failed for ${method} ${path}`, error);
13
+ * // Show toast notification, log to Sentry, etc.
14
+ * });
15
+ * ```
16
+ */
17
+
18
+ import type { ZodError } from 'zod'
19
+
20
+ /**
21
+ * Validation error event detail
22
+ */
23
+ export interface ValidationErrorDetail {
24
+ /** Operation/function name that failed validation */
25
+ operation: string
26
+ /** API endpoint path */
27
+ path: string
28
+ /** HTTP method */
29
+ method: string
30
+ /** Zod validation error */
31
+ error: ZodError
32
+ /** Raw response data that failed validation */
33
+ response: any
34
+ /** Timestamp of the error */
35
+ timestamp: Date
36
+ }
37
+
38
+ /**
39
+ * Custom event type for Zod validation errors
40
+ */
41
+ export type ValidationErrorEvent = CustomEvent<ValidationErrorDetail>
42
+
43
+ /**
44
+ * Dispatch a Zod validation error event.
45
+ *
46
+ * Only dispatches in browser environment (when window is defined).
47
+ * Safe to call in Node.js/SSR - will be a no-op.
48
+ *
49
+ * @param detail - Validation error details
50
+ */
51
+ export function dispatchValidationError(detail: ValidationErrorDetail): void {
52
+ // Check if running in browser
53
+ if (typeof window === 'undefined') {
54
+ return
55
+ }
56
+
57
+ try {
58
+ const event = new CustomEvent<ValidationErrorDetail>('zod-validation-error', {
59
+ detail,
60
+ bubbles: true,
61
+ cancelable: false,
62
+ })
63
+
64
+ window.dispatchEvent(event)
65
+ } catch (error) {
66
+ // Silently fail - validation event dispatch should never crash the app
67
+ console.warn('Failed to dispatch validation error event:', error)
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Add a global listener for Zod validation errors.
73
+ *
74
+ * @param callback - Function to call when validation error occurs
75
+ * @returns Cleanup function to remove the listener
76
+ *
77
+ * @example
78
+ * ```typescript
79
+ * const cleanup = onValidationError(({ operation, error }) => {
80
+ * toast.error(`Validation failed in ${operation}`);
81
+ * logToSentry(error);
82
+ * });
83
+ *
84
+ * // Later, remove listener
85
+ * cleanup();
86
+ * ```
87
+ */
88
+ export function onValidationError(
89
+ callback: (detail: ValidationErrorDetail) => void
90
+ ): () => void {
91
+ if (typeof window === 'undefined') {
92
+ // Return no-op cleanup function for SSR
93
+ return () => {}
94
+ }
95
+
96
+ const handler = (event: Event) => {
97
+ if (event instanceof CustomEvent) {
98
+ callback(event.detail)
99
+ }
100
+ }
101
+
102
+ window.addEventListener('zod-validation-error', handler)
103
+
104
+ // Return cleanup function
105
+ return () => {
106
+ window.removeEventListener('zod-validation-error', handler)
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Format Zod error for logging/display.
112
+ *
113
+ * @param error - Zod validation error
114
+ * @returns Formatted error message
115
+ */
116
+ export function formatZodError(error: ZodError): string {
117
+ const issues = error.issues.map((issue, index) => {
118
+ const path = issue.path.join('.') || 'root'
119
+ const parts = [`${index + 1}. ${path}: ${issue.message}`]
120
+
121
+ if ('expected' in issue && issue.expected) {
122
+ parts.push(` Expected: ${issue.expected}`)
123
+ }
124
+
125
+ if ('received' in issue && issue.received) {
126
+ parts.push(` Received: ${issue.received}`)
127
+ }
128
+
129
+ return parts.join('\n')
130
+ })
131
+
132
+ return issues.join('\n')
133
+ }
@@ -63,11 +63,44 @@ def get_openapi_urls() -> List[Any]:
63
63
 
64
64
 
65
65
  # Export urlpatterns for django.urls.include()
66
- # Only create urlpatterns if Django is configured
67
- if _is_django_configured():
68
- urlpatterns = get_openapi_urls()
69
- else:
70
- urlpatterns = []
66
+ # CRITICAL: Use lazy evaluation to avoid importing DRF/drf-spectacular
67
+ # before Django settings are fully loaded. This prevents api_settings
68
+ # from being cached with wrong DEFAULT_SCHEMA_CLASS value.
69
+ class LazyURLPatterns:
70
+ """Lazy URLpatterns that only initialize when accessed."""
71
+
72
+ def __init__(self):
73
+ self._patterns = None
74
+
75
+ def _get_patterns(self):
76
+ if self._patterns is None:
77
+ if _is_django_configured():
78
+ self._patterns = get_openapi_urls()
79
+ else:
80
+ self._patterns = []
81
+ return self._patterns
82
+
83
+ def __iter__(self):
84
+ return iter(self._get_patterns())
85
+
86
+ def __getitem__(self, index):
87
+ return self._get_patterns()[index]
88
+
89
+ def __len__(self):
90
+ return len(self._get_patterns())
91
+
92
+ def clear(self):
93
+ """Clear all patterns."""
94
+ patterns = self._get_patterns()
95
+ patterns.clear()
96
+
97
+ def extend(self, items):
98
+ """Extend patterns with new items."""
99
+ patterns = self._get_patterns()
100
+ patterns.extend(items)
101
+
102
+
103
+ urlpatterns = LazyURLPatterns()
71
104
 
72
105
 
73
106
  __all__ = ["get_openapi_urls", "urlpatterns"]
@@ -111,7 +111,9 @@ class EmailOTPService(BaseTwilioService):
111
111
  template_data: Optional[Dict[str, Any]] = None
112
112
  ) -> Tuple[bool, str, str]:
113
113
  """Async version of send_otp."""
114
- return await sync_to_async(self.send_otp)(email, subject, template_data)
114
+ # sync_to_async is appropriate here for external SendGrid API calls (not Django ORM)
115
+ # thread_sensitive=False for better performance since no database access occurs
116
+ return await sync_to_async(self.send_otp, thread_sensitive=False)(email, subject, template_data)
115
117
 
116
118
  def _send_template_email(
117
119
  self,
@@ -87,7 +87,9 @@ class SMSOTPService(BaseTwilioService):
87
87
 
88
88
  async def asend_otp(self, phone_number: str) -> Tuple[bool, str]:
89
89
  """Async version of send_otp."""
90
- return await sync_to_async(self.send_otp)(phone_number)
90
+ # sync_to_async is appropriate here for external Twilio API calls (not Django ORM)
91
+ # thread_sensitive=False for better performance since no database access occurs
92
+ return await sync_to_async(self.send_otp, thread_sensitive=False)(phone_number)
91
93
 
92
94
 
93
95
  __all__ = [
@@ -105,7 +105,9 @@ class UnifiedOTPService(BaseTwilioService):
105
105
  enable_fallback: bool = True
106
106
  ) -> Tuple[bool, str, TwilioChannelType]:
107
107
  """Async version of send_otp."""
108
- return await sync_to_async(self.send_otp)(identifier, preferred_channel, enable_fallback)
108
+ # sync_to_async is appropriate here for external Twilio API calls (not Django ORM)
109
+ # thread_sensitive=False for better performance since no database access occurs
110
+ return await sync_to_async(self.send_otp, thread_sensitive=False)(identifier, preferred_channel, enable_fallback)
109
111
 
110
112
  def verify_otp(self, identifier: str, code: str) -> Tuple[bool, str]:
111
113
  """
@@ -129,7 +131,9 @@ class UnifiedOTPService(BaseTwilioService):
129
131
 
130
132
  async def averify_otp(self, identifier: str, code: str) -> Tuple[bool, str]:
131
133
  """Async version of verify_otp."""
132
- return await sync_to_async(self.verify_otp)(identifier, code)
134
+ # sync_to_async is appropriate here for external Twilio API calls (not Django ORM)
135
+ # thread_sensitive=False for better performance since no database access occurs
136
+ return await sync_to_async(self.verify_otp, thread_sensitive=False)(identifier, code)
133
137
 
134
138
  def _get_available_channels(self, is_email: bool, config: TwilioConfig) -> List[TwilioChannelType]:
135
139
  """Get list of available channels based on configuration."""
@@ -101,7 +101,9 @@ class WhatsAppOTPService(BaseTwilioService):
101
101
 
102
102
  async def asend_otp(self, phone_number: str, fallback_to_sms: bool = True) -> Tuple[bool, str]:
103
103
  """Async version of send_otp."""
104
- return await sync_to_async(self.send_otp)(phone_number, fallback_to_sms)
104
+ # sync_to_async is appropriate here for external Twilio API calls (not Django ORM)
105
+ # thread_sensitive=False for better performance since no database access occurs
106
+ return await sync_to_async(self.send_otp, thread_sensitive=False)(phone_number, fallback_to_sms)
105
107
 
106
108
  def _send_sms_otp(self, phone_number: str, client: Client, verify_config: TwilioVerifyConfig) -> Tuple[bool, str]:
107
109
  """Internal SMS fallback method."""
django_cfg/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "django-cfg"
7
- version = "1.5.29"
7
+ version = "1.5.31"
8
8
  description = "Modern Django framework with type-safe Pydantic v2 configuration, Next.js admin integration, real-time WebSockets, and 8 enterprise apps. Replace settings.py with validated models, 90% less code. Production-ready with AI agents, auto-generated TypeScript clients, and zero-config features."
9
9
  readme = "README.md"
10
10
  keywords = [ "django", "configuration", "pydantic", "settings", "type-safety", "pydantic-settings", "django-environ", "startup-validation", "ide-autocomplete", "nextjs-admin", "react-admin", "websocket", "centrifugo", "real-time", "typescript-generation", "ai-agents", "enterprise-django", "django-settings", "type-safe-config", "modern-django",]
Binary file
@@ -424,7 +424,7 @@
424
424
  const authToken = localStorage.getItem('auth_token');
425
425
  const refreshToken = localStorage.getItem('refresh_token');
426
426
 
427
- // console.log('[Django-CFG] sendAuth: authToken:', authToken ? authToken.substring(0, 20) + '...' : 'null', 'refreshToken:', refreshToken ? refreshToken.substring(0, 20) + '...' : 'null');
427
+ console.log('[Django-CFG] sendAuth: authToken:', authToken ? authToken.substring(0, 20) + '...' : 'null', 'refreshToken:', refreshToken ? refreshToken.substring(0, 20) + '...' : 'null');
428
428
 
429
429
  if (!authToken) {
430
430
  console.warn('[Django-CFG] sendAuth: No auth token found in localStorage');
@@ -434,12 +434,12 @@
434
434
  try {
435
435
  // Use '*' in dev mode to handle localhost vs 127.0.0.1 mismatch
436
436
  const targetOrigin = '*'; // In production, use specific origin for security
437
- // console.log('[Django-CFG] Sending parent-auth message to iframe:', this.iframe.id);
437
+ console.log('[Django-CFG] Sending parent-auth message to iframe:', this.iframe.id);
438
438
  this.iframe.contentWindow.postMessage({
439
439
  type: 'parent-auth',
440
440
  data: { authToken, refreshToken }
441
441
  }, targetOrigin);
442
- // console.log('[Django-CFG] parent-auth message sent successfully');
442
+ console.log('[Django-CFG] parent-auth message sent successfully');
443
443
  } catch (e) {
444
444
  console.error('[Django-CFG] Failed to send auth:', e);
445
445
  }
@@ -449,10 +449,10 @@
449
449
  * Send all data (theme + auth)
450
450
  */
451
451
  sendAllData() {
452
- // console.log('[Django-CFG] sendAllData called');
452
+ console.log('[Django-CFG] sendAllData called');
453
453
  this.sendTheme();
454
454
  this.sendAuth();
455
- // console.log('[Django-CFG] sendAllData completed');
455
+ console.log('[Django-CFG] sendAllData completed');
456
456
  }
457
457
 
458
458
  /**
@@ -526,15 +526,15 @@
526
526
  }
527
527
 
528
528
  handleMessage(type, data) {
529
- // console.log('[Django-CFG] Received message from iframe:', type, data);
529
+ console.log('[Django-CFG] Received message from iframe:', type, data);
530
530
  switch (type) {
531
531
  case 'iframe-ready':
532
- // console.log('[Django-CFG] iframe-ready received, sending auth and theme data');
532
+ console.log('[Django-CFG] iframe-ready received, sending auth and theme data');
533
533
  this.messageBridge.sendAllData();
534
534
  break;
535
535
 
536
536
  case 'iframe-auth-status':
537
- // console.log('[Django-CFG] iframe-auth-status:', data);
537
+ console.log('[Django-CFG] iframe-auth-status:', data);
538
538
  break;
539
539
 
540
540
  case 'iframe-navigation':
@@ -640,26 +640,25 @@
640
640
  * Setup ONE global message listener for ALL iframes
641
641
  */
642
642
  _setupGlobalMessageListener() {
643
- // console.log('[Django-CFG] Setting up global message listener');
643
+ console.log('[Django-CFG] Setting up global message listener');
644
644
  window.addEventListener('message', (event) => {
645
- // console.log('[Django-CFG] Received message:', {
646
- // type: event.data?.type,
647
- // origin: event.origin,
648
- // source: event.source,
649
- // mapSize: this.iframeMap.size,
650
- // mapKeys: Array.from(this.iframeMap.keys())
651
- // });
645
+ console.log('[Django-CFG] Received message:', {
646
+ type: event.data?.type,
647
+ origin: event.origin,
648
+ source: event.source === window ? 'self' : 'iframe',
649
+ mapSize: this.iframeMap.size,
650
+ });
652
651
 
653
652
  // Find which handler should handle this message
654
653
  const handler = this.iframeMap.get(event.source);
655
654
 
656
655
  if (handler) {
657
- // console.log('[Django-CFG] Routing message to handler:', handler.iframe.id);
656
+ console.log('[Django-CFG] Routing message to handler:', handler.iframe.id);
658
657
  const { type, data } = event.data || {};
659
658
  handler.handleMessage(type, data);
660
659
  } else {
661
660
  // Message from unknown source (browser extension, etc.)
662
- // console.log('[Django-CFG] Ignoring message from unknown source');
661
+ console.log('[Django-CFG] Ignoring message from unknown source, origin:', event.origin);
663
662
  }
664
663
  });
665
664
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-cfg
3
- Version: 1.5.29
3
+ Version: 1.5.31
4
4
  Summary: Modern Django framework with type-safe Pydantic v2 configuration, Next.js admin integration, real-time WebSockets, and 8 enterprise apps. Replace settings.py with validated models, 90% less code. Production-ready with AI agents, auto-generated TypeScript clients, and zero-config features.
5
5
  Project-URL: Homepage, https://djangocfg.com
6
6
  Project-URL: Documentation, https://djangocfg.com
@@ -1,5 +1,5 @@
1
1
  django_cfg/README.md,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- django_cfg/__init__.py,sha256=oQaPnoJ11bqhg0gfor6sbcxzCTeYmYqrScQTcsou_Lk,1822
2
+ django_cfg/__init__.py,sha256=D5Xv2whEAdJnyt2NXLAkpDOQ700x6HQ92QzLkYJRwUs,1822
3
3
  django_cfg/apps.py,sha256=72m3uuvyqGiLx6gOfE-BD3P61jddCCERuBOYpxTX518,1605
4
4
  django_cfg/config.py,sha256=ZqvSbFje8LJS5NtXTZsnUErgcUSCHa251LNfjEatZtY,2152
5
5
  django_cfg/requirements.txt,sha256=NQjf3cPCjzPMaGFewhJxGNHnUdE9gE8yTXDEBIt0vuA,2682
@@ -426,7 +426,7 @@ django_cfg/apps/integrations/centrifugo/serializers/stats.py,sha256=OAoulxUY_8Lh
426
426
  django_cfg/apps/integrations/centrifugo/services/__init__.py,sha256=q0Dzm_gIrKcCgCrRAPLxKY-n_FZUWHtrKMCgZ110ZVg,507
427
427
  django_cfg/apps/integrations/centrifugo/services/config_helper.py,sha256=d2eT8AczcCRl-v2OqZUu0n-MvSt42GWJOD7tJTHKiSg,1583
428
428
  django_cfg/apps/integrations/centrifugo/services/dashboard_notifier.py,sha256=uagZMbykw4GjX-mqlekCYjR6tgLMifWe2c9mwIjYrLU,4793
429
- django_cfg/apps/integrations/centrifugo/services/logging.py,sha256=zJWrJsP4k_epixgvYnx_HCMPOCiQMt1XHjamdX4ypkk,23328
429
+ django_cfg/apps/integrations/centrifugo/services/logging.py,sha256=_A07mXEz0X5hvtQUOFq5H-aBJbbvhQri9ceMutBz8WU,24434
430
430
  django_cfg/apps/integrations/centrifugo/services/publisher.py,sha256=9p2vF03yJWZQOrMMpWFwmv29azsTIAI31it7ac37giU,11232
431
431
  django_cfg/apps/integrations/centrifugo/services/token_generator.py,sha256=hjNKRg5neOYv1LytjrpOQBsBiBPx4wcPrbbglTJieh8,3375
432
432
  django_cfg/apps/integrations/centrifugo/services/client/__init__.py,sha256=IH3RKQTnahybxlFp20X189A5MzWs-kxTnSSoN0Bt8pQ,1003
@@ -437,9 +437,9 @@ django_cfg/apps/integrations/centrifugo/services/client/exceptions.py,sha256=VsY
437
437
  django_cfg/apps/integrations/centrifugo/views/__init__.py,sha256=ieaKBQSQeOi57BS3CUJltoqLDeAIgcixkT9YzsGK9c4,370
438
438
  django_cfg/apps/integrations/centrifugo/views/admin_api.py,sha256=BBxyJo3c7ROivU191gjI7kiJojC4HwMTQsS3td7NSXI,13855
439
439
  django_cfg/apps/integrations/centrifugo/views/monitoring.py,sha256=UM9tdgeFpcXYai3dYpf6lJT4jq8DkrT1OMjiuiTCjBE,13756
440
- django_cfg/apps/integrations/centrifugo/views/testing_api.py,sha256=3K7AvUTOyq6chgIVAOzYKMxbFYY7aFl6ZaDkKrINzh8,14089
440
+ django_cfg/apps/integrations/centrifugo/views/testing_api.py,sha256=HzLplNbKGJHyztkBseVJ9FIGE6fVLGu2E1WCmEvBlLI,14462
441
441
  django_cfg/apps/integrations/centrifugo/views/token_api.py,sha256=j5_9DALkFzzOmiC_JTdIlppra67WzJq-E_OAzaxWeqQ,3499
442
- django_cfg/apps/integrations/centrifugo/views/wrapper.py,sha256=rHarWGtMD2qDPB3pa7K476Usc3CeTudaZxUyyQ3M7yA,8632
442
+ django_cfg/apps/integrations/centrifugo/views/wrapper.py,sha256=sf14OqS2VoJFJGsptGhcASP0hbLujgYNJLN57inp4GQ,9005
443
443
  django_cfg/apps/integrations/grpc/__init__.py,sha256=mzsALD2x44rGQNNh3mIUFi7flQj6PvS1wPq-0TeBz3c,235
444
444
  django_cfg/apps/integrations/grpc/apps.py,sha256=RF605ivjVMIVbQKIWhGKoDkYfJR9hk3Ou-hCQbBDfzY,3148
445
445
  django_cfg/apps/integrations/grpc/urls.py,sha256=Hq1tpLQljoWifXIYudDDYytMeFO60m1FPE17gTAMlTg,1375
@@ -458,7 +458,7 @@ django_cfg/apps/integrations/grpc/management/__init__.py,sha256=qeaIQO1EY_-VkrUF
458
458
  django_cfg/apps/integrations/grpc/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
459
459
  django_cfg/apps/integrations/grpc/management/commands/compile_proto.py,sha256=5978_OnFOz6j1m_XG1amlZ3U2dVw3w3395I5ktx2mIA,3440
460
460
  django_cfg/apps/integrations/grpc/management/commands/generate_protos.py,sha256=zCGEncUf3kfT_PkvECbAOzBjrzzBx9iTYxq49tie9yg,6618
461
- django_cfg/apps/integrations/grpc/management/commands/rungrpc.py,sha256=dhPVqUuP2nOr6tzpD1vTc_fS_pE1L6EZB2WKrcqAOs8,30875
461
+ django_cfg/apps/integrations/grpc/management/commands/rungrpc.py,sha256=PvQK-bRJjCRNuP4Ip9HJIuNM2ehbuEFKHM1rDh3JA8A,30974
462
462
  django_cfg/apps/integrations/grpc/management/commands/test_grpc_integration.py,sha256=lyKkjIUVNSmMgQdmd9Ko82hp3CZQgdVDCGU1NSxxvd8,2146
463
463
  django_cfg/apps/integrations/grpc/management/proto/__init__.py,sha256=2esCFQpr2J0ERGvQaHsR-LOElHV1iXT1AK9McWlJ9g0,57
464
464
  django_cfg/apps/integrations/grpc/management/proto/compiler.py,sha256=9qcIcPUeXqpJ-U0WdHYArCH0l0it_9nLNWt0GfK2E4E,5827
@@ -511,7 +511,7 @@ django_cfg/apps/integrations/grpc/services/commands/examples/start.py,sha256=9Yi
511
511
  django_cfg/apps/integrations/grpc/services/commands/examples/stop.py,sha256=4q4RS9pmk4GbiYsmAvJ7GoQjtq9F05vVdp1f2PYcabg,2836
512
512
  django_cfg/apps/integrations/grpc/services/discovery/__init__.py,sha256=FEC3x2zfJNelUqfulTlFpAxUDOTpTJVdW8dEoV3XOdc,914
513
513
  django_cfg/apps/integrations/grpc/services/discovery/discovery.py,sha256=d1bDjVEdJEtrbXFuQV_zuQ5p3JfRGNfUAQQiVNYN7E8,20391
514
- django_cfg/apps/integrations/grpc/services/discovery/registry.py,sha256=gX8QReiXnbUFnUxjh6b4DfqFjIScY25Ya-2nVdaFixA,18326
514
+ django_cfg/apps/integrations/grpc/services/discovery/registry.py,sha256=3PH4pfxoKco_ZGad8Pn4HvQ3SFbFOfT7bpxG3PQ36VU,18305
515
515
  django_cfg/apps/integrations/grpc/services/interceptors/__init__.py,sha256=Gp2cQ7PZ3VbpulsU-vB1VwF67DFY5JIURuZoRSopOoM,603
516
516
  django_cfg/apps/integrations/grpc/services/interceptors/centrifugo.py,sha256=xhIH6JC6gwudPUfQrUX4MDz2mdUp9SYT455AvBGQ_3Q,19880
517
517
  django_cfg/apps/integrations/grpc/services/interceptors/errors.py,sha256=rbhUhD_gwKESiGqFDsGR9bQ-x3zJdM0FxZ2ytb-rK_E,7968
@@ -870,7 +870,7 @@ django_cfg/modules/django_admin/widgets/registry.py,sha256=P2kQiB3eYXhjRiCXlcImH
870
870
  django_cfg/modules/django_client/__init__.py,sha256=iHaGKbsyR2wMmVCWNsETC7cwB60fZudvnFMiK1bchW8,529
871
871
  django_cfg/modules/django_client/apps.py,sha256=xfkw2aXy08xXlkFhbCiTFveMmRwlDk3SQOAWdqXraFM,1952
872
872
  django_cfg/modules/django_client/pytest.ini,sha256=yC1PeQKS4nWQD-jVo5fWF9y1Uv6rywcH0mtHREsiAp0,668
873
- django_cfg/modules/django_client/urls.py,sha256=0Q0uDhiRqI-jlTU_tsylg-HtRHNXARko6gGGz2-63Aw,1799
873
+ django_cfg/modules/django_client/urls.py,sha256=mvoeGUh7X3XcX82a3kAW0HD4rp05jGM4dqvyGs_dkdk,2751
874
874
  django_cfg/modules/django_client/core/__init__.py,sha256=xAqXcFoY3H0oCBp8g7d9PzO99bOGIDdtHUKgVAOoS_c,1135
875
875
  django_cfg/modules/django_client/core/archive/__init__.py,sha256=SKeOKjRnw_NsL7_fOU_2Neo-bcDj4Hv2sVFYztJYiSI,171
876
876
  django_cfg/modules/django_client/core/archive/manager.py,sha256=2qSi1ymdjfjU-X0cWzl_hq2adt09EmOCz4z4pMQMAQw,4345
@@ -940,8 +940,8 @@ django_cfg/modules/django_client/core/generator/python/templates/utils/schema.py
940
940
  django_cfg/modules/django_client/core/generator/typescript/__init__.py,sha256=eHOZp7M65WZ9u3tA_xQlON5-oijZZiGXDhz22Bq73s0,371
941
941
  django_cfg/modules/django_client/core/generator/typescript/client_generator.py,sha256=5CKeXuw6OvbRrq-ezdooqbpLpCrjYLmkL-kWkDdlD14,6068
942
942
  django_cfg/modules/django_client/core/generator/typescript/fetchers_generator.py,sha256=nOaPFpSRb1izEpyXjJcnC2gdRcSLqhtmTPY0QWXUWkI,9184
943
- django_cfg/modules/django_client/core/generator/typescript/files_generator.py,sha256=GU28mlTK7wrBw3NG5mq2dAGi4gE77ZFtHPtIXVg3qVk,7072
944
- django_cfg/modules/django_client/core/generator/typescript/generator.py,sha256=7C0cFf3zDlgz2Ij-yxt7mGHKGKnqkcKyo0WZqRmL8jY,20862
943
+ django_cfg/modules/django_client/core/generator/typescript/files_generator.py,sha256=iX4WpiQdL6PnTToseEZWvhYrZiNKo27i2As50E1Pxfg,7512
944
+ django_cfg/modules/django_client/core/generator/typescript/generator.py,sha256=SajtFMQDu5Q47CLwX9COsGIQcB3sVC0YoMOHS3bsaSY,21268
945
945
  django_cfg/modules/django_client/core/generator/typescript/hooks_generator.py,sha256=0yT4cuUa1BP5LcF17jj3poeR7udJQ14A9TN0gB21d18,16002
946
946
  django_cfg/modules/django_client/core/generator/typescript/models_generator.py,sha256=xhIaEn76_Jt9KeW_JE7PCBzdJMF7WZdE75T11Ih_ay0,9597
947
947
  django_cfg/modules/django_client/core/generator/typescript/naming.py,sha256=kSZwPGqD42G2KBqoPGDiy4CoWjaQWseKAWbdJOF7VaU,2574
@@ -953,7 +953,7 @@ django_cfg/modules/django_client/core/generator/typescript/templates/api_instanc
953
953
  django_cfg/modules/django_client/core/generator/typescript/templates/app_index.ts.jinja,sha256=gLsoYyEzKD6Gv64vsO9sQHMPiFMGdaB5XVufLHeRyvQ,62
954
954
  django_cfg/modules/django_client/core/generator/typescript/templates/client_file.ts.jinja,sha256=LHUt72fO2eRNAHYEscIYvqVR69GC6mxqjcgSlUzeCtc,251
955
955
  django_cfg/modules/django_client/core/generator/typescript/templates/index.ts.jinja,sha256=OtQxzqV_6SYvugk_oS0F9_WXty2tnKY_wl2n9-WeJqo,127
956
- django_cfg/modules/django_client/core/generator/typescript/templates/main_index.ts.jinja,sha256=gyO5BPgycniHdLRILmIwav4MTFVYUIwpvJrOJ3zFiTw,7624
956
+ django_cfg/modules/django_client/core/generator/typescript/templates/main_index.ts.jinja,sha256=9u30Lti9OPN9h6sliB5MyUUTjdi2dQSAP1-Nwagv2p0,7870
957
957
  django_cfg/modules/django_client/core/generator/typescript/templates/package.json.jinja,sha256=XOgZJG3twzWJ8gsZ5VZ_gQx7zBvrnwXy-yUFHfdyBBs,1280
958
958
  django_cfg/modules/django_client/core/generator/typescript/templates/tsconfig.json.jinja,sha256=QKbo6hYoVdRXrm7psRzBGStzAP5omQrnamSQT6b44gE,482
959
959
  django_cfg/modules/django_client/core/generator/typescript/templates/client/app_client.ts.jinja,sha256=jaFN_QIQU2eyu-6uiwUjwACYNr06LI14XGJN0Dgv-9E,260
@@ -963,7 +963,7 @@ django_cfg/modules/django_client/core/generator/typescript/templates/client/main
963
963
  django_cfg/modules/django_client/core/generator/typescript/templates/client/operation.ts.jinja,sha256=z6GD-r7Y-S_yhDtlOAjMgDSL10AF3YrBLLNLPiCt8rE,1869
964
964
  django_cfg/modules/django_client/core/generator/typescript/templates/client/sub_client.ts.jinja,sha256=1rtFMqJO8ynjNNblhMPwCbVFhbSbLJJwiMhuJJYf9Lw,215
965
965
  django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/fetchers.ts.jinja,sha256=vk-yQgNuS4jxnN6bqUl7gDrrK7_q5eLnWXiBvHVSvSQ,1291
966
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/function.ts.jinja,sha256=5jUCSWbMPdG1n5jxOVFKDfRnI4ocwsMcJsaM1vP6wyo,1567
966
+ django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/function.ts.jinja,sha256=d3ImACQDM-KoqiU6ytElXSr82m7rY5Q_mRQxpMPaKOg,2352
967
967
  django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/index.ts.jinja,sha256=DoAVm8EoglJKtLrE917Fk7Na6rn5Bt9L1nI39o9AwzM,747
968
968
  django_cfg/modules/django_client/core/generator/typescript/templates/hooks/hooks.ts.jinja,sha256=RdImlx61kBn1uCrrpqeTfxa1l-5a3AYU1ZKMVw1f6TU,870
969
969
  django_cfg/modules/django_client/core/generator/typescript/templates/hooks/index.ts.jinja,sha256=c4Jru6DAKD-y8nzig1my0iE-BO3OwjSCENALbHvAp7E,694
@@ -980,6 +980,7 @@ django_cfg/modules/django_client/core/generator/typescript/templates/utils/logge
980
980
  django_cfg/modules/django_client/core/generator/typescript/templates/utils/retry.ts.jinja,sha256=xv1wvOqyIITi7e-318Wxu_af9T72H68TWufVRsHiCqw,4080
981
981
  django_cfg/modules/django_client/core/generator/typescript/templates/utils/schema.ts.jinja,sha256=KC8zJ6R91LtyeVW-dp9ljbwCTZVPyvf0glnRD8pJzAA,167
982
982
  django_cfg/modules/django_client/core/generator/typescript/templates/utils/storage.ts.jinja,sha256=Sbszp1L7b1pN9EScq7c2WmltdC7yXrbqU55v4FmRdT0,4899
983
+ django_cfg/modules/django_client/core/generator/typescript/templates/utils/validation-events.ts.jinja,sha256=u5nVpO_TjPLvAX-t5bl-9MWEeAb-u7qAwQkEPZ9F6h0,3413
983
984
  django_cfg/modules/django_client/core/groups/__init__.py,sha256=4n80S-cdywX4wITntbo9tsmVwv8iRQEkPKrtsaXz7jQ,226
984
985
  django_cfg/modules/django_client/core/groups/detector.py,sha256=nCCqpXPGNznm57KzyuriL0D7hBJ2yGA3woL9upf4i5o,5543
985
986
  django_cfg/modules/django_client/core/groups/manager.py,sha256=MBXVhHBb19antrrbwFWP47v9y2OMz0oQ1RAz406Yip4,11201
@@ -1124,16 +1125,16 @@ django_cfg/modules/django_twilio/README.md,sha256=Zmh_mG0loPTHlr4piqRfz7AWp-uSRt
1124
1125
  django_cfg/modules/django_twilio/__init__.py,sha256=R51VI_lxA47h2nRx1jkcXnwdYrdsDfKJK7TSojYc5yI,2378
1125
1126
  django_cfg/modules/django_twilio/_imports.py,sha256=qZo8urnj-K66z4HcIzaAZWhdAtqZU_lHVHL1ipqCU9A,782
1126
1127
  django_cfg/modules/django_twilio/base.py,sha256=wizSK-AelOpH0oCzVgqAJHPzvxekBmIX-hKseWbhHJM,6426
1127
- django_cfg/modules/django_twilio/email_otp.py,sha256=udK5_GiIwKIworbPiqkNIS8ygYu3-Bp8N8jPOSd-7bA,7743
1128
+ django_cfg/modules/django_twilio/email_otp.py,sha256=iGk7O8lGwp1bJqZnE2EQWgXB2VfQGryhW_7kacncpnc,7948
1128
1129
  django_cfg/modules/django_twilio/exceptions.py,sha256=KWdiEc7mFYvxEwvOgLb14d_XmidJXtUVt-i2VNVWOwE,10498
1129
1130
  django_cfg/modules/django_twilio/models.py,sha256=u7PHSiHfbkuTpfh9Z3WiPPBvV0Ps9ZW8ZxAUXcKMVMo,11599
1130
1131
  django_cfg/modules/django_twilio/sendgrid_service.py,sha256=Xk0BAWKW2hgfKQ4-Y4oUNidqkcdwf-wgeFHoUTBqFUM,18296
1131
1132
  django_cfg/modules/django_twilio/simple_service.py,sha256=zDV8hm7dVWqdL-Y0XF-T80wxJvIpe3JR1JpwdUFv-OY,9672
1132
- django_cfg/modules/django_twilio/sms.py,sha256=IYbVh0HBd89Y0ssiGOZ4DKRD6tJkwJnhPsqC0B41GUA,2762
1133
+ django_cfg/modules/django_twilio/sms.py,sha256=EUIwOfhS8fDn8UGyANLu8PZCk5sFqIrecWSt-taymSI,2965
1133
1134
  django_cfg/modules/django_twilio/twilio_service.py,sha256=PPjoemzZAgpEOZBENyHHC1F8s77NQh3gMN3EcH6iG-w,19299
1134
- django_cfg/modules/django_twilio/unified.py,sha256=hS_OZzqBtsI_q9KXbDYwImIzkhOttXe5RalH6R5Y5QY,11370
1135
+ django_cfg/modules/django_twilio/unified.py,sha256=VnuauH9Ys5ZDtjNv2LX_VK_-ybKTgXsp2xb6GedrJoU,11776
1135
1136
  django_cfg/modules/django_twilio/utils.py,sha256=6PSyvd0Jgvf3QuiHTHM8t0Sc4vYUXxAnJACd8WHWzQ8,4898
1136
- django_cfg/modules/django_twilio/whatsapp.py,sha256=E0MWT7Um0g38nP0hu0OldMCJ6JlWbFkGXaaJFFHkiFs,4923
1137
+ django_cfg/modules/django_twilio/whatsapp.py,sha256=AR2Ez0vsZIllhYaE_BVMI3NPkaA8exkK8zrODIbQyV8,5126
1137
1138
  django_cfg/modules/django_twilio/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1138
1139
  django_cfg/modules/django_twilio/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1139
1140
  django_cfg/modules/django_twilio/management/commands/test_twilio.py,sha256=hFQjks4ZoZj1-K9rKCHdYFlwp71IUxNE6rYmJ8jEjHo,4118
@@ -1181,7 +1182,7 @@ django_cfg/static/admin/js/alpine/commands-section.js,sha256=8z2MQNwZF9Tx_2EK1AY
1181
1182
  django_cfg/static/admin/js/alpine/dashboard-tabs.js,sha256=ob8Q_I9lFLDv_hFERXgTyvqMDBspAGfzCxI_7slRur4,1354
1182
1183
  django_cfg/static/admin/js/alpine/system-metrics.js,sha256=m-Fg55K_vpHXToD46PXL9twl4OBF_V9MONvbSWbQqDw,440
1183
1184
  django_cfg/static/admin/js/alpine/toggle-section.js,sha256=T141NFmy0fRJyGGuuaCJRjJXwPam-xxtQNW1hi8BJbc,672
1184
- django_cfg/static/frontend/admin.zip,sha256=T5_NtV-j-f-7eCuJdqoKBESib_yj0DL11HL_TztiwsM,24302314
1185
+ django_cfg/static/frontend/admin.zip,sha256=q-mvBZpSOujXj1ZyQeHUY8H4__JjfRR2Tp9D5kPS1MM,24319401
1185
1186
  django_cfg/static/js/api-loader.mjs,sha256=boGqqRGnFR-Mzo_RQOjhAzNvsb7QxZddSwMKROzkk9Q,5163
1186
1187
  django_cfg/static/js/api/base.mjs,sha256=KUxZHHdELAV8mNnACpwJRvaQhdJxp-n5LFEQ4oUZxBo,4707
1187
1188
  django_cfg/static/js/api/index.mjs,sha256=_-Q04jjHcgwi4CGfiaLyiOR6NW7Yu1HBhJWp2J1cjpc,2538
@@ -1204,7 +1205,7 @@ django_cfg/static/js/api/tasks/client.mjs,sha256=tIy8K-finXzTUL9kOo_L4Q1kchDaHyu
1204
1205
  django_cfg/static/js/api/tasks/index.mjs,sha256=yCY1GzdD-RtFZ3pAfk1l0msgO1epyo0lsGCjH0g1Afc,294
1205
1206
  django_cfg/templates/__init__.py,sha256=IzLjt-a7VIJ0OutmAE1_-w0_LpL2u0MgGpnIabjZuW8,19
1206
1207
  django_cfg/templates/admin/DUAL_TAB_ARCHITECTURE.md,sha256=CL8E3K4rFpXeQiNgrYSMvCW1y-eFaoXxxsI58zPf9dY,17562
1207
- django_cfg/templates/admin/index.html,sha256=qF821_eo6eCx1PeBmw9q3P498iN6F6FgVMqt1Z3cHDs,34897
1208
+ django_cfg/templates/admin/index.html,sha256=MfDz35In0iOI_HjoHldTq0KbR9LIcp1FgQZTZmzhlJE,34826
1208
1209
  django_cfg/templates/admin/constance/includes/results_list.html,sha256=Itzs1lGqOYg6ftJUjQ1jWmsbdXDKdov3cDtqMllxih8,3835
1209
1210
  django_cfg/templates/emails/base_email.html,sha256=TWcvYa2IHShlF_E8jf1bWZStRO0v8G4L_GexPxvz6XQ,8836
1210
1211
  django_cfg/templates/unfold/layouts/skeleton.html,sha256=2ArkcNZ34mFs30cOAsTQ1EZiDXcB0aVxkO71lJq9SLE,718
@@ -1218,9 +1219,9 @@ django_cfg/utils/version_check.py,sha256=WO51J2m2e-wVqWCRwbultEwu3q1lQasV67Mw2aa
1218
1219
  django_cfg/CHANGELOG.md,sha256=jtT3EprqEJkqSUh7IraP73vQ8PmKUMdRtznQsEnqDZk,2052
1219
1220
  django_cfg/CONTRIBUTING.md,sha256=DU2kyQ6PU0Z24ob7O_OqKWEYHcZmJDgzw-lQCmu6uBg,3041
1220
1221
  django_cfg/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
1221
- django_cfg/pyproject.toml,sha256=5zsoysCAJSPz41gaD1lnRT_jebaXJbKqQO8TNxp8WvA,8868
1222
- django_cfg-1.5.29.dist-info/METADATA,sha256=xS2dloSDCHxM1rCDLBBaUQy7XvJC1mdTsvK2BMBIGl8,28377
1223
- django_cfg-1.5.29.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
1224
- django_cfg-1.5.29.dist-info/entry_points.txt,sha256=Ucmde4Z2wEzgb4AggxxZ0zaYDb9HpyE5blM3uJ0_VNg,56
1225
- django_cfg-1.5.29.dist-info/licenses/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
1226
- django_cfg-1.5.29.dist-info/RECORD,,
1222
+ django_cfg/pyproject.toml,sha256=q2h83EyAumlN37F-_O6bas7tz-Dbtb1w-RcMZKsqtLg,8868
1223
+ django_cfg-1.5.31.dist-info/METADATA,sha256=2AFiJZJDolxFzpZQQNQEVnM7loaXthn6r0QEJFMpwRo,28377
1224
+ django_cfg-1.5.31.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
1225
+ django_cfg-1.5.31.dist-info/entry_points.txt,sha256=Ucmde4Z2wEzgb4AggxxZ0zaYDb9HpyE5blM3uJ0_VNg,56
1226
+ django_cfg-1.5.31.dist-info/licenses/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
1227
+ django_cfg-1.5.31.dist-info/RECORD,,