django-cfg 1.4.5__py3-none-any.whl → 1.4.7__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.
@@ -6,7 +6,8 @@ Utility for checking all registered Django URL endpoints.
6
6
 
7
7
  import time
8
8
  import re
9
- from typing import List, Dict, Any, Optional
9
+ import uuid
10
+ from typing import List, Dict, Any, Optional, Tuple
10
11
  from django.urls import get_resolver, URLPattern, URLResolver
11
12
  from django.test import Client
12
13
  from django.utils import timezone
@@ -49,6 +50,7 @@ def should_check_endpoint(url_pattern: str, url_name: Optional[str] = None) -> b
49
50
  - Static/media files
50
51
  - Django internal endpoints
51
52
  - Schema/Swagger/Redoc documentation endpoints
53
+ - DRF format suffix patterns (causes kwarg errors)
52
54
 
53
55
  Args:
54
56
  url_pattern: URL pattern string
@@ -73,6 +75,11 @@ def should_check_endpoint(url_pattern: str, url_name: Optional[str] = None) -> b
73
75
  if re.match(pattern, url_pattern):
74
76
  return False
75
77
 
78
+ # Exclude DRF format suffix patterns (e.g., \.(?P<format>[a-z0-9]+))
79
+ # These cause "got an unexpected keyword argument 'format'" errors in action methods
80
+ if r'\.(?P<format>' in url_pattern or '<drf_format_suffix:' in url_pattern:
81
+ return False
82
+
76
83
  # Exclude URL names
