django-cfg 1.4.3__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.
@@ -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 = [DjangoFilterBackend, SearchFilter, OrderingFilter]
158
- filterset_fields = ['event_type', 'status', 'provider']
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"]}'))
@@ -228,6 +228,7 @@ class DjangoCfgRPCClient:
228
228
  "method": method,
229
229
  "params": json.loads(params_json), # Embedded as dict
230
230
  "correlation_id": cid,
231
+ "reply_to": reply_key, # Redis List key for response
231
232
  "timeout": timeout,
232
233
  }
233
234