django-cfg 1.4.5__py3-none-any.whl → 1.4.6__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/management/commands/check_endpoints.py +29 -6
- django_cfg/management/commands/rundramatiq.py +3 -3
- django_cfg/modules/django_unfold/dashboard.py +1 -0
- django_cfg/pyproject.toml +13 -13
- {django_cfg-1.4.5.dist-info → django_cfg-1.4.6.dist-info}/METADATA +1 -1
- {django_cfg-1.4.5.dist-info → django_cfg-1.4.6.dist-info}/RECORD +13 -13
- {django_cfg-1.4.5.dist-info → django_cfg-1.4.6.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.5.dist-info → django_cfg-1.4.6.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.5.dist-info → django_cfg-1.4.6.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
|
|
@@ -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()
|
@@ -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,7 +4,7 @@ build-backend = "hatchling.build"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "django-cfg"
|
7
|
-
version = "1.4.
|
7
|
+
version = "1.4.6"
|
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",]
|
@@ -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,20 @@ 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
|
+
|
140
144
|
[tool.hatch.build.targets.wheel.force-include]
|
141
145
|
LICENSE = "django_cfg/LICENSE"
|
142
146
|
"CONTRIBUTING.md" = "django_cfg/CONTRIBUTING.md"
|
143
147
|
"CHANGELOG.md" = "django_cfg/CHANGELOG.md"
|
144
148
|
"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.6
|
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
|
@@ -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
|
@@ -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
|
@@ -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=88nmJKNOuzteCBLdh9Py3ZzYRYY__N6BnFZiT5mSr4s,8216
|
954
|
+
django_cfg-1.4.6.dist-info/METADATA,sha256=7L9aQv_xWde-XuQHa12P4iVN_9-0tKNTJzdWJ0vfxmk,22542
|
955
|
+
django_cfg-1.4.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
956
|
+
django_cfg-1.4.6.dist-info/entry_points.txt,sha256=Ucmde4Z2wEzgb4AggxxZ0zaYDb9HpyE5blM3uJ0_VNg,56
|
957
|
+
django_cfg-1.4.6.dist-info/licenses/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
|
958
|
+
django_cfg-1.4.6.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|