django-cfg 1.4.4__py3-none-any.whl → 1.4.5__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/CHANGELOG.md +57 -0
- django_cfg/CONTRIBUTING.md +145 -0
- django_cfg/LICENSE +21 -0
- django_cfg/apps/api/endpoints/__init__.py +5 -0
- django_cfg/apps/api/endpoints/checker.py +406 -0
- django_cfg/apps/api/endpoints/drf_views.py +52 -0
- django_cfg/apps/api/endpoints/serializers.py +103 -0
- django_cfg/apps/api/endpoints/tests.py +281 -0
- django_cfg/apps/api/endpoints/urls.py +14 -0
- django_cfg/apps/api/endpoints/views.py +41 -0
- django_cfg/apps/payments/admin_interface/views/api/webhook_admin.py +2 -2
- django_cfg/apps/urls.py +2 -0
- django_cfg/management/commands/check_endpoints.py +146 -0
- django_cfg/modules/django_ipc_client/client.py +1 -0
- django_cfg/pyproject.toml +148 -0
- django_cfg/routing/routers.py +15 -2
- {django_cfg-1.4.4.dist-info → django_cfg-1.4.5.dist-info}/METADATA +1 -1
- {django_cfg-1.4.4.dist-info → django_cfg-1.4.5.dist-info}/RECORD +21 -9
- {django_cfg-1.4.4.dist-info → django_cfg-1.4.5.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.4.dist-info → django_cfg-1.4.5.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.4.dist-info → django_cfg-1.4.5.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,103 @@
|
|
1
|
+
"""
|
2
|
+
Django CFG Endpoints Status Serializers
|
3
|
+
|
4
|
+
DRF serializers for endpoints status check API.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from rest_framework import serializers
|
8
|
+
|
9
|
+
|
10
|
+
class EndpointSerializer(serializers.Serializer):
|
11
|
+
"""Serializer for single endpoint status."""
|
12
|
+
|
13
|
+
url = serializers.CharField(
|
14
|
+
help_text="URL pattern of the endpoint"
|
15
|
+
)
|
16
|
+
url_name = serializers.CharField(
|
17
|
+
required=False,
|
18
|
+
allow_null=True,
|
19
|
+
help_text="Django URL name (if available)"
|
20
|
+
)
|
21
|
+
namespace = serializers.CharField(
|
22
|
+
required=False,
|
23
|
+
allow_blank=True,
|
24
|
+
help_text="URL namespace"
|
25
|
+
)
|
26
|
+
group = serializers.CharField(
|
27
|
+
help_text="URL group (up to 3 depth)"
|
28
|
+
)
|
29
|
+
view = serializers.CharField(
|
30
|
+
required=False,
|
31
|
+
help_text="View function/class name"
|
32
|
+
)
|
33
|
+
status = serializers.CharField(
|
34
|
+
help_text="Status: healthy, unhealthy, warning, error, skipped, pending"
|
35
|
+
)
|
36
|
+
status_code = serializers.IntegerField(
|
37
|
+
required=False,
|
38
|
+
allow_null=True,
|
39
|
+
help_text="HTTP status code"
|
40
|
+
)
|
41
|
+
response_time_ms = serializers.FloatField(
|
42
|
+
required=False,
|
43
|
+
allow_null=True,
|
44
|
+
help_text="Response time in milliseconds"
|
45
|
+
)
|
46
|
+
is_healthy = serializers.BooleanField(
|
47
|
+
required=False,
|
48
|
+
allow_null=True,
|
49
|
+
help_text="Whether endpoint is healthy"
|
50
|
+
)
|
51
|
+
error = serializers.CharField(
|
52
|
+
required=False,
|
53
|
+
allow_blank=True,
|
54
|
+
help_text="Error message if check failed"
|
55
|
+
)
|
56
|
+
reason = serializers.CharField(
|
57
|
+
required=False,
|
58
|
+
allow_blank=True,
|
59
|
+
help_text="Reason for skip (if skipped)"
|
60
|
+
)
|
61
|
+
last_checked = serializers.DateTimeField(
|
62
|
+
required=False,
|
63
|
+
allow_null=True,
|
64
|
+
help_text="Timestamp of last check"
|
65
|
+
)
|
66
|
+
required_auth = serializers.BooleanField(
|
67
|
+
required=False,
|
68
|
+
default=False,
|
69
|
+
help_text="Whether endpoint required JWT authentication"
|
70
|
+
)
|
71
|
+
|
72
|
+
|
73
|
+
class EndpointsStatusSerializer(serializers.Serializer):
|
74
|
+
"""Serializer for overall endpoints status response."""
|
75
|
+
|
76
|
+
status = serializers.CharField(
|
77
|
+
help_text="Overall status: healthy, degraded, or unhealthy"
|
78
|
+
)
|
79
|
+
timestamp = serializers.DateTimeField(
|
80
|
+
help_text="Timestamp of the check"
|
81
|
+
)
|
82
|
+
total_endpoints = serializers.IntegerField(
|
83
|
+
help_text="Total number of endpoints checked"
|
84
|
+
)
|
85
|
+
healthy = serializers.IntegerField(
|
86
|
+
help_text="Number of healthy endpoints"
|
87
|
+
)
|
88
|
+
unhealthy = serializers.IntegerField(
|
89
|
+
help_text="Number of unhealthy endpoints"
|
90
|
+
)
|
91
|
+
warnings = serializers.IntegerField(
|
92
|
+
help_text="Number of endpoints with warnings"
|
93
|
+
)
|
94
|
+
errors = serializers.IntegerField(
|
95
|
+
help_text="Number of endpoints with errors"
|
96
|
+
)
|
97
|
+
skipped = serializers.IntegerField(
|
98
|
+
help_text="Number of skipped endpoints"
|
99
|
+
)
|
100
|
+
endpoints = EndpointSerializer(
|
101
|
+
many=True,
|
102
|
+
help_text="List of all endpoints with their status"
|
103
|
+
)
|
@@ -0,0 +1,281 @@
|
|
1
|
+
"""
|
2
|
+
Tests for Django CFG Endpoints Status API
|
3
|
+
"""
|
4
|
+
|
5
|
+
from django.test import TestCase, Client
|
6
|
+
from django.urls import reverse, path
|
7
|
+
from django.http import JsonResponse
|
8
|
+
from django.views import View
|
9
|
+
from .checker import (
|
10
|
+
get_url_group,
|
11
|
+
should_check_endpoint,
|
12
|
+
collect_endpoints,
|
13
|
+
check_endpoint,
|
14
|
+
check_all_endpoints,
|
15
|
+
)
|
16
|
+
|
17
|
+
|
18
|
+
class DummyView(View):
|
19
|
+
"""Dummy view for testing."""
|
20
|
+
def get(self, request):
|
21
|
+
return JsonResponse({'status': 'ok'})
|
22
|
+
|
23
|
+
|
24
|
+
class GetUrlGroupTests(TestCase):
|
25
|
+
"""Test URL grouping logic."""
|
26
|
+
|
27
|
+
def test_simple_url(self):
|
28
|
+
"""Test simple URL grouping."""
|
29
|
+
self.assertEqual(get_url_group('/api/accounts/profile/'), 'api/accounts/profile')
|
30
|
+
|
31
|
+
def test_url_with_depth_limit(self):
|
32
|
+
"""Test URL grouping respects depth limit."""
|
33
|
+
self.assertEqual(get_url_group('/api/payments/webhook/status/', depth=3), 'api/payments/webhook')
|
34
|
+
self.assertEqual(get_url_group('/api/payments/webhook/status/', depth=2), 'api/payments')
|
35
|
+
|
36
|
+
def test_url_with_parameters(self):
|
37
|
+
"""Test URL grouping ignores parameters."""
|
38
|
+
self.assertEqual(get_url_group('/api/users/<int:pk>/orders/'), 'api/users/orders')
|
39
|
+
|
40
|
+
def test_root_url(self):
|
41
|
+
"""Test root URL."""
|
42
|
+
self.assertEqual(get_url_group('/'), 'root')
|
43
|
+
|
44
|
+
def test_trailing_slash(self):
|
45
|
+
"""Test URLs with/without trailing slash."""
|
46
|
+
self.assertEqual(get_url_group('/api/test'), 'api/test')
|
47
|
+
self.assertEqual(get_url_group('/api/test/'), 'api/test')
|
48
|
+
|
49
|
+
|
50
|
+
class ShouldCheckEndpointTests(TestCase):
|
51
|
+
"""Test endpoint filtering logic."""
|
52
|
+
|
53
|
+
def test_should_check_api_endpoints(self):
|
54
|
+
"""API endpoints should be checked."""
|
55
|
+
self.assertTrue(should_check_endpoint('/api/accounts/profile/'))
|
56
|
+
self.assertTrue(should_check_endpoint('/api/payments/create/'))
|
57
|
+
|
58
|
+
def test_should_exclude_health_endpoints(self):
|
59
|
+
"""Health endpoints should be excluded to avoid recursion."""
|
60
|
+
self.assertFalse(should_check_endpoint('/cfg/health/'))
|
61
|
+
self.assertFalse(should_check_endpoint('/cfg/health/drf/'))
|
62
|
+
self.assertFalse(should_check_endpoint('/cfg/health/quick/'))
|
63
|
+
|
64
|
+
def test_should_exclude_endpoints_endpoints(self):
|
65
|
+
"""Endpoints status endpoints should be excluded to avoid recursion."""
|
66
|
+
self.assertFalse(should_check_endpoint('/cfg/api/endpoints/'))
|
67
|
+
self.assertFalse(should_check_endpoint('/cfg/api/endpoints/drf/'))
|
68
|
+
|
69
|
+
def test_should_exclude_admin(self):
|
70
|
+
"""Admin endpoints should be excluded."""
|
71
|
+
self.assertFalse(should_check_endpoint('/admin/'))
|
72
|
+
self.assertFalse(should_check_endpoint('/admin/auth/user/'))
|
73
|
+
|
74
|
+
def test_should_exclude_static(self):
|
75
|
+
"""Static/media endpoints should be excluded."""
|
76
|
+
self.assertFalse(should_check_endpoint('/static/css/style.css'))
|
77
|
+
self.assertFalse(should_check_endpoint('/media/uploads/image.png'))
|
78
|
+
|
79
|
+
def test_should_exclude_by_name(self):
|
80
|
+
"""URLs with excluded names should be excluded."""
|
81
|
+
self.assertFalse(should_check_endpoint('/some/url/', 'django_cfg_health'))
|
82
|
+
self.assertFalse(should_check_endpoint('/some/url/', 'endpoints_status'))
|
83
|
+
|
84
|
+
|
85
|
+
class CollectEndpointsTests(TestCase):
|
86
|
+
"""Test endpoint collection."""
|
87
|
+
|
88
|
+
def test_collect_simple_endpoint(self):
|
89
|
+
"""Test collecting a simple endpoint."""
|
90
|
+
urlpatterns = [
|
91
|
+
path('test/', DummyView.as_view(), name='test_view')
|
92
|
+
]
|
93
|
+
|
94
|
+
endpoints = collect_endpoints(urlpatterns)
|
95
|
+
|
96
|
+
# Should find the test endpoint
|
97
|
+
test_endpoints = [e for e in endpoints if 'test' in e['url']]
|
98
|
+
self.assertGreater(len(test_endpoints), 0)
|
99
|
+
|
100
|
+
def test_skip_unnamed_endpoints(self):
|
101
|
+
"""Test skipping unnamed endpoints."""
|
102
|
+
urlpatterns = [
|
103
|
+
path('named/', DummyView.as_view(), name='named_view'),
|
104
|
+
path('unnamed/', DummyView.as_view()),
|
105
|
+
]
|
106
|
+
|
107
|
+
endpoints = collect_endpoints(urlpatterns, include_unnamed=False)
|
108
|
+
|
109
|
+
# Should only include named endpoint
|
110
|
+
urls = [e['url'] for e in endpoints]
|
111
|
+
self.assertIn('/named/', urls)
|
112
|
+
self.assertNotIn('/unnamed/', urls)
|
113
|
+
|
114
|
+
def test_include_unnamed_endpoints(self):
|
115
|
+
"""Test including unnamed endpoints."""
|
116
|
+
urlpatterns = [
|
117
|
+
path('unnamed/', DummyView.as_view()),
|
118
|
+
]
|
119
|
+
|
120
|
+
endpoints = collect_endpoints(urlpatterns, include_unnamed=True)
|
121
|
+
|
122
|
+
# Should include unnamed endpoint
|
123
|
+
urls = [e['url'] for e in endpoints]
|
124
|
+
self.assertIn('/unnamed/', urls)
|
125
|
+
|
126
|
+
def test_skip_parameterized_urls(self):
|
127
|
+
"""Test that URLs with parameters are skipped."""
|
128
|
+
urlpatterns = [
|
129
|
+
path('users/<int:pk>/', DummyView.as_view(), name='user_detail'),
|
130
|
+
]
|
131
|
+
|
132
|
+
endpoints = collect_endpoints(urlpatterns)
|
133
|
+
|
134
|
+
# Should be marked as skipped
|
135
|
+
param_endpoints = [e for e in endpoints if '<int:pk>' in e['url']]
|
136
|
+
if param_endpoints:
|
137
|
+
self.assertEqual(param_endpoints[0]['status'], 'skipped')
|
138
|
+
self.assertEqual(param_endpoints[0]['reason'], 'requires_parameters')
|
139
|
+
|
140
|
+
def test_url_grouping(self):
|
141
|
+
"""Test that URLs are grouped correctly."""
|
142
|
+
urlpatterns = [
|
143
|
+
path('api/accounts/profile/', DummyView.as_view(), name='profile'),
|
144
|
+
]
|
145
|
+
|
146
|
+
endpoints = collect_endpoints(urlpatterns)
|
147
|
+
|
148
|
+
# Find the profile endpoint
|
149
|
+
profile_endpoints = [e for e in endpoints if 'profile' in e.get('url', '')]
|
150
|
+
if profile_endpoints:
|
151
|
+
self.assertEqual(profile_endpoints[0]['group'], 'api/accounts/profile')
|
152
|
+
|
153
|
+
|
154
|
+
class CheckEndpointTests(TestCase):
|
155
|
+
"""Test single endpoint checking."""
|
156
|
+
|
157
|
+
def test_check_healthy_endpoint(self):
|
158
|
+
"""Test checking a healthy endpoint."""
|
159
|
+
endpoint = {
|
160
|
+
'url': '/',
|
161
|
+
'url_name': 'home',
|
162
|
+
'namespace': '',
|
163
|
+
'group': 'root',
|
164
|
+
'view': 'DummyView',
|
165
|
+
'status': 'pending',
|
166
|
+
}
|
167
|
+
|
168
|
+
client = Client()
|
169
|
+
result = check_endpoint(endpoint, client=client, timeout=5)
|
170
|
+
|
171
|
+
# Check result structure
|
172
|
+
self.assertIn('status_code', result)
|
173
|
+
self.assertIn('response_time_ms', result)
|
174
|
+
self.assertIn('is_healthy', result)
|
175
|
+
self.assertIn('status', result)
|
176
|
+
self.assertIn('last_checked', result)
|
177
|
+
|
178
|
+
def test_skip_skipped_endpoint(self):
|
179
|
+
"""Test that skipped endpoints remain skipped."""
|
180
|
+
endpoint = {
|
181
|
+
'url': '/api/users/<int:pk>/',
|
182
|
+
'status': 'skipped',
|
183
|
+
'reason': 'requires_parameters',
|
184
|
+
}
|
185
|
+
|
186
|
+
result = check_endpoint(endpoint)
|
187
|
+
|
188
|
+
# Should remain skipped
|
189
|
+
self.assertEqual(result['status'], 'skipped')
|
190
|
+
self.assertNotIn('status_code', result)
|
191
|
+
|
192
|
+
|
193
|
+
class CheckAllEndpointsTests(TestCase):
|
194
|
+
"""Test complete endpoint checking."""
|
195
|
+
|
196
|
+
def test_check_all_endpoints_structure(self):
|
197
|
+
"""Test that check_all_endpoints returns correct structure."""
|
198
|
+
result = check_all_endpoints(include_unnamed=False, timeout=5)
|
199
|
+
|
200
|
+
# Check top-level structure
|
201
|
+
self.assertIn('status', result)
|
202
|
+
self.assertIn('timestamp', result)
|
203
|
+
self.assertIn('total_endpoints', result)
|
204
|
+
self.assertIn('healthy', result)
|
205
|
+
self.assertIn('unhealthy', result)
|
206
|
+
self.assertIn('warnings', result)
|
207
|
+
self.assertIn('errors', result)
|
208
|
+
self.assertIn('skipped', result)
|
209
|
+
self.assertIn('endpoints', result)
|
210
|
+
|
211
|
+
def test_check_all_endpoints_overall_status(self):
|
212
|
+
"""Test that overall status is calculated correctly."""
|
213
|
+
result = check_all_endpoints(include_unnamed=False, timeout=5)
|
214
|
+
|
215
|
+
# Status should be one of the valid options
|
216
|
+
self.assertIn(result['status'], ['healthy', 'degraded', 'unhealthy'])
|
217
|
+
|
218
|
+
def test_statistics_add_up(self):
|
219
|
+
"""Test that endpoint statistics add up correctly."""
|
220
|
+
result = check_all_endpoints(include_unnamed=False, timeout=5)
|
221
|
+
|
222
|
+
total = result['total_endpoints']
|
223
|
+
sum_stats = (
|
224
|
+
result['healthy'] +
|
225
|
+
result['unhealthy'] +
|
226
|
+
result['warnings'] +
|
227
|
+
result['errors'] +
|
228
|
+
result['skipped']
|
229
|
+
)
|
230
|
+
|
231
|
+
# Total should equal sum of all categories
|
232
|
+
self.assertEqual(total, sum_stats)
|
233
|
+
|
234
|
+
|
235
|
+
class EndpointsAPITests(TestCase):
|
236
|
+
"""Test the API endpoints themselves."""
|
237
|
+
|
238
|
+
def test_endpoints_status_view_accessible(self):
|
239
|
+
"""Test that the endpoints status view is accessible."""
|
240
|
+
# This will fail if URLs are not properly configured
|
241
|
+
try:
|
242
|
+
url = reverse('endpoints_status')
|
243
|
+
response = self.client.get(url)
|
244
|
+
|
245
|
+
# Should return 200 or 503
|
246
|
+
self.assertIn(response.status_code, [200, 503])
|
247
|
+
|
248
|
+
# Should return JSON
|
249
|
+
self.assertEqual(response['Content-Type'], 'application/json')
|
250
|
+
|
251
|
+
except Exception as e:
|
252
|
+
# If reverse fails, URLs might not be loaded yet
|
253
|
+
self.skipTest(f"URL reverse failed: {e}")
|
254
|
+
|
255
|
+
def test_endpoints_status_drf_view_accessible(self):
|
256
|
+
"""Test that the DRF endpoints status view is accessible."""
|
257
|
+
try:
|
258
|
+
url = reverse('endpoints_status_drf')
|
259
|
+
response = self.client.get(url)
|
260
|
+
|
261
|
+
# Should return 200 or 503
|
262
|
+
self.assertIn(response.status_code, [200, 503])
|
263
|
+
|
264
|
+
except Exception as e:
|
265
|
+
self.skipTest(f"URL reverse failed: {e}")
|
266
|
+
|
267
|
+
def test_endpoints_status_query_params(self):
|
268
|
+
"""Test query parameters work."""
|
269
|
+
try:
|
270
|
+
url = reverse('endpoints_status')
|
271
|
+
|
272
|
+
# Test with include_unnamed
|
273
|
+
response = self.client.get(url, {'include_unnamed': 'true'})
|
274
|
+
self.assertIn(response.status_code, [200, 503])
|
275
|
+
|
276
|
+
# Test with timeout
|
277
|
+
response = self.client.get(url, {'timeout': '10'})
|
278
|
+
self.assertIn(response.status_code, [200, 503])
|
279
|
+
|
280
|
+
except Exception as e:
|
281
|
+
self.skipTest(f"URL reverse failed: {e}")
|
@@ -0,0 +1,14 @@
|
|
1
|
+
"""
|
2
|
+
Django CFG Endpoints Status URLs.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from django.urls import path
|
6
|
+
from . import views, drf_views
|
7
|
+
|
8
|
+
urlpatterns = [
|
9
|
+
# Original JSON endpoint
|
10
|
+
path('', views.EndpointsStatusView.as_view(), name='endpoints_status'),
|
11
|
+
|
12
|
+
# DRF Browsable API endpoint with Tailwind theme
|
13
|
+
path('drf/', drf_views.DRFEndpointsStatusView.as_view(), name='endpoints_status_drf'),
|
14
|
+
]
|
@@ -0,0 +1,41 @@
|
|
1
|
+
"""
|
2
|
+
Django CFG Endpoints Status Views
|
3
|
+
|
4
|
+
Plain Django views for endpoints status checking.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from django.http import JsonResponse
|
8
|
+
from django.views import View
|
9
|
+
from .checker import check_all_endpoints
|
10
|
+
|
11
|
+
|
12
|
+
class EndpointsStatusView(View):
|
13
|
+
"""
|
14
|
+
Django CFG endpoints status check.
|
15
|
+
|
16
|
+
Checks all registered URL endpoints and returns their health status.
|
17
|
+
Excludes health check endpoints to avoid recursion.
|
18
|
+
"""
|
19
|
+
|
20
|
+
def get(self, request):
|
21
|
+
"""Return endpoints status data."""
|
22
|
+
# Get query parameters
|
23
|
+
include_unnamed = request.GET.get('include_unnamed', 'false').lower() == 'true'
|
24
|
+
timeout = int(request.GET.get('timeout', 5))
|
25
|
+
auto_auth = request.GET.get('auto_auth', 'true').lower() == 'true'
|
26
|
+
|
27
|
+
# Check all endpoints
|
28
|
+
status_data = check_all_endpoints(
|
29
|
+
include_unnamed=include_unnamed,
|
30
|
+
timeout=timeout,
|
31
|
+
auto_auth=auto_auth
|
32
|
+
)
|
33
|
+
|
34
|
+
# Return appropriate HTTP status
|
35
|
+
status_code = 200
|
36
|
+
if status_data["status"] == "unhealthy":
|
37
|
+
status_code = 503
|
38
|
+
elif status_data["status"] == "degraded":
|
39
|
+
status_code = 200 # Still operational
|
40
|
+
|
41
|
+
return JsonResponse(status_data, status=status_code)
|
@@ -154,8 +154,8 @@ class AdminWebhookEventViewSet(AdminReadOnlyViewSet):
|
|
154
154
|
|
155
155
|
# No model - using mock data for now
|
156
156
|
serializer_class = WebhookEventListSerializer
|
157
|
-
filter_backends = [
|
158
|
-
filterset_fields
|
157
|
+
filter_backends = [SearchFilter, OrderingFilter]
|
158
|
+
# filterset_fields removed - not compatible with mock data approach
|
159
159
|
search_fields = ['event_type', 'webhook_url']
|
160
160
|
ordering_fields = ['timestamp', 'event_type', 'status']
|
161
161
|
ordering = ['-timestamp']
|
django_cfg/apps/urls.py
CHANGED
@@ -21,7 +21,9 @@ def get_django_cfg_urlpatterns() -> List[URLPattern]:
|
|
21
21
|
patterns = [
|
22
22
|
# Core APIs (always enabled)
|
23
23
|
path('health/', include('django_cfg.apps.api.health.urls')),
|
24
|
+
path('endpoints/', include('django_cfg.apps.api.endpoints.urls')),
|
24
25
|
path('commands/', include('django_cfg.apps.api.commands.urls')),
|
26
|
+
|
25
27
|
]
|
26
28
|
|
27
29
|
try:
|
@@ -0,0 +1,146 @@
|
|
1
|
+
"""
|
2
|
+
Django management command to check all API endpoints status.
|
3
|
+
|
4
|
+
Usage:
|
5
|
+
python manage.py check_endpoints
|
6
|
+
python manage.py check_endpoints --include-unnamed
|
7
|
+
python manage.py check_endpoints --timeout 10
|
8
|
+
python manage.py check_endpoints --json
|
9
|
+
"""
|
10
|
+
|
11
|
+
from django.core.management.base import BaseCommand
|
12
|
+
from django.urls import reverse
|
13
|
+
from django_cfg.apps.api.endpoints.checker import check_all_endpoints
|
14
|
+
import json
|
15
|
+
|
16
|
+
|
17
|
+
class Command(BaseCommand):
|
18
|
+
help = 'Check status of all Django CFG API endpoints'
|
19
|
+
|
20
|
+
def add_arguments(self, parser):
|
21
|
+
parser.add_argument(
|
22
|
+
'--include-unnamed',
|
23
|
+
action='store_true',
|
24
|
+
help='Include unnamed URL patterns in the check',
|
25
|
+
)
|
26
|
+
parser.add_argument(
|
27
|
+
'--timeout',
|
28
|
+
type=int,
|
29
|
+
default=5,
|
30
|
+
help='Request timeout in seconds (default: 5)',
|
31
|
+
)
|
32
|
+
parser.add_argument(
|
33
|
+
'--json',
|
34
|
+
action='store_true',
|
35
|
+
help='Output results as JSON',
|
36
|
+
)
|
37
|
+
parser.add_argument(
|
38
|
+
'--url',
|
39
|
+
type=str,
|
40
|
+
help='Check specific endpoint by URL name (e.g., "endpoints_status")',
|
41
|
+
)
|
42
|
+
parser.add_argument(
|
43
|
+
'--no-auth',
|
44
|
+
action='store_true',
|
45
|
+
help='Disable automatic JWT authentication retry (default: enabled)',
|
46
|
+
)
|
47
|
+
|
48
|
+
def handle(self, *args, **options):
|
49
|
+
include_unnamed = options['include_unnamed']
|
50
|
+
timeout = options['timeout']
|
51
|
+
output_json = options['json']
|
52
|
+
url_name = options.get('url')
|
53
|
+
auto_auth = not options['no_auth'] # Auto-auth enabled by default
|
54
|
+
|
55
|
+
# If specific URL requested, just resolve and display it
|
56
|
+
if url_name:
|
57
|
+
try:
|
58
|
+
url = reverse(url_name)
|
59
|
+
self.stdout.write(self.style.SUCCESS(f'✅ URL name "{url_name}" resolves to: {url}'))
|
60
|
+
return
|
61
|
+
except Exception as e:
|
62
|
+
self.stdout.write(self.style.ERROR(f'❌ Error resolving URL "{url_name}": {e}'))
|
63
|
+
return
|
64
|
+
|
65
|
+
# Check all endpoints
|
66
|
+
auth_msg = "with auto-auth" if auto_auth else "without auth"
|
67
|
+
self.stdout.write(self.style.WARNING(f'🔍 Checking endpoints (timeout: {timeout}s, {auth_msg})...'))
|
68
|
+
|
69
|
+
status_data = check_all_endpoints(
|
70
|
+
include_unnamed=include_unnamed,
|
71
|
+
timeout=timeout,
|
72
|
+
auto_auth=auto_auth
|
73
|
+
)
|
74
|
+
|
75
|
+
# Output as JSON if requested
|
76
|
+
if output_json:
|
77
|
+
self.stdout.write(json.dumps(status_data, indent=2))
|
78
|
+
return
|
79
|
+
|
80
|
+
# Pretty print results
|
81
|
+
self._print_results(status_data)
|
82
|
+
|
83
|
+
def _print_results(self, data):
|
84
|
+
"""Print formatted results to console."""
|
85
|
+
|
86
|
+
# Overall status
|
87
|
+
status = data['status']
|
88
|
+
if status == 'healthy':
|
89
|
+
status_style = self.style.SUCCESS
|
90
|
+
emoji = '✅'
|
91
|
+
elif status == 'degraded':
|
92
|
+
status_style = self.style.WARNING
|
93
|
+
emoji = '⚠️'
|
94
|
+
else:
|
95
|
+
status_style = self.style.ERROR
|
96
|
+
emoji = '❌'
|
97
|
+
|
98
|
+
self.stdout.write('')
|
99
|
+
self.stdout.write(status_style(f'{emoji} Overall Status: {status.upper()}'))
|
100
|
+
self.stdout.write('')
|
101
|
+
|
102
|
+
# Summary
|
103
|
+
self.stdout.write(self.style.HTTP_INFO('📊 Summary:'))
|
104
|
+
self.stdout.write(f' Total endpoints: {data["total_endpoints"]}')
|
105
|
+
self.stdout.write(self.style.SUCCESS(f' ✅ Healthy: {data["healthy"]}'))
|
106
|
+
self.stdout.write(self.style.WARNING(f' ⚠️ Warnings: {data["warnings"]}'))
|
107
|
+
self.stdout.write(self.style.ERROR(f' ❌ Unhealthy: {data["unhealthy"]}'))
|
108
|
+
self.stdout.write(self.style.ERROR(f' ❌ Errors: {data["errors"]}'))
|
109
|
+
self.stdout.write(f' ⏭️ Skipped: {data["skipped"]}')
|
110
|
+
self.stdout.write('')
|
111
|
+
|
112
|
+
# Endpoints details
|
113
|
+
self.stdout.write(self.style.HTTP_INFO('🔗 Endpoints:'))
|
114
|
+
|
115
|
+
for endpoint in data['endpoints']:
|
116
|
+
name = endpoint.get('name', 'unnamed')
|
117
|
+
url = endpoint['url']
|
118
|
+
status = endpoint['status']
|
119
|
+
|
120
|
+
if status == 'healthy':
|
121
|
+
icon = '✅'
|
122
|
+
style = self.style.SUCCESS
|
123
|
+
elif status == 'degraded':
|
124
|
+
icon = '⚠️'
|
125
|
+
style = self.style.WARNING
|
126
|
+
else:
|
127
|
+
icon = '❌'
|
128
|
+
style = self.style.ERROR
|
129
|
+
|
130
|
+
self.stdout.write(f' {icon} {name}')
|
131
|
+
self.stdout.write(f' URL: {url}')
|
132
|
+
self.stdout.write(style(f' Status: {status}'))
|
133
|
+
|
134
|
+
if endpoint.get('response_time'):
|
135
|
+
self.stdout.write(f' Response time: {endpoint["response_time"]:.3f}s')
|
136
|
+
|
137
|
+
if endpoint.get('error'):
|
138
|
+
self.stdout.write(self.style.ERROR(f' Error: {endpoint["error"]}'))
|
139
|
+
|
140
|
+
if endpoint.get('required_auth'):
|
141
|
+
self.stdout.write(f' 🔐 Required JWT authentication')
|
142
|
+
|
143
|
+
self.stdout.write('')
|
144
|
+
|
145
|
+
# Timestamp
|
146
|
+
self.stdout.write(self.style.HTTP_INFO(f'🕐 Checked at: {data["timestamp"]}'))
|