77
84
  exclude_names = [
78
85
  'django_cfg_health',
@@ -89,6 +96,147 @@ def should_check_endpoint(url_pattern: str, url_name: Optional[str] = None) -> b
89
96
  return True
90
97
 
91
98
 
99
+ def get_test_value_for_parameter(param_name: str, param_pattern: str) -> str:
100
+ """
101
+ Generate appropriate test value for URL parameter based on name and pattern.
102
+
103
+ Args:
104
+ param_name: Parameter name (e.g., 'slug', 'pk', 'id', 'uuid')
105
+ param_pattern: Regex pattern for the parameter
106
+
107
+ Returns:
108
+ Test value string
109
+
110
+ Examples:
111
+ slug -> 'test-slug'
112
+ pk -> '1'
113
+ id -> '1'
114
+ uuid -> generated UUID
115
+ format -> 'json'
116
+ """
117
+ param_lower = param_name.lower()
118
+
119
+ # UUID parameters
120
+ if 'uuid' in param_lower:
121
+ return str(uuid.uuid4())
122
+
123
+ # Primary key / ID parameters
124
+ if param_lower in ['pk', 'id']:
125
+ return '1'
126
+
127
+ # Slug parameters
128
+ if 'slug' in param_lower:
129
+ return 'test-slug'
130
+
131
+ # Format parameters (for DRF format suffixes)
132
+ if 'format' in param_lower:
133
+ return 'json'
134
+
135
+ # Username parameters
136
+ if 'username' in param_lower:
137
+ return 'testuser'
138
+
139
+ # Year/Month/Day parameters
140
+ if param_lower == 'year':
141
+ return '2024'
142
+ if param_lower == 'month':
143
+ return '01'
144
+ if param_lower == 'day':
145
+ return '01'
146
+
147
+ # Generic string parameter - check pattern
148
+ if '[a-z0-9]' in param_pattern or '[\\w]' in param_pattern:
149
+ return 'test'
150
+
151
+ # Numeric parameter
152
+ if '[0-9]' in param_pattern or '\\d' in param_pattern:
153
+ return '1'
154
+
155
+ # Default
156
+ return 'test'
157
+
158
+
159
+ def resolve_parametrized_url(url_pattern: str) -> Optional[str]:
160
+ """
161
+ Resolve URL pattern with parameters to concrete URL with test values.
162
+
163
+ Supports both regex patterns and Django typed path converters.
164
+
165
+ Args:
166
+ url_pattern: URL pattern with Django parameters
167
+
168
+ Returns:
169
+ Resolved URL with test values, or None if cannot resolve
170
+
171
+ Examples:
172
+ Regex patterns:
173
+ '/api/products/(?P<slug>[^/]+)/' -> '/api/products/test-slug/'
174
+ '/api/users/(?P<pk>[0-9]+)/' -> '/api/users/1/'
175
+
176
+ Typed converters:
177
+ '/api/products/<int:pk>/' -> '/api/products/1/'
178
+ '/api/posts/<slug:slug>/' -> '/api/posts/test-slug/'
179
+ '/api/items/<uuid:item_id>/' -> '/api/items/<uuid>/'
180
+ """
181
+ resolved_url = url_pattern
182
+
183
+ # First, handle Django typed path converters: <converter:name>
184
+ # Pattern: <type:name>
185
+ typed_converter_regex = r'<([^:>]+):([^>]+)>'
186
+
187
+ typed_matches = list(re.finditer(typed_converter_regex, url_pattern))
188
+
189
+ for match in typed_matches:
190
+ converter_type = match.group(1)
191
+ param_name = match.group(2)
192
+ full_match = match.group(0)
193
+
194
+ # Get test value based on converter type
195
+ if converter_type == 'int':
196
+ test_value = '1'
197
+ elif converter_type == 'slug':
198
+ test_value = 'test-slug'
199
+ elif converter_type == 'uuid':
200
+ test_value = str(uuid.uuid4())
201
+ elif converter_type == 'str':
202
+ test_value = get_test_value_for_parameter(param_name, '')
203
+ elif converter_type == 'path':
204
+ test_value = 'test/path'
205
+ elif converter_type == 'drf_format_suffix':
206
+ test_value = 'json'
207
+ else:
208
+ # Unknown converter - use parameter name to guess
209
+ test_value = get_test_value_for_parameter(param_name, '')
210
+
211
+ # Replace typed converter with test value
212
+ resolved_url = resolved_url.replace(full_match, test_value, 1)
213
+
214
+ # Then handle regex patterns: (?P<name>pattern)
215
+ param_regex = r'\(\?P<([^>]+)>([^)]+)\)'
216
+
217
+ regex_matches = re.finditer(param_regex, resolved_url)
218
+
219
+ for match in regex_matches:
220
+ param_name = match.group(1)
221
+ param_pattern = match.group(2)
222
+ full_match = match.group(0)
223
+
224
+ # Get test value for this parameter
225
+ test_value = get_test_value_for_parameter(param_name, param_pattern)
226
+
227
+ # Replace parameter with test value
228
+ resolved_url = resolved_url.replace(full_match, test_value, 1)
229
+
230
+ # Clean up any remaining regex syntax
231
+ resolved_url = re.sub(r'[\^$\\]', '', resolved_url)
232
+
233
+ # Check if resolution was successful (no parameter patterns left)
234
+ if '(?P<' in resolved_url or '<' in resolved_url:
235
+ return None
236
+
237
+ return resolved_url
238
+
239
+
92
240
  def collect_endpoints(
93
241
  urlpatterns=None,
94
242
  prefix: str = '',
@@ -158,18 +306,6 @@ def collect_endpoints(
158
306
  if not should_check_endpoint(clean_pattern, url_name):
159
307
  continue
160
308
 
161
- # Skip patterns with required parameters (for now)
162
- if '<' in clean_pattern:
163
- endpoints.append({
164
- 'url': clean_pattern,
165
- 'url_name': url_name,
166
- 'namespace': namespace,
167
- 'group': get_url_group(clean_pattern),
168
- 'status': 'skipped',
169
- 'reason': 'requires_parameters',
170
- })
171
- continue
172
-
173
309
  # Get view info
174
310
  view_name = 'unknown'
175
311
  if hasattr(pattern, 'callback'):
@@ -179,14 +315,44 @@ def collect_endpoints(
179
315
  elif hasattr(callback, '__name__'):
180
316
  view_name = callback.__name__
181
317
 
182
- endpoints.append({
183
- 'url': clean_pattern,
184
- 'url_name': url_name,
185
- 'namespace': namespace,
186
- 'group': get_url_group(clean_pattern),
187
- 'view': view_name,
188
- 'status': 'pending',
189
- })
318
+ # Handle patterns with parameters
319
+ if '<' in clean_pattern or '(?P<' in clean_pattern:
320
+ # Try to resolve with test values
321
+ resolved_url = resolve_parametrized_url(clean_pattern)
322
+
323
+ if resolved_url:
324
+ # Successfully resolved - can test it
325
+ endpoints.append({
326
+ 'url': resolved_url,
327
+ 'url_pattern': clean_pattern, # Keep original pattern for reference
328
+ 'url_name': url_name,
329
+ 'namespace': namespace,
330
+ 'group': get_url_group(clean_pattern),
331
+ 'view': view_name,
332
+ 'status': 'pending',
333
+ 'has_parameters': True,
334
+ })
335
+ else:
336
+ # Cannot resolve - skip
337
+ endpoints.append({
338
+ 'url': clean_pattern,
339
+ 'url_name': url_name,
340
+ 'namespace': namespace,
341
+ 'group': get_url_group(clean_pattern),
342
+ 'view': view_name,
343
+ 'status': 'skipped',
344
+ 'reason': 'cannot_resolve_parameters',
345
+ })
346
+ else:
347
+ # No parameters - can test directly
348
+ endpoints.append({
349
+ 'url': clean_pattern,
350
+ 'url_name': url_name,
351
+ 'namespace': namespace,
352
+ 'group': get_url_group(clean_pattern),
353
+ 'view': view_name,
354
+ 'status': 'pending',
355
+ })
190
356
 
191
357
  return endpoints
192
358
 
@@ -266,7 +432,11 @@ def check_endpoint(
266
432
  start_time = time.time()
267
433
 
268
434
  # First attempt - without auth
269
- extra_headers = {'SERVER_NAME': 'localhost'}
435
+ # Add special header to bypass rate limiting for internal checks
436
+ extra_headers = {
437
+ 'SERVER_NAME': 'localhost',
438
+ 'HTTP_X_DJANGO_CFG_INTERNAL_CHECK': 'true'
439
+ }
270
440
  response = client.get(url, timeout=timeout, **extra_headers)
271
441
  response_time = (time.time() - start_time) * 1000 # Convert to ms
272
442
  status_code = response.status_code
@@ -294,6 +464,7 @@ def check_endpoint(
294
464
  # 401, 403: Auth required (expected, still healthy)
295
465
  # 404: Not found (might be OK if endpoint exists but has no data)
296
466
  # 405: Method not allowed (endpoint exists, just wrong method)
467
+ # 429: Rate limited (expected for rate-limited APIs, still healthy)
297
468
  # 500+: Server errors (unhealthy)
298
469
 
299
470
  is_healthy = status_code in [
@@ -301,13 +472,16 @@ def check_endpoint(
301
472
  301, 302, 303, 307, 308, # Redirects
302
473
  401, 403, # Auth required (expected)
303
474
  405, # Method not allowed (endpoint exists)
475
+ 429, # Rate limited (expected for rate-limited APIs)
304
476
  ]
305
477
 
306
478
  # Special handling for 404
479
+ reason = None
307
480
  if status_code == 404:
308
481
  # 404 might be OK for some endpoints (e.g., detail views with no data)
309
482
  # Mark as warning rather than unhealthy
310
483
  is_healthy = None # Will be marked as 'warning'
484
+ reason = 'Not Found - endpoint works but no data exists (empty list or test object not found)'
311
485
 
312
486
  endpoint.update({
313
487
  'status_code': status_code,
@@ -317,16 +491,39 @@ def check_endpoint(
317
491
  'last_checked': timezone.now().isoformat(),
318
492
  })
319
493
 
494
+ if reason:
495
+ endpoint['reason'] = reason
496
+
320
497
  if requires_auth:
321
498
  endpoint['required_auth'] = True
322
499
 
500
+ if status_code == 429:
501
+ endpoint['rate_limited'] = True
502
+
323
503
  except Exception as e:
504
+ from django.db import DatabaseError, OperationalError
505
+
506
+ # Multi-database compatibility: treat DB errors as warnings, not errors
507
+ # Common in multi-database setups with db_constraint=False ForeignKeys
508
+ is_db_error = isinstance(e, (DatabaseError, OperationalError))
509
+ error_message = str(e)[:200]
510
+
511
+ # Check for cross-database JOIN errors (common with SQLite multi-db)
512
+ is_cross_db_error = any(keyword in str(e).lower() for keyword in [
513
+ 'no such table',
514
+ 'no such column',
515
+ 'cannot join',
516
+ 'cross-database',
517
+ 'multi-database'
518
+ ])
519
+
324
520
  endpoint.update({
325
521
  'status_code': None,
326
522
  'response_time_ms': None,
327
- 'is_healthy': False,
328
- 'status': 'error',
329
- 'error': str(e)[:200], # Truncate long errors
523
+ 'is_healthy': False if not (is_db_error or is_cross_db_error) else None,
524
+ 'status': 'warning' if (is_db_error or is_cross_db_error) else 'error',
525
+ 'error': error_message,
526
+ 'error_type': 'database' if (is_db_error or is_cross_db_error) else 'general',
330
527
  'last_checked': timezone.now().isoformat(),
331
528
  })
332
529
 
@@ -23,6 +23,7 @@ class DRFEndpointsStatusView(APIView):
23
23
  Query Parameters:
24
24
  - include_unnamed: Include endpoints without names (default: false)
25
25
  - timeout: Request timeout in seconds (default: 5)
26
+ - auto_auth: Auto-retry with JWT on 401/403 (default: true)
26
27
 
27
28
  This endpoint uses DRF Browsable API with Tailwind CSS theme! 🎨
28
29
  """
@@ -35,11 +36,13 @@ class DRFEndpointsStatusView(APIView):
35
36
  # Get query parameters
36
37
  include_unnamed = request.query_params.get('include_unnamed', 'false').lower() == 'true'
37
38
  timeout = int(request.query_params.get('timeout', 5))
39
+ auto_auth = request.query_params.get('auto_auth', 'true').lower() == 'true'
38
40
 
39
41
  # Check all endpoints
40
42
  status_data = check_all_endpoints(
41
43
  include_unnamed=include_unnamed,
42
- timeout=timeout
44
+ timeout=timeout,
45
+ auto_auth=auto_auth
43
46
  )
44
47
 
45
48
  # Return appropriate HTTP status
@@ -11,7 +11,12 @@ class EndpointSerializer(serializers.Serializer):
11
11
  """Serializer for single endpoint status."""
12
12
 
13
13
  url = serializers.CharField(
14
- help_text="URL pattern of the endpoint"
14
+ help_text="Resolved URL (for parametrized URLs) or URL pattern"
15
+ )
16
+ url_pattern = serializers.CharField(
17
+ required=False,
18
+ allow_null=True,
19
+ help_text="Original URL pattern (for parametrized URLs)"
15
20
  )
16
21
  url_name = serializers.CharField(
17
22
  required=False,
@@ -53,21 +58,36 @@ class EndpointSerializer(serializers.Serializer):
53
58
  allow_blank=True,
54
59
  help_text="Error message if check failed"
55
60
  )
61
+ error_type = serializers.CharField(
62
+ required=False,
63
+ allow_blank=True,
64
+ help_text="Error type: database, general, etc."
65
+ )
56
66
  reason = serializers.CharField(
57
67
  required=False,
58
68
  allow_blank=True,
59
- help_text="Reason for skip (if skipped)"
69
+ help_text="Reason for warning/skip"
60
70
  )
61
71
  last_checked = serializers.DateTimeField(
62
72
  required=False,
63
73
  allow_null=True,
64
74
  help_text="Timestamp of last check"
65
75
  )
76
+ has_parameters = serializers.BooleanField(
77
+ required=False,
78
+ default=False,
79
+ help_text="Whether URL has parameters that were resolved with test values"
80
+ )
66
81
  required_auth = serializers.BooleanField(
67
82
  required=False,
68
83
  default=False,
69
84
  help_text="Whether endpoint required JWT authentication"
70
85
  )
86
+ rate_limited = serializers.BooleanField(
87
+ required=False,
88
+ default=False,
89
+ help_text="Whether endpoint returned 429 (rate limited)"
90
+ )
71
91
 
72
92
 
73
93
  class EndpointsStatusSerializer(serializers.Serializer):
@@ -52,6 +52,7 @@ class RateLimitingMiddleware(MiddlewareMixin):
52
52
  self.window_precision = 10 # sub-windows
53
53
  self.exempt_paths = [
54
54
  '/api/health/',
55
+ '/cfg/', # Exempt all django-cfg internal endpoints
55
56
  '/admin/',
56
57
  '/static/',
57
58
  '/media/',
@@ -73,7 +74,7 @@ class RateLimitingMiddleware(MiddlewareMixin):
73
74
  self.burst_allowance = 0.5
74
75
  self.window_size = 60
75
76
  self.window_precision = 10
76
- self.exempt_paths = ['/api/health/', '/admin/']
77
+ self.exempt_paths = ['/api/health/', '/cfg/', '/admin/']
77
78
  self.cache_timeout = 300
78
79
 
79
80
  logger.info(f"Rate Limiting Middleware initialized", extra={
@@ -92,9 +93,14 @@ class RateLimitingMiddleware(MiddlewareMixin):
92
93
  if not self.enabled:
93
94
  return None
94
95
 
95
- # Check if path is exempt
96
- if request.path in self.exempt_paths:
96
+ # Check if this is a django-cfg internal endpoint check (bypass rate limiting)
97
+ if request.META.get('HTTP_X_DJANGO_CFG_INTERNAL_CHECK') == 'true':
97
98
  return None
99
+
100
+ # Check if path is exempt (supports prefix matching)
101
+ for exempt_path in self.exempt_paths:
102
+ if request.path.startswith(exempt_path):
103
+ return None
98
104
 
99
105
  start_time = time.time()
100
106
 
@@ -16,7 +16,7 @@ from drf_spectacular.utils import extend_schema, extend_schema_view
16
16
  from .base import PaymentBaseViewSet, NestedPaymentViewSet, ReadOnlyPaymentViewSet
17
17
  from ...models import APIKey
18
18
  from ..serializers.api_keys import (
19
- APIKeySerializer,
19
+ APIKeyDetailSerializer,
20
20
  APIKeyCreateSerializer,
21
21
  APIKeyListSerializer,
22
22
  APIKeyUpdateSerializer,
@@ -39,16 +39,16 @@ class APIKeyViewSet(PaymentBaseViewSet):
39
39
  """
40
40
 
41
41
  queryset = APIKey.objects.all()
42
- serializer_class = APIKeySerializer
42
+ serializer_class = APIKeyDetailSerializer
43
43
  permission_classes = [permissions.IsAdminUser] # Admin only for global access
44
44
  filterset_fields = ['is_active', 'user']
45
45
  search_fields = ['name', 'user__username', 'user__email']
46
46
  ordering_fields = ['created_at', 'updated_at', 'last_used_at', 'expires_at', 'total_requests']
47
-
47
+
48
48
  serializer_classes = {
49
49
  'list': APIKeyListSerializer,
50
50
  'create': APIKeyCreateSerializer,
51
- 'retrieve': APIKeySerializer,
51
+ 'retrieve': APIKeyDetailSerializer,
52
52
  'update': APIKeyUpdateSerializer,
53
53
  'partial_update': APIKeyUpdateSerializer,
54
54
  }
@@ -209,20 +209,20 @@ class UserAPIKeyViewSet(NestedPaymentViewSet):
209
209
  """
210
210
 
211
211
  queryset = APIKey.objects.all()
212
- serializer_class = APIKeySerializer
212
+ serializer_class = APIKeyDetailSerializer
213
213
  permission_classes = [permissions.IsAuthenticated]
214
214
  filterset_fields = ['is_active']
215
215
  search_fields = ['name']
216
216
  ordering_fields = ['created_at', 'updated_at', 'last_used_at', 'expires_at']
217
-
217
+
218
218
  # Nested ViewSet configuration
219
219
  parent_lookup_field = 'user_pk'
220
220
  parent_model_field = 'user'
221
-
221
+
222
222
  serializer_classes = {
223
223
  'list': APIKeyListSerializer,
224
224
  'create': APIKeyCreateSerializer,
225
- 'retrieve': APIKeySerializer,
225
+ 'retrieve': APIKeyDetailSerializer,
226
226
  'update': APIKeyUpdateSerializer,
227
227
  'partial_update': APIKeyUpdateSerializer,
228
228
  }
@@ -41,7 +41,7 @@ from .currencies import (
41
41
 
42
42
  # API Key serializers
43
43
  from .api_keys import (
44
- APIKeySerializer,
44
+ APIKeyDetailSerializer,
45
45
  APIKeyCreateSerializer,
46
46
  APIKeyListSerializer,
47
47
  APIKeyUpdateSerializer,
@@ -85,7 +85,7 @@ __all__ = [
85
85
  'CurrencyConversionSerializer',
86
86
 
87
87
  # API Key serializers
88
- 'APIKeySerializer',
88
+ 'APIKeyDetailSerializer',
89
89
  'APIKeyCreateSerializer',
90
90
  'APIKeyListSerializer',
91
91
  'APIKeyUpdateSerializer',
@@ -7,6 +7,7 @@ DRF serializers for API key operations with service integration.
7
7
  from rest_framework import serializers
8
8
  from typing import Dict, Any
9
9
  from django.contrib.auth import get_user_model
10
+ from drf_spectacular.utils import extend_schema_serializer, extend_schema_field, OpenApiTypes
10
11
 
11
12
  from ...models import APIKey
12
13
  from django_cfg.modules.django_logging import get_logger
@@ -43,19 +44,19 @@ class APIKeyListSerializer(serializers.ModelSerializer):
43
44
  read_only_fields = fields
44
45
 
45
46
 
46
- class APIKeySerializer(serializers.ModelSerializer):
47
+ class APIKeyDetailSerializer(serializers.ModelSerializer):
47
48
  """
48
49
  Complete API key serializer with full details.
49
-
50
+
50
51
  Used for API key detail views (no key value for security).
51
52
  """
52
-
53
+
53
54
  user = serializers.StringRelatedField(read_only=True)
54
55
  key_preview = serializers.CharField(read_only=True)
55
56
  is_expired = serializers.BooleanField(read_only=True)
56
57
  is_valid = serializers.BooleanField(read_only=True)
57
58
  days_until_expiry = serializers.IntegerField(read_only=True)
58
-
59
+
59
60
  class Meta:
60
61
  model = APIKey
61
62
  fields = [
@@ -73,18 +74,7 @@ class APIKeySerializer(serializers.ModelSerializer):
73
74
  'created_at',
74
75
  'updated_at',
75
76
  ]
76
- read_only_fields = [
77
- 'id',
78
- 'user',
79
- 'key_preview',
80
- 'is_expired',
81
- 'is_valid',
82
- 'days_until_expiry',
83
- 'total_requests',
84
- 'last_used_at',
85
- 'created_at',
86
- 'updated_at',
87
- ]
77
+ read_only_fields = fields # All fields are read-only to prevent TypeScript split
88
78
 
89
79
 
90
80
  class APIKeyCreateSerializer(serializers.Serializer):
@@ -153,7 +143,7 @@ class APIKeyCreateSerializer(serializers.Serializer):
153
143
 
154
144
  def to_representation(self, instance: APIKey) -> Dict[str, Any]:
155
145
  """Return API key data with full key value (only on creation)."""
156
- data = APIKeySerializer(instance, context=self.context).data
146
+ data = APIKeyDetailSerializer(instance, context=self.context).data
157
147
 
158
148
  # Add full key value only on creation (security: shown only once)
159
149
  data['key'] = instance.key
@@ -288,7 +278,7 @@ class APIKeyActionSerializer(serializers.Serializer):
288
278
  return {
289
279
  'success': True,
290
280
  'message': message,
291
- 'api_key': APIKeySerializer(api_key, context=self.context).data
281
+ 'api_key': APIKeyDetailSerializer(api_key, context=self.context).data
292
282
  }
293
283
  else:
294
284
  return {
@@ -341,7 +331,7 @@ class APIKeyValidationSerializer(serializers.Serializer):
341
331
  return {
342
332
  'success': True,
343
333
  'valid': True,
344
- 'api_key': APIKeySerializer(api_key, context=self.context).data,
334
+ 'api_key': APIKeyDetailSerializer(api_key, context=self.context).data,
345
335
  'message': 'API key is valid'
346
336
  }
347
337
  else:
@@ -364,12 +354,12 @@ class APIKeyValidationSerializer(serializers.Serializer):
364
354
  class APIKeyValidationResponseSerializer(serializers.Serializer):
365
355
  """
366
356
  API key validation response serializer.
367
-
357
+
368
358
  Defines the structure of API key validation response for OpenAPI schema.
369
359
  """
370
360
  success = serializers.BooleanField(help_text="Whether the validation was successful")
371
361
  valid = serializers.BooleanField(help_text="Whether the API key is valid")
372
- api_key = APIKeySerializer(allow_null=True, help_text="API key details if valid")
362
+ api_key = APIKeyDetailSerializer(allow_null=True, read_only=True, required=False, help_text="API key details if valid")
373
363
  message = serializers.CharField(help_text="Validation message")
374
364
  error = serializers.CharField(required=False, help_text="Error message if validation failed")
375
365
  error_code = serializers.CharField(required=False, help_text="Error code if validation failed")
@@ -113,7 +113,7 @@ class Command(BaseCommand):
113
113
  self.stdout.write(self.style.HTTP_INFO('🔗 Endpoints:'))
114
114
 
115
115
  for endpoint in data['endpoints']:
116
- name = endpoint.get('name', 'unnamed')
116
+ name = endpoint.get('url_name') or 'unnamed'
117
117
  url = endpoint['url']
118
118
  status = endpoint['status']
119
119
 
@@ -128,18 +128,41 @@ class Command(BaseCommand):
128
128
  style = self.style.ERROR
129
129
 
130
130
  self.stdout.write(f' {icon} {name}')
131
- self.stdout.write(f' URL: {url}')
132
- self.stdout.write(style(f' Status: {status}'))
133
131
 
134
- if endpoint.get('response_time'):
135
- self.stdout.write(f' Response time: {endpoint["response_time"]:.3f}s')
132
+ # Show both pattern and resolved URL for parametrized endpoints
133
+ if endpoint.get('has_parameters') and endpoint.get('url_pattern'):
134
+ self.stdout.write(f' Pattern: {endpoint["url_pattern"]}')
135
+ self.stdout.write(f' Resolved: {url}')
136
+ else:
137
+ self.stdout.write(f' URL: {url}')
138
+
139
+ # Show status with status code
140
+ status_code = endpoint.get('status_code')
141
+ if status_code:
142
+ self.stdout.write(style(f' Status: {status} ({status_code})'))
143
+ else:
144
+ self.stdout.write(style(f' Status: {status}'))
145
+
146
+ if endpoint.get('response_time_ms'):
147
+ self.stdout.write(f' Response time: {endpoint["response_time_ms"]:.2f}ms')
136
148
 
137
149
  if endpoint.get('error'):
138
- self.stdout.write(self.style.ERROR(f' Error: {endpoint["error"]}'))
150
+ error_type = endpoint.get('error_type', 'general')
151
+ if error_type == 'database':
152
+ self.stdout.write(self.style.WARNING(f' ⚠️ DB Error (multi-db): {endpoint["error"]}'))
153
+ else:
154
+ self.stdout.write(self.style.ERROR(f' Error: {endpoint["error"]}'))
155
+
156
+ # Show reason for warnings (e.g., 404 explanations)
157
+ if endpoint.get('reason') and status == 'warning':
158
+ self.stdout.write(self.style.WARNING(f' ⚠️ {endpoint["reason"]}'))
139
159
 
140
160
  if endpoint.get('required_auth'):
141
161
  self.stdout.write(f' 🔐 Required JWT authentication')
142
162
 
163
+ if endpoint.get('rate_limited'):
164
+ self.stdout.write(f' ⏱️ Rate limited (429)')
165
+
143
166
  self.stdout.write('')
144
167
 
145
168
  # Timestamp
@@ -117,7 +117,7 @@ class Command(BaseCommand):
117
117
 
118
118
  process_args = [
119
119
  executable_name,
120
- "django_cfg.modules.dramatiq_setup", # Broker module
120
+ "django_cfg.modules.django_tasks.dramatiq_setup", # Broker module
121
121
  "--processes", str(processes),
122
122
  "--threads", str(threads),
123
123
  "--worker-shutdown-timeout", str(worker_shutdown_timeout),
@@ -155,7 +155,7 @@ class Command(BaseCommand):
155
155
  # Build process arguments exactly like django_dramatiq
156
156
  process_args = [
157
157
  executable_name,
158
- "django_cfg.modules.dramatiq_setup", # Broker module
158
+ "django_cfg.modules.django_tasks.dramatiq_setup", # Broker module
159
159
  "--processes", str(processes),
160
160
  "--threads", str(threads),
161
161
  "--worker-shutdown-timeout", str(worker_shutdown_timeout),
@@ -213,7 +213,7 @@ class Command(BaseCommand):
213
213
  def _discover_tasks_modules(self):
214
214
  """Discover task modules like django_dramatiq does."""
215
215
  # Always include our broker setup module first
216
- tasks_modules = ["django_cfg.modules.dramatiq_setup"]
216
+ tasks_modules = ["django_cfg.modules.django_tasks.dramatiq_setup"]
217
217
 
218
218
  # Get task service for configuration
219
219
  task_service = get_task_service()
@@ -3,6 +3,19 @@ Django Revolution Configuration with DRF Integration
3
3
 
4
4
  Extended configuration model that includes DRF parameters for automatic
5
5
  integration with django_revolution's create_drf_config.
6
+
7
+ TypeScript Client Generation Issue & Solution:
8
+ ----------------------------------------------
9
+ Problem: @hey-api/openapi-ts splits types with mixed readonly/writable fields into
10
+ Readable/Writable versions (e.g., ApiKeyDetailReadable, ApiKeyDetailWritable),
11
+ but references inside other types still use the base name (e.g., ApiKeyDetail),
12
+ causing "Cannot find name 'ApiKeyDetail'" errors.
13
+
14
+ Solution: Make all fields in detail serializers read-only to prevent splitting:
15
+ class Meta:
16
+ read_only_fields = fields # All fields read-only prevents TS split
17
+
18
+ This ensures TypeScript generator creates a single type without Readable/Writable suffix.
6
19
  """
7
20
 
8
21
  from typing import Dict, Any, Optional
@@ -104,7 +117,7 @@ class ExtendedRevolutionConfig(BaseDjangoRevolutionConfig):
104
117
  description="Support tickets and messages API",
105
118
  public=False,
106
119
  auth_required=True,
107
- version="v1",
120
+ # version="v1",
108
121
  )
109
122
 
110
123
  # Add Accounts zone if enabled
@@ -116,9 +129,9 @@ class ExtendedRevolutionConfig(BaseDjangoRevolutionConfig):
116
129
  description="User management, OTP, profiles, and activity tracking API",
117
130
  public=False,
118
131
  auth_required=True,
119
- version="v1",
132
+ # version="v1",
120
133
  )
121
-
134
+
122
135
  # Add Newsletter zone if enabled
123
136
  default_newsletter_zone = 'cfg_newsletter'
124
137
  if newsletter_enabled and default_newsletter_zone not in zones:
@@ -128,9 +141,9 @@ class ExtendedRevolutionConfig(BaseDjangoRevolutionConfig):
128
141
  description="Email campaigns, subscriptions, and newsletter management API",
129
142
  public=False,
130
143
  auth_required=True,
131
- version="v1",
144
+ # version="v1",
132
145
  )
133
-
146
+
134
147
  # Add Leads zone if enabled
135
148
  default_leads_zone = 'cfg_leads'
136
149
  if leads_enabled and default_leads_zone not in zones:
@@ -140,9 +153,9 @@ class ExtendedRevolutionConfig(BaseDjangoRevolutionConfig):
140
153
  description="Lead collection, contact forms, and CRM integration API",
141
154
  public=True, # Leads can be public for contact forms
142
155
  auth_required=False,
143
- version="v1",
156
+ # version="v1",
144
157
  )
145
-
158
+
146
159
  # Add Knowbase zone if enabled
147
160
  default_knowbase_zone = 'cfg_knowbase'
148
161
  if knowbase_enabled and default_knowbase_zone not in zones:
@@ -152,9 +165,9 @@ class ExtendedRevolutionConfig(BaseDjangoRevolutionConfig):
152
165
  description="Knowledge base, AI chat, embeddings, and search API",
153
166
  public=False,
154
167
  auth_required=True,
155
- version="v1",
168
+ # version="v1",
156
169
  )
157
-
170
+
158
171
  # Add Agents zone if enabled
159
172
  default_agents_zone = 'cfg_agents'
160
173
  if agents_enabled and default_agents_zone not in zones:
@@ -164,9 +177,9 @@ class ExtendedRevolutionConfig(BaseDjangoRevolutionConfig):
164
177
  description="Agent definitions, executions, workflows, and tools API",
165
178
  public=False,
166
179
  auth_required=True,
167
- version="v1",
180
+ # version="v1",
168
181
  )
169
-
182
+
170
183
  # Add Tasks zone if enabled
171
184
  default_tasks_zone = 'cfg_tasks'
172
185
  if tasks_enabled and default_tasks_zone not in zones:
@@ -176,7 +189,7 @@ class ExtendedRevolutionConfig(BaseDjangoRevolutionConfig):
176
189
  description="Tasks, workflows, and automation API",
177
190
  public=False,
178
191
  auth_required=True,
179
- version="v1",
192
+ # version="v1",
180
193
  )
181
194
 
182
195
  # Add Payments zone if enabled
@@ -188,7 +201,7 @@ class ExtendedRevolutionConfig(BaseDjangoRevolutionConfig):
188
201
  description="Payments, subscriptions, and billing API",
189
202
  public=False,
190
203
  auth_required=True,
191
- version="v1",
204
+ # version="v1",
192
205
  )
