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.
- django_cfg/apps/api/endpoints/checker.py +222 -25
- django_cfg/apps/api/endpoints/drf_views.py +4 -1
- django_cfg/apps/api/endpoints/serializers.py +22 -2
- django_cfg/apps/payments/middleware/rate_limiting.py +9 -3
- django_cfg/apps/payments/views/api/api_keys.py +8 -8
- django_cfg/apps/payments/views/serializers/__init__.py +2 -2
- django_cfg/apps/payments/views/serializers/api_keys.py +11 -21
- django_cfg/management/commands/check_endpoints.py +29 -6
- django_cfg/management/commands/rundramatiq.py +3 -3
- django_cfg/models/django/revolution.py +26 -13
- django_cfg/modules/django_unfold/dashboard.py +1 -0
- django_cfg/pyproject.toml +18 -14
- {django_cfg-1.4.5.dist-info → django_cfg-1.4.7.dist-info}/METADATA +2 -2
- {django_cfg-1.4.5.dist-info → django_cfg-1.4.7.dist-info}/RECORD +17 -17
- {django_cfg-1.4.5.dist-info → django_cfg-1.4.7.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.5.dist-info → django_cfg-1.4.7.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.5.dist-info → django_cfg-1.4.7.dist-info}/licenses/LICENSE +0 -0
@@ -6,7 +6,8 @@ Utility for checking all registered Django URL endpoints.
|
|
6
6
|
|
7
7
|
import time
|
8
8
|
import re
|
9
|
-
|
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
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
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
|
-
|
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':
|
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
|
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
|
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
|
96
|
-
if request.
|
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
|
-
|
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 =
|
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':
|
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 =
|
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':
|
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
|
-
|
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
|
-
'
|
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
|
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 =
|
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':
|
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':
|
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 =
|
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('
|
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
|
-
|
135
|
-
|
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
|
-
|
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.
|
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.
|
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.
|
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.
|
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=
|
107
|
-
django_cfg/apps/api/endpoints/drf_views.py,sha256=
|
108
|
-
django_cfg/apps/api/endpoints/serializers.py,sha256=
|
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=
|
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=
|
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=
|
475
|
-
django_cfg/apps/payments/views/serializers/api_keys.py,sha256=
|
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
|
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=
|
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=
|
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=
|
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=
|
954
|
-
django_cfg-1.4.
|
955
|
-
django_cfg-1.4.
|
956
|
-
django_cfg-1.4.
|
957
|
-
django_cfg-1.4.
|
958
|
-
django_cfg-1.4.
|
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,,
|
File without changes
|
File without changes
|
File without changes
|