django-cfg 1.2.31__py3-none-any.whl → 1.3.1__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/__init__.py +1 -1
- django_cfg/apps/api/health/views.py +4 -2
- django_cfg/apps/knowbase/config/settings.py +16 -15
- django_cfg/apps/payments/README.md +326 -0
- django_cfg/apps/payments/admin/__init__.py +20 -10
- django_cfg/apps/payments/admin/api_keys_admin.py +521 -237
- django_cfg/apps/payments/admin/balance_admin.py +592 -297
- django_cfg/apps/payments/admin/currencies_admin.py +526 -222
- django_cfg/apps/payments/admin/filters.py +306 -199
- django_cfg/apps/payments/admin/payments_admin.py +465 -70
- django_cfg/apps/payments/admin/subscriptions_admin.py +578 -128
- django_cfg/apps/payments/admin_interface/__init__.py +18 -0
- django_cfg/apps/payments/admin_interface/templates/payments/base.html +162 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/dev_tool_card.html +38 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/loading_spinner.html +16 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/notification.html +27 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/provider_card.html +86 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/status_card.html +39 -0
- django_cfg/apps/payments/admin_interface/templates/payments/currency_converter.html +382 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_dashboard.html +300 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +303 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_list.html +382 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_status.html +500 -0
- django_cfg/apps/payments/admin_interface/templates/payments/webhook_dashboard.html +594 -0
- django_cfg/apps/payments/admin_interface/views/__init__.py +23 -0
- django_cfg/apps/payments/admin_interface/views/payment_views.py +259 -0
- django_cfg/apps/payments/admin_interface/views/webhook_dashboard.py +37 -0
- django_cfg/apps/payments/apps.py +34 -9
- django_cfg/apps/payments/config/__init__.py +28 -51
- django_cfg/apps/payments/config/constance/__init__.py +22 -0
- django_cfg/apps/payments/config/constance/config_service.py +123 -0
- django_cfg/apps/payments/config/constance/fields.py +69 -0
- django_cfg/apps/payments/config/constance/settings.py +160 -0
- django_cfg/apps/payments/config/django_cfg_integration.py +202 -0
- django_cfg/apps/payments/config/helpers.py +130 -0
- django_cfg/apps/payments/management/__init__.py +1 -3
- django_cfg/apps/payments/management/commands/__init__.py +1 -3
- django_cfg/apps/payments/management/commands/manage_currencies.py +303 -151
- django_cfg/apps/payments/management/commands/manage_providers.py +333 -160
- django_cfg/apps/payments/middleware/__init__.py +3 -1
- django_cfg/apps/payments/middleware/api_access.py +329 -222
- django_cfg/apps/payments/middleware/rate_limiting.py +342 -152
- django_cfg/apps/payments/middleware/usage_tracking.py +249 -240
- django_cfg/apps/payments/migrations/0001_initial.py +708 -536
- django_cfg/apps/payments/models/__init__.py +13 -18
- django_cfg/apps/payments/models/api_keys.py +121 -43
- django_cfg/apps/payments/models/balance.py +150 -115
- django_cfg/apps/payments/models/base.py +68 -15
- django_cfg/apps/payments/models/currencies.py +172 -148
- django_cfg/apps/payments/models/managers/__init__.py +44 -0
- django_cfg/apps/payments/models/managers/api_key_managers.py +329 -0
- django_cfg/apps/payments/models/managers/balance_managers.py +599 -0
- django_cfg/apps/payments/models/managers/currency_managers.py +385 -0
- django_cfg/apps/payments/models/managers/payment_managers.py +511 -0
- django_cfg/apps/payments/models/managers/subscription_managers.py +641 -0
- django_cfg/apps/payments/models/payments.py +235 -285
- django_cfg/apps/payments/models/subscriptions.py +257 -177
- django_cfg/apps/payments/models/tariffs.py +147 -40
- django_cfg/apps/payments/services/__init__.py +209 -56
- django_cfg/apps/payments/services/cache/__init__.py +6 -6
- django_cfg/apps/payments/services/cache/{simple_cache.py → cache_service.py} +112 -12
- django_cfg/apps/payments/services/core/__init__.py +10 -6
- django_cfg/apps/payments/services/core/balance_service.py +435 -360
- django_cfg/apps/payments/services/core/base.py +166 -0
- django_cfg/apps/payments/services/core/currency_service.py +478 -0
- django_cfg/apps/payments/services/core/payment_service.py +346 -467
- django_cfg/apps/payments/services/core/subscription_service.py +425 -481
- django_cfg/apps/payments/services/core/webhook_service.py +410 -0
- django_cfg/apps/payments/services/integrations/__init__.py +29 -0
- django_cfg/apps/payments/services/integrations/ngrok_service.py +47 -0
- django_cfg/apps/payments/services/integrations/providers_config.py +107 -0
- django_cfg/apps/payments/services/providers/__init__.py +9 -14
- django_cfg/apps/payments/services/providers/base.py +234 -174
- django_cfg/apps/payments/services/providers/nowpayments.py +478 -0
- django_cfg/apps/payments/services/providers/registry.py +367 -301
- django_cfg/apps/payments/services/types/__init__.py +78 -0
- django_cfg/apps/payments/services/types/data.py +177 -0
- django_cfg/apps/payments/services/types/requests.py +150 -0
- django_cfg/apps/payments/services/types/responses.py +156 -0
- django_cfg/apps/payments/services/types/webhooks.py +232 -0
- django_cfg/apps/payments/signals/__init__.py +33 -8
- django_cfg/apps/payments/signals/api_key_signals.py +210 -129
- django_cfg/apps/payments/signals/balance_signals.py +174 -0
- django_cfg/apps/payments/signals/payment_signals.py +128 -103
- django_cfg/apps/payments/signals/subscription_signals.py +194 -142
- django_cfg/apps/payments/static/payments/css/components.css +380 -0
- django_cfg/apps/payments/static/payments/css/dashboard.css +188 -0
- django_cfg/apps/payments/static/payments/js/components.js +545 -0
- django_cfg/apps/payments/static/payments/js/utils.js +412 -0
- django_cfg/apps/payments/templatetags/__init__.py +1 -1
- django_cfg/apps/payments/templatetags/payment_tags.py +466 -0
- django_cfg/apps/payments/urls.py +45 -48
- django_cfg/apps/payments/urls_admin.py +33 -42
- django_cfg/apps/payments/views/api/__init__.py +101 -0
- django_cfg/apps/payments/views/api/api_keys.py +387 -0
- django_cfg/apps/payments/views/api/balances.py +381 -0
- django_cfg/apps/payments/views/api/base.py +298 -0
- django_cfg/apps/payments/views/api/currencies.py +402 -0
- django_cfg/apps/payments/views/api/payments.py +415 -0
- django_cfg/apps/payments/views/api/subscriptions.py +475 -0
- django_cfg/apps/payments/views/api/webhooks.py +476 -0
- django_cfg/apps/payments/views/serializers/__init__.py +99 -0
- django_cfg/apps/payments/views/serializers/api_keys.py +424 -0
- django_cfg/apps/payments/views/serializers/balances.py +300 -0
- django_cfg/apps/payments/views/serializers/currencies.py +335 -0
- django_cfg/apps/payments/views/serializers/payments.py +387 -0
- django_cfg/apps/payments/views/serializers/subscriptions.py +429 -0
- django_cfg/apps/payments/views/serializers/webhooks.py +137 -0
- django_cfg/config.py +1 -1
- django_cfg/core/config.py +40 -4
- django_cfg/core/generation.py +25 -4
- django_cfg/core/integration/README.md +363 -0
- django_cfg/core/integration/__init__.py +47 -0
- django_cfg/core/integration/commands_collector.py +239 -0
- django_cfg/core/integration/display/__init__.py +15 -0
- django_cfg/core/integration/display/base.py +157 -0
- django_cfg/core/integration/display/ngrok.py +164 -0
- django_cfg/core/integration/display/startup.py +815 -0
- django_cfg/core/integration/url_integration.py +123 -0
- django_cfg/core/integration/version_checker.py +160 -0
- django_cfg/management/commands/auto_generate.py +4 -0
- django_cfg/management/commands/check_settings.py +6 -0
- django_cfg/management/commands/clear_constance.py +5 -2
- django_cfg/management/commands/create_token.py +6 -0
- django_cfg/management/commands/list_urls.py +6 -0
- django_cfg/management/commands/migrate_all.py +6 -0
- django_cfg/management/commands/migrator.py +3 -0
- django_cfg/management/commands/rundramatiq.py +6 -0
- django_cfg/management/commands/runserver_ngrok.py +51 -29
- django_cfg/management/commands/script.py +6 -0
- django_cfg/management/commands/show_config.py +12 -2
- django_cfg/management/commands/show_urls.py +4 -0
- django_cfg/management/commands/superuser.py +6 -0
- django_cfg/management/commands/task_clear.py +4 -1
- django_cfg/management/commands/task_status.py +3 -1
- django_cfg/management/commands/test_email.py +3 -0
- django_cfg/management/commands/test_telegram.py +6 -0
- django_cfg/management/commands/test_twilio.py +6 -0
- django_cfg/management/commands/tree.py +6 -0
- django_cfg/management/commands/validate_config.py +155 -149
- django_cfg/models/constance.py +31 -11
- django_cfg/models/payments.py +175 -492
- django_cfg/modules/django_logger.py +160 -146
- django_cfg/modules/django_unfold/dashboard.py +64 -16
- django_cfg/registry/core.py +1 -0
- django_cfg/template_archive/django_sample.zip +0 -0
- django_cfg/utils/smart_defaults.py +222 -571
- django_cfg/utils/toolkit.py +51 -11
- {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/METADATA +4 -1
- {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/RECORD +153 -185
- django_cfg/apps/payments/__init__.py +0 -8
- django_cfg/apps/payments/admin/tariffs_admin.py +0 -199
- django_cfg/apps/payments/config/module.py +0 -70
- django_cfg/apps/payments/config/providers.py +0 -105
- django_cfg/apps/payments/config/settings.py +0 -96
- django_cfg/apps/payments/config/utils.py +0 -52
- django_cfg/apps/payments/decorators.py +0 -291
- django_cfg/apps/payments/management/commands/README.md +0 -146
- django_cfg/apps/payments/management/commands/currency_stats.py +0 -304
- django_cfg/apps/payments/managers/__init__.py +0 -23
- django_cfg/apps/payments/managers/api_key_manager.py +0 -35
- django_cfg/apps/payments/managers/balance_manager.py +0 -361
- django_cfg/apps/payments/managers/currency_manager.py +0 -306
- django_cfg/apps/payments/managers/payment_manager.py +0 -192
- django_cfg/apps/payments/managers/subscription_manager.py +0 -37
- django_cfg/apps/payments/managers/tariff_manager.py +0 -29
- django_cfg/apps/payments/migrations/0002_network_providercurrency_and_more.py +0 -241
- django_cfg/apps/payments/migrations/0003_add_usd_rate_cache.py +0 -30
- django_cfg/apps/payments/models/events.py +0 -73
- django_cfg/apps/payments/serializers/__init__.py +0 -57
- django_cfg/apps/payments/serializers/api_keys.py +0 -51
- django_cfg/apps/payments/serializers/balance.py +0 -59
- django_cfg/apps/payments/serializers/currencies.py +0 -63
- django_cfg/apps/payments/serializers/payments.py +0 -62
- django_cfg/apps/payments/serializers/subscriptions.py +0 -71
- django_cfg/apps/payments/serializers/tariffs.py +0 -56
- django_cfg/apps/payments/services/billing/__init__.py +0 -8
- django_cfg/apps/payments/services/cache/base.py +0 -30
- django_cfg/apps/payments/services/core/fallback_service.py +0 -432
- django_cfg/apps/payments/services/internal_types.py +0 -461
- django_cfg/apps/payments/services/middleware/__init__.py +0 -8
- django_cfg/apps/payments/services/monitoring/__init__.py +0 -22
- django_cfg/apps/payments/services/monitoring/api_schemas.py +0 -76
- django_cfg/apps/payments/services/monitoring/provider_health.py +0 -372
- django_cfg/apps/payments/services/providers/cryptapi/__init__.py +0 -4
- django_cfg/apps/payments/services/providers/cryptapi/config.py +0 -8
- django_cfg/apps/payments/services/providers/cryptapi/models.py +0 -192
- django_cfg/apps/payments/services/providers/cryptapi/provider.py +0 -439
- django_cfg/apps/payments/services/providers/cryptomus/__init__.py +0 -4
- django_cfg/apps/payments/services/providers/cryptomus/models.py +0 -176
- django_cfg/apps/payments/services/providers/cryptomus/provider.py +0 -429
- django_cfg/apps/payments/services/providers/cryptomus/provider_v2.py +0 -564
- django_cfg/apps/payments/services/providers/models/__init__.py +0 -34
- django_cfg/apps/payments/services/providers/models/currencies.py +0 -190
- django_cfg/apps/payments/services/providers/nowpayments/__init__.py +0 -4
- django_cfg/apps/payments/services/providers/nowpayments/models.py +0 -196
- django_cfg/apps/payments/services/providers/nowpayments/provider.py +0 -380
- django_cfg/apps/payments/services/providers/stripe/__init__.py +0 -4
- django_cfg/apps/payments/services/providers/stripe/models.py +0 -184
- django_cfg/apps/payments/services/providers/stripe/provider.py +0 -109
- django_cfg/apps/payments/services/security/__init__.py +0 -34
- django_cfg/apps/payments/services/security/error_handler.py +0 -635
- django_cfg/apps/payments/services/security/payment_notifications.py +0 -342
- django_cfg/apps/payments/services/security/webhook_validator.py +0 -474
- django_cfg/apps/payments/static/payments/css/payments.css +0 -340
- django_cfg/apps/payments/static/payments/js/notifications.js +0 -202
- django_cfg/apps/payments/static/payments/js/payment-utils.js +0 -318
- django_cfg/apps/payments/static/payments/js/theme.js +0 -86
- django_cfg/apps/payments/tasks/__init__.py +0 -12
- django_cfg/apps/payments/tasks/webhook_processing.py +0 -177
- django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +0 -50
- django_cfg/apps/payments/templates/payments/base.html +0 -182
- django_cfg/apps/payments/templates/payments/components/payment_card.html +0 -201
- django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +0 -109
- django_cfg/apps/payments/templates/payments/components/progress_bar.html +0 -43
- django_cfg/apps/payments/templates/payments/components/provider_stats.html +0 -40
- django_cfg/apps/payments/templates/payments/components/status_badge.html +0 -34
- django_cfg/apps/payments/templates/payments/components/status_overview.html +0 -148
- django_cfg/apps/payments/templates/payments/dashboard.html +0 -258
- django_cfg/apps/payments/templates/payments/dashboard_simple_test.html +0 -35
- django_cfg/apps/payments/templates/payments/payment_create.html +0 -579
- django_cfg/apps/payments/templates/payments/payment_detail.html +0 -373
- django_cfg/apps/payments/templates/payments/payment_list.html +0 -354
- django_cfg/apps/payments/templates/payments/stats.html +0 -261
- django_cfg/apps/payments/templates/payments/test.html +0 -213
- django_cfg/apps/payments/templatetags/payments_tags.py +0 -315
- django_cfg/apps/payments/utils/__init__.py +0 -43
- django_cfg/apps/payments/utils/billing_utils.py +0 -342
- django_cfg/apps/payments/utils/config_utils.py +0 -239
- django_cfg/apps/payments/utils/middleware_utils.py +0 -228
- django_cfg/apps/payments/utils/validation_utils.py +0 -94
- django_cfg/apps/payments/views/__init__.py +0 -63
- django_cfg/apps/payments/views/api_key_views.py +0 -164
- django_cfg/apps/payments/views/balance_views.py +0 -75
- django_cfg/apps/payments/views/currency_views.py +0 -122
- django_cfg/apps/payments/views/payment_views.py +0 -149
- django_cfg/apps/payments/views/subscription_views.py +0 -135
- django_cfg/apps/payments/views/tariff_views.py +0 -131
- django_cfg/apps/payments/views/templates/__init__.py +0 -25
- django_cfg/apps/payments/views/templates/ajax.py +0 -451
- django_cfg/apps/payments/views/templates/base.py +0 -212
- django_cfg/apps/payments/views/templates/dashboard.py +0 -60
- django_cfg/apps/payments/views/templates/payment_detail.py +0 -102
- django_cfg/apps/payments/views/templates/payment_management.py +0 -158
- django_cfg/apps/payments/views/templates/qr_code.py +0 -174
- django_cfg/apps/payments/views/templates/stats.py +0 -244
- django_cfg/apps/payments/views/templates/utils.py +0 -181
- django_cfg/apps/payments/views/webhook_views.py +0 -266
- django_cfg/apps/payments/viewsets.py +0 -66
- django_cfg/core/integration.py +0 -160
- django_cfg/template_archive/.gitignore +0 -1
- django_cfg/template_archive/__init__.py +0 -0
- django_cfg/urls.py +0 -33
- {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,206 +1,396 @@
|
|
1
1
|
"""
|
2
|
-
Rate Limiting Middleware.
|
3
|
-
|
2
|
+
Rate Limiting Middleware for the Universal Payment System v2.0.
|
3
|
+
|
4
|
+
Advanced rate limiting with sliding window algorithm and subscription-aware limits.
|
4
5
|
"""
|
5
6
|
|
6
|
-
from django_cfg.modules.django_logger import get_logger
|
7
7
|
import time
|
8
|
-
|
9
|
-
from
|
8
|
+
import json
|
9
|
+
from typing import Optional, Dict, Any, Tuple
|
10
|
+
from django.http import JsonResponse, HttpRequest, HttpResponse
|
10
11
|
from django.utils.deprecation import MiddlewareMixin
|
11
|
-
from django.conf import settings
|
12
12
|
from django.utils import timezone
|
13
13
|
from django.core.cache import cache
|
14
|
+
from datetime import datetime, timedelta
|
14
15
|
|
15
|
-
|
16
|
+
from ..config.helpers import MiddlewareConfigHelper
|
17
|
+
from django_cfg.modules.django_logger import get_logger
|
18
|
+
|
19
|
+
logger = get_logger("rate_limiting_middleware")
|
16
20
|
|
17
21
|
|
18
22
|
class RateLimitingMiddleware(MiddlewareMixin):
|
19
23
|
"""
|
20
|
-
Rate
|
24
|
+
Advanced Rate Limiting Middleware with sliding window algorithm.
|
21
25
|
|
22
26
|
Features:
|
23
|
-
-
|
24
|
-
-
|
25
|
-
-
|
26
|
-
-
|
27
|
-
-
|
27
|
+
- Sliding window rate limiting
|
28
|
+
- Subscription-aware rate limits
|
29
|
+
- Per-user and per-IP limiting
|
30
|
+
- Burst allowance
|
31
|
+
- Rate limit headers
|
32
|
+
- Redis-based distributed limiting
|
33
|
+
- Graceful degradation
|
28
34
|
"""
|
29
35
|
|
30
36
|
def __init__(self, get_response=None):
|
31
37
|
super().__init__(get_response)
|
32
38
|
|
33
|
-
#
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
39
|
+
# Load configuration from django-cfg
|
40
|
+
try:
|
41
|
+
middleware_config = MiddlewareConfigHelper.get_middleware_config()
|
42
|
+
|
43
|
+
# Configuration from django-cfg
|
44
|
+
self.enabled = middleware_config['rate_limiting_enabled']
|
45
|
+
self.default_limits = middleware_config['default_rate_limits']
|
46
|
+
self.cache_timeout = middleware_config['cache_timeouts']['rate_limit']
|
47
|
+
|
48
|
+
# Static defaults
|
49
|
+
self.strict_mode = True
|
50
|
+
self.burst_allowance = 0.5
|
51
|
+
self.window_size = 60 # seconds
|
52
|
+
self.window_precision = 10 # sub-windows
|
53
|
+
self.exempt_paths = [
|
54
|
+
'/api/health/',
|
55
|
+
'/admin/',
|
56
|
+
'/static/',
|
57
|
+
'/media/',
|
58
|
+
]
|
59
|
+
|
60
|
+
except Exception as e:
|
61
|
+
logger.warning(f"Failed to load rate limiting config, using defaults: {e}")
|
62
|
+
# Fallback defaults
|
63
|
+
self.enabled = True
|
64
|
+
self.strict_mode = True
|
65
|
+
self.default_limits = {
|
66
|
+
'anonymous': 60,
|
67
|
+
'authenticated': 300,
|
68
|
+
'free': 100,
|
69
|
+
'basic': 500,
|
70
|
+
'premium': 2000,
|
71
|
+
'enterprise': 10000,
|
72
|
+
}
|
73
|
+
self.burst_allowance = 0.5
|
74
|
+
self.window_size = 60
|
75
|
+
self.window_precision = 10
|
76
|
+
self.exempt_paths = ['/api/health/', '/admin/']
|
77
|
+
self.cache_timeout = 300
|
45
78
|
|
46
|
-
|
47
|
-
|
79
|
+
logger.info(f"Rate Limiting Middleware initialized", extra={
|
80
|
+
'enabled': self.enabled,
|
81
|
+
'default_limits': self.default_limits,
|
82
|
+
'window_size': self.window_size,
|
83
|
+
'burst_allowance': self.burst_allowance
|
84
|
+
})
|
48
85
|
|
49
86
|
def process_request(self, request: HttpRequest) -> Optional[JsonResponse]:
|
50
|
-
"""
|
87
|
+
"""
|
88
|
+
Process request for rate limiting.
|
51
89
|
|
90
|
+
Returns JsonResponse if rate limit exceeded, None to continue.
|
91
|
+
"""
|
52
92
|
if not self.enabled:
|
53
93
|
return None
|
54
94
|
|
55
|
-
#
|
56
|
-
if self.
|
95
|
+
# Check if path is exempt
|
96
|
+
if request.path in self.exempt_paths:
|
57
97
|
return None
|
58
98
|
|
59
|
-
|
60
|
-
rate_key = self._get_rate_key(request)
|
61
|
-
if not rate_key:
|
62
|
-
return None
|
99
|
+
start_time = time.time()
|
63
100
|
|
64
|
-
|
65
|
-
|
101
|
+
try:
|
102
|
+
# Determine rate limit for this request
|
103
|
+
rate_limit, limit_type = self._get_rate_limit(request)
|
104
|
+
|
105
|
+
# Get client identifier
|
106
|
+
client_id = self._get_client_identifier(request)
|
107
|
+
|
108
|
+
# Check rate limit
|
109
|
+
allowed, current_usage, reset_time = self._check_rate_limit(
|
110
|
+
client_id, rate_limit, limit_type
|
111
|
+
)
|
112
|
+
|
113
|
+
if not allowed:
|
114
|
+
# Rate limit exceeded
|
115
|
+
processing_time = (time.time() - start_time) * 1000
|
116
|
+
|
117
|
+
logger.warning(f"Rate limit exceeded", extra={
|
118
|
+
'client_id': client_id,
|
119
|
+
'limit_type': limit_type,
|
120
|
+
'rate_limit': rate_limit,
|
121
|
+
'current_usage': current_usage,
|
122
|
+
'path': request.path,
|
123
|
+
'processing_time_ms': round(processing_time, 2)
|
124
|
+
})
|
125
|
+
|
126
|
+
return self._create_rate_limit_response(
|
127
|
+
rate_limit, current_usage, reset_time
|
128
|
+
)
|
129
|
+
|
130
|
+
# Add rate limit info to request
|
131
|
+
request.rate_limit_info = {
|
132
|
+
'limit': rate_limit,
|
133
|
+
'remaining': max(0, rate_limit - current_usage),
|
134
|
+
'reset_time': reset_time,
|
135
|
+
'limit_type': limit_type
|
136
|
+
}
|
137
|
+
|
138
|
+
# Log successful rate limit check
|
139
|
+
processing_time = (time.time() - start_time) * 1000
|
140
|
+
logger.debug(f"Rate limit check passed", extra={
|
141
|
+
'client_id': client_id,
|
142
|
+
'limit_type': limit_type,
|
143
|
+
'usage': current_usage,
|
144
|
+
'limit': rate_limit,
|
145
|
+
'processing_time_ms': round(processing_time, 2)
|
146
|
+
})
|
147
|
+
|
148
|
+
return None # Continue processing
|
149
|
+
|
150
|
+
except Exception as e:
|
151
|
+
logger.error(f"Rate limiting error", extra={
|
152
|
+
'path': request.path,
|
153
|
+
'error': str(e),
|
154
|
+
'processing_time_ms': round((time.time() - start_time) * 1000, 2)
|
155
|
+
})
|
156
|
+
|
157
|
+
if self.strict_mode:
|
158
|
+
return JsonResponse({
|
159
|
+
'success': False,
|
160
|
+
'error': 'Rate limiting service unavailable',
|
161
|
+
'error_code': 'rate_limit_service_error'
|
162
|
+
}, status=503)
|
163
|
+
else:
|
164
|
+
# Graceful degradation: allow request
|
165
|
+
return None
|
166
|
+
|
167
|
+
def _get_rate_limit(self, request: HttpRequest) -> Tuple[int, str]:
|
168
|
+
"""
|
169
|
+
Determine the appropriate rate limit for this request.
|
66
170
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
171
|
+
Returns tuple of (rate_limit, limit_type).
|
172
|
+
"""
|
173
|
+
# Check if user is authenticated via API key
|
174
|
+
if hasattr(request, 'api_key') and hasattr(request, 'subscription_access'):
|
175
|
+
subscription_access = request.subscription_access
|
176
|
+
|
177
|
+
if subscription_access.get('allowed'):
|
178
|
+
tier = subscription_access.get('tier', 'free')
|
179
|
+
rate_limit = self.default_limits.get(tier, self.default_limits['free'])
|
180
|
+
return rate_limit, f"subscription_{tier}"
|
71
181
|
|
72
|
-
#
|
73
|
-
|
182
|
+
# Check if user is authenticated (Django auth)
|
183
|
+
if hasattr(request, 'user') and request.user.is_authenticated:
|
184
|
+
return self.default_limits['authenticated'], 'authenticated'
|
74
185
|
|
75
|
-
|
186
|
+
# Anonymous user
|
187
|
+
return self.default_limits['anonymous'], 'anonymous'
|
76
188
|
|
77
|
-
def
|
78
|
-
"""
|
79
|
-
|
80
|
-
return any(path.startswith(exempt) for exempt in self.exempt_paths)
|
81
|
-
|
82
|
-
def _get_rate_key(self, request: HttpRequest) -> Optional[str]:
|
83
|
-
"""Get rate limiting key (API key preferred, IP as fallback)."""
|
189
|
+
def _get_client_identifier(self, request: HttpRequest) -> str:
|
190
|
+
"""
|
191
|
+
Get unique identifier for the client.
|
84
192
|
|
85
|
-
|
86
|
-
|
87
|
-
|
193
|
+
Prioritizes API key, then user ID, then IP address.
|
194
|
+
"""
|
195
|
+
# Use API key if available (most specific)
|
196
|
+
if hasattr(request, 'api_key'):
|
197
|
+
return f"api_key:{request.api_key.id}"
|
88
198
|
|
89
|
-
#
|
90
|
-
|
91
|
-
|
92
|
-
return f"ip:{ip}"
|
199
|
+
# Use user ID if authenticated
|
200
|
+
if hasattr(request, 'user') and request.user.is_authenticated:
|
201
|
+
return f"user:{request.user.id}"
|
93
202
|
|
94
|
-
|
203
|
+
# Fall back to IP address
|
204
|
+
return f"ip:{self._get_client_ip(request)}"
|
95
205
|
|
96
|
-
def
|
97
|
-
"""
|
206
|
+
def _check_rate_limit(self, client_id: str, rate_limit: int, limit_type: str) -> Tuple[bool, int, int]:
|
207
|
+
"""
|
208
|
+
Check rate limit using sliding window algorithm.
|
98
209
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
210
|
+
Returns tuple of (allowed, current_usage, reset_time).
|
211
|
+
"""
|
212
|
+
now = int(time.time())
|
213
|
+
window_start = now - self.window_size
|
103
214
|
|
104
|
-
#
|
105
|
-
|
106
|
-
if real_ip:
|
107
|
-
return real_ip
|
215
|
+
# Cache key for this client's rate limit data
|
216
|
+
cache_key = f"rate_limit:{client_id}:{limit_type}"
|
108
217
|
|
109
|
-
#
|
110
|
-
|
111
|
-
|
112
|
-
def _get_rate_limits(self, request: HttpRequest) -> dict:
|
113
|
-
"""Get rate limits for this request."""
|
218
|
+
# Get current window data
|
219
|
+
window_data = cache.get(cache_key, {})
|
114
220
|
|
115
|
-
#
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
# Check if user has subscription with custom limits
|
120
|
-
if hasattr(request, 'payment_subscription'):
|
121
|
-
subscription = request.payment_subscription
|
122
|
-
# Custom limits based on subscription tier could be implemented here
|
123
|
-
# For now, use default limits
|
124
|
-
pass
|
125
|
-
|
126
|
-
return self.default_limits
|
127
|
-
|
128
|
-
def _is_rate_limited(self, rate_key: str, window: str, limit: int) -> bool:
|
129
|
-
"""Check if rate limit is exceeded for given window."""
|
221
|
+
# Clean old entries and count current usage
|
222
|
+
current_usage = 0
|
223
|
+
cleaned_data = {}
|
130
224
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
# Use Redis sliding window
|
138
|
-
current_time = int(time.time())
|
139
|
-
window_start = current_time - window_seconds
|
140
|
-
|
141
|
-
# Get request count in window
|
142
|
-
redis_key = f"rate_limit:{rate_key}:{window}"
|
143
|
-
|
144
|
-
# Simple cache-based rate limiting
|
145
|
-
count = cache.get(redis_key, 0)
|
146
|
-
|
147
|
-
return count >= limit
|
148
|
-
|
149
|
-
except Exception as e:
|
150
|
-
logger.error(f"Error checking rate limit: {e}")
|
151
|
-
# On error, allow request (fail open)
|
152
|
-
return False
|
153
|
-
|
154
|
-
def _get_window_seconds(self, window: str) -> Optional[int]:
|
155
|
-
"""Convert window name to seconds."""
|
225
|
+
for timestamp_str, count in window_data.items():
|
226
|
+
timestamp = int(timestamp_str)
|
227
|
+
if timestamp > window_start:
|
228
|
+
cleaned_data[timestamp_str] = count
|
229
|
+
current_usage += count
|
156
230
|
|
157
|
-
|
158
|
-
|
159
|
-
'per_hour': 3600,
|
160
|
-
'per_day': 86400,
|
161
|
-
}
|
231
|
+
# Calculate reset time (next window)
|
232
|
+
reset_time = now + self.window_size
|
162
233
|
|
163
|
-
|
164
|
-
|
165
|
-
def _record_request(self, rate_key: str):
|
166
|
-
"""Record request for rate limiting."""
|
234
|
+
# Check if we can allow this request
|
235
|
+
burst_limit = int(rate_limit * (1 + self.burst_allowance))
|
167
236
|
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
237
|
+
if current_usage >= burst_limit:
|
238
|
+
# Hard limit exceeded
|
239
|
+
return False, current_usage, reset_time
|
240
|
+
elif current_usage >= rate_limit:
|
241
|
+
# Soft limit exceeded, but within burst allowance
|
242
|
+
logger.info(f"Burst allowance used", extra={
|
243
|
+
'client_id': client_id,
|
244
|
+
'current_usage': current_usage,
|
245
|
+
'rate_limit': rate_limit,
|
246
|
+
'burst_limit': burst_limit
|
247
|
+
})
|
248
|
+
|
249
|
+
# Add current request to window
|
250
|
+
current_window = str(now // (self.window_size // self.window_precision) * (self.window_size // self.window_precision))
|
251
|
+
cleaned_data[current_window] = cleaned_data.get(current_window, 0) + 1
|
252
|
+
current_usage += 1
|
253
|
+
|
254
|
+
# Save updated window data
|
255
|
+
cache.set(cache_key, cleaned_data, timeout=self.cache_timeout)
|
256
|
+
|
257
|
+
return True, current_usage, reset_time
|
183
258
|
|
184
|
-
def
|
185
|
-
"""
|
259
|
+
def _get_client_ip(self, request: HttpRequest) -> str:
|
260
|
+
"""
|
261
|
+
Get client IP address from request.
|
262
|
+
"""
|
263
|
+
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
264
|
+
if x_forwarded_for:
|
265
|
+
return x_forwarded_for.split(',')[0].strip()
|
186
266
|
|
187
|
-
|
188
|
-
|
267
|
+
x_real_ip = request.META.get('HTTP_X_REAL_IP')
|
268
|
+
if x_real_ip:
|
269
|
+
return x_real_ip
|
189
270
|
|
271
|
+
return request.META.get('REMOTE_ADDR', 'unknown')
|
272
|
+
|
273
|
+
def _create_rate_limit_response(self, rate_limit: int, current_usage: int, reset_time: int) -> JsonResponse:
|
274
|
+
"""
|
275
|
+
Create rate limit exceeded response with proper headers.
|
276
|
+
"""
|
190
277
|
response = JsonResponse({
|
191
|
-
'
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
'
|
196
|
-
'
|
197
|
-
'
|
198
|
-
|
278
|
+
'success': False,
|
279
|
+
'error': 'Rate limit exceeded',
|
280
|
+
'error_code': 'rate_limit_exceeded',
|
281
|
+
'rate_limit': {
|
282
|
+
'limit': rate_limit,
|
283
|
+
'current': current_usage,
|
284
|
+
'reset_at': reset_time,
|
285
|
+
'reset_in_seconds': max(0, reset_time - int(time.time()))
|
286
|
+
},
|
287
|
+
'timestamp': timezone.now().isoformat()
|
199
288
|
}, status=429)
|
200
289
|
|
201
290
|
# Add rate limit headers
|
202
|
-
response['X-RateLimit-Limit'] = str(
|
203
|
-
response['X-RateLimit-
|
204
|
-
response['
|
291
|
+
response['X-RateLimit-Limit'] = str(rate_limit)
|
292
|
+
response['X-RateLimit-Remaining'] = '0'
|
293
|
+
response['X-RateLimit-Reset'] = str(reset_time)
|
294
|
+
response['Retry-After'] = str(max(1, reset_time - int(time.time())))
|
295
|
+
|
296
|
+
return response
|
297
|
+
|
298
|
+
def process_response(self, request: HttpRequest, response: HttpResponse) -> HttpResponse:
|
299
|
+
"""
|
300
|
+
Add rate limit headers to response.
|
301
|
+
"""
|
302
|
+
if hasattr(request, 'rate_limit_info'):
|
303
|
+
info = request.rate_limit_info
|
304
|
+
|
305
|
+
response['X-RateLimit-Limit'] = str(info['limit'])
|
306
|
+
response['X-RateLimit-Remaining'] = str(info['remaining'])
|
307
|
+
response['X-RateLimit-Reset'] = str(info['reset_time'])
|
308
|
+
response['X-RateLimit-Type'] = info['limit_type']
|
205
309
|
|
206
310
|
return response
|
311
|
+
|
312
|
+
|
313
|
+
class AdaptiveRateLimitingMiddleware(RateLimitingMiddleware):
|
314
|
+
"""
|
315
|
+
Adaptive Rate Limiting Middleware that adjusts limits based on system load.
|
316
|
+
|
317
|
+
Extends base rate limiting with:
|
318
|
+
- System load monitoring
|
319
|
+
- Dynamic limit adjustment
|
320
|
+
- Circuit breaker pattern
|
321
|
+
- Performance-based throttling
|
322
|
+
"""
|
323
|
+
|
324
|
+
def __init__(self, get_response=None):
|
325
|
+
super().__init__(get_response)
|
326
|
+
|
327
|
+
# Adaptive configuration
|
328
|
+
self.adaptive_enabled = getattr(settings, 'ADAPTIVE_RATE_LIMITING_ENABLED', False)
|
329
|
+
self.load_threshold_high = getattr(settings, 'RATE_LIMIT_LOAD_THRESHOLD_HIGH', 0.8)
|
330
|
+
self.load_threshold_critical = getattr(settings, 'RATE_LIMIT_LOAD_THRESHOLD_CRITICAL', 0.95)
|
331
|
+
|
332
|
+
# Performance monitoring
|
333
|
+
self.response_time_threshold = getattr(settings, 'RATE_LIMIT_RESPONSE_TIME_THRESHOLD', 1000) # ms
|
334
|
+
|
335
|
+
logger.info(f"Adaptive Rate Limiting initialized", extra={
|
336
|
+
'adaptive_enabled': self.adaptive_enabled,
|
337
|
+
'load_thresholds': {
|
338
|
+
'high': self.load_threshold_high,
|
339
|
+
'critical': self.load_threshold_critical
|
340
|
+
}
|
341
|
+
})
|
342
|
+
|
343
|
+
def _get_rate_limit(self, request: HttpRequest) -> Tuple[int, str]:
|
344
|
+
"""
|
345
|
+
Get adaptive rate limit based on system load and performance.
|
346
|
+
"""
|
347
|
+
base_limit, limit_type = super()._get_rate_limit(request)
|
348
|
+
|
349
|
+
if not self.adaptive_enabled:
|
350
|
+
return base_limit, limit_type
|
351
|
+
|
352
|
+
# Get system load factor
|
353
|
+
load_factor = self._get_system_load_factor()
|
354
|
+
|
355
|
+
# Adjust rate limit based on load
|
356
|
+
if load_factor >= self.load_threshold_critical:
|
357
|
+
# Critical load: reduce to 25% of normal
|
358
|
+
adjusted_limit = int(base_limit * 0.25)
|
359
|
+
limit_type += "_critical"
|
360
|
+
elif load_factor >= self.load_threshold_high:
|
361
|
+
# High load: reduce to 50% of normal
|
362
|
+
adjusted_limit = int(base_limit * 0.5)
|
363
|
+
limit_type += "_high_load"
|
364
|
+
else:
|
365
|
+
# Normal load
|
366
|
+
adjusted_limit = base_limit
|
367
|
+
|
368
|
+
return max(1, adjusted_limit), limit_type # Ensure at least 1 request allowed
|
369
|
+
|
370
|
+
def _get_system_load_factor(self) -> float:
|
371
|
+
"""
|
372
|
+
Calculate system load factor (0.0 to 1.0).
|
373
|
+
|
374
|
+
This is a simplified implementation. In production, you might want to:
|
375
|
+
- Monitor CPU usage
|
376
|
+
- Check database connection pool
|
377
|
+
- Monitor Redis performance
|
378
|
+
- Check response times
|
379
|
+
"""
|
380
|
+
try:
|
381
|
+
# Get average response time from cache
|
382
|
+
avg_response_time = cache.get('system_avg_response_time', 100) # ms
|
383
|
+
|
384
|
+
# Simple load calculation based on response time
|
385
|
+
if avg_response_time <= 200:
|
386
|
+
return 0.1 # Low load
|
387
|
+
elif avg_response_time <= 500:
|
388
|
+
return 0.3 # Medium load
|
389
|
+
elif avg_response_time <= 1000:
|
390
|
+
return 0.6 # High load
|
391
|
+
else:
|
392
|
+
return 0.9 # Critical load
|
393
|
+
|
394
|
+
except Exception as e:
|
395
|
+
logger.warning(f"Failed to get system load factor: {e}")
|
396
|
+
return 0.5 # Default to medium load
|