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
django_cfg/CHANGELOG.md
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
All notable changes to this project will be documented in this file.
|
4
|
+
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
|
+
|
8
|
+
## [1.2.25] - 2025-09-24
|
9
|
+
|
10
|
+
### Added
|
11
|
+
- **Payment System Enhancements**: Unified payment provider configurations
|
12
|
+
- New `PaymentsConfig` model with provider-specific settings
|
13
|
+
- Enhanced validation utilities for API keys and subscription access
|
14
|
+
- Improved webhook handling and reliability
|
15
|
+
- Support for multiple payment providers (NowPayments, Cryptomus, etc.)
|
16
|
+
- **Template System**: Enhanced project template management
|
17
|
+
- Improved template extraction and project name replacement
|
18
|
+
- Better integration with CLI `create-project` command
|
19
|
+
- More reliable template archiving system
|
20
|
+
|
21
|
+
### Changed
|
22
|
+
- **Project Structure**: Reorganized template location
|
23
|
+
- Moved Django sample project to `examples/django_sample`
|
24
|
+
- Improved template packaging for better distribution
|
25
|
+
- **Dependencies**: Updated dependency management
|
26
|
+
- Better version constraint handling
|
27
|
+
- Improved package compatibility
|
28
|
+
|
29
|
+
### Fixed
|
30
|
+
- **Payment Validation**: Enhanced security for payment processing
|
31
|
+
- Improved API key validation
|
32
|
+
- Better webhook verification
|
33
|
+
- Fixed subscription access control issues
|
34
|
+
- **CLI Tools**: Improved reliability of project creation
|
35
|
+
- Fixed template extraction issues
|
36
|
+
- Better error handling for project setup
|
37
|
+
- Improved project name replacement logic
|
38
|
+
|
39
|
+
### Security
|
40
|
+
- **Payment Processing**: Enhanced security measures
|
41
|
+
- Stronger API key validation
|
42
|
+
- Improved webhook verification
|
43
|
+
- Better access control for subscription features
|
44
|
+
|
45
|
+
## [Previous Versions]
|
46
|
+
|
47
|
+
### [1.2.24] and earlier
|
48
|
+
- Core Django-CFG functionality
|
49
|
+
- Basic payment provider support
|
50
|
+
- Configuration management system
|
51
|
+
- CLI tools for project creation
|
52
|
+
- Health monitoring modules
|
53
|
+
- Database and Redis integration
|
54
|
+
|
55
|
+
---
|
56
|
+
|
57
|
+
**Note**: This changelog focuses on user-facing features and API changes in the Django-CFG package.
|
@@ -0,0 +1,145 @@
|
|
1
|
+
# 🤝 Contributing to Django-CFG
|
2
|
+
|
3
|
+
Thank you for your interest in contributing to Django-CFG! This project aims to make Django configuration simple, type-safe, and developer-friendly.
|
4
|
+
|
5
|
+
## 🚀 Quick Start
|
6
|
+
|
7
|
+
### Development Setup
|
8
|
+
|
9
|
+
1. **Clone the repository**
|
10
|
+
```bash
|
11
|
+
git clone https://github.com/markolofsen/django-cfg.git
|
12
|
+
cd django-cfg
|
13
|
+
```
|
14
|
+
|
15
|
+
2. **Install dependencies**
|
16
|
+
```bash
|
17
|
+
poetry install --extras dev
|
18
|
+
```
|
19
|
+
|
20
|
+
3. **Run tests**
|
21
|
+
```bash
|
22
|
+
poetry run pytest
|
23
|
+
```
|
24
|
+
|
25
|
+
## 📋 Development Workflow
|
26
|
+
|
27
|
+
### Making Changes
|
28
|
+
|
29
|
+
1. **Create a feature branch**
|
30
|
+
```bash
|
31
|
+
git checkout -b feature/your-feature-name
|
32
|
+
```
|
33
|
+
|
34
|
+
2. **Make your changes**
|
35
|
+
- Follow the existing code style
|
36
|
+
- Add tests for new features
|
37
|
+
- Update documentation if needed
|
38
|
+
|
39
|
+
3. **Test your changes**
|
40
|
+
```bash
|
41
|
+
# Run all tests
|
42
|
+
poetry run pytest
|
43
|
+
|
44
|
+
# Run with coverage
|
45
|
+
poetry run pytest --cov=django_cfg
|
46
|
+
|
47
|
+
# Run linting
|
48
|
+
poetry run black src/ tests/
|
49
|
+
poetry run isort src/ tests/
|
50
|
+
poetry run flake8 src/ tests/
|
51
|
+
```
|
52
|
+
|
53
|
+
4. **Update version and generate requirements**
|
54
|
+
```bash
|
55
|
+
# Use our development CLI
|
56
|
+
poetry run python scripts/dev_cli.py
|
57
|
+
|
58
|
+
# Or manually bump version
|
59
|
+
poetry run python scripts/version_manager.py bump --bump-type patch
|
60
|
+
```
|
61
|
+
|
62
|
+
### Code Style
|
63
|
+
|
64
|
+
- **Python**: Follow PEP 8, use Black for formatting
|
65
|
+
- **Type Hints**: Required for all public APIs
|
66
|
+
- **Documentation**: Add docstrings for public methods
|
67
|
+
- **Tests**: Write tests for new features and bug fixes
|
68
|
+
|
69
|
+
## 🧪 Testing
|
70
|
+
|
71
|
+
### Running Tests
|
72
|
+
|
73
|
+
```bash
|
74
|
+
# All tests
|
75
|
+
poetry run pytest
|
76
|
+
|
77
|
+
# Specific test file
|
78
|
+
poetry run pytest tests/test_basic_config.py
|
79
|
+
|
80
|
+
# With coverage report
|
81
|
+
poetry run pytest --cov=django_cfg --cov-report=html
|
82
|
+
```
|
83
|
+
|
84
|
+
### Writing Tests
|
85
|
+
|
86
|
+
- Place tests in the `tests/` directory
|
87
|
+
- Use descriptive test names
|
88
|
+
- Test both success and error cases
|
89
|
+
- Mock external dependencies
|
90
|
+
|
91
|
+
## 📝 Pull Request Process
|
92
|
+
|
93
|
+
1. **Ensure tests pass**
|
94
|
+
```bash
|
95
|
+
poetry run pytest
|
96
|
+
```
|
97
|
+
|
98
|
+
2. **Update documentation** if your change affects the public API
|
99
|
+
|
100
|
+
3. **Create a pull request** with:
|
101
|
+
- Clear description of changes
|
102
|
+
- Link to any related issues
|
103
|
+
- Screenshots for UI changes
|
104
|
+
|
105
|
+
4. **Code review** - address any feedback from maintainers
|
106
|
+
|
107
|
+
## 🐛 Bug Reports
|
108
|
+
|
109
|
+
When reporting bugs, please include:
|
110
|
+
|
111
|
+
- Django-CFG version
|
112
|
+
- Django version
|
113
|
+
- Python version
|
114
|
+
- Minimal code example
|
115
|
+
- Full error traceback
|
116
|
+
|
117
|
+
## 💡 Feature Requests
|
118
|
+
|
119
|
+
For new features:
|
120
|
+
|
121
|
+
- Check existing issues first
|
122
|
+
- Describe the use case clearly
|
123
|
+
- Provide code examples if possible
|
124
|
+
- Consider backward compatibility
|
125
|
+
|
126
|
+
## 📚 Documentation
|
127
|
+
|
128
|
+
Documentation improvements are always welcome:
|
129
|
+
|
130
|
+
- Fix typos and grammar
|
131
|
+
- Add examples and use cases
|
132
|
+
- Improve API documentation
|
133
|
+
- Update README with new features
|
134
|
+
|
135
|
+
## ⚖️ License
|
136
|
+
|
137
|
+
By contributing, you agree that your contributions will be licensed under the MIT License.
|
138
|
+
|
139
|
+
## 🙏 Recognition
|
140
|
+
|
141
|
+
All contributors will be recognized in our README and release notes.
|
142
|
+
|
143
|
+
---
|
144
|
+
|
145
|
+
**Questions?** Open an issue or start a discussion. We're here to help! 🚀
|
django_cfg/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 ReformsAI Team
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
@@ -0,0 +1,406 @@
|
|
1
|
+
"""
|
2
|
+
Endpoints Status Checker
|
3
|
+
|
4
|
+
Utility for checking all registered Django URL endpoints.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import time
|
8
|
+
import re
|
9
|
+
from typing import List, Dict, Any, Optional
|
10
|
+
from django.urls import get_resolver, URLPattern, URLResolver
|
11
|
+
from django.test import Client
|
12
|
+
from django.utils import timezone
|
13
|
+
from django.contrib.auth import get_user_model
|
14
|
+
|
15
|
+
|
16
|
+
def get_url_group(url_pattern: str, depth: int = 3) -> str:
|
17
|
+
"""
|
18
|
+
Extract group from URL pattern up to specified depth.
|
19
|
+
|
20
|
+
Examples:
|
21
|
+
/api/accounts/profile/ → api/accounts
|
22
|
+
/api/payments/webhook/status/ → api/payments/webhook
|
23
|
+
/cfg/health/drf/ → cfg/health/drf
|
24
|
+
/admin/auth/user/ → admin/auth/user
|
25
|
+
|
26
|
+
Args:
|
27
|
+
url_pattern: URL pattern string
|
28
|
+
depth: Maximum depth for grouping (default: 3)
|
29
|
+
|
30
|
+
Returns:
|
31
|
+
Group name as string
|
32
|
+
"""
|
33
|
+
# Remove leading/trailing slashes and split
|
34
|
+
parts = [p for p in url_pattern.strip('/').split('/') if p and '<' not in p]
|
35
|
+
|
36
|
+
# Take up to depth parts
|
37
|
+
group_parts = parts[:depth]
|
38
|
+
|
39
|
+
return '/'.join(group_parts) if group_parts else 'root'
|
40
|
+
|
41
|
+
|
42
|
+
def should_check_endpoint(url_pattern: str, url_name: Optional[str] = None) -> bool:
|
43
|
+
"""
|
44
|
+
Determine if endpoint should be checked.
|
45
|
+
|
46
|
+
Excludes:
|
47
|
+
- Health check endpoints (to avoid recursion)
|
48
|
+
- Admin endpoints
|
49
|
+
- Static/media files
|
50
|
+
- Django internal endpoints
|
51
|
+
- Schema/Swagger/Redoc documentation endpoints
|
52
|
+
|
53
|
+
Args:
|
54
|
+
url_pattern: URL pattern string
|
55
|
+
url_name: Optional URL name
|
56
|
+
|
57
|
+
Returns:
|
58
|
+
True if endpoint should be checked
|
59
|
+
"""
|
60
|
+
# Exclude patterns
|
61
|
+
exclude_patterns = [
|
62
|
+
r'^/?static/',
|
63
|
+
r'^/?media/',
|
64
|
+
r'^/?admin/',
|
65
|
+
r'^/?cfg/health/', # Exclude health endpoints (recursion prevention)
|
66
|
+
r'^/?cfg/api/endpoints/', # Exclude ourselves
|
67
|
+
r'^/__debug__/',
|
68
|
+
r'^/__reload__/',
|
69
|
+
r'^/?schema/', # Exclude schema/swagger/redoc documentation endpoints
|
70
|
+
]
|
71
|
+
|
72
|
+
for pattern in exclude_patterns:
|
73
|
+
if re.match(pattern, url_pattern):
|
74
|
+
return False
|
75
|
+
|
76
|
+
# Exclude URL names
|
77
|
+
exclude_names = [
|
78
|
+
'django_cfg_health',
|
79
|
+
'django_cfg_quick_health',
|
80
|
+
'django_cfg_drf_health',
|
81
|
+
'django_cfg_drf_quick_health',
|
82
|
+
'endpoints_status',
|
83
|
+
'endpoints_status_drf',
|
84
|
+
]
|
85
|
+
|
86
|
+
if url_name in exclude_names:
|
87
|
+
return False
|
88
|
+
|
89
|
+
return True
|
90
|
+
|
91
|
+
|
92
|
+
def collect_endpoints(
|
93
|
+
urlpatterns=None,
|
94
|
+
prefix: str = '',
|
95
|
+
namespace: str = '',
|
96
|
+
include_unnamed: bool = True
|
97
|
+
) -> List[Dict[str, Any]]:
|
98
|
+
"""
|
99
|
+
Recursively collect all URL endpoints.
|
100
|
+
|
101
|
+
Args:
|
102
|
+
urlpatterns: URL patterns to process (default: root resolver)
|
103
|
+
prefix: Current URL prefix
|
104
|
+
namespace: Current URL namespace
|
105
|
+
include_unnamed: Include endpoints without names
|
106
|
+
|
107
|
+
Returns:
|
108
|
+
List of endpoint dictionaries
|
109
|
+
"""
|
110
|
+
if urlpatterns is None:
|
111
|
+
resolver = get_resolver()
|
112
|
+
urlpatterns = resolver.url_patterns
|
113
|
+
|
114
|
+
endpoints = []
|
115
|
+
|
116
|
+
for pattern in urlpatterns:
|
117
|
+
if isinstance(pattern, URLResolver):
|
118
|
+
# This is an include() - recurse
|
119
|
+
new_prefix = prefix + str(pattern.pattern)
|
120
|
+
new_namespace = namespace
|
121
|
+
|
122
|
+
if hasattr(pattern, 'namespace') and pattern.namespace:
|
123
|
+
new_namespace = (
|
124
|
+
f"{namespace}:{pattern.namespace}"
|
125
|
+
if namespace
|
126
|
+
else pattern.namespace
|
127
|
+
)
|
128
|
+
|
129
|
+
# Recursively collect nested patterns
|
130
|
+
endpoints.extend(
|
131
|
+
collect_endpoints(
|
132
|
+
pattern.url_patterns,
|
133
|
+
new_prefix,
|
134
|
+
new_namespace,
|
135
|
+
include_unnamed
|
136
|
+
)
|
137
|
+
)
|
138
|
+
|
139
|
+
elif isinstance(pattern, URLPattern):
|
140
|
+
# Regular URL pattern
|
141
|
+
full_pattern = prefix + str(pattern.pattern)
|
142
|
+
|
143
|
+
# Clean up the pattern
|
144
|
+
clean_pattern = re.sub(r'\^|\$', '', full_pattern)
|
145
|
+
clean_pattern = re.sub(r'\\/', '/', clean_pattern)
|
146
|
+
|
147
|
+
# Ensure leading slash
|
148
|
+
if not clean_pattern.startswith('/'):
|
149
|
+
clean_pattern = '/' + clean_pattern
|
150
|
+
|
151
|
+
url_name = getattr(pattern, 'name', None)
|
152
|
+
|
153
|
+
# Skip unnamed if requested
|
154
|
+
if not include_unnamed and not url_name:
|
155
|
+
continue
|
156
|
+
|
157
|
+
# Check if should include this endpoint
|
158
|
+
if not should_check_endpoint(clean_pattern, url_name):
|
159
|
+
continue
|
160
|
+
|
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
|
+
# Get view info
|
174
|
+
view_name = 'unknown'
|
175
|
+
if hasattr(pattern, 'callback'):
|
176
|
+
callback = pattern.callback
|
177
|
+
if hasattr(callback, 'view_class'):
|
178
|
+
view_name = callback.view_class.__name__
|
179
|
+
elif hasattr(callback, '__name__'):
|
180
|
+
view_name = callback.__name__
|
181
|
+
|
182
|
+
endpoints.append({
|
183
|
+
'url': clean_pattern,
|
184
|
+
'url_name': url_name,
|
185
|
+
'namespace': namespace,
|
186
|
+
'group': get_url_group(clean_pattern),
|
187
|
+
'view': view_name,
|
188
|
+
'status': 'pending',
|
189
|
+
})
|
190
|
+
|
191
|
+
return endpoints
|
192
|
+
|
193
|
+
|
194
|
+
def create_test_user_and_get_token() -> Optional[str]:
|
195
|
+
"""
|
196
|
+
Create test user and generate JWT token.
|
197
|
+
|
198
|
+
Returns:
|
199
|
+
JWT access token or None if JWT not available
|
200
|
+
"""
|
201
|
+
try:
|
202
|
+
from rest_framework_simplejwt.tokens import RefreshToken
|
203
|
+
|
204
|
+
User = get_user_model()
|
205
|
+
|
206
|
+
# Create or get test user
|
207
|
+
username = 'endpoint_test_user'
|
208
|
+
email = 'endpoint_test@test.com'
|
209
|
+
|
210
|
+
user, created = User.objects.get_or_create(
|
211
|
+
username=username,
|
212
|
+
defaults={'email': email, 'is_active': True}
|
213
|
+
)
|
214
|
+
|
215
|
+
if created:
|
216
|
+
user.set_password('testpass123')
|
217
|
+
user.save()
|
218
|
+
|
219
|
+
# Generate JWT token
|
220
|
+
refresh = RefreshToken.for_user(user)
|
221
|
+
access_token = str(refresh.access_token)
|
222
|
+
|
223
|
+
return access_token
|
224
|
+
|
225
|
+
except ImportError:
|
226
|
+
# JWT not installed
|
227
|
+
return None
|
228
|
+
except Exception:
|
229
|
+
# Any other error
|
230
|
+
return None
|
231
|
+
|
232
|
+
|
233
|
+
def check_endpoint(
|
234
|
+
endpoint: Dict[str, Any],
|
235
|
+
client: Optional[Client] = None,
|
236
|
+
timeout: int = 5,
|
237
|
+
auth_token: Optional[str] = None,
|
238
|
+
auto_auth: bool = True
|
239
|
+
) -> tuple[Dict[str, Any], Optional[str]]:
|
240
|
+
"""
|
241
|
+
Check a single endpoint health.
|
242
|
+
|
243
|
+
Automatically creates test user and retries with JWT if endpoint returns 401/403.
|
244
|
+
|
245
|
+
Args:
|
246
|
+
endpoint: Endpoint dictionary from collect_endpoints()
|
247
|
+
client: Django test client (creates new if None)
|
248
|
+
timeout: Request timeout in seconds
|
249
|
+
auth_token: JWT token (created automatically on first 401/403)
|
250
|
+
auto_auth: Auto-retry with JWT on 401/403 (default: True)
|
251
|
+
|
252
|
+
Returns:
|
253
|
+
Tuple of (updated endpoint dictionary, auth_token if created)
|
254
|
+
"""
|
255
|
+
if client is None:
|
256
|
+
client = Client()
|
257
|
+
|
258
|
+
# Skip if already marked as skipped
|
259
|
+
if endpoint.get('status') == 'skipped':
|
260
|
+
return endpoint, auth_token
|
261
|
+
|
262
|
+
url = endpoint['url']
|
263
|
+
token_created = False
|
264
|
+
|
265
|
+
try:
|
266
|
+
start_time = time.time()
|
267
|
+
|
268
|
+
# First attempt - without auth
|
269
|
+
extra_headers = {'SERVER_NAME': 'localhost'}
|
270
|
+
response = client.get(url, timeout=timeout, **extra_headers)
|
271
|
+
response_time = (time.time() - start_time) * 1000 # Convert to ms
|
272
|
+
status_code = response.status_code
|
273
|
+
|
274
|
+
# If unauthorized and auto_auth enabled, retry with token
|
275
|
+
requires_auth = False
|
276
|
+
if status_code in [401, 403] and auto_auth:
|
277
|
+
requires_auth = True
|
278
|
+
|
279
|
+
# Create token if not provided (only once!)
|
280
|
+
if auth_token is None:
|
281
|
+
auth_token = create_test_user_and_get_token()
|
282
|
+
token_created = True
|
283
|
+
|
284
|
+
if auth_token:
|
285
|
+
start_time = time.time()
|
286
|
+
extra_headers['HTTP_AUTHORIZATION'] = f'Bearer {auth_token}'
|
287
|
+
response = client.get(url, timeout=timeout, **extra_headers)
|
288
|
+
response_time = (time.time() - start_time) * 1000 # Convert to ms
|
289
|
+
status_code = response.status_code
|
290
|
+
|
291
|
+
# Determine if healthy
|
292
|
+
# 200-299: Success
|
293
|
+
# 300-399: Redirects (OK)
|
294
|
+
# 401, 403: Auth required (expected, still healthy)
|
295
|
+
# 404: Not found (might be OK if endpoint exists but has no data)
|
296
|
+
# 405: Method not allowed (endpoint exists, just wrong method)
|
297
|
+
# 500+: Server errors (unhealthy)
|
298
|
+
|
299
|
+
is_healthy = status_code in [
|
300
|
+
200, 201, 204, # Success
|
301
|
+
301, 302, 303, 307, 308, # Redirects
|
302
|
+
401, 403, # Auth required (expected)
|
303
|
+
405, # Method not allowed (endpoint exists)
|
304
|
+
]
|
305
|
+
|
306
|
+
# Special handling for 404
|
307
|
+
if status_code == 404:
|
308
|
+
# 404 might be OK for some endpoints (e.g., detail views with no data)
|
309
|
+
# Mark as warning rather than unhealthy
|
310
|
+
is_healthy = None # Will be marked as 'warning'
|
311
|
+
|
312
|
+
endpoint.update({
|
313
|
+
'status_code': status_code,
|
314
|
+
'response_time_ms': round(response_time, 2),
|
315
|
+
'is_healthy': is_healthy,
|
316
|
+
'status': 'healthy' if is_healthy else ('warning' if is_healthy is None else 'unhealthy'),
|
317
|
+
'last_checked': timezone.now().isoformat(),
|
318
|
+
})
|
319
|
+
|
320
|
+
if requires_auth:
|
321
|
+
endpoint['required_auth'] = True
|
322
|
+
|
323
|
+
except Exception as e:
|
324
|
+
endpoint.update({
|
325
|
+
'status_code': None,
|
326
|
+
'response_time_ms': None,
|
327
|
+
'is_healthy': False,
|
328
|
+
'status': 'error',
|
329
|
+
'error': str(e)[:200], # Truncate long errors
|
330
|
+
'last_checked': timezone.now().isoformat(),
|
331
|
+
})
|
332
|
+
|
333
|
+
# Return endpoint and token (if it was created)
|
334
|
+
return endpoint, (auth_token if token_created else None)
|
335
|
+
|
336
|
+
|
337
|
+
def check_all_endpoints(
|
338
|
+
include_unnamed: bool = False,
|
339
|
+
timeout: int = 5,
|
340
|
+
auto_auth: bool = True
|
341
|
+
) -> Dict[str, Any]:
|
342
|
+
"""
|
343
|
+
Check all registered endpoints.
|
344
|
+
|
345
|
+
Args:
|
346
|
+
include_unnamed: Include endpoints without names
|
347
|
+
timeout: Request timeout in seconds
|
348
|
+
auto_auth: Automatically retry with JWT auth on 401/403 (default: True)
|
349
|
+
|
350
|
+
Returns:
|
351
|
+
Dictionary with overall status and all endpoints
|
352
|
+
"""
|
353
|
+
# Collect endpoints
|
354
|
+
endpoints = collect_endpoints(include_unnamed=include_unnamed)
|
355
|
+
|
356
|
+
# Create client once
|
357
|
+
client = Client()
|
358
|
+
|
359
|
+
# Token will be created lazily on first 401/403
|
360
|
+
auth_token = None
|
361
|
+
|
362
|
+
# Check each endpoint
|
363
|
+
checked_endpoints = []
|
364
|
+
for endpoint in endpoints:
|
365
|
+
# Check endpoint (will auto-retry with JWT if needed)
|
366
|
+
checked, new_token = check_endpoint(
|
367
|
+
endpoint,
|
368
|
+
client=client,
|
369
|
+
timeout=timeout,
|
370
|
+
auth_token=auth_token,
|
371
|
+
auto_auth=auto_auth
|
372
|
+
)
|
373
|
+
|
374
|
+
# If token was created on first 401/403, save it for ALL subsequent endpoints
|
375
|
+
if new_token and auth_token is None:
|
376
|
+
auth_token = new_token
|
377
|
+
|
378
|
+
checked_endpoints.append(checked)
|
379
|
+
|
380
|
+
# Calculate statistics
|
381
|
+
total = len(checked_endpoints)
|
382
|
+
healthy = sum(1 for e in checked_endpoints if e.get('status') == 'healthy')
|
383
|
+
unhealthy = sum(1 for e in checked_endpoints if e.get('status') == 'unhealthy')
|
384
|
+
warnings = sum(1 for e in checked_endpoints if e.get('status') == 'warning')
|
385
|
+
errors = sum(1 for e in checked_endpoints if e.get('status') == 'error')
|
386
|
+
skipped = sum(1 for e in checked_endpoints if e.get('status') == 'skipped')
|
387
|
+
|
388
|
+
# Determine overall status
|
389
|
+
if errors > 0 or unhealthy > 0:
|
390
|
+
overall_status = 'unhealthy'
|
391
|
+
elif warnings > 0:
|
392
|
+
overall_status = 'degraded'
|
393
|
+
else:
|
394
|
+
overall_status = 'healthy'
|
395
|
+
|
396
|
+
return {
|
397
|
+
'status': overall_status,
|
398
|
+
'timestamp': timezone.now().isoformat(),
|
399
|
+
'total_endpoints': total,
|
400
|
+
'healthy': healthy,
|
401
|
+
'unhealthy': unhealthy,
|
402
|
+
'warnings': warnings,
|
403
|
+
'errors': errors,
|
404
|
+
'skipped': skipped,
|
405
|
+
'endpoints': checked_endpoints,
|
406
|
+
}
|
@@ -0,0 +1,52 @@
|
|
1
|
+
"""
|
2
|
+
Django CFG Endpoints Status DRF Views
|
3
|
+
|
4
|
+
DRF browsable API views with Tailwind theme support.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from rest_framework.views import APIView
|
8
|
+
from rest_framework.response import Response
|
9
|
+
from rest_framework import status
|
10
|
+
from rest_framework.permissions import AllowAny
|
11
|
+
|
12
|
+
from .checker import check_all_endpoints
|
13
|
+
from .serializers import EndpointsStatusSerializer
|
14
|
+
|
15
|
+
|
16
|
+
class DRFEndpointsStatusView(APIView):
|
17
|
+
"""
|
18
|
+
Django CFG endpoints status check with DRF Browsable API.
|
19
|
+
|
20
|
+
Checks all registered URL endpoints and returns their health status.
|
21
|
+
Excludes health check endpoints and admin to avoid recursion.
|
22
|
+
|
23
|
+
Query Parameters:
|
24
|
+
- include_unnamed: Include endpoints without names (default: false)
|
25
|
+
- timeout: Request timeout in seconds (default: 5)
|
26
|
+
|
27
|
+
This endpoint uses DRF Browsable API with Tailwind CSS theme! 🎨
|
28
|
+
"""
|
29
|
+
|
30
|
+
permission_classes = [AllowAny] # Public endpoint
|
31
|
+
serializer_class = EndpointsStatusSerializer # For schema generation
|
32
|
+
|
33
|
+
def get(self, request):
|
34
|
+
"""Return endpoints status data."""
|
35
|
+
# Get query parameters
|
36
|
+
include_unnamed = request.query_params.get('include_unnamed', 'false').lower() == 'true'
|
37
|
+
timeout = int(request.query_params.get('timeout', 5))
|
38
|
+
|
39
|
+
# Check all endpoints
|
40
|
+
status_data = check_all_endpoints(
|
41
|
+
include_unnamed=include_unnamed,
|
42
|
+
timeout=timeout
|
43
|
+
)
|
44
|
+
|
45
|
+
# Return appropriate HTTP status
|
46
|
+
http_status = status.HTTP_200_OK
|
47
|
+
if status_data["status"] == "unhealthy":
|
48
|
+
http_status = status.HTTP_503_SERVICE_UNAVAILABLE
|
49
|
+
elif status_data["status"] == "degraded":
|
50
|
+
http_status = status.HTTP_200_OK # Still operational
|
51
|
+
|
52
|
+
return Response(status_data, status=http_status)
|