193
206
 
194
207
  except Exception:
@@ -61,6 +61,7 @@ class DashboardManager(BaseCfgModule):
61
61
  NavigationItem(title="Overview", icon=Icons.DASHBOARD, link="/admin/"),
62
62
  NavigationItem(title="Settings", icon=Icons.SETTINGS, link="/admin/constance/config/"),
63
63
  NavigationItem(title="Health Check", icon=Icons.HEALTH_AND_SAFETY, link="/cfg/health/"),
64
+ NavigationItem(title="Endpoints Status", icon=Icons.API, link="/cfg/endpoints/drf/"),
64
65
  ]
65
66
  ),
66
67
  ]
django_cfg/pyproject.toml CHANGED
@@ -4,13 +4,13 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "django-cfg"
7
- version = "1.4.5"
7
+ version = "1.4.7"
8
8
  description = "Django AI framework with built-in agents, type-safe Pydantic v2 configuration, and 8 enterprise apps. Replace settings.py, validate at startup, 90% less code. Production-ready AI workflows for Django."
9
9
  readme = "README.md"
10
10
  keywords = [ "django", "configuration", "pydantic", "settings", "type-safety", "pydantic-settings", "django-environ", "startup-validation", "ide-autocomplete", "ai-agents", "enterprise-django", "django-settings", "type-safe-config",]
11
11
  classifiers = [ "Development Status :: 4 - Beta", "Framework :: Django", "Framework :: Django :: 5.2", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Systems Administration", "Typing :: Typed",]
12
12
  requires-python = ">=3.12,<4.0"
13
- dependencies = [ "pydantic>=2.11.0,<3.0", "pydantic[email]>=2.11.0,<3.0", "PyYAML>=6.0,<7.0", "pydantic-yaml>=1.6.0,<2.0", "click>=8.2.0,<9.0", "questionary>=2.1.0,<3.0", "rich>=14.0.0,<15.0", "cloudflare>=4.3.0,<5.0", "loguru>=0.7.0,<1.0", "colorlog>=6.9.0,<7.0", "cachetools>=5.3.0,<7.0", "toml>=0.10.2,<0.11.0", "ngrok>=1.5.1; python_version>='3.12'", "psycopg[binary,pool]>=3.2.0,<4.0", "dj-database-url>=3.0.0,<4.0", "whitenoise>=6.8.0,<7.0", "django-cors-headers>=4.7.0,<5.0", "djangorestframework>=3.16.0,<4.0", "djangorestframework-simplejwt>=5.5.0,<6.0", "djangorestframework-simplejwt[token-blacklist]>=5.5.0,<6.0", "drf-nested-routers>=0.94.0,<1.0", "django-filter>=25.0,<26.0", "django-ratelimit>=4.1.0,<5.0.0", "drf-spectacular>=0.28.0,<1.0", "drf-spectacular-sidecar>=2025.8.0,<2026.0", "django-json-widget>=2.0.0,<3.0", "django-import-export>=4.3.0,<5.0", "django-extensions>=4.1.0,<5.0", "django-constance>=4.3.0,<5.0", "django-unfold>=0.64.0,<1.0", "django-redis>=6.0.0,<7.0", "redis>=6.4.0,<7.0", "hiredis>=2.0.0,<4.0", "dramatiq[redis]>=1.18.0,<2.0", "django-dramatiq>=0.14.0,<1.0", "pyTelegramBotAPI>=4.28.0,<5.0", "coolname>=2.2.0,<3.0", "django-admin-rangefilter>=0.13.0,<1.0", "python-json-logger>=3.3.0,<4.0", "requests>=2.32.0,<3.0", "tiktoken>=0.11.0,<1.0", "openai>=1.107.0,<2.0", "twilio>=9.8.0,<10.0", "sendgrid>=6.12.0,<7.0", "beautifulsoup4>=4.13.0,<5.0", "lxml>=6.0.0,<7.0", "pgvector>=0.4.0,<1.0", "pydantic-ai>=1.0.10,<2.0", "django-revolution>=1.0.43,<2.0", "tenacity>=9.1.2,<10.0.0", "mypy (>=1.18.2,<2.0.0)", "django-tailwind[reload] (>=4.2.0,<5.0.0)",]
13
+ dependencies = [ "pydantic>=2.11.0,<3.0", "pydantic[email]>=2.11.0,<3.0", "PyYAML>=6.0,<7.0", "pydantic-yaml>=1.6.0,<2.0", "click>=8.2.0,<9.0", "questionary>=2.1.0,<3.0", "rich>=14.0.0,<15.0", "cloudflare>=4.3.0,<5.0", "loguru>=0.7.0,<1.0", "colorlog>=6.9.0,<7.0", "cachetools>=5.3.0,<7.0", "toml>=0.10.2,<0.11.0", "ngrok>=1.5.1; python_version>='3.12'", "psycopg[binary,pool]>=3.2.0,<4.0", "dj-database-url>=3.0.0,<4.0", "whitenoise>=6.8.0,<7.0", "django-cors-headers>=4.7.0,<5.0", "djangorestframework>=3.16.0,<4.0", "djangorestframework-simplejwt>=5.5.0,<6.0", "djangorestframework-simplejwt[token-blacklist]>=5.5.0,<6.0", "drf-nested-routers>=0.94.0,<1.0", "django-filter>=25.0,<26.0", "django-ratelimit>=4.1.0,<5.0.0", "drf-spectacular>=0.28.0,<1.0", "drf-spectacular-sidecar>=2025.8.0,<2026.0", "django-json-widget>=2.0.0,<3.0", "django-import-export>=4.3.0,<5.0", "django-extensions>=4.1.0,<5.0", "django-constance>=4.3.0,<5.0", "django-unfold>=0.64.0,<1.0", "django-redis>=6.0.0,<7.0", "redis>=6.4.0,<7.0", "hiredis>=2.0.0,<4.0", "dramatiq[redis]>=1.18.0,<2.0", "django-dramatiq>=0.14.0,<1.0", "pyTelegramBotAPI>=4.28.0,<5.0", "coolname>=2.2.0,<3.0", "django-admin-rangefilter>=0.13.0,<1.0", "python-json-logger>=3.3.0,<4.0", "requests>=2.32.0,<3.0", "tiktoken>=0.11.0,<1.0", "openai>=1.107.0,<2.0", "twilio>=9.8.0,<10.0", "sendgrid>=6.12.0,<7.0", "beautifulsoup4>=4.13.0,<5.0", "lxml>=6.0.0,<7.0", "pgvector>=0.4.0,<1.0", "pydantic-ai>=1.0.10,<2.0", "django-revolution>=1.0.44,<2.0", "tenacity>=9.1.2,<10.0.0", "mypy (>=1.18.2,<2.0.0)", "django-tailwind[reload] (>=4.2.0,<5.0.0)",]
14
14
  [[project.authors]]
15
15
  name = "Django-CFG Team"
16
16
  email = "info@djangocfg.com"
@@ -116,14 +116,6 @@ directory = "htmlcov"
116
116
  [tool.poetry.group.local]
117
117
  optional = true
118
118
 
119
- [tool.hatch.build.targets.wheel]
120
- packages = [ "src/django_cfg",]
121
- exclude = [ "scripts/",]
122
-
123
- [tool.hatch.build.targets.sdist]
124
- include = [ "src/django_cfg", "README.md", "LICENSE", "CHANGELOG.md", "CONTRIBUTING.md", "requirements*.txt", "MANIFEST.in",]
125
- exclude = [ "@*", "tests", "scripts", "*.log", ".env*",]
126
-
127
119
  [tool.poetry.group.dev.dependencies]
128
120
  tomlkit = "^0.13.3"
129
121
  build = "^1.3.0"
@@ -137,12 +129,24 @@ pytest = "^8.4.2"
137
129
  pytest-django = "^4.11.1"
138
130
  fakeredis = "^2.31.3"
139
131
 
132
+ [tool.hatch.build.targets.wheel]
133
+ packages = [ "src/django_cfg",]
134
+ exclude = [ "scripts/",]
135
+
136
+ [tool.hatch.build.targets.sdist]
137
+ include = [ "src/django_cfg", "README.md", "LICENSE", "CHANGELOG.md", "CONTRIBUTING.md", "requirements*.txt", "MANIFEST.in",]
138
+ exclude = [ "@*", "tests", "scripts", "*.log", ".env*",]
139
+
140
+ [tool.poetry.group.local.dependencies.django-ipc]
141
+ path = "/Users/markinmatrix/djangoipc"
142
+ develop = true
143
+
144
+ [tool.poetry.group.local.dependencies.django-revolution]
145
+ path = "/Users/markinmatrix/revolution"
146
+ develop = true
147
+
140
148
  [tool.hatch.build.targets.wheel.force-include]
141
149
  LICENSE = "django_cfg/LICENSE"
142
150
  "CONTRIBUTING.md" = "django_cfg/CONTRIBUTING.md"
143
151
  "CHANGELOG.md" = "django_cfg/CHANGELOG.md"
144
152
  "pyproject.toml" = "django_cfg/pyproject.toml"
145
-
146
- [tool.poetry.group.local.dependencies.django-ipc]
147
- path = "/Users/markinmatrix/djangoipc"
148
- develop = true
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-cfg
3
- Version: 1.4.5
3
+ Version: 1.4.7
4
4
  Summary: Django AI framework with built-in agents, type-safe Pydantic v2 configuration, and 8 enterprise apps. Replace settings.py, validate at startup, 90% less code. Production-ready AI workflows for Django.
5
5
  Project-URL: Homepage, https://djangocfg.com
6
6
  Project-URL: Documentation, https://djangocfg.com
@@ -45,7 +45,7 @@ Requires-Dist: django-import-export<5.0,>=4.3.0
45
45
  Requires-Dist: django-json-widget<3.0,>=2.0.0
46
46
  Requires-Dist: django-ratelimit<5.0.0,>=4.1.0
47
47
  Requires-Dist: django-redis<7.0,>=6.0.0
48
- Requires-Dist: django-revolution<2.0,>=1.0.43
48
+ Requires-Dist: django-revolution<2.0,>=1.0.44
49
49
  Requires-Dist: django-tailwind[reload]<5.0.0,>=4.2.0
50
50
  Requires-Dist: django-unfold<1.0,>=0.64.0
51
51
  Requires-Dist: djangorestframework-simplejwt<6.0,>=5.5.0
@@ -103,9 +103,9 @@ django_cfg/apps/api/commands/__init__.py,sha256=FTmBMxSpI9rO6EljgkWn8e9pxh07ao5Y
103
103
  django_cfg/apps/api/commands/urls.py,sha256=k7auWLPi3FiCSkBwuDMK9R-jeHKfrFNzr7LvOWuGdxA,360
104
104
  django_cfg/apps/api/commands/views.py,sha256=OdSWzEjTM5SL9NJvaJB6pe1_wjevjWrOWWWwo4sB7Uo,10069
105
105
  django_cfg/apps/api/endpoints/__init__.py,sha256=uHjV4E24Aj0UFgv7bW1Z0kH_NFe8PItNFuS105vvRz0,108
106
- django_cfg/apps/api/endpoints/checker.py,sha256=n9jYoDHuwkMHRYGn5J5QEwtUQlS3nh3IV37B9B6Vsuc,12537
107
- django_cfg/apps/api/endpoints/drf_views.py,sha256=T5Fy4-0OY5nr2OpGM8YOdcXt53O7f1V79MQeXs0DUQo,1769
108
- django_cfg/apps/api/endpoints/serializers.py,sha256=bsTAKuk8rodeVPRln4bCGq95_Q0pw9ePnSRsFtb9RJY,2978
106
+ django_cfg/apps/api/endpoints/checker.py,sha256=mwaISxa26u0OTYEmM_ith_T03EeXkZQPA5bVrGrCsf0,19373
107
+ django_cfg/apps/api/endpoints/drf_views.py,sha256=oZLNq_OKs5DK9rZzGu5yVDwn7-GiIAS7MSokiQ3tGdo,1954
108
+ django_cfg/apps/api/endpoints/serializers.py,sha256=W5Az0m1jUi8rLLIMoKVBADk6KHFyAWrwJ_gzSinktno,3656
109
109
  django_cfg/apps/api/endpoints/tests.py,sha256=hvMgYVSWI_dfoSgb1ow9EwVXXUE9uZeCUEvNo1H995k,9992
110
110
  django_cfg/apps/api/endpoints/urls.py,sha256=QaGFwwA_rSq-qN0kcqM8LvTgTq_YlMd7MOAZCbC9E-k,372
111
111
  django_cfg/apps/api/endpoints/views.py,sha256=sqvl07krrJur4ZhzB3QWK1ppEvuVSEX0zSoIQ-farDQ,1242
@@ -358,7 +358,7 @@ django_cfg/apps/payments/management/commands/process_pending_payments.py,sha256=
358
358
  django_cfg/apps/payments/management/commands/test_providers.py,sha256=IvvJhTNw6KQm1EeWYTUMew0ZHzgUGWpG07JOhrpJEP0,18476
359
359
  django_cfg/apps/payments/middleware/__init__.py,sha256=eL5TmlCKmpW53Ift5rtwS8ss1wUqp4j2gzjGhcAQUQY,380
360
360
  django_cfg/apps/payments/middleware/api_access.py,sha256=lWX9A1UpIwPNC5320QcSVhHU_mg-LxzKaYG1bNjvN-I,16713
361
- django_cfg/apps/payments/middleware/rate_limiting.py,sha256=m7GqkbQvLQMGqXACwm8m-2DkcyhedCvqbH0pItBcfwU,14814
361
+ django_cfg/apps/payments/middleware/rate_limiting.py,sha256=nbObLQwIssxsVwovG6S_SiEpQcYmJIyAmA4tUtIt2cg,15163
362
362
  django_cfg/apps/payments/middleware/usage_tracking.py,sha256=dY7n6lZFUo-5a49VzJnZAT6UmbaU7kHei9Xu02KXACw,11815
363
363
  django_cfg/apps/payments/migrations/0001_initial.py,sha256=uLkgbaSvdPjoTHZ3pVB7aEgVPq_hAyQRWXFqC9-2oGc,48359
364
364
  django_cfg/apps/payments/migrations/0002_rename_payments_un_user_id_7f6e79_idx_payments_un_user_id_8ce187_idx_and_more.py,sha256=MmxPMKYOzafTeVRj1cOOzVEDszKa_VYyqkUD8z7jZEk,1512
@@ -459,7 +459,7 @@ django_cfg/apps/payments/templates/admin/payments/subscription/change_list.html,
459
459
  django_cfg/apps/payments/templatetags/__init__.py,sha256=K8PcAAZu-q6LgCbrH29BSxwsWH0GQx-PhT7J-hBATMM,24
460
460
  django_cfg/apps/payments/templatetags/payment_tags.py,sha256=ooO9a5E6kWyaGoLmx2fam4dHnzb9_drhFYmgPT9em1g,11910
461
461
  django_cfg/apps/payments/views/api/__init__.py,sha256=gKd8dE4i-ebCxjquFHI6UaW6KKgV0RE3RlH22c8O4pU,2005
462
- django_cfg/apps/payments/views/api/api_keys.py,sha256=J5qfPfSjZtaWKaRMU_kxOWX8FNBZuacFYFSTGhTe5MQ,13836
462
+ django_cfg/apps/payments/views/api/api_keys.py,sha256=nzlRdkMjDY3-hChbSnAo3MFDqd5lulOcKgeajdQr7KQ,13854
463
463
  django_cfg/apps/payments/views/api/balances.py,sha256=1x8wyGZrM_dyXfkYMtB8YkLnLMFKv4Vv_mauXP-nI5Y,13090
464
464
  django_cfg/apps/payments/views/api/base.py,sha256=iUsI3L4eolTOqVZ-ZP8AgZp58_EsTkhZUcVcCxZuIis,10389
465
465
  django_cfg/apps/payments/views/api/currencies.py,sha256=B1Wi9MCoSgyFISrQLiqMHXtVwxOHbTLZ3X6BMEIUW3Y,13866
@@ -471,8 +471,8 @@ django_cfg/apps/payments/views/overview/serializers.py,sha256=uxDlzMIrn7hhS0aNEx
471
471
  django_cfg/apps/payments/views/overview/services.py,sha256=g7ypbE9v3dDyYvOv3nfwd9_1ZqWuH02UX6dcGCDv0fM,16259
472
472
  django_cfg/apps/payments/views/overview/urls.py,sha256=6Ldh8-hrkUakZDkZ7eMXpcYrdAnR8xs2UpmyDUOk4z8,1212
473
473
  django_cfg/apps/payments/views/overview/views.py,sha256=F1chPhhQYeey1Wpg7z7fKvlzqT5Nxr2dKIc5PioLc-U,7991
474
- django_cfg/apps/payments/views/serializers/__init__.py,sha256=mPY55D6bhRLyPHw6ORrEqtIdgyU2Uezg00EhgD5HK40,2310
475
- django_cfg/apps/payments/views/serializers/api_keys.py,sha256=8BGI44qijAQAZ4c1Skg8-_tvmydN04fgceGQHkVuONc,14874
474
+ django_cfg/apps/payments/views/serializers/__init__.py,sha256=jFDXboouH5lNIQbAz-X97DCDt3ucRzUKtLotr3nw1j0,2322
475
+ django_cfg/apps/payments/views/serializers/api_keys.py,sha256=7sG4mt9j1v7I2_dmnMTFBEiGm2UVasZV8fBdltgUgQg,14807
476
476
  django_cfg/apps/payments/views/serializers/balances.py,sha256=CUUl9weGK-f_4KCGJ4Abv5VvJWq7ZpclaR49NpOlWHw,9415
477
477
  django_cfg/apps/payments/views/serializers/currencies.py,sha256=bpWBTE2kHaz7nKZwwYnzkyK6HYtOgULQ87ZBECY3q6Q,11330
478
478
  django_cfg/apps/payments/views/serializers/payments.py,sha256=SMRZEqga5KMFKZ-UiIcyafWmVC4nSeUZwwg9QPUG3JA,12543
@@ -617,7 +617,7 @@ django_cfg/dashboard/sections/stats.py,sha256=k4ogZtZtR1CEkFTvoWgU-HTysr-Y2S4Lie
617
617
  django_cfg/dashboard/sections/system.py,sha256=IP4SJMPOL-gqDancE_g46ZbmlveYDvljpszRJmz1tSc,2025
618
618
  django_cfg/management/__init__.py,sha256=NrLAhiS59hqjy-bipOC1abNuRiNm5BpKXmjN05VzKbM,28
619
619
  django_cfg/management/commands/__init__.py,sha256=GqJDbjiwRa9Y9uvf695EZ-Y42vQIMHp5YkjhMoeAM9I,417
620
- django_cfg/management/commands/check_endpoints.py,sha256=-65NG14047mGTUnZZzupYp4vD7nFBxTLvAkwECbTQfg,5126
620
+ django_cfg/management/commands/check_endpoints.py,sha256=5I-8cbVD6aA9g6_j_JI76NoZCSwZTy21B7hxNKbR4OI,6275
621
621
  django_cfg/management/commands/check_settings.py,sha256=YyYKBMT3XaILD6PFKQGPALNnDv9rNtnF2U8RlkCoYiA,11770
622
622
  django_cfg/management/commands/clear_constance.py,sha256=tX6YUeJsmxJxXLQRDX3VUkUP9t6B1gAyVrBl4krQ6K0,8263
623
623
  django_cfg/management/commands/create_token.py,sha256=NspV-9j-T0dDjqY6ccJeuVqTB3v4ne1Jc43G2tKuioI,12015
@@ -625,7 +625,7 @@ django_cfg/management/commands/generate.py,sha256=tvBahlXOu63H7d-7Ree1WuR_6saebU
625
625
  django_cfg/management/commands/list_urls.py,sha256=3J1Lxhi6ImFOZQ9D047tR2lQ6Hl2Zd7f6YmEYohlmqQ,11261
626
626
  django_cfg/management/commands/migrate_all.py,sha256=gM5-Yp4k9dDWt8kpxBHAfvlSM8QW9ApUHV6tdRaVgVc,5454
627
627
  django_cfg/management/commands/migrator.py,sha256=qH1lARoKaInkTXN87lav7yxkVkWzHrZ6XZzwa5Sqrg0,16484
628
- django_cfg/management/commands/rundramatiq.py,sha256=DtwY5Qtr1wANrPVNvQZmDvHH9tzmVQK-2PNutQbOMgc,9385
628
+ django_cfg/management/commands/rundramatiq.py,sha256=I5_OlmfpeIujyF0G6nTr0JWQeQIFb6-kSTaYai2Oqck,9424
629
629
  django_cfg/management/commands/rundramatiq_simulator.py,sha256=dOLabc4bDVwpdWWFzi8H8SCIbVp0pg6dqviMLL0abxA,16101
630
630
  django_cfg/management/commands/runserver_ngrok.py,sha256=d_kiTqCgRN9MUNlJf_ERorPqTyd6QZVJPVTURd7mKLA,6520
631
631
  django_cfg/management/commands/script.py,sha256=Mrhpiz3leSzWOzULHTfsplIvRqmquaA67rVGQGKajsI,17538
@@ -661,7 +661,7 @@ django_cfg/models/base/module.py,sha256=P6YowmE-VOqp_L25Ijxj2hjjNhB9xtlm8G35DHWq
661
661
  django_cfg/models/django/__init__.py,sha256=i0GblTO8rF1J_WjT55WjhdbWN10l0UeR6mSFqdnOfak,347
662
662
  django_cfg/models/django/constance.py,sha256=IVklMTtusxWnWaU3PSatGLQfg5qY_Y89MZQjsJFwbCk,9175
663
663
  django_cfg/models/django/environment.py,sha256=jdg6DXQrnuLSdfZNV4KoFlkiPl1n2jOicPU8NFzyB5U,9439
664
- django_cfg/models/django/revolution.py,sha256=16fQ09iovrmlepNcTr2J0hZ-ShdMnMSQ2rTXa60yrus,8134
664
+ django_cfg/models/django/revolution.py,sha256=H4FqK24I3JOCsi5-FDPwMgRhRJxlYTMPMEOC53Ixj_U,8741
665
665
  django_cfg/models/infrastructure/__init__.py,sha256=If0XLyDNaR_F6rOhDJBCT5RmkOOoNcY61L70pQvaO1s,369
666
666
  django_cfg/models/infrastructure/cache.py,sha256=N6LWinyokWcmuJmInn0q48TQq0Je-xXMJdZ0DbelGPU,12175
667
667
  django_cfg/models/infrastructure/logging.py,sha256=6nQNQPUKEL7TAYX1KcYgsqHBKnHWoUnqhR54bxcr40s,10637
@@ -856,7 +856,7 @@ django_cfg/modules/django_twilio/templates/guide.md,sha256=nZfwx-sgWyK5NApm93zOe
856
856
  django_cfg/modules/django_twilio/templates/sendgrid_otp_email.html,sha256=sXR6_D9hmOFfk9CrfPizpLddVhkRirBWpZd_ioEsxVk,6671
857
857
  django_cfg/modules/django_twilio/templates/sendgrid_test_data.json,sha256=fh1VyuSiDELHsS_CIz9gp7tlsMAEjaDOoqbAPSZ3yyo,339
858
858
  django_cfg/modules/django_unfold/__init__.py,sha256=Z91x1iGmkzlRbEb2L9OCFmYDKNAV9C4G3i15j5S0esc,1898
859
- django_cfg/modules/django_unfold/dashboard.py,sha256=8ZvrF45PFZVK5Jtu0kmAY7xT6A087_q6pAzKDicQ9So,18002
859
+ django_cfg/modules/django_unfold/dashboard.py,sha256=WXz7TUQp2mchdAcPBWn7HaFWwkHzEUomckKNZK224mY,18108
860
860
  django_cfg/modules/django_unfold/models.py,sha256=bY6QSSaH_-r9vOTkSQjxeIkl5RaED7XkxXkT8-W5stk,4014
861
861
  django_cfg/modules/django_unfold/system_monitor.py,sha256=cznZqldRJqiSLSJbs4U7R2rX8ClzoIpqdfXdXqI2iQw,6955
862
862
  django_cfg/modules/django_unfold/tailwind.py,sha256=X9o1K3QL0VwUISgJ26sLb6zkdK-00qiDuekqTw-fydc,10846
@@ -950,9 +950,9 @@ django_cfg/utils/version_check.py,sha256=jI4v3YMdQriUEeb_TvRl511sDghy6I75iKRDUaN
950
950
  django_cfg/CHANGELOG.md,sha256=jtT3EprqEJkqSUh7IraP73vQ8PmKUMdRtznQsEnqDZk,2052
951
951
  django_cfg/CONTRIBUTING.md,sha256=DU2kyQ6PU0Z24ob7O_OqKWEYHcZmJDgzw-lQCmu6uBg,3041
952
952
  django_cfg/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
953
- django_cfg/pyproject.toml,sha256=A3K-d7sTJDTOl2iTI727wbrLqllgOvg6TPp6w1ly5TQ,8216
954
- django_cfg-1.4.5.dist-info/METADATA,sha256=XthRxgznWbMSQD4kkLoeDm2wkOfVu3h1TJJ-Pw6osN8,22542
955
- django_cfg-1.4.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
956
- django_cfg-1.4.5.dist-info/entry_points.txt,sha256=Ucmde4Z2wEzgb4AggxxZ0zaYDb9HpyE5blM3uJ0_VNg,56
957
- django_cfg-1.4.5.dist-info/licenses/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
958
- django_cfg-1.4.5.dist-info/RECORD,,
953
+ django_cfg/pyproject.toml,sha256=iHxb5y1vf2rBAMFn10U_JhKS9YMAI5yjFNi2guXam3M,8329
954
+ django_cfg-1.4.7.dist-info/METADATA,sha256=oAn4eWmJcby2cXPo2jT8TJz07k6AqY8xni4LaEwnGdI,22542
955
+ django_cfg-1.4.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
956
+ django_cfg-1.4.7.dist-info/entry_points.txt,sha256=Ucmde4Z2wEzgb4AggxxZ0zaYDb9HpyE5blM3uJ0_VNg,56
957
+ django_cfg-1.4.7.dist-info/licenses/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
958
+ django_cfg-1.4.7.dist-info/RECORD